fix(query): keep Oracle NUM column results editable

This commit is contained in:
onenewcode authored and GitHub committed 2026-09-15 14:51:44 +08:00
1 parent 715d834c32
commit d79dc05cf5
5 files changed
+124 -9

No files matched your search

@@ -216,6 +216,20 @@ describe("editable query hidden primary keys", () => {
expect(analyzeEditableQueryEditability("select id from jobs union select id from archived_jobs")).toEqual({ editable: false, reason: "set-operation" });
});
it("keeps Oracle NUM columns and aliases mapped when result labels disagree", () => {
const named = analyzeEditableQueryEditability("select id, num from users");
expect(named.editable).toBe(true);
if (!named.editable) return;
expect(sourceColumnsForResult(named.analysis, ["ID", "NUM"], undefined, "oracle")).toEqual(["id", "num"]);
expect(sourceColumnsForResult(named.analysis, ["ID", "ROWNUM"], undefined, "oracle")).toEqual(["id", undefined]);
const aliased = analyzeEditableQueryEditability("select id, amount as num from users");
expect(aliased.editable).toBe(true);
if (!aliased.editable) return;
expect(sourceColumnsForResult(aliased.analysis, ["ID", "AMOUNT"], undefined, "oracle")).toEqual(["id", "amount"]);
expect(sourceColumnsForResult(aliased.analysis, ["ID", "NUM", "ROWNUM"], undefined, "oracle")).toEqual(["id", "amount", undefined]);
});
it("maps a qualified Oracle ROWID projection to the synthetic row key", () => {
const analysis: EditableQueryInfo = {
schema: "APP",
+50 -8
View File
@@ -861,13 +861,32 @@ export function allPrimaryKeysPresent(primaryKeys: string[], resultColumns: stri
return primaryKeys.every((pk) => colSet.has(pk));
}
function matchColumnsForResult(analysis: EditableQueryInfo, resultColumns: string[]): EditableQueryColumn[] | undefined {
// Preserve projection order before searching by label: folded duplicate names are not unique.
if (analysis.columns.length === resultColumns.length && analysis.columns.every((column, index) => column.resultName.toLowerCase() === resultColumns[index]!.toLowerCase())) return analysis.columns;
const SYNTHETIC_RESULT_ROW_NUMBER_LABELS = new Set(["__dbx_row_num", "dbx_rn"]);
function isOracleFamilyDatabase(databaseType?: DatabaseType | string): boolean {
return !!databaseType && ORACLE_FOLDED_IDENTIFIER_TYPES.has(databaseType);
}
function isTrailingPaginationResultLabel(label: string, analysisResultNames: Set<string>): boolean {
const normalized = label.toLowerCase();
if (SYNTHETIC_RESULT_ROW_NUMBER_LABELS.has(normalized)) return true;
return normalized === "rownum" && !analysisResultNames.has("rownum");
}
function resultPrefixLengthForEditMatching(analysis: EditableQueryInfo, resultColumns: string[]): number {
const analysisNames = new Set(analysis.columns.map((column) => column.resultName.toLowerCase()));
let end = resultColumns.length;
while (end > analysis.columns.length) {
if (!isTrailingPaginationResultLabel(resultColumns[end - 1]!, analysisNames)) break;
end -= 1;
}
return end;
}
function matchResultPrefixByLabel(analysis: EditableQueryInfo, prefix: string[]): EditableQueryColumn[] | undefined {
const matches: EditableQueryColumn[] = [];
let searchFrom = 0;
for (const resultColumn of resultColumns) {
for (const resultColumn of prefix) {
let matchIndex = analysis.columns.findIndex((column, index) => index >= searchFrom && column.resultName === resultColumn);
if (matchIndex < 0) {
const normalized = resultColumn.toLowerCase();
@@ -881,17 +900,40 @@ function matchColumnsForResult(analysis: EditableQueryInfo, resultColumns: strin
return matches;
}
export function allEditableColumnsWriteable(analysis: EditableQueryInfo, resultColumns: string[], sourceKey?: string): boolean {
function isRownumLabelSubstitution(resultLabel: string, column: EditableQueryColumn): boolean {
if (resultLabel.toLowerCase() !== "rownum") return false;
return column.resultName.toLowerCase() !== "rownum" && column.sourceName?.toLowerCase() !== "rownum";
}
function padMatchedColumnsToResultLength(matches: Array<EditableQueryColumn | undefined>, resultColumnCount: number): Array<EditableQueryColumn | undefined> {
if (matches.length === resultColumnCount) return matches;
return [...matches, ...Array.from({ length: resultColumnCount - matches.length }, () => undefined)];
}
function matchColumnsForResult(analysis: EditableQueryInfo, resultColumns: string[], databaseType?: DatabaseType | string): Array<EditableQueryColumn | undefined> | undefined {
const prefixLength = resultPrefixLengthForEditMatching(analysis, resultColumns);
const prefix = resultColumns.slice(0, prefixLength);
const labeled = matchResultPrefixByLabel(analysis, prefix);
if (labeled) return padMatchedColumnsToResultLength(labeled, resultColumns.length);
if (prefix.length !== analysis.columns.length || !isOracleFamilyDatabase(databaseType)) return undefined;
const ordinal = analysis.columns.map((column, index) => (isRownumLabelSubstitution(prefix[index]!, column) ? undefined : column));
return padMatchedColumnsToResultLength(ordinal, resultColumns.length);
}
export function allEditableColumnsWriteable(analysis: EditableQueryInfo, resultColumns: string[], sourceKey?: string, databaseType?: DatabaseType | string): boolean {
if (analysis.selectStar) return true;
const matchedColumns = matchColumnsForResult(analysis, resultColumns);
return !!matchedColumns && matchedColumns.every((source) => !sourceKey || !source.sourceName || source.sourceKey === sourceKey);
const matchedColumns = matchColumnsForResult(analysis, resultColumns, databaseType);
return !!matchedColumns && matchedColumns.every((source) => !source || !sourceKey || !source.sourceName || source.sourceKey === sourceKey);
}
export function sourceColumnsForResult(analysis: EditableQueryInfo, resultColumns: string[], sourceKey?: string, databaseType?: DatabaseType, primaryKeys?: readonly string[]): Array<string | undefined> | undefined {
if (analysis.selectStar) return undefined;
const matchedColumns = matchColumnsForResult(analysis, resultColumns);
const matchedColumns = matchColumnsForResult(analysis, resultColumns, databaseType);
if (!matchedColumns) return undefined;
return matchedColumns.map((column) => {
if (!column) return undefined;
if (sourceKey && column.sourceKey !== sourceKey) return undefined;
if (databaseType === "oracle" && !column.sourceNameQuoted && column.sourceName?.toUpperCase() === "ROWID" && column.sourceKey === sourceKey) {
return primaryKeys?.length === 1 && primaryKeys[0] === DBX_ROWID_COLUMN ? DBX_ROWID_COLUMN : undefined;
+1 -1
View File
@@ -5764,7 +5764,7 @@ export const useQueryStore = defineStore("query", () => {
};
}
if (!allEditableColumnsWriteable(metadataAnalysis, tab.result.columns)) {
if (!allEditableColumnsWriteable(metadataAnalysis, tab.result.columns, undefined, dbType)) {
return {
queryAnalysis: undefined,
querySourceColumns: undefined,