fix(oracle): qualify wildcard before appending ROWID

This commit is contained in:
zipg 2026-07-14 17:12:32 +08:00 committed by GitHub
parent da73867604
commit 358a736c6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 4 deletions

View File

@ -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({

View File

@ -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;
}