From 3e031a1c2ad5e4e09c63fce884d622f424e56fe9 Mon Sep 17 00:00:00 2001 From: efeire <15682810+efeire@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:13:23 +0800 Subject: [PATCH 1/2] fix(oceanbase): prepare stable ROWID for single-table star queries --- .../sql/editableQueryHiddenKeys.spec.ts | 12 ++++ .../src/lib/sql/editableQueryHiddenKeys.ts | 4 +- .../queryStore.oceanbaseRowId.spec.ts | 69 ++++++++++++++++++- apps/desktop/src/stores/queryStore.ts | 7 +- 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts b/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts index 7c943e691a..4e78dbee5f 100644 --- a/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts @@ -3,6 +3,18 @@ import { buildQueryWithHiddenPrimaryKeys, hiddenResultColumnIndexes } from "@/li import { analyzeEditableQueryEditability, allPrimaryKeysPresent, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sql/sqlAnalysis"; describe("editable query hidden primary keys", () => { + it.each(["oracle", "oceanbase-oracle"] as const)("preserves source spelling and projection boundaries for %s star queries", (databaseType) => { + for (const [sql, expected] of [ + ['SELECT * FROM "Owner"."Items" "i"', 'SELECT "i".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_1" FROM "Owner"."Items" "i"'], + ['SELECT "i".* FROM "Owner"."Items" "i"', 'SELECT "i".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_1" FROM "Owner"."Items" "i"'], + ['SELECT /*+ FULL(t) */ * -- columns\nFROM APP.ITEMS t', 'SELECT /*+ FULL(t) */ t.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_1" -- columns\nFROM APP.ITEMS t'], + ]) { + const result = buildQueryWithHiddenPrimaryKeys({ sql, databaseType, primaryKeys: ["__DBX_ROWID"], existingResultNames: ["rowid", "__DBX_PK_0"], sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" } }); + expect(result?.sql).toBe(expected); + expect(result?.projections).toEqual([{ sourceName: "__DBX_ROWID", alias: "__DBX_PK_1" }]); + } + }); + it("appends quoted MySQL primary keys without changing the visible projection", () => { expect( buildQueryWithHiddenPrimaryKeys({ diff --git a/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts b/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts index 60e5815e13..b4a40e3708 100644 --- a/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts +++ b/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts @@ -61,7 +61,9 @@ function appendSelectProjections(sql: string, expressions: string[], databaseTyp const projectionTokens = tokens.slice(selectIndex + 1, fromIndex).filter((token) => token.kind !== "comment"); const bareWildcard = projectionTokens.length === 1 && projectionTokens[0]?.text === "*" ? projectionTokens[0] : undefined; - const sourceReference = bareWildcard && databaseType === "oracle" ? singleOracleSourceReference(sql, bareWildcard.span.start) : undefined; + const oracleWildcard = bareWildcard && (databaseType === "oracle" || databaseType === "oceanbase-oracle"); + const sourceReference = oracleWildcard ? singleOracleSourceReference(sql, bareWildcard.span.start) : undefined; + if (oracleWildcard && !sourceReference) return undefined; if (!sourceReference || !bareWildcard) { return `${sql.slice(0, projectionEnd)}, ${expressions.join(", ")}${sql.slice(projectionEnd)}`; } diff --git a/apps/desktop/src/stores/__tests__/queryStore.oceanbaseRowId.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.oceanbaseRowId.spec.ts index 5f499581e4..53ff72d19f 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.oceanbaseRowId.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.oceanbaseRowId.spec.ts @@ -80,10 +80,74 @@ describe("OceanBase Oracle query ROWID preparation", () => { expect(sql).not.toContain('"__DBX_ROWID"'); }); - it.each(["select * from sipf.t_sipf_debug_log a", "select a.rowid, a.taskname from sipf.t_sipf_debug_log a", "select q.taskname from (select a.taskname from sipf.t_sipf_debug_log a) q"])("preserves queries not needing an extra ROWID: %s", async (sql) => { + it.each(["select a.rowid, a.taskname from sipf.t_sipf_debug_log a", "select q.taskname from (select a.taskname from sipf.t_sipf_debug_log a) q"])("preserves queries not needing an extra ROWID: %s", async (sql) => { expect(await emittedSql(sql)).toBe(sql); }); + it.each([ + ["select * from sipf.t_sipf_debug_log a", "select a.*"], + ["select a.* from sipf.t_sipf_debug_log a", "select a.*"], + ['select * from sipf.t_sipf_debug_log "Log"', 'select "Log".*'], + ['select "Log".* from sipf.t_sipf_debug_log "Log"', 'select "Log".*'], + ["select * from sipf.t_sipf_debug_log", "select t_sipf_debug_log.*"], + ])("adds a hidden identity to a safe star query: %s", async (originalSql, projection) => { + const { analyzeEditableQueryEditability } = await import("@/lib/sql/sqlAnalysis"); + mocks.analyze.mockImplementation(async (sql: string) => analyzeEditableQueryEditability(sql)); + mocks.executeMulti.mockResolvedValue([{ columns: ["TASKNAME", "__DBX_PK_0"], rows: [["duplicate", "ROW-A"], ["duplicate", "ROW-B"]], affected_rows: 0, execution_time_ms: 1 }]); + const { store, id, sql } = await executedQuery(originalSql); + expect(sql).toBe(`${projection}, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0"${originalSql.slice(originalSql.indexOf(" from "))}`); + const tab = store.tabs.find((item) => item.id === id)!; + expect(tab.result?.hidden_column_indexes).toEqual([1]); + expect(tab.result?.rows).toEqual([["duplicate", "ROW-A"], ["duplicate", "ROW-B"]]); + await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["TASKNAME", "__DBX_ROWID"])); + expect(tab.tableMeta?.primaryKeys).toEqual(["__DBX_ROWID"]); + }); + + it.each(["*", "a.*"])("keeps physical rowid and hidden-alias columns visible for %s", async (projection) => { + const { analyzeEditableQueryEditability } = await import("@/lib/sql/sqlAnalysis"); + mocks.analyze.mockImplementation(async (sql: string) => analyzeEditableQueryEditability(sql)); + mocks.getColumns.mockResolvedValue([ + { name: "rowid", data_type: "VARCHAR2(100)", is_nullable: true, is_primary_key: false }, + { name: "__DBX_PK_0", data_type: "VARCHAR2(100)", is_nullable: true, is_primary_key: false }, + { name: "TASKNAME", data_type: "VARCHAR2(100)", is_nullable: true, is_primary_key: false }, + ]); + mocks.executeMulti.mockResolvedValue([{ columns: ["rowid", "__DBX_PK_0", "TASKNAME", "__DBX_PK_1"], rows: [["physical", "visible", "sample", "ROW-A"]], affected_rows: 0, execution_time_ms: 1 }]); + const { store, id, sql } = await executedQuery(`select ${projection} from sipf.t_sipf_debug_log a`); + expect(sql).toContain('ROWIDTOCHAR(ROWID) AS "__DBX_PK_1"'); + const tab = store.tabs.find((item) => item.id === id)!; + expect(tab.result?.hidden_column_indexes).toEqual([3]); + await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["rowid", "__DBX_PK_0", "TASKNAME", "__DBX_ROWID"])); + }); + + it.each(["*", "a.*"])("does not append ROWID when a star returns the physical primary key: %s", async (projection) => { + mocks.getColumns.mockResolvedValue([ + { name: "ID", data_type: "NUMBER", is_nullable: false, is_primary_key: true }, + { name: "TASKNAME", data_type: "VARCHAR2(100)", is_nullable: true, is_primary_key: false }, + ]); + const sql = `select ${projection} from sipf.t_sipf_debug_log a`; + expect(await emittedSql(sql)).toBe(sql); + }); + + it.each(["view", "unknown"])("keeps star queries unchanged for a %s source", async (type) => { + mocks.lookupLocalCompletionTables.mockReturnValue(type === "unknown" ? [] : [{ name: "T_SIPF_DEBUG_LOG", type, schema: "SIPF" }]); + const sql = "select * from sipf.t_sipf_debug_log a"; + expect(await emittedSql(sql)).toBe(sql); + expect(mocks.listTables).not.toHaveBeenCalled(); + }); + + it("uses a synonym's confirmed base-table identity without changing its SQL source", async () => { + const { analyzeEditableQueryEditability } = await import("@/lib/sql/sqlAnalysis"); + mocks.analyze.mockImplementation(async (sql: string) => analyzeEditableQueryEditability(sql)); + mocks.lookupLocalCompletionTables.mockReturnValue([]); + mocks.getColumns.mockResolvedValue([{ name: "TASKNAME", data_type: "VARCHAR2(100)", is_nullable: true, is_primary_key: false, resolved_schema: "TargetOwner", resolved_table: "ActualLog", resolved_object_type: "TABLE" }]); + mocks.executeMulti.mockResolvedValue([{ columns: ["TASKNAME", "__DBX_PK_0"], rows: [["sample", "ROW-A"]], affected_rows: 0, execution_time_ms: 1 }]); + const { store, id, sql } = await executedQuery('select * from "AliasOwner"."LogAlias" "l"'); + expect(sql).toBe('select "l".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" from "AliasOwner"."LogAlias" "l"'); + const tab = store.tabs.find((item) => item.id === id)!; + await vi.waitFor(() => expect(tab.tableMeta).toMatchObject({ schema: "TargetOwner", tableName: "ActualLog", tableType: "TABLE", primaryKeys: ["__DBX_ROWID"] })); + expect(tab.result?.hidden_column_indexes).toEqual([1]); + }); + it.each(["view", "unknown"])("does not inject ROWID for a %s source", async (type) => { mocks.lookupLocalCompletionTables.mockReturnValue(type === "unknown" ? [] : [{ name: "T_SIPF_DEBUG_LOG", type, schema: "SIPF" }]); const sql = "select a.taskname from sipf.t_sipf_debug_log a"; @@ -166,6 +230,9 @@ describe("OceanBase Oracle query ROWID preparation", () => { }); it.each([ + "select distinct a.* from sipf.t_sipf_debug_log a", + "select a.*, count(*) from sipf.t_sipf_debug_log a group by a.taskname", + "select a.*, b.* from sipf.t_sipf_debug_log a join sipf.t_sipf_debug_log b on a.taskname = b.taskname", "select distinct a.taskname from sipf.t_sipf_debug_log a", "select a.taskname, count(*) from sipf.t_sipf_debug_log a group by a.taskname", "select a.taskname, b.taskname from sipf.t_sipf_debug_log a join sipf.t_sipf_debug_log b on a.taskname = b.taskname", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index b898b5dfb2..6c4f2d01a2 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -940,7 +940,8 @@ function expandStarProjectionColumnsForSource(analysis: EditableQueryInfo, sourc if (column.sourceKey && column.sourceKey !== source.key) return [column]; return tableColumns.map((tableColumn) => ({ sourceName: tableColumn.name, - sourceNameQuoted: false, + // These names come from metadata, including physical quoted "rowid". + sourceNameQuoted: true, ...(column.sourceQualifier ? { sourceQualifier: column.sourceQualifier } : {}), sourceKey: source.key, resultName: tableColumn.name, @@ -6435,10 +6436,10 @@ export const useQueryStore = defineStore("query", () => { const wholeSourceProjected = projectsAllColumnsForSource(analysis, source.key); const hasDirectSourceProjection = analysis.columns.some((column) => Boolean(column.sourceName) && (!column.sourceKey || column.sourceKey === source.key)); if (!wholeSourceProjected && !hasDirectSourceProjection) return unchanged; - // Whole-source projections already include declared primary keys. Oracle + // Whole-source projections already include declared primary keys. Oracle, OB // and Xugu may need a synthetic key, while DB2 needs column metadata to // decide whether LOB materialization can be deferred safely. - if (databaseType !== "oracle" && databaseType !== "xugu" && databaseType !== "db2" && wholeSourceProjected) return unchanged; + if (databaseType !== "oracle" && databaseType !== "oceanbase-oracle" && databaseType !== "xugu" && databaseType !== "db2" && wholeSourceProjected) return unchanged; const target = resolveEditableSourceMetadataTarget(tab, analysis, source, conn, databaseType, executionDatabase); const cached = getCachedTableMetadata(target.request); From c5cf366e8717c1fce76930e419893b3a6fa8b306 Mon Sep 17 00:00:00 2001 From: efeire <15682810+efeire@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:37:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(web):=20=E4=B8=BA=E6=BC=94=E7=A4=BA?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=BF=81=E7=A7=BB=E6=B5=8B=E8=AF=95=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E8=BF=9E=E6=8E=A5=E5=A4=B9=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/dbx-web/src/routes/transfer.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/dbx-web/src/routes/transfer.rs b/crates/dbx-web/src/routes/transfer.rs index c633e0c31b..40a042433d 100644 --- a/crates/dbx-web/src/routes/transfer.rs +++ b/crates/dbx-web/src/routes/transfer.rs @@ -931,6 +931,13 @@ mod tests { async fn demo_mode_transfer_does_not_write_task_history() { let (mut state, dir) = test_web_state().await; Arc::get_mut(&mut state).unwrap().demo_mode = true; + let src = sqlite_config("src", &dir.join("src.db").to_string_lossy()); + let dst = sqlite_config("dst", &dir.join("dst.db").to_string_lossy()); + std::fs::write(dir.join("src.db"), b"").unwrap(); + std::fs::write(dir.join("dst.db"), b"").unwrap(); + std::fs::write(dir.join("main.db"), b"").unwrap(); + state.app.configs.write().await.insert("src".to_string(), src); + state.app.configs.write().await.insert("dst".to_string(), dst); let req = transfer_request("src", "dst", &dir); let transfer_id = req.transfer_id.clone(); let _ = start_transfer(State(state.clone()), Json(StartTransferRequest { request: req })).await.unwrap();