From 1268dcdf52de217853bc577dcf003838978a772b Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 8 Sep 2026 19:30:44 +0800 Subject: [PATCH] fix(sqlserver): avoid legacy schema switch during writes --- crates/dbx-core/src/query.rs | 43 ++++++++++++++++++++++++++++++--- crates/dbx-core/src/transfer.rs | 2 +- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 3c9e56a50d..c288d57879 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -832,7 +832,9 @@ async fn connection_database_type_for_pool_key(state: &AppState, pool_key: &str) } fn schema_for_execution_context(db_type: Option, schema: Option<&str>) -> Option<&str> { - if matches!(db_type, Some(DatabaseType::Iris)) { + // SQL Server has no session-level schema switch. Data-grid DML already uses + // qualified names, while legacy jTDS can mis-handle schema as catalog. + if matches!(db_type, Some(DatabaseType::Iris | DatabaseType::SqlServer)) { None } else { schema @@ -4965,6 +4967,7 @@ async fn begin_transaction_session( (TxnConnection::Mysql(Some(conn)), probe_pool_key.clone()) } TxnPoolHandle::Agent => { + let db_type = connection_database_type(state, connection_id).await; let client_session_id = format!("manual-txn-{}", uuid::Uuid::new_v4()); let agent_pool_key = state.get_or_create_pool_for_session(connection_id, pool_database, Some(&client_session_id)).await?; @@ -4980,7 +4983,9 @@ async fn begin_transaction_session( }; let begin_result = { let mut locked = client.lock().await; - locked.begin_manual_transaction::(schema).await + locked + .begin_manual_transaction::(schema_for_execution_context(db_type, schema)) + .await }; if let Err(error) = begin_result { let _ = state.close_client_session_pool(connection_id, pool_database, &client_session_id).await; @@ -6821,6 +6826,18 @@ for line in sys.stdin: 'rows': [[req['params']['sql']]], 'affected_rows': 0, 'execution_time_ms': 1, 'truncated': False, 'session_id': None, 'has_more': False } + elif req['method'] in ('execute_batch', 'execute_transaction'): + if req['params'].get('schema') is not None: + print(json.dumps({ + 'jsonrpc': '2.0', 'id': req['id'], + 'error': {'code': -1, 'message': 'legacy SQL Server schema switch attempted'} + }), flush=True) + continue + result = { + 'columns': [], 'column_types': [], 'column_sortables': [], 'rows': [], + 'affected_rows': 1, '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) @@ -6884,6 +6901,25 @@ for line in sys.stdin: let _ = std::fs::remove_dir_all(dir); } + #[tokio::test] + async fn sqlserver_agent_write_paths_do_not_request_schema_switch() { + let (state, dir, runtime) = sqlserver_agent_echo_state().await; + let statements = ["UPDATE [dbo].[users] SET [active] = 1 WHERE [id] = 7".to_string()]; + + let batch = execute_statements(&state, "conn-1", "", &statements, Some("dbo"), None).await.unwrap(); + let transaction = + execute_statements_in_transaction_on_pool(&state, "conn-1", "conn-1", "", &statements, Some("dbo"), None) + .await + .unwrap(); + + assert_eq!(batch.affected_rows, 1); + assert_eq!(transaction.affected_rows, 1); + + 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) { @@ -9300,8 +9336,9 @@ for line in sys.stdin: } #[test] - fn iris_execution_context_omits_schema() { + fn agent_execution_context_omits_unsupported_schema_switches() { assert_eq!(schema_for_execution_context(Some(DatabaseType::Iris), Some("SQLUser")), None); + assert_eq!(schema_for_execution_context(Some(DatabaseType::SqlServer), Some("dbo")), None); assert_eq!(schema_for_execution_context(Some(DatabaseType::Oracle), Some("APP")), Some("APP")); assert_eq!(schema_for_execution_context(None, Some("APP")), Some("APP")); } diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 9d7f81b36c..be4d9016d9 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -8887,7 +8887,7 @@ where .get(&table) .map(|foreign_keys| HashMap::from([(table.clone(), foreign_keys.clone())])) .unwrap_or_default(); - let preexisting_backup_names = preexisting_backup_names.map(|m| m.clone()); + let preexisting_backup_names = preexisting_backup_names.cloned(); // Kept outside the spawned task: `request` moves into it, and the error path below // still needs the backup's qualified name to point the user at their data. let backup_for_this_table = request