From 11d9df0169a9b01c12ac6b57a599d78da467a4a7 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 11 Aug 2026 10:59:54 +0000 Subject: [PATCH] fix(gbase8s): omit owner from data grid writes Closes #5911 --- .../src/composables/useDataGridEditor.ts | 9 +- apps/desktop/src/lib/backend/http.ts | 4 +- apps/desktop/src/lib/backend/tauri.ts | 4 +- crates/dbx-core/src/data_grid_sql.rs | 93 +++++++++++++++++-- crates/dbx-core/src/sql_dialect.rs | 2 +- .../dbx-core/src/sql_dialect/table_select.rs | 20 ++-- crates/dbx-web/src/routes/query.rs | 4 +- packages/app-tests/dataGridEditor.test.ts | 32 ++++++- src-tauri/src/commands/query.rs | 3 +- 9 files changed, 144 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index b2c337476f..a7ad84e384 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -1388,6 +1388,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { }; } + function saveDriverProfile() { + const id = connectionId.value; + return id ? connectionStore.getConfig(id)?.driver_profile : undefined; + } + function tableHistoryTarget() { if (!tableMeta.value) return ""; return [tableMeta.value.schema, tableMeta.value.tableName].filter(Boolean).join("."); @@ -1514,7 +1519,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { let preparedSave: Awaited> | undefined; if (stmtOptions) { try { - preparedSave = await api.prepareDataGridSave(stmtOptions); + preparedSave = await api.prepareDataGridSave(stmtOptions, saveDriverProfile()); } catch (e: any) { saveError.value = normalizeDataGridSaveError(databaseType.value, e); await finishInterruptedSaveChanges(snapshot); @@ -1720,7 +1725,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { } const stmtOptions = saveStatementOptions(); if (!stmtOptions) return []; - const prepared = await api.prepareDataGridSave(stmtOptions); + const prepared = await api.prepareDataGridSave(stmtOptions, saveDriverProfile()); if (prepared?.validationError) { saveError.value = prepared.validationError; return []; diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index e87a028716..b3f7cdb6a5 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -1251,8 +1251,8 @@ export async function analyzeEditableQueryEditability(sql: string): Promise { - return post("/api/query/prepare-data-grid-save", { options }); +export async function prepareDataGridSave(options: DataGridSaveStatementOptions, driverProfile?: string): Promise { + return post("/api/query/prepare-data-grid-save", { options, driverProfile }); } export async function extractDataGridSelection(request: DataGridExtractRequest): Promise { diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index b3993e6b88..8a7f65e1ea 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -1462,8 +1462,8 @@ export interface DataGridSavePreparation { executionSchema?: string; } -export async function prepareDataGridSave(options: DataGridSaveStatementOptions): Promise { - return invoke("prepare_data_grid_save", { options }); +export async function prepareDataGridSave(options: DataGridSaveStatementOptions, driverProfile?: string): Promise { + return invoke("prepare_data_grid_save", { options, driverProfile }); } export async function extractDataGridSelection(request: DataGridExtractRequest): Promise { diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 25e50dfea2..da3c1b8d39 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -281,21 +281,32 @@ pub struct DataGridSavePreparation { } pub fn prepare_data_grid_save(options: DataGridSaveStatementOptions) -> DataGridSavePreparation { + prepare_data_grid_save_for_driver_profile(options, None) +} + +pub fn prepare_data_grid_save_for_driver_profile( + options: DataGridSaveStatementOptions, + driver_profile: Option<&str>, +) -> DataGridSavePreparation { let validation_error = validate_data_grid_save(&options); if validation_error.is_some() { return DataGridSavePreparation { validation_error, statements: Vec::new(), rollback_statements: Vec::new(), - execution_schema: data_grid_save_execution_schema(options.database_type, &options.table_meta), + execution_schema: data_grid_save_execution_schema( + options.database_type, + driver_profile, + &options.table_meta, + ), }; } DataGridSavePreparation { validation_error: None, - statements: build_data_grid_save_statements(&options), - rollback_statements: build_data_grid_rollback_statements(&options), - execution_schema: data_grid_save_execution_schema(options.database_type, &options.table_meta), + statements: build_data_grid_save_statements(&options, driver_profile), + rollback_statements: build_data_grid_rollback_statements(&options, driver_profile), + execution_schema: data_grid_save_execution_schema(options.database_type, driver_profile, &options.table_meta), } } @@ -1082,7 +1093,10 @@ fn validate_inserted_primary_keys(options: &DataGridSaveStatementOptions) -> Opt None } -fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Vec { +fn build_data_grid_save_statements( + options: &DataGridSaveStatementOptions, + driver_profile: Option<&str>, +) -> Vec { if options.database_type == Some(DatabaseType::Neo4j) { return build_neo4j_data_grid_save_statements(options); } @@ -1092,10 +1106,15 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve let save_columns = effective_columns(options); let column_info = options.table_meta.columns.as_deref().unwrap_or(&[]); + let schema = crate::sql_dialect::table_data_schema( + options.database_type, + driver_profile, + options.table_meta.schema.as_deref(), + ); let table = data_grid_qualified_table_name( options.database_type, options.table_meta.catalog.as_deref(), - options.table_meta.schema.as_deref(), + schema, options.table_meta.database.as_deref(), &options.table_meta.table_name, options.identifier_quote.as_deref(), @@ -1244,7 +1263,10 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve statements } -fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -> Vec { +fn build_data_grid_rollback_statements( + options: &DataGridSaveStatementOptions, + driver_profile: Option<&str>, +) -> Vec { if options.database_type == Some(DatabaseType::Neo4j) { return build_neo4j_data_grid_rollback_statements(options); } @@ -1257,10 +1279,15 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) - let save_columns = effective_columns(options); let column_info = options.table_meta.columns.as_deref().unwrap_or(&[]); + let schema = crate::sql_dialect::table_data_schema( + options.database_type, + driver_profile, + options.table_meta.schema.as_deref(), + ); let table = data_grid_qualified_table_name( options.database_type, options.table_meta.catalog.as_deref(), - options.table_meta.schema.as_deref(), + schema, options.table_meta.database.as_deref(), &options.table_meta.table_name, options.identifier_quote.as_deref(), @@ -1706,12 +1733,14 @@ fn copy_column_info( fn data_grid_save_execution_schema( database_type: Option, + driver_profile: Option<&str>, table_meta: &DataGridTableMeta, ) -> Option { if matches!(database_type, Some(DatabaseType::Neo4j | DatabaseType::Oracle)) { return None; } - table_meta.schema.clone() + crate::sql_dialect::table_data_schema(database_type, driver_profile, table_meta.schema.as_deref()) + .map(str::to_string) } pub fn normalize_data_grid_save_error(database_type: Option, error: &str) -> String { @@ -4914,7 +4943,51 @@ mod tests { } #[test] - fn gbase8s_save_keeps_unquoted_owner_when_driver_reports_no_identifier_quote() { + fn gbase8s_save_omits_owner_for_insert_update_delete_and_rollback() { + let result = prepare_data_grid_save_for_driver_profile( + DataGridSaveStatementOptions { + database_type: Some(DatabaseType::Informix), + identifier_quote: Some(String::new()), + table_meta: DataGridTableMeta { + catalog: None, + database: Some("webcenter".to_string()), + schema: Some("gbasedbt".to_string()), + table_name: "user_device".to_string(), + primary_keys: vec!["id".to_string()], + columns: Some(vec![column("id", "integer", false, None), column("dev_id", "varchar", false, None)]), + }, + columns: vec!["id".to_string(), "dev_id".to_string()], + source_columns: None, + rows: vec![vec![json!(1), json!("1")], vec![json!(2), json!("deleted")]], + dirty_rows: vec![(0, vec![(1, json!("2"))])], + deleted_rows: vec![1], + new_rows: vec![vec![json!(3), json!("new")]], + }, + Some("GBASE8S"), + ); + + assert_eq!(result.validation_error, None); + assert_eq!(result.execution_schema, None); + assert_eq!( + result.statements, + vec![ + "UPDATE user_device SET dev_id = '2' WHERE id = 1;", + "DELETE FROM user_device WHERE id = 2;", + "INSERT INTO user_device (id, dev_id) VALUES (3, 'new');", + ] + ); + assert_eq!( + result.rollback_statements, + vec![ + "DELETE FROM user_device WHERE id = 3 AND dev_id = 'new';", + "INSERT INTO user_device (id, dev_id) VALUES (2, 'deleted');", + "UPDATE user_device SET dev_id = '1' WHERE id = 1 AND dev_id = '2';", + ] + ); + } + + #[test] + fn standard_informix_save_preserves_owner_qualification_without_gbase8s_profile() { let result = prepare_data_grid_save(DataGridSaveStatementOptions { database_type: Some(DatabaseType::Informix), identifier_quote: Some(String::new()), diff --git a/crates/dbx-core/src/sql_dialect.rs b/crates/dbx-core/src/sql_dialect.rs index d80c1202db..7d2e5eee5a 100644 --- a/crates/dbx-core/src/sql_dialect.rs +++ b/crates/dbx-core/src/sql_dialect.rs @@ -40,7 +40,7 @@ pub use identifiers::{ pub(crate) use identifiers::{parse_sqlserver_linked_schema_ref, qualified_transfer_table, quote_transfer_identifier}; pub use table_select::{build_count_table_sql, build_table_data_select_sql, build_table_select_sql}; pub(crate) use table_select::{ - quote_table_data_identifier, table_data_qualified_table_name, uses_connection_identifier_quote, + quote_table_data_identifier, table_data_qualified_table_name, table_data_schema, uses_connection_identifier_quote, }; pub use type_rewrite::{ apply_auto_inc_to_column_def, column_is_auto_increment, rewrite_column_type, split_type_base_params, diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index 8aa5d5f591..5330491dc4 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -19,15 +19,23 @@ pub fn build_count_table_sql(database_type: Option, schema: Option format!("SELECT COUNT(*) AS row_count FROM {}", qualified_table_name(database_type, schema, table_name)) } -pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String { - let database_type = options.database_type; - let schema = if database_type == Some(DatabaseType::Informix) - && options.driver_profile.as_deref().is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")) +pub(crate) fn table_data_schema<'a>( + database_type: Option, + driver_profile: Option<&str>, + schema: Option<&'a str>, +) -> Option<&'a str> { + if database_type == Some(DatabaseType::Informix) + && driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")) { None } else { - options.schema.as_deref() - }; + schema + } +} + +pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String { + let database_type = options.database_type; + let schema = table_data_schema(database_type, options.driver_profile.as_deref(), options.schema.as_deref()); let limit = options.limit.unwrap_or(100); if database_type == Some(DatabaseType::Neo4j) { return build_neo4j_table_select_sql(&options, limit); diff --git a/crates/dbx-web/src/routes/query.rs b/crates/dbx-web/src/routes/query.rs index 50a97e4f8d..5381ac65d6 100644 --- a/crates/dbx-web/src/routes/query.rs +++ b/crates/dbx-web/src/routes/query.rs @@ -252,6 +252,8 @@ pub struct BuildSingleColumnAlterSqlRequest { #[serde(rename_all = "camelCase")] pub struct PrepareDataGridSaveRequest { pub options: dbx_core::data_grid_sql::DataGridSaveStatementOptions, + #[serde(default)] + pub driver_profile: Option, } #[derive(Deserialize, utoipa::ToSchema)] @@ -802,7 +804,7 @@ pub async fn analyze_editable_query_editability( pub async fn prepare_data_grid_save( Json(req): Json, ) -> Json { - Json(dbx_core::data_grid_sql::prepare_data_grid_save(req.options)) + Json(dbx_core::data_grid_sql::prepare_data_grid_save_for_driver_profile(req.options, req.driver_profile.as_deref())) } #[utoipa::path( diff --git a/packages/app-tests/dataGridEditor.test.ts b/packages/app-tests/dataGridEditor.test.ts index 2a62d3b0e3..3536117947 100644 --- a/packages/app-tests/dataGridEditor.test.ts +++ b/packages/app-tests/dataGridEditor.test.ts @@ -6,6 +6,7 @@ import { DATA_GRID_MAX_BATCH_INSERT_ROWS, DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, us import type { CellValue } from "../../apps/desktop/src/lib/dataGrid/cellValue.ts"; import type { DataGridSaveStatementOptions } from "../../apps/desktop/src/lib/dataGrid/dataGridSql.ts"; import { matchesRowStatusFilter, type RowStatusFilter } from "../../apps/desktop/src/lib/dataGrid/gridRowStatus.ts"; +import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts"; import type { ColumnInfo } from "../../apps/desktop/src/types/database.ts"; function installBrowserTestGlobals() { @@ -191,7 +192,7 @@ function createQuickEntryEditor(options: { return editor; } -function createPeopleGridEditor(result = computed(() => ({ columns: ["id", "name"], rows: [[1, "Ada"] as CellValue[]] }))) { +function createPeopleGridEditor(result = computed(() => ({ columns: ["id", "name"], rows: [[1, "Ada"] as CellValue[]] })), connectionId?: string) { const rowStatusFilter = ref("all"); let editor: ReturnType; @@ -199,7 +200,7 @@ function createPeopleGridEditor(result = computed(() => ({ columns: ["id", "name result, editable: computed(() => true), databaseType: computed(() => "postgres"), - connectionId: computed(() => undefined), + connectionId: computed(() => connectionId), database: computed(() => undefined), tableMeta: computed(() => ({ tableName: "people", @@ -250,6 +251,33 @@ function createPeopleGridEditor(result = computed(() => ({ columns: ["id", "name return editor; } +test("data grid save forwards the GBase 8s driver profile", async () => { + setActivePinia(createPinia()); + installBrowserTestGlobals(); + useConnectionStore().addEphemeralConnection({ + id: "gbase8s-1", + name: "GBase 8s", + db_type: "informix", + driver_profile: "gbase8s", + host: "localhost", + port: 9088, + username: "gbasedbt", + password: "", + }); + let requestBody: Record | undefined; + globalThis.fetch = (async (input, init) => { + if (String(input) !== "/api/query/prepare-data-grid-save") return new Response("unexpected request", { status: 500 }); + requestBody = JSON.parse(String(init?.body ?? "{}")); + return Response.json({ statements: [], rollbackStatements: [] }); + }) as typeof fetch; + + const editor = createPeopleGridEditor(undefined, "gbase8s-1"); + editor.applyCellValue(0, 1, "Grace"); + await editor.previewChanges(); + + assert.equal(requestBody?.driverProfile, "gbase8s"); +}); + test("row data helper reuses unchanged rows and clones dirty rows only", () => { setActivePinia(createPinia()); installBrowserTestGlobals(); diff --git a/src-tauri/src/commands/query.rs b/src-tauri/src/commands/query.rs index ed4904f8e2..a9700c6544 100644 --- a/src-tauri/src/commands/query.rs +++ b/src-tauri/src/commands/query.rs @@ -598,8 +598,9 @@ pub fn analyze_editable_query_editability(sql: String) -> Result, ) -> Result { - Ok(dbx_core::data_grid_sql::prepare_data_grid_save(options)) + Ok(dbx_core::data_grid_sql::prepare_data_grid_save_for_driver_profile(options, driver_profile.as_deref())) } #[tauri::command]