mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 07:08:16 +00:00
fix(oceanbase): prepare stable ROWID for single-table star queries
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user