diff --git a/apps/desktop/src/components/layout/__tests__/EditorToolbar.transactionMode.spec.ts b/apps/desktop/src/components/layout/__tests__/EditorToolbar.transactionMode.spec.ts index d3b7fa844f..2173ee73c1 100644 --- a/apps/desktop/src/components/layout/__tests__/EditorToolbar.transactionMode.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/EditorToolbar.transactionMode.spec.ts @@ -156,6 +156,22 @@ describe("EditorToolbar commit/rollback visibility", () => { host.remove(); }); + it("hides Commit/Rollback for a clean GaussDB sticky manual session", async () => { + const host = await mountToolbar({ dbType: "gaussdb", stickyProvenReadOnlyState: true, txnPossiblyDirty: false, txnSessionId: "txn-1" }); + + expect(host.querySelector(commitSelector)).toBeNull(); + expect(host.querySelector(rollbackSelector)).toBeNull(); + host.remove(); + }); + + it("shows Commit/Rollback once the GaussDB sticky session is dirty", async () => { + const host = await mountToolbar({ dbType: "gaussdb", stickyProvenReadOnlyState: true, txnPossiblyDirty: true, txnSessionId: "txn-1" }); + + expect(host.querySelector(commitSelector)).not.toBeNull(); + expect(host.querySelector(rollbackSelector)).not.toBeNull(); + host.remove(); + }); + it("keeps the legacy always-visible rule for non-sticky dialects with a session", async () => { const host = await mountToolbar({ dbType: "jdbc", stickyProvenReadOnlyState: false, txnPossiblyDirty: false, txnSessionId: "txn-1" }); diff --git a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts index 7fd1fe94d2..b0e61aa7d8 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts @@ -176,6 +176,8 @@ describe("database and schema qualifiers", () => { describe("supportsTransaction", () => { it("returns true for supported database types", () => { expect(supportsTransaction("postgres")).toBe(true); + expect(supportsTransaction("gaussdb")).toBe(true); + expect(supportsTransaction("opengauss")).toBe(true); expect(supportsTransaction("mysql")).toBe(true); expect(supportsTransaction("oracle")).toBe(true); expect(supportsTransaction("jdbc")).toBe(true); @@ -215,6 +217,8 @@ describe("defaultAutoCommitForDbType", () => { it("honors the configured default transaction mode", () => { expect(defaultAutoCommitForDbType("mysql", "manual")).toBe(false); expect(defaultAutoCommitForDbType("postgres", "manual")).toBe(false); + expect(defaultAutoCommitForDbType("gaussdb", "manual")).toBe(false); + expect(defaultAutoCommitForDbType("opengauss", "manual")).toBe(false); expect(defaultAutoCommitForDbType("sqlserver", "manual")).toBe(false); expect(defaultAutoCommitForDbType("sqlserver", "auto")).toBe(true); expect(defaultAutoCommitForDbType("oracle", "manual")).toBe(false); @@ -249,11 +253,13 @@ describe("usesOracleStickyTransactionState", () => { }); describe("usesProvenReadOnlyStickyTransactionState", () => { - it("enables the sticky toolbar for the Oracle family, MySQL and PostgreSQL", () => { + it("enables the sticky toolbar for the Oracle family, MySQL and the PostgreSQL family", () => { expect(usesProvenReadOnlyStickyTransactionState("oracle")).toBe(true); expect(usesProvenReadOnlyStickyTransactionState("oceanbase-oracle")).toBe(true); expect(usesProvenReadOnlyStickyTransactionState("mysql")).toBe(true); expect(usesProvenReadOnlyStickyTransactionState("postgres")).toBe(true); + expect(usesProvenReadOnlyStickyTransactionState("gaussdb")).toBe(true); + expect(usesProvenReadOnlyStickyTransactionState("opengauss")).toBe(true); }); it("keeps databases without manual-transaction support on the legacy toolbar", () => { @@ -267,7 +273,7 @@ describe("usesProvenReadOnlyStickyTransactionState", () => { it("only enables sticky state where manual mode is reachable", () => { // The sticky UX gates commit/rollback in manual mode; a member without // transaction support could never reach it. Guards against drift. - for (const dbType of ["oracle", "oceanbase-oracle", "mysql", "postgres"]) { + for (const dbType of ["oracle", "oceanbase-oracle", "mysql", "postgres", "gaussdb", "opengauss"]) { expect(supportsTransaction(dbType)).toBe(true); } }); diff --git a/apps/desktop/src/lib/database/databaseFeatureSupport.ts b/apps/desktop/src/lib/database/databaseFeatureSupport.ts index e39321c2ef..afbab44007 100644 --- a/apps/desktop/src/lib/database/databaseFeatureSupport.ts +++ b/apps/desktop/src/lib/database/databaseFeatureSupport.ts @@ -281,7 +281,7 @@ export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean { return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType); } -const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql", "oracle", "jdbc", "oceanbase-oracle", "dameng", "sqlserver"]; +const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "gaussdb", "opengauss", "mysql", "oracle", "jdbc", "oceanbase-oracle", "dameng", "sqlserver"]; /** Oracle-family databases, kept ONLY for the Oracle-specific ALTER SESSION SET * CURRENT_SCHEMA compensation in queryStore. Do not use for toolbar/dirty-bit @@ -294,9 +294,10 @@ const ORACLE_STICKY_TRANSACTION_TYPES: ReadonlySet = new Set(["oracle", * (crates/dbx-core/src/query/mod.rs + sql_risk.rs `prove_read_only_for_database`). * Every member must also be in TRANSACTION_SUPPORTED_TYPES above: a database * cannot reach manual mode (and this UX) without explicit transaction control - * (#9018). Family members like doris/kingbase join only when their transaction - * support lands. */ -const PROVEN_READ_ONLY_STICKY_TYPES: ReadonlySet = new Set(["oracle", "oceanbase-oracle", "mysql", "postgres"]); + * (#9018). The PostgreSQL family (PostgreSQL, GaussDB, openGauss) and MySQL use + * the strict `sql_risk` proof; Oracle keeps its lexical classifier. Family + * members like doris/kingbase join only when their transaction support lands. */ +const PROVEN_READ_ONLY_STICKY_TYPES: ReadonlySet = new Set(["oracle", "oceanbase-oracle", "mysql", "postgres", "gaussdb", "opengauss"]); /** * Returns true if the given database type supports explicit transaction control diff --git a/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts index 1ea1524a28..5fc0e6a818 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts @@ -99,6 +99,22 @@ describe("queryStore default transaction mode", () => { expect(store.tabs.find((item) => item.id === tabId)!.autoCommit).toBe(false); }); + it("creates a GaussDB query tab in manual transaction mode when configured", async () => { + editorSettings.defaultTransactionMode = "manual"; + mocks.getConnectionConfig.mockReturnValue({ + id: "gaussdb-1", + name: "GaussDB", + db_type: "gaussdb", + database: "postgres", + }); + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("gaussdb-1", "postgres", "Query", "query", "public"); + + expect(store.tabs.find((item) => item.id === tabId)!.autoCommit).toBe(false); + }); + it("creates a query tab in auto-commit mode by default", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const store = useQueryStore(); diff --git a/apps/desktop/src/stores/__tests__/queryStore.provenReadOnlyStickyState.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.provenReadOnlyStickyState.spec.ts index b745d0becd..8c9c7a6e7f 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.provenReadOnlyStickyState.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.provenReadOnlyStickyState.spec.ts @@ -31,6 +31,8 @@ vi.mock("@/lib/backend/api", () => ({ vi.mock("@/stores/connectionStore", () => ({ useConnectionStore: () => ({ ensureConnected: vi.fn().mockResolvedValue(undefined), + ensureDatabaseCompatibilityMode: vi.fn().mockResolvedValue(undefined), + databaseCompatibilityMode: vi.fn().mockReturnValue(undefined), getConfig: mocks.getConnectionConfig, recordConnectionLostError: vi.fn(), }), @@ -524,6 +526,8 @@ describe("queryStore Oracle/OceanBase manual-transaction sticky state (behavior describe.each([ { label: "MySQL", configId: "mysql-1", database: "test", dbType: "mysql", txnId: "txn-mysql" }, { label: "PostgreSQL", configId: "pg-1", database: "postgres", dbType: "postgres", txnId: "txn-pg" }, + { label: "GaussDB", configId: "gaussdb-1", database: "postgres", dbType: "gaussdb", txnId: "txn-gaussdb" }, + { label: "openGauss", configId: "opengauss-1", database: "postgres", dbType: "opengauss", txnId: "txn-opengauss" }, ])("queryStore $label proven-read-only sticky state", ({ configId, database, dbType, txnId }) => { beforeEach(() => { vi.clearAllMocks(); diff --git a/crates/dbx-core/src/query/mod.rs b/crates/dbx-core/src/query/mod.rs index d73b129953..ed34eb1430 100644 --- a/crates/dbx-core/src/query/mod.rs +++ b/crates/dbx-core/src/query/mod.rs @@ -6063,11 +6063,12 @@ fn mysql_error_is_syntax_error(error: &mysql_async::Error) -> bool { } /// Compute per-execution-statement proven-read-only markers for the sticky -/// manual-transaction UX (#7122 Oracle, #9018 MySQL/PostgreSQL). The user-facing -/// classification SQL is split with the same dialect-aware splitter as the -/// execution SQL and paired by count/position; any mismatch is fail-closed (no -/// markers). Oracle/OceanBase-Oracle keep the lexical classifier, MySQL and -/// PostgreSQL use the strict `sql_risk` proof; every other dialect is unproven. +/// manual-transaction UX (#7122 Oracle, #9018 MySQL/PostgreSQL, ... GaussDB). +/// The user-facing classification SQL is split with the same dialect-aware +/// splitter as the execution SQL and paired by count/position; any mismatch is +/// fail-closed (no markers). Oracle/OceanBase-Oracle keep the lexical +/// classifier, the PostgreSQL family (MySQL is separate) use the strict +/// `sql_risk` proof; every other dialect is unproven. fn classify_manual_transaction_statements( database_type: Option, execution_statement_count: usize, @@ -6076,7 +6077,12 @@ fn classify_manual_transaction_statements( let Some(database_type) = database_type.filter(|database_type| { matches!( database_type, - DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::Mysql | DatabaseType::Postgres + DatabaseType::Oracle + | DatabaseType::OceanbaseOracle + | DatabaseType::Mysql + | DatabaseType::Postgres + | DatabaseType::Gaussdb + | DatabaseType::OpenGauss ) }) else { return Vec::new(); @@ -6092,7 +6098,7 @@ fn classify_manual_transaction_statements( user_statements .iter() .map(|statement| match database_type { - DatabaseType::Mysql | DatabaseType::Postgres => { + DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::OpenGauss => { crate::sql_risk::prove_read_only_for_database(statement, database_type) == crate::sql_risk::ReadProof::ProvenReadOnly } @@ -6447,7 +6453,14 @@ pub async fn execute_in_manual_transaction_with_options( ); if matches!( db_type, - Some(DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::Mysql | DatabaseType::Postgres) + Some( + DatabaseType::Oracle + | DatabaseType::OceanbaseOracle + | DatabaseType::Mysql + | DatabaseType::Postgres + | DatabaseType::Gaussdb + | DatabaseType::OpenGauss + ) ) { result = result.with_manual_transaction_no_statement(); } @@ -12095,6 +12108,15 @@ for line in sys.stdin: classify_manual_transaction_statements(Some(DatabaseType::Postgres), 1, Some("SELECT * FROM users")), vec![true] ); + // GaussDB/openGauss share the PostgreSQL-family proof. + assert_eq!( + classify_manual_transaction_statements(Some(DatabaseType::Gaussdb), 1, Some("SELECT * FROM users")), + vec![true] + ); + assert_eq!( + classify_manual_transaction_statements(Some(DatabaseType::OpenGauss), 1, Some("UPDATE users SET x = 1")), + vec![false] + ); // Mixed script: every statement is classified individually. assert_eq!( classify_manual_transaction_statements(Some(DatabaseType::Mysql), 2, Some("SELECT 1; DELETE FROM t")), diff --git a/crates/dbx-sql-core/src/sql_risk.rs b/crates/dbx-sql-core/src/sql_risk.rs index 8347d77ef7..34718fa6ee 100644 --- a/crates/dbx-sql-core/src/sql_risk.rs +++ b/crates/dbx-sql-core/src/sql_risk.rs @@ -1133,14 +1133,17 @@ const POSTGRES_PROOF_SAFE_FUNCTIONS: &[&str] = &[ ]; /// Prove that a single SQL statement is, by DBX's strict heuristic, an -/// ordinary read. Only MySQL/PostgreSQL take part in this proof; every other -/// database type is Unproven so its manual-transaction toolbar keeps the -/// legacy behavior. Oracle keeps its own lexical classifier -/// (`is_oracle_proven_read_only_statement`) — do not reroute it here. +/// ordinary read. Only MySQL and the PostgreSQL family (PostgreSQL, GaussDB, +/// openGauss) take part in this proof; every other database type is Unproven so +/// its manual-transaction toolbar keeps the legacy behavior. Oracle keeps its +/// own lexical classifier (`is_oracle_proven_read_only_statement`) — do not +/// reroute it here. pub fn prove_read_only_for_database(sql: &str, database_type: DatabaseType) -> ReadProof { match database_type { DatabaseType::Mysql => prove_read_only_statement(sql, "mysql", MYSQL_PROOF_SAFE_FUNCTIONS), - DatabaseType::Postgres => prove_read_only_statement(sql, "postgres", POSTGRES_PROOF_SAFE_FUNCTIONS), + DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::OpenGauss => { + prove_read_only_statement(sql, "postgres", POSTGRES_PROOF_SAFE_FUNCTIONS) + } _ => ReadProof::Unproven, } } @@ -1374,6 +1377,24 @@ mod tests { ); } + #[test] + fn proof_maps_gaussdb_family_to_postgres_proof() { + // GaussDB/openGauss are PostgreSQL-family engines: they share the PG + // proof so their sticky manual-transaction toolbar behaves like PG. + for sql in ["SELECT 1", "SELECT * FROM users", "EXPLAIN SELECT * FROM t"] { + assert_eq!( + prove_read_only_for_database(sql, DatabaseType::Gaussdb), + ReadProof::ProvenReadOnly, + "expected proven for Gaussdb: {sql}" + ); + } + assert_eq!( + prove_read_only_for_database("UPDATE users SET x = 1", DatabaseType::OpenGauss), + ReadProof::Unproven + ); + assert_eq!(prove_read_only_for_database("SELECT nextval('seq')", DatabaseType::Gaussdb), ReadProof::Unproven); + } + #[test] fn proof_only_applies_to_enabled_dialects() { // Oracle keeps its own lexical classifier; the gate in query.rs must