fix(query): match primary keys by dialect-aware casing

This commit is contained in:
zipg authored and GitHub committed 2026-07-15 20:11:04 +08:00
1 parent 2457fac04f
commit 5b985b670d
6 files changed
+502 -51

No files matched your search

+50 -12
View File
@@ -48,6 +48,44 @@ export interface EditableQuerySource {
alias?: string;
}
const POSTGRES_FOLDED_IDENTIFIER_TYPES = new Set(["postgres", "redshift", "gaussdb", "highgo", "vastbase", "kwdb", "opengauss", "questdb"]);
const ORACLE_FOLDED_IDENTIFIER_TYPES = new Set(["oracle", "dameng", "oceanbase-oracle"]);
/**
* Resolve a SQL source identifier to the canonical name returned by table
* metadata. Quoted identifiers are always exact. PostgreSQL-compatible
* unquoted identifiers fold to lower case, while Oracle-compatible identifiers
* fold to upper case. Other dialects may use a case-insensitive match only when
* it identifies exactly one physical column.
*/
export function resolveMetadataColumnName(databaseType: string, sourceName: string, sourceNameQuoted: boolean | undefined, metadataColumns: readonly string[]): string | undefined {
if (sourceNameQuoted) return metadataColumns.find((column) => column === sourceName);
// Result-set labels are already database-resolved names rather than SQL
// source tokens. Preserve exact PostgreSQL/Oracle casing instead of folding
// a physical quoted column such as `"ID"` to `id`.
if (sourceNameQuoted === undefined) {
const exact = metadataColumns.find((column) => column === sourceName);
if (exact || POSTGRES_FOLDED_IDENTIFIER_TYPES.has(databaseType) || ORACLE_FOLDED_IDENTIFIER_TYPES.has(databaseType)) return exact;
const caseOnlyMatches = metadataColumns.filter((column) => column.toLowerCase() === sourceName.toLowerCase());
return caseOnlyMatches.length === 1 ? caseOnlyMatches[0] : undefined;
}
if (POSTGRES_FOLDED_IDENTIFIER_TYPES.has(databaseType)) {
const folded = sourceName.toLowerCase();
return metadataColumns.find((column) => column === folded);
}
if (ORACLE_FOLDED_IDENTIFIER_TYPES.has(databaseType)) {
const folded = sourceName.toUpperCase();
return metadataColumns.find((column) => column === folded);
}
const exact = metadataColumns.find((column) => column === sourceName);
if (exact) return exact;
const caseOnlyMatches = metadataColumns.filter((column) => column.toLowerCase() === sourceName.toLowerCase());
return caseOnlyMatches.length === 1 ? caseOnlyMatches[0] : undefined;
}
export type QueryEditabilityReason = "not-select" | "cte" | "set-operation" | "aggregation" | "external-source" | "complex-source" | "computed-columns" | "no-table" | "no-primary-key" | "primary-key-not-returned" | "aliased-columns" | "metadata-unavailable";
export type QueryEditability = { editable: true; analysis: EditableQueryInfo } | { editable: false; reason: QueryEditabilityReason };
@@ -451,7 +489,9 @@ function escapeRegExp(value: string): string {
/**
* Check if all primary key columns are present in the result set columns.
* Comparison is case-insensitive.
* Source names must already be resolved to canonical metadata names. Exact
* comparison prevents `id` from being mistaken for a distinct quoted `"ID"`
* column in PostgreSQL.
*/
export function allPrimaryKeysPresent(primaryKeys: string[], resultColumns: string[], analysis?: EditableQueryInfo, sourceKey?: string): boolean {
if (analysis && !analysis.selectStar) {
@@ -459,26 +499,24 @@ export function allPrimaryKeysPresent(primaryKeys: string[], resultColumns: stri
analysis.columns.flatMap((column) => {
if (!column.sourceName) return [];
if (sourceKey && column.sourceKey !== sourceKey) return [];
return [column.sourceName.toLowerCase()];
return [column.sourceName];
}),
);
return primaryKeys.every((pk) => sourceColumns.has(pk.toLowerCase()));
return primaryKeys.every((pk) => sourceColumns.has(pk));
}
const colSet = new Set(resultColumns.map((c) => c.toLowerCase()));
return primaryKeys.every((pk) => colSet.has(pk.toLowerCase()));
const colSet = new Set(resultColumns);
return primaryKeys.every((pk) => colSet.has(pk));
}
function matchColumnsForResult(analysis: EditableQueryInfo, resultColumns: string[]): EditableQueryColumn[] | undefined {
const matches: EditableQueryColumn[] = [];
let searchFrom = 0;
for (const resultColumn of resultColumns) {
const normalized = resultColumn.toLowerCase();
let matchIndex = -1;
for (let index = searchFrom; index < analysis.columns.length; index++) {
if (analysis.columns[index]!.resultName.toLowerCase() === normalized) {
matchIndex = index;
break;
}
let matchIndex = analysis.columns.findIndex((column, index) => index >= searchFrom && column.resultName === resultColumn);
if (matchIndex < 0) {
const normalized = resultColumn.toLowerCase();
const caseOnlyMatches = analysis.columns.flatMap((column, index) => (index >= searchFrom && column.resultName.toLowerCase() === normalized ? [index] : []));
if (caseOnlyMatches.length === 1) matchIndex = caseOnlyMatches[0]!;
}
if (matchIndex < 0) return undefined;
matches.push(analysis.columns[matchIndex]!);