diff --git a/.github/scripts/ci-rust.mjs b/.github/scripts/ci-rust.mjs index 76bab8b474..da5be9e0bf 100644 --- a/.github/scripts/ci-rust.mjs +++ b/.github/scripts/ci-rust.mjs @@ -8,14 +8,14 @@ export function rustCommand(action, group, mode) { || (group !== "workspace" && !Object.hasOwn(rustGroups, group)) || (action === "clippy" && group !== "workspace")) { throw new Error(`Unknown Rust CI configuration: ${action}/${group}/${mode}`); } - const capabilityFeatures = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher"]; + const capabilityFeatures = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "sqlserver-native-attention"]; if (mode === "full") capabilityFeatures.push("system-fonts"); const appFeatures = ["dbx", "dbx-core", "dbx-web"].flatMap((name) => capabilityFeatures.map((feature) => `${name}/${feature}`)); const foundationFeatures = ["dbx-types/mq-admin", "dbx-types/openapi", "dbx-sql/duckdb-sidecar", "dbx-sql/openapi", "dbx-sql-data/openapi", "dbx-sql-schema/duckdb-sidecar", "dbx-platform/downloads", "dbx-platform/host-prompts", "dbx-platform/test-support", "dbx-plugin-runtime/default", "dbx-plugin-runtime/test-support"]; const features = group === "foundation" ? foundationFeatures : group === "drivers" ? [ - ...["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-bundled", "sqlite-sqlcipher", "test-support"].map((feature) => `dbx-drivers/${feature}`), + ...["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-bundled", "sqlite-sqlcipher", "sqlserver-native-attention", "test-support"].map((feature) => `dbx-drivers/${feature}`), "dbx-driver-agent/test-support", "dbx-driver-mysql/test-support", "dbx-driver-redis/test-support", "dbx-driver-support/test-support", "dbx-sqlite-worker/runtime", "dbx-types/openapi", "dbx-sql-data/openapi", diff --git a/Makefile b/Makefile index 63bd606767..7acfe72319 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ dev: node_modules/.modules.yaml check-tauri-dev-port dev-fast: node_modules/.modules.yaml check-tauri-dev-port # os-keyring must stay in sync with src-tauri defaults: without it the keychain # key is unreadable and the secret-store migration wizard reappears on every launch. - RUST_MIN_STACK=16777216 $(PNPM) dev:tauri -- --no-default-features --features duckdb-sidecar,dynamodb,sqlite-bundled,os-keyring + RUST_MIN_STACK=16777216 $(PNPM) dev:tauri -- --no-default-features --features duckdb-sidecar,dynamodb,sqlite-bundled,os-keyring,sqlserver-native-attention dev-web: node_modules/.modules.yaml $(PNPM) dev:web diff --git a/agents/drivers/duckdb/Cargo.lock b/agents/drivers/duckdb/Cargo.lock index f3bc332b18..614d19a8be 100644 --- a/agents/drivers/duckdb/Cargo.lock +++ b/agents/drivers/duckdb/Cargo.lock @@ -5886,6 +5886,8 @@ dependencies = [ [[package]] name = "tiberius" version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d" dependencies = [ "async-trait", "asynchronous-codec", diff --git a/agents/drivers/duckdb/Cargo.toml b/agents/drivers/duckdb/Cargo.toml index 371b4d7621..5b1a243ec9 100644 --- a/agents/drivers/duckdb/Cargo.toml +++ b/agents/drivers/duckdb/Cargo.toml @@ -42,7 +42,6 @@ tokio-postgres = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", r postgres-types = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "ac8ad476de59093d33ab9f294531c956e0c6a724" } postgres-protocol = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "ac8ad476de59093d33ab9f294531c956e0c6a724" } mysql_async = { git = "https://github.com/t8y2/mysql_async.git", rev = "b615bb0b10b5975182a4aa069cabc3d09021f6ab" } -tiberius = { path = "../../../vendor/tiberius" } [profile.release] panic = "abort" diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index a03e377771..7bc5367834 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -10,6 +10,10 @@ default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "system duckdb-sidecar = ["dbx-sql/duckdb-sidecar", "dbx-drivers/duckdb-sidecar"] dynamodb = ["dbx-drivers/dynamodb"] mq-admin = ["dep:rumqttc", "dbx-types/mq-admin", "dbx-drivers/mq-admin"] +# SQL Server native TDS ATTENTION on interrupt (SQL Server 2000 batch abort). +# Requires the vendored tiberius patched in by the root workspace; standalone +# agent driver workspaces build dbx-core without it. Desktop enables this. +sqlserver-native-attention = ["dbx-drivers/sqlserver-native-attention"] sqlite-sqlcipher = ["rusqlite/bundled-sqlcipher-vendored-openssl", "dbx-drivers/sqlite-sqlcipher"] sqlite-multiple-ciphers = ["dbx-drivers/sqlite-multiple-ciphers"] # Keeps bundled SQLite for consumers that enable no other sqlite source diff --git a/crates/dbx-core/src/query/sqlserver_manual_transaction.rs b/crates/dbx-core/src/query/sqlserver_manual_transaction.rs index bbbf265679..63f0f70ab2 100644 --- a/crates/dbx-core/src/query/sqlserver_manual_transaction.rs +++ b/crates/dbx-core/src/query/sqlserver_manual_transaction.rs @@ -362,6 +362,8 @@ impl Drop for ExecutionGuard { /// TCP FIN alone need not stop a SQL2000 batch immediately. ATTENTION is /// best effort, never proof of rollback; callers always discard the connection. +/// The wire call needs the vendored tiberius (dbx-driver-sqlserver feature +/// `native-attention`); standalone crates.io builds no-op inside the client. async fn send_native_attention(conn: &mut TxnConnection) { if let TxnConnection::SqlServer { client: Some(client), .. } = conn { let attention = async { client.lock().await.send_attention().await }; diff --git a/crates/dbx-driver-sqlserver/Cargo.toml b/crates/dbx-driver-sqlserver/Cargo.toml index a1d6542c6f..26d0aaf4c4 100644 --- a/crates/dbx-driver-sqlserver/Cargo.toml +++ b/crates/dbx-driver-sqlserver/Cargo.toml @@ -5,6 +5,13 @@ edition = "2021" license = "Apache-2.0" publish = false +[features] +# TDS ATTENTION (tiberius Client::send_attention) exists only in the vendored +# tiberius that the root workspace patches in (vendor/tiberius). Standalone +# agent driver workspaces resolve tiberius from crates.io and must keep this +# off; SqlServerClient::send_attention then degrades to a no-op. +native-attention = [] + [dependencies] dbx-driver-support = { path = "../dbx-driver-support" } dbx-sql-core = { path = "../dbx-sql-core" } diff --git a/crates/dbx-driver-sqlserver/src/sqlserver.rs b/crates/dbx-driver-sqlserver/src/sqlserver.rs index 4cfaa43e8f..bc1ca362df 100644 --- a/crates/dbx-driver-sqlserver/src/sqlserver.rs +++ b/crates/dbx-driver-sqlserver/src/sqlserver.rs @@ -91,6 +91,24 @@ impl SqlServerClient { self.inner.query(query, params).await } } + + /// Best-effort TDS ATTENTION to interrupt the in-flight batch. SQL Server + /// 2000 keeps executing a batch after TCP FIN, so desktop builds (feature + /// `native-attention`, vendored tiberius) signal the server over the wire + /// before the connection is discarded. ATTENTION is never proof of + /// rollback: every caller discards the connection right after. + #[cfg(feature = "native-attention")] + pub async fn send_attention(&mut self) -> tiberius::Result<()> { + self.inner.send_attention().await + } + + /// No-op fallback for builds whose tiberius comes from crates.io + /// (standalone agent driver workspaces), where the wire call above does + /// not exist. Connection discard still aborts the session. + #[cfg(not(feature = "native-attention"))] + pub async fn send_attention(&mut self) -> tiberius::Result<()> { + Ok(()) + } } impl Deref for SqlServerClient { diff --git a/crates/dbx-drivers/Cargo.toml b/crates/dbx-drivers/Cargo.toml index f6d04b922a..2ed7c22922 100644 --- a/crates/dbx-drivers/Cargo.toml +++ b/crates/dbx-drivers/Cargo.toml @@ -10,6 +10,8 @@ default = [] duckdb-sidecar = [] dynamodb = ["dep:aws-sdk-dynamodb"] mq-admin = ["dbx-types/mq-admin"] +# Desktop/native SQL Server batch interruption; see dbx-driver-sqlserver. +sqlserver-native-attention = ["dbx-driver-sqlserver/native-attention"] sqlite-bundled = ["rusqlite/bundled"] sqlite-sqlcipher = ["rusqlite/bundled-sqlcipher-vendored-openssl"] sqlite-multiple-ciphers = ["dep:libsqlite3-hotbundle"] diff --git a/crates/dbx-web/Cargo.toml b/crates/dbx-web/Cargo.toml index db4b9d0c2a..6fc5bf79d9 100644 --- a/crates/dbx-web/Cargo.toml +++ b/crates/dbx-web/Cargo.toml @@ -15,6 +15,10 @@ dynamodb = ["dbx-core/dynamodb"] mq-admin = ["dbx-core/mq-admin", "dbx-mcp/mq-admin"] sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"] system-fonts = ["dbx-core/system-fonts"] +# Pass-through so CI feature lanes can compile-check the vendored ATTENTION +# path; the web server never executes manual transactions, so it stays off by +# default here. +sqlserver-native-attention = ["dbx-core/sqlserver-native-attention"] # Embed the frontend build output (../../dist) into the binary. When enabled # and DBX_STATIC_DIR is not set, the embedded files are served by default. # Requires the frontend to have been built before invoking cargo. diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 50f0b67321..e5dd2e657d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,10 +13,11 @@ name = "dbx_lib" crate-type = ["rlib"] [features] -default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-multiple-ciphers", "system-fonts", "os-keyring"] +default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-multiple-ciphers", "system-fonts", "os-keyring", "sqlserver-native-attention"] custom-protocol = ["tauri/custom-protocol"] duckdb-sidecar = ["dbx-core/duckdb-sidecar"] os-keyring = ["dbx-core/os-keyring"] +sqlserver-native-attention = ["dbx-core/sqlserver-native-attention"] dynamodb = ["dbx-core/dynamodb"] mq-admin = ["dbx-core/mq-admin"] sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]