From 358a736c6e1f9baa90fbe87dfc3f6dae1d6efca8 Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 14 Jul 2026 17:12:32 +0800 Subject: [PATCH] fix(oracle): qualify wildcard before appending ROWID --- .../sql/editableQueryHiddenKeys.spec.ts | 46 ++++++++++++++++++- .../src/lib/sql/editableQueryHiddenKeys.ts | 28 +++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts b/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts index e3358f212..38c2177bf 100644 --- a/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/editableQueryHiddenKeys.spec.ts @@ -59,11 +59,55 @@ describe("editable query hidden primary keys", () => { sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" }, }), ).toEqual({ - sql: 'SELECT *, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.USERS t WHERE t.ACTIVE = 1', + sql: 'SELECT t.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.USERS t WHERE t.ACTIVE = 1', projections: [{ sourceName: "__DBX_ROWID", alias: "__DBX_PK_0" }], }); }); + it("qualifies a bare Oracle wildcard when appending a hidden key", () => { + expect( + buildQueryWithHiddenPrimaryKeys({ + sql: 'SELECT /*+ FULL("Users") */ * FROM APP."Users"', + databaseType: "oracle", + primaryKeys: ["__DBX_ROWID"], + existingResultNames: ["ID", "NAME"], + sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" }, + })?.sql, + ).toBe('SELECT /*+ FULL("Users") */ "Users".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP."Users"'); + + expect( + buildQueryWithHiddenPrimaryKeys({ + sql: 'SELECT * FROM APP.USERS "u"', + databaseType: "oracle", + primaryKeys: ["__DBX_ROWID"], + existingResultNames: ["ID", "NAME"], + sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" }, + })?.sql, + ).toBe('SELECT "u".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.USERS "u"'); + }); + + it("rewrites the reported Oracle queries without changing their filters", () => { + expect( + buildQueryWithHiddenPrimaryKeys({ + sql: "select * from t_zyys_vte_yyfxbd", + databaseType: "oracle", + primaryKeys: ["__DBX_ROWID"], + existingResultNames: ["ID", "MBMC"], + sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" }, + })?.sql, + ).toBe('select t_zyys_vte_yyfxbd.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" from t_zyys_vte_yyfxbd'); + + expect( + buildQueryWithHiddenPrimaryKeys({ + sql: "SELECT * from T_XT_MB WHERE mbmc ='结束时'", + databaseType: "oracle", + primaryKeys: ["__DBX_ROWID"], + existingResultNames: ["ID", "MBMC"], + sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" }, + })?.sql, + ).toBe(`SELECT T_XT_MB.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" from T_XT_MB WHERE mbmc ='结束时'`); + }); + it("preserves a WHERE subquery when adding an Oracle ROWID", () => { expect( buildQueryWithHiddenPrimaryKeys({ diff --git a/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts b/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts index f47db692d..dbcaa44b8 100644 --- a/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts +++ b/apps/desktop/src/lib/sql/editableQueryHiddenKeys.ts @@ -1,4 +1,5 @@ import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect"; +import { buildSqlSemanticModel } from "@/lib/sql/semantic/model"; import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens"; import type { DatabaseType } from "@/types/database"; @@ -30,7 +31,7 @@ export function buildQueryWithHiddenPrimaryKeys(options: { sql: string; database return { sourceName, alias }; }); const expressions = projections.map(({ sourceName, alias }) => `${options.sourceExpressions?.[sourceName] ?? dialect.quoteIdentifier(sourceName)} AS ${dialect.quoteIdentifier(alias)}`); - const sql = appendSelectProjections(options.sql, expressions); + const sql = appendSelectProjections(options.sql, expressions, options.databaseType); return sql ? { sql, projections } : undefined; } @@ -41,7 +42,7 @@ export function hiddenResultColumnIndexes(columns: string[], projections: Hidden }); } -function appendSelectProjections(sql: string, expressions: string[]): string | undefined { +function appendSelectProjections(sql: string, expressions: string[], databaseType: DatabaseType): string | undefined { if (expressions.length === 0) return sql; const tokens = tokenizeSqlSemantic(sql); const selectIndex = tokens.findIndex((token) => token.kind === "word" && token.depth === 0 && token.normalized === "select"); @@ -57,5 +58,26 @@ function appendSelectProjections(sql: string, expressions: string[]): string | u break; } if (projectionEnd === undefined) return undefined; - return `${sql.slice(0, projectionEnd)}, ${expressions.join(", ")}${sql.slice(projectionEnd)}`; + + 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; + if (!sourceReference || !bareWildcard) { + return `${sql.slice(0, projectionEnd)}, ${expressions.join(", ")}${sql.slice(projectionEnd)}`; + } + + const qualifiedWildcard = `${sourceReference}.*`; + const qualifiedSql = `${sql.slice(0, bareWildcard.span.start)}${qualifiedWildcard}${sql.slice(bareWildcard.span.end)}`; + const qualifiedProjectionEnd = projectionEnd + qualifiedWildcard.length - bareWildcard.text.length; + return `${qualifiedSql.slice(0, qualifiedProjectionEnd)}, ${expressions.join(", ")}${qualifiedSql.slice(qualifiedProjectionEnd)}`; +} + +function singleOracleSourceReference(sql: string, cursor: number): string | undefined { + const model = buildSqlSemanticModel(sql, cursor, { databaseType: "oracle" }); + const sources = model.rowSources.filter((source) => source.kind === "table"); + if (sources.length !== 1) return undefined; + + const source = sources[0]!; + if (source.aliasSpan) return sql.slice(source.aliasSpan.start, source.aliasSpan.end); + return source.qualifiedName?.parts[source.qualifiedName.parts.length - 1]?.raw; }