feat(grid): edit Oracle join query results via ROWID
This commit is contained in:
4 files changed
+110
-9
No files matched your search
@@ -557,6 +557,61 @@ describe("queryStore hidden primary key editing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a qualified Oracle ROWID from a joined query as the synthetic row key", async () => {
|
||||
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
|
||||
getColumns.mockImplementation(async (_connectionId: string, _database: string, _schema: string, tableName: string) =>
|
||||
tableName === "USERS"
|
||||
? [
|
||||
{ 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 },
|
||||
]
|
||||
: [{ name: "LABEL", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null }],
|
||||
);
|
||||
lookupLocalCompletionTables.mockReturnValue([
|
||||
{ name: "USERS", type: "table", schema: "APP" },
|
||||
{ name: "ORDERS", type: "table", schema: "APP" },
|
||||
]);
|
||||
analyzeEditableQueryEditability.mockResolvedValue({
|
||||
editable: true,
|
||||
analysis: {
|
||||
schema: "APP",
|
||||
tableName: "USERS",
|
||||
selectStar: false,
|
||||
sources: [
|
||||
{ key: "t:0", schema: "APP", tableName: "USERS", alias: "t" },
|
||||
{ key: "o:1", schema: "APP", tableName: "ORDERS", alias: "o" },
|
||||
],
|
||||
columns: [
|
||||
{ sourceName: "ROWID", sourceQualifier: "t", sourceKey: "t:0", resultName: "ROWID", expression: "t.ROWID" },
|
||||
{ sourceName: "NAME", sourceQualifier: "t", sourceKey: "t:0", resultName: "NAME", expression: "t.NAME" },
|
||||
{ sourceName: "LABEL", sourceQualifier: "o", sourceKey: "o:1", resultName: "LABEL", expression: "o.LABEL" },
|
||||
],
|
||||
},
|
||||
});
|
||||
executeMulti.mockResolvedValue([
|
||||
{
|
||||
columns: ["ROWID", "NAME", "LABEL"],
|
||||
rows: [["AAAPr9AAEAAAACXAAA", "Alice", "Order"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT t.ROWID, t.NAME, o.LABEL FROM APP.USERS t LEFT JOIN APP.ORDERS o ON o.USER_ID = t.ID");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
await vi.waitFor(() => expect(tab.result?.columns).toEqual(["ROWID", "NAME", "LABEL"]));
|
||||
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["__DBX_ROWID", "NAME", undefined]), { timeout: 5000 });
|
||||
expect(tab.tableMeta?.primaryKeys).toEqual(["__DBX_ROWID"]);
|
||||
expect(tab.queryAnalysis).toBeDefined();
|
||||
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||
}, 10_000);
|
||||
|
||||
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([
|
||||
|
||||
@@ -657,6 +657,7 @@ function bindColumnsForSource(
|
||||
if (!column.sourceName) return column;
|
||||
if (column.sourceKey) {
|
||||
if (column.sourceKey !== source.key) return column;
|
||||
if (dbType === "oracle" && !column.sourceNameQuoted && column.sourceName.toUpperCase() === "ROWID") return column;
|
||||
const canonicalName = resolveSourceColumnName(dbType, column.sourceName, column.sourceNameQuoted, tableColumns);
|
||||
return { ...column, sourceName: canonicalName };
|
||||
}
|
||||
@@ -672,7 +673,7 @@ function bindColumnsForSource(
|
||||
}
|
||||
|
||||
function primaryKeysPresentForSource(dbType: string, primaryKeys: string[], resultColumns: string[], analysis: EditableQueryInfo, sourceKey: string, tableColumns: readonly { name: string }[]): boolean {
|
||||
if (!analysis.selectStar) return allPrimaryKeysPresent(primaryKeys, resultColumns, analysis, sourceKey);
|
||||
if (!analysis.selectStar) return allPrimaryKeysPresent(primaryKeys, resultColumns, analysis, sourceKey, dbType as DatabaseType);
|
||||
const metadataNames = tableColumns.map((column) => column.name);
|
||||
const canonicalResultColumns = resultColumns.flatMap((column) => {
|
||||
const canonicalName = resolveMetadataColumnName(dbType, column, undefined, metadataNames);
|
||||
@@ -4791,9 +4792,15 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return loadedEditableSourceFromMetadata(target, loadedMetadata.metadata);
|
||||
}
|
||||
|
||||
function missingPrimaryKeysForSource(primaryKeys: string[], analysis: EditableQueryInfo, sourceKey: string): string[] {
|
||||
function missingPrimaryKeysForSource(databaseType: DatabaseType, primaryKeys: string[], analysis: EditableQueryInfo, sourceKey: string): string[] {
|
||||
if (analysis.selectStar) return [];
|
||||
const selectedColumns = new Set(analysis.columns.flatMap((column) => (column.sourceName && column.sourceKey === sourceKey ? [column.sourceName] : [])));
|
||||
const selectedColumns = new Set(
|
||||
analysis.columns.flatMap((column) => {
|
||||
if (!column.sourceName || column.sourceKey !== sourceKey) return [];
|
||||
if (databaseType === "oracle" && !column.sourceNameQuoted && column.sourceName.toUpperCase() === "ROWID") return [DBX_ROWID_COLUMN, column.sourceName];
|
||||
return [column.sourceName];
|
||||
}),
|
||||
);
|
||||
return primaryKeys.filter((primaryKey) => !selectedColumns.has(primaryKey));
|
||||
}
|
||||
|
||||
@@ -4847,7 +4854,10 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(databaseType, loaded.analysis, loaded.source, loaded.tableMeta.columns), loaded.source, loaded.tableMeta.columns);
|
||||
const oracleLobPreview = databaseType === "oracle" && primaryKeys.length > 0 && oracleRowIdIsSafeForQuery(tab, loaded) && oracleColumnsAllowDeferredLobMarkers(loaded.tableMeta.columns) && oracleQueryProjectsDeferredLob(metadataAnalysis, loaded.source.key, loaded.tableMeta.columns);
|
||||
const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [], oracleLobPreview };
|
||||
const missingPrimaryKeys = declaredPrimaryKeys.length === 0 ? primaryKeys : missingPrimaryKeysForSource(primaryKeys, metadataAnalysis, loaded.source.key);
|
||||
const missingPrimaryKeys =
|
||||
declaredPrimaryKeys.length === 0
|
||||
? primaryKeys.filter((primaryKey) => !(databaseType === "oracle" && primaryKey === DBX_ROWID_COLUMN && metadataAnalysis.columns.some((column) => column.sourceKey === loaded.source.key && !column.sourceNameQuoted && column.sourceName?.toUpperCase() === "ROWID")))
|
||||
: missingPrimaryKeysForSource(databaseType, primaryKeys, metadataAnalysis, loaded.source.key);
|
||||
if (missingPrimaryKeys.length === 0) return unchanged;
|
||||
const primaryKeySet = new Set(primaryKeys);
|
||||
const hasWritableProjection = metadataAnalysis.selectStar ? loaded.tableMeta.columns.some((column) => !primaryKeySet.has(column.name)) : metadataAnalysis.columns.some((column) => column.sourceName && column.sourceKey === loaded.source.key && !primaryKeySet.has(column.sourceName));
|
||||
@@ -5095,7 +5105,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
.map((loaded) => {
|
||||
const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(dbType, loaded.analysis, loaded.source, loaded.tableMeta.columns, allSourceColumns), loaded.source, loaded.tableMeta.columns);
|
||||
const primaryKeys = loaded.tableMeta.primaryKeys;
|
||||
const sourceColumns = sourceColumnsForResult(metadataAnalysis, tab.result!.columns, loaded.source.key);
|
||||
const sourceColumns = sourceColumnsForResult(metadataAnalysis, tab.result!.columns, loaded.source.key, dbType as DatabaseType, primaryKeys);
|
||||
const primaryKeysPresent = primaryKeysPresentForSource(dbType, primaryKeys, tab.result!.columns, metadataAnalysis, loaded.source.key, loaded.tableMeta.columns);
|
||||
const keylessAllowed = sources.length === 1 && canUseKeylessRowPredicate(dbType as DatabaseType, primaryKeys);
|
||||
const primaryKeySet = new Set(primaryKeys);
|
||||
@@ -5117,7 +5127,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const syntheticRowIdProjection = hiddenPrimaryKeys.find((projection) => projection.sourceName.toUpperCase() === DBX_ROWID_COLUMN);
|
||||
const primaryKeys = loaded.tableMeta.primaryKeys.length === 0 && syntheticRowIdProjection ? [DBX_ROWID_COLUMN] : loaded.tableMeta.primaryKeys;
|
||||
const displaySourceInfo = resolveResultColumnInfo(dbType, analysis, tab.result.columns, loadedSources);
|
||||
const sourceColumns = sourceColumnsForResult(metadataAnalysis, tab.result.columns, loaded.source.key);
|
||||
const sourceColumns = sourceColumnsForResult(metadataAnalysis, tab.result.columns, loaded.source.key, dbType as DatabaseType, primaryKeys);
|
||||
if (sourceColumns && syntheticRowIdProjection) {
|
||||
const resultIndex = tab.result.columns.findIndex((column) => column.toLowerCase() === syntheticRowIdProjection.alias.toLowerCase());
|
||||
if (resultIndex >= 0) sourceColumns[resultIndex] = DBX_ROWID_COLUMN;
|
||||
|
||||
Reference in new issue
Block a user