fix(sqlserver): keep table variable scope in agent batches

This commit is contained in:
zipg authored and GitHub committed 2026-09-03 13:43:45 +08:00
1 parent 4b7b72dc65
commit 76c51b5fa6
6 files changed
+334 -11

No files matched your search

@@ -196,7 +196,9 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
options.getMaxRows(),
options.getFetchSize(),
options.getTimeoutSecs(),
resultValueReader()
resultValueReader(),
JdbcExecutor.StatementMessageReader.NONE,
advancePastUpdateCounts()
);
}
@@ -211,7 +213,8 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
this::setSchemaSQL,
this::resetSchemaSQL,
options,
resultValueReader()
resultValueReader(),
advancePastUpdateCounts()
);
}
@@ -529,6 +532,10 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
protected void beforeQueryExecution(Connection connection, int timeoutSecs) throws Exception {
}
protected boolean advancePastUpdateCounts() {
return false;
}
protected void beforePooledConnectionReturn(Connection connection) throws Exception {
}
@@ -102,6 +102,34 @@ public final class JdbcExecutor {
int timeoutSecs,
ResultValueReader valueReader,
StatementMessageReader statementMessageReader
) {
return execute(
conn,
sql,
schema,
setSchemaSql,
resetSchemaSql,
maxRows,
fetchSize,
timeoutSecs,
valueReader,
statementMessageReader,
false
);
}
public QueryResult execute(
Connection conn,
String sql,
String schema,
Function<String, String> setSchemaSql,
Supplier<String> resetSchemaSql,
int maxRows,
Integer fetchSize,
int timeoutSecs,
ResultValueReader valueReader,
StatementMessageReader statementMessageReader,
boolean advancePastUpdateCounts
) {
return unchecked(() -> {
String trimmedSql = trimSql(sql);
@@ -122,17 +150,31 @@ public final class JdbcExecutor {
// Do not translate them to Connection.commit(), which requires autoCommit=false.
boolean hasResultSet = stmt.execute(trimmedSql);
long elapsed = System.currentTimeMillis() - start;
long affectedRows;
if (advancePastUpdateCounts) {
affectedRows = 0L;
while (!hasResultSet) {
int updateCount = stmt.getUpdateCount();
if (updateCount < 0) {
break;
}
affectedRows += updateCount;
hasResultSet = stmt.getMoreResults();
}
} else {
int updateCount = hasResultSet ? -1 : stmt.getUpdateCount();
affectedRows = updateCount >= 0 ? updateCount : 0L;
}
QueryResult result;
if (hasResultSet) {
try (ResultSet rs = stmt.getResultSet()) {
result = readResultSet(rs, elapsed, effectiveMaxRows, valueReader);
}
} else {
int updateCount = stmt.getUpdateCount();
result = new QueryResult(
Collections.emptyList(),
Collections.emptyList(),
updateCount >= 0 ? updateCount : 0,
affectedRows,
elapsed,
false
);
@@ -240,6 +282,30 @@ public final class JdbcExecutor {
return executePage(conn, sql, schema, setSchemaSql, resetSchemaSql, options, valueReader, StatementMessageReader.NONE, sessions);
}
public QueryPageResult executePage(
Connection conn,
String sql,
String schema,
Function<String, String> setSchemaSql,
Supplier<String> resetSchemaSql,
QueryPageOptions options,
ResultValueReader valueReader,
boolean advancePastUpdateCounts
) {
return executePage(
conn,
sql,
schema,
setSchemaSql,
resetSchemaSql,
options,
valueReader,
StatementMessageReader.NONE,
sessions,
advancePastUpdateCounts
);
}
public QueryPageResult startTableRead(
Connection conn,
String sql,
@@ -273,6 +339,32 @@ public final class JdbcExecutor {
ResultValueReader valueReader,
StatementMessageReader statementMessageReader,
ConcurrentHashMap<String, QuerySession> targetSessions
) {
return executePage(
conn,
sql,
schema,
setSchemaSql,
resetSchemaSql,
options,
valueReader,
statementMessageReader,
targetSessions,
false
);
}
private QueryPageResult executePage(
Connection conn,
String sql,
String schema,
Function<String, String> setSchemaSql,
Supplier<String> resetSchemaSql,
QueryPageOptions options,
ResultValueReader valueReader,
StatementMessageReader statementMessageReader,
ConcurrentHashMap<String, QuerySession> targetSessions,
boolean advancePastUpdateCounts
) {
return unchecked(() -> {
expireIdleSessions(targetSessions, System.currentTimeMillis(), QUERY_SESSION_IDLE_TIMEOUT_MILLIS);
@@ -293,12 +385,26 @@ public final class JdbcExecutor {
// JDBC transaction APIs are reserved for executeTransaction.
boolean hasResultSet = stmt.execute(trimmedSql);
long elapsed = System.currentTimeMillis() - start;
long affectedRows;
if (advancePastUpdateCounts) {
affectedRows = 0L;
while (!hasResultSet) {
int updateCount = stmt.getUpdateCount();
if (updateCount < 0) {
break;
}
affectedRows += updateCount;
hasResultSet = stmt.getMoreResults();
}
} else {
int updateCount = hasResultSet ? -1 : stmt.getUpdateCount();
affectedRows = updateCount >= 0 ? updateCount : 0L;
}
if (!hasResultSet) {
int updateCount = stmt.getUpdateCount();
QueryResult result = new QueryResult(
Collections.emptyList(),
Collections.emptyList(),
updateCount >= 0 ? updateCount : 0,
affectedRows,
elapsed,
false
);
@@ -242,6 +242,47 @@ class JdbcExecutorTest {
assertEquals(Arrays.asList(Arrays.asList(1, "Ada")), result.getRows());
}
@Test
void executeAdvancesPastUpdateCountsToTheFollowingResultSet() {
CountingResultSetFixture fixture = countingResultSet(new Object[][]{{42, "Bill_Record"}});
QueryResult result = JdbcExecutor.INSTANCE.execute(
updateThenResultSetConnection(1, fixture.resultSet()),
"DECLARE @tables TABLE (id INT, name NVARCHAR(128)); INSERT INTO @tables VALUES (42, 'Bill_Record'); SELECT * FROM @tables;",
"",
schema -> "",
() -> "",
100,
null,
0,
JdbcExecutor.INSTANCE::defaultResultValue,
JdbcExecutor.StatementMessageReader.NONE,
true
);
assertEquals(Arrays.asList("id", "name"), result.getColumns());
assertEquals(Arrays.asList(Arrays.asList(42, "Bill_Record")), result.getRows());
}
@Test
void executePageAdvancesPastUpdateCountsToTheFollowingResultSet() {
CountingResultSetFixture fixture = countingResultSet(new Object[][]{{42, "Bill_Record"}});
QueryPageResult result = JdbcExecutor.INSTANCE.executePage(
updateThenResultSetConnection(1, fixture.resultSet()),
"DECLARE @tables TABLE (id INT, name NVARCHAR(128)); INSERT INTO @tables VALUES (42, 'Bill_Record'); SELECT * FROM @tables;",
"",
schema -> "",
() -> "",
new QueryPageOptions(100, null, 100),
JdbcExecutor.INSTANCE::defaultResultValue,
true
);
assertEquals(Arrays.asList("id", "name"), result.getColumns());
assertEquals(Arrays.asList(Arrays.asList(42, "Bill_Record")), result.getRows());
}
@Test
void executeStillPropagatesStatementErrors() {
SQLException failure = new SQLException("permission denied", "42000", 229);
@@ -467,6 +508,42 @@ class JdbcExecutorTest {
);
}
private static Connection updateThenResultSetConnection(int updateCount, ResultSet resultSet) {
AtomicInteger resultIndex = new AtomicInteger();
InvocationHandler statementHandler = (Object unused, Method method, Object[] args) -> {
switch (method.getName()) {
case "execute":
return false;
case "getUpdateCount":
return resultIndex.get() == 0 ? updateCount : -1;
case "getMoreResults":
return resultIndex.incrementAndGet() == 1;
case "getResultSet":
return resultSet;
case "close":
return null;
default:
return defaultValue(method.getReturnType());
}
};
Statement statement = (Statement) Proxy.newProxyInstance(
Statement.class.getClassLoader(),
new Class<?>[]{Statement.class},
statementHandler
);
InvocationHandler connectionHandler = (Object unused, Method method, Object[] args) -> {
if (method.getName().equals("createStatement")) {
return statement;
}
return defaultValue(method.getReturnType());
};
return (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(),
new Class<?>[]{Connection.class},
connectionHandler
);
}
private static Object defaultValue(Class<?> type) {
if (type == Boolean.TYPE) {
return false;