fix(oracle): avoid object discovery before queries

Closes #5907
This commit is contained in:
t8y2 committed 2026-08-11 12:53:05 +00:00
1 parent e04dad4622
commit 79eb0a21e7
2 files changed
+152 -10

No files matched your search

@@ -1,5 +1,5 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const executeMulti = vi.fn();
const executeQuery = vi.fn();
@@ -8,6 +8,7 @@ const getColumns = vi.fn();
const listIndexes = vi.fn();
const listObjects = vi.fn();
const getConnectionConfig = vi.fn();
const lookupLocalCompletionTables = vi.fn();
const buildSortedQuerySql = vi.fn();
const buildDataGridCountSql = vi.fn();
const prepareQueryPaginationExecutionPlan = vi.fn(async (options) => ({
@@ -50,6 +51,7 @@ vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
ensureConnected: vi.fn().mockResolvedValue(undefined),
getConfig: getConnectionConfig,
lookupLocalCompletionTables,
recordConnectionLostError: vi.fn(),
}),
}));
@@ -86,6 +88,7 @@ describe("queryStore hidden primary key editing", () => {
]);
listIndexes.mockResolvedValue([]);
listObjects.mockResolvedValue([]);
lookupLocalCompletionTables.mockReturnValue([]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => queryAnalysis(sql));
buildSortedQuerySql.mockImplementation(async (options) => ({ ok: true, sql: `${options.originalSql} ORDER BY ${options.column} ${options.direction.toUpperCase()}` }));
buildDataGridCountSql.mockResolvedValue("SELECT COUNT(*) FROM `users`");
@@ -115,6 +118,10 @@ describe("queryStore hidden primary key editing", () => {
]);
});
afterEach(() => {
expect(listObjects).not.toHaveBeenCalled();
});
it("executes and hides an omitted primary key while retaining its source mapping", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
@@ -329,7 +336,10 @@ describe("queryStore hidden primary key editing", () => {
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
{ name: "PLATFORM", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
listObjects.mockResolvedValue([{ name: "TT_PLATFORM_CARS", object_type: "TABLE", schema: "SH_SMCVDMS_OVERSEAS_DRSSITB" }]);
lookupLocalCompletionTables.mockReturnValue([
{ name: "TT_PLATFORM_CARS", type: "view", schema: "REPORTING" },
{ name: "TT_PLATFORM_CARS", type: "table", schema: "SH_SMCVDMS_OVERSEAS_DRSSITB" },
]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => {
const hidden = sql.includes("__DBX_PK_0");
return {
@@ -364,6 +374,7 @@ describe("queryStore hidden primary key editing", () => {
await store.executeTabSql(tabId, "SELECT t.* FROM SH_SMCVDMS_OVERSEAS_DRSSITB.TT_PLATFORM_CARS t WHERE t.PLATFORM = '轻卡'");
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT t.*, ROWIDTOCHAR(ROWID) AS \"__DBX_PK_0\" FROM SH_SMCVDMS_OVERSEAS_DRSSITB.TT_PLATFORM_CARS t WHERE t.PLATFORM = '轻卡'", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
expect(lookupLocalCompletionTables).toHaveBeenCalledWith("oracle-1", "ORCL", "TT_PLATFORM_CARS", 20, "SH_SMCVDMS_OVERSEAS_DRSSITB", undefined);
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(tab.result?.hidden_column_indexes).toEqual([2]);
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["ID", "PLATFORM", "__DBX_ROWID"]));
@@ -373,6 +384,53 @@ describe("queryStore hidden primary key editing", () => {
expect(tab.queryEditabilityReason).toBeUndefined();
});
it("uses the configured current schema to keep an unqualified Oracle base-table query editable", async () => {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", default_schema: "APP", query_timeout_secs: 30 });
getColumns.mockResolvedValue([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
{ name: "NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
lookupLocalCompletionTables.mockReturnValue([
{ name: "CUSTOMERS", type: "view", schema: "REPORTING" },
{ name: "CUSTOMERS", type: "table", schema: "APP" },
]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => {
const hidden = sql.includes("__DBX_PK_0");
return {
editable: true,
analysis: {
schema: undefined,
tableName: "CUSTOMERS",
selectStar: !hidden,
columns: hidden
? [
{ star: true, sourceKey: "CUSTOMERS:0", resultName: "*", expression: "*" },
{ resultName: "__DBX_PK_0", expression: "ROWIDTOCHAR(ROWID)" },
]
: [],
},
};
});
executeMulti.mockResolvedValue([
{
columns: ["ID", "NAME", "__DBX_PK_0"],
rows: [[1, "Alice", "AAAPr9AAEAAAACXAAA"]],
affected_rows: 0,
execution_time_ms: 1,
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.executeTabSql(tabId, "SELECT * FROM CUSTOMERS");
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", 'SELECT CUSTOMERS.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM CUSTOMERS', undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
expect(lookupLocalCompletionTables).toHaveBeenCalledWith("oracle-1", "ORCL", "CUSTOMERS", 20, "APP", undefined);
expect(store.tabs.find((item) => item.id === tabId)?.result?.hidden_column_indexes).toEqual([2]);
});
it("does not check Oracle ROWID eligibility when query metadata returns no columns", async () => {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockResolvedValue([]);
@@ -407,6 +465,44 @@ describe("queryStore hidden primary key editing", () => {
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT * FROM aa", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
});
it("executes an Oracle cache-miss query without waiting for object discovery", async () => {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ncdb", query_timeout_secs: 30 });
getColumns.mockResolvedValue([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
{ name: "PRODUCT_NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
listIndexes.mockResolvedValue([]);
listObjects.mockReturnValue(new Promise(() => undefined));
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
schema: undefined,
tableName: "VITF_PRODUCT_INFOOA",
selectStar: true,
columns: [],
},
});
executeMulti.mockResolvedValue([
{
columns: ["ID", "PRODUCT_NAME"],
rows: [],
affected_rows: 0,
execution_time_ms: 473,
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ncdb", "Query");
const sql = "SELECT * FROM VITF_PRODUCT_INFOOA WHERE PK_MATERIAL IN ('1', '2')";
const execution = store.executeTabSql(tabId, sql);
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ncdb", sql, undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 })), { timeout: 1000 });
await execution;
expect(lookupLocalCompletionTables).not.toHaveBeenCalled();
});
it("starts an Oracle primary-key star query before slow column metadata finishes", async () => {
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
@@ -452,7 +548,7 @@ describe("queryStore hidden primary key editing", () => {
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
{ name: "CUSTOMER_NO", data_type: "NUMBER", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
listObjects.mockResolvedValue([{ name: "PLATFORM_CARS", object_type: "TABLE", schema: "APP" }]);
lookupLocalCompletionTables.mockReturnValue([{ name: "PLATFORM_CARS", type: "table", schema: "APP" }]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => {
const hidden = sql.includes("__DBX_PK_0");
return {
@@ -510,7 +606,7 @@ describe("queryStore hidden primary key editing", () => {
{ name: "PLATFORM", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
listIndexes.mockResolvedValue([]);
listObjects.mockResolvedValue([{ name: "PLATFORM_VIEW", object_type: "VIEW", schema: "APP" }]);
lookupLocalCompletionTables.mockReturnValue([{ name: "PLATFORM_VIEW", type: "view", schema: "APP" }]);
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
@@ -532,14 +628,52 @@ describe("queryStore hidden primary key editing", () => {
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
const tab = store.tabs.find((item) => item.id === tabId)!;
tab.tableMeta = { schema: "APP", tableName: "PLATFORM_VIEW", tableType: "VIEW", columns: [], primaryKeys: [] };
await store.executeTabSql(tabId, "SELECT * FROM APP.PLATFORM_VIEW");
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT * FROM APP.PLATFORM_VIEW", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
expect(lookupLocalCompletionTables).toHaveBeenCalledWith("oracle-1", "ORCL", "PLATFORM_VIEW", 20, "APP", undefined);
expect(tab.result?.hidden_column_indexes).toBeUndefined();
});
it.each([
["only a foreign owner is cached", [{ name: "CUSTOMERS", type: "table", schema: "APP" }]],
[
"multiple owners are cached",
[
{ name: "CUSTOMERS", type: "table", schema: "APP" },
{ name: "CUSTOMERS", type: "table", schema: "REPORTING" },
],
],
])("does not infer the current schema when %s", async (_caseName, cachedTables) => {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockResolvedValue([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
{ name: "NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
lookupLocalCompletionTables.mockReturnValue(cachedTables);
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
schema: undefined,
tableName: "CUSTOMERS",
selectStar: true,
columns: [],
},
});
executeMulti.mockResolvedValue([{ columns: ["ID", "NAME"], rows: [[1, "Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.executeTabSql(tabId, "SELECT * FROM CUSTOMERS");
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT * FROM CUSTOMERS", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
expect(lookupLocalCompletionTables).not.toHaveBeenCalled();
expect(store.tabs.find((item) => item.id === tabId)?.result?.hidden_column_indexes).toBeUndefined();
});
it("keeps hidden primary keys and editability after database sorting", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
+13 -5
View File
@@ -3317,12 +3317,20 @@ export const useQueryStore = defineStore("query", () => {
return primaryKeys.filter((primaryKey) => !selectedColumns.has(primaryKey));
}
async function oracleRowIdIsSafeForQuery(tab: QueryTab, loaded: LoadedEditableSource): Promise<boolean> {
function oracleRowIdIsSafeForQuery(tab: QueryTab, loaded: LoadedEditableSource): boolean {
const knownType = loaded.tableMeta.tableType?.trim().toUpperCase();
if (knownType) return knownType === "TABLE";
const objects = await api.listObjects(tab.connectionId!, loaded.tableMeta.database ?? tab.database, loaded.tableMeta.schema ?? "", ["TABLE", "VIEW", "MATERIALIZED_VIEW"], loaded.tableMeta.tableName, 20, 0, loaded.tableMeta.catalog);
const matching = objects.find((object) => object.name.toLowerCase() === loaded.tableMeta.tableName.toLowerCase());
return matching?.object_type.trim().toUpperCase() === "TABLE";
const connectionStore = useConnectionStore();
const normalizeIdentifier = (value: string | undefined) => value?.trim().toLowerCase() ?? "";
const targetName = normalizeIdentifier(loaded.tableMeta.tableName);
const resolvedSchema = loaded.tableMeta.schema?.trim() || tab.schema?.trim() || connectionStore.getConfig(tab.connectionId!)?.default_schema?.trim();
if (!resolvedSchema) return false;
const targetSchema = normalizeIdentifier(resolvedSchema);
const targetCatalog = loaded.tableMeta.catalog?.trim() ? normalizeIdentifier(loaded.tableMeta.catalog) : undefined;
const matches = connectionStore
.lookupLocalCompletionTables(tab.connectionId!, loaded.tableMeta.database ?? tab.database, loaded.tableMeta.tableName, 20, resolvedSchema, loaded.tableMeta.catalog)
.filter((table) => normalizeIdentifier(table.name) === targetName && normalizeIdentifier(table.schema) === targetSchema && (!targetCatalog || normalizeIdentifier(table.catalog) === targetCatalog));
return matches.length === 1 && matches[0]?.type === "table";
}
function primaryKeyIndex(indexes: IndexInfo[]): IndexInfo | undefined {
@@ -3395,7 +3403,7 @@ export const useQueryStore = defineStore("query", () => {
// Oracle base tables without declared keys use the same ROWID identity as
// table-data tabs. Confirm the object is a base table because selecting
// ROWID from a view can fail with ORA-01445.
if (databaseType === "oracle" && declaredPrimaryKeys.length === 0 && !(await oracleRowIdIsSafeForQuery(tab, loaded))) return unchanged;
if (databaseType === "oracle" && declaredPrimaryKeys.length === 0 && !oracleRowIdIsSafeForQuery(tab, loaded)) return unchanged;
const primaryKeys = editablePrimaryKeys(databaseType, loaded.tableMeta.columns, loaded.tableMeta.tableType);
return buildHiddenPrimaryKeyPreparation(sql, databaseType, loaded, primaryKeys, declaredPrimaryKeys, traceId, elapsed);
} catch (error) {