fix(oceanbase): prepare stable ROWID for single-table star queries

This commit is contained in:
efeire
2026-10-09 17:13:23 +08:00
parent e704ce6215
commit 65c4d16dca
4 changed files with 87 additions and 5 deletions
@@ -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({
@@ -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)}`;
}
@@ -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",
+4 -3
View File
@@ -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,
@@ -6432,10 +6433,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);