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

+132 -5
View File
@@ -2929,7 +2929,13 @@ pub async fn execute_multi_core_with_options_for_client_and_progress_typed(
state.touch_pool_activity(&pool_key).await;
let _activity_touch = state.pool_activity_touch(pool_key.as_str());
let is_sqlserver = { matches!(state.pool_handle(&pool_key).await, Some(PoolKind::SqlServer(_))) };
let (is_sqlserver, is_sqlserver_agent) = {
match state.pool_handle(&pool_key).await {
Some(PoolKind::SqlServer(_)) => (true, false),
Some(PoolKind::Agent(_)) if db_type == Some(DatabaseType::SqlServer) => (false, true),
_ => (false, false),
}
};
if is_sqlserver {
return execute_multi_sqlserver(state, &pool_key, sql, cancel_token, options).await.map_err(Into::into);
@@ -2961,10 +2967,7 @@ pub async fn execute_multi_core_with_options_for_client_and_progress_typed(
);
}
let execution_plan = db_type.map_or_else(
|| crate::sql::SqlExecutionPlan { statements: split_sql_statements(sql), stop_on_error: false },
|db_type| crate::sql::sql_execution_plan_for_database(sql, db_type),
);
let execution_plan = query_execution_plan(sql, db_type, is_sqlserver_agent);
let continue_on_error = options.continue_on_error && !execution_plan.stop_on_error;
let statements = execution_plan.statements;
if statements.is_empty() {
@@ -3103,6 +3106,21 @@ pub async fn execute_multi_core_with_options_for_client_and_progress_typed(
Ok(results)
}
fn query_execution_plan(
sql: &str,
db_type: Option<DatabaseType>,
preserve_sqlserver_batches: bool,
) -> crate::sql::SqlExecutionPlan {
if preserve_sqlserver_batches && db_type == Some(DatabaseType::SqlServer) {
return crate::sql::SqlExecutionPlan { statements: split_sql_batches(sql), stop_on_error: false };
}
db_type.map_or_else(
|| crate::sql::SqlExecutionPlan { statements: split_sql_statements(sql), stop_on_error: false },
|db_type| crate::sql::sql_execution_plan_for_database(sql, db_type),
)
}
fn single_statement_multi_result(
result: Result<db::QueryResult, QueryExecutionError>,
table_data_preview: bool,
@@ -6211,6 +6229,88 @@ for line in sys.stdin:
let _ = std::fs::remove_dir_all(dir);
}
async fn sqlserver_agent_echo_state(
) -> (AppState, std::path::PathBuf, std::sync::Arc<crate::db::agent_driver::AgentRuntimeClient>) {
let dir = std::env::temp_dir().join(format!("dbx-query-sqlserver-agent-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
let script_path = dir.join("agent.py");
std::fs::write(
&script_path,
r#"import json, sys
print(json.dumps({'ready': True}), flush=True)
for line in sys.stdin:
req = json.loads(line)
if req['method'] == 'handshake':
result = {'protocolVersion': 2, 'agentProtocolVersion': 2, 'capabilities': ['multi_session']}
elif req['method'] == 'execute_query':
result = {
'columns': ['sql'], 'column_types': ['nvarchar'], 'column_sortables': [],
'rows': [[req['params']['sql']]], 'affected_rows': 0, 'execution_time_ms': 1,
'truncated': False, 'session_id': None, 'has_more': False
}
else:
result = {}
print(json.dumps({'jsonrpc': '2.0', 'id': req['id'], 'result': result}), flush=True)
"#,
)
.unwrap();
let python = if cfg!(windows) { "python" } else { "python3" };
let runtime = crate::db::agent_driver::AgentRuntimeClient::spawn(
crate::db::agent_driver::AgentLaunchSpec::new(python)
.with_args([script_path.to_string_lossy().to_string()]),
"test",
)
.await
.unwrap();
runtime.increment_session_count();
let storage = Storage::open(&dir.join("storage.db")).await.unwrap();
let state = AppState::new(storage);
state.configs.write().await.insert("conn-1".to_string(), test_connection_config(DatabaseType::SqlServer));
state
.update_connection_pools(|connections| {
connections.insert(
"conn-1".to_string(),
PoolKind::agent(crate::db::agent_driver::AgentDriverClient::shared_session(
runtime.clone(),
"session-1".to_string(),
)),
);
})
.await;
(state, dir, runtime)
}
#[tokio::test]
async fn sqlserver_agent_multi_execution_sends_table_variable_script_as_one_batch() {
let (state, dir, runtime) = sqlserver_agent_echo_state().await;
let sql = "DECLARE @TargetTables TABLE (TableName NVARCHAR(128));\n\
INSERT INTO @TargetTables VALUES ('Bill_Record');\n\
SELECT TableName FROM @TargetTables;";
let results = execute_multi_core_with_options_for_client_and_progress_typed(
&state,
"conn-1",
"",
sql,
None,
None,
QueryExecutionOptions::default(),
None,
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].result.rows, vec![vec![serde_json::Value::String(sql.to_string())]]);
runtime.kill();
drop(state);
let _ = std::fs::remove_dir_all(dir);
}
async fn agent_error_state(
disposition: &str,
) -> (AppState, std::path::PathBuf, std::sync::Arc<crate::db::agent_driver::AgentRuntimeClient>) {
@@ -7312,6 +7412,33 @@ for line in sys.stdin:
assert_eq!(serialized.get("server_message"), Some(&serde_json::Value::Bool(true)));
}
#[test]
fn sqlserver_agent_execution_plan_preserves_table_variable_batch() {
let sql = "DECLARE @TargetTables TABLE (TableName NVARCHAR(128));\n\
INSERT INTO @TargetTables VALUES ('Bill_Record');\n\
SELECT TableName FROM @TargetTables;";
let plan = query_execution_plan(sql, Some(DatabaseType::SqlServer), true);
assert_eq!(plan.statements, vec![sql]);
}
#[test]
fn sqlserver_agent_execution_plan_splits_only_on_go() {
let sql = "DECLARE @x TABLE (id INT);\nINSERT INTO @x VALUES (1);\nGO\nSELECT 2;";
let plan = query_execution_plan(sql, Some(DatabaseType::SqlServer), true);
assert_eq!(plan.statements, vec!["DECLARE @x TABLE (id INT);\nINSERT INTO @x VALUES (1);", "SELECT 2;"]);
}
#[test]
fn ordinary_agent_execution_plan_still_splits_semicolon_statements() {
let plan = query_execution_plan("SELECT 1; SELECT 2;", Some(DatabaseType::Dameng), false);
assert_eq!(plan.statements, vec!["SELECT 1", "SELECT 2"]);
}
// Regression test for #6097: SQL Server queries share a single mutex-guarded
// connection (see PoolKind::SqlServer), so a fast query can queue for seconds
// behind another operation (e.g. autocomplete/schema metadata) holding that