feat(grid): edit simple query results

This commit is contained in:
t8y2
2026-05-16 10:25:05 +08:00
parent d054f04813
commit 2024171c8d
11 changed files with 656 additions and 112 deletions
+54 -23
View File
@@ -21,6 +21,7 @@ export interface DataGridSaveStatementOptions {
databaseType?: DatabaseType;
tableMeta: DataGridTableMeta;
columns: string[];
sourceColumns?: Array<string | undefined>;
rows: GridCellValue[][];
dirtyRows: Array<[number, Array<[number, GridCellValue]>]>;
deletedRows: number[];
@@ -31,6 +32,7 @@ export interface DataGridSaveValidationOptions {
databaseType?: DatabaseType;
tableMeta?: DataGridTableMeta;
columns: string[];
sourceColumns?: Array<string | undefined>;
rows?: GridCellValue[][];
columnInfo?: DataGridColumnInfo[];
dirtyRows: Array<[number, Array<[number, GridCellValue]>]>;
@@ -56,17 +58,20 @@ export function validateDataGridSave(options: DataGridSaveValidationOptions): st
for (const [, changes] of options.dirtyRows) {
for (const [columnIndex, value] of changes) {
const column = options.columns[columnIndex];
if (isNullWriteToNotNullColumn(options.databaseType, notNullColumns, column, value)) {
return nullWriteError(column);
const sourceColumn = effectiveColumn(options.sourceColumns, options.columns, columnIndex);
if (!sourceColumn) continue;
if (isNullWriteToNotNullColumn(options.databaseType, notNullColumns, sourceColumn, value)) {
return nullWriteError(sourceColumn);
}
}
}
for (const row of options.newRows) {
for (const [columnIndex, column] of options.columns.entries()) {
if (isNullWriteToNotNullColumn(options.databaseType, notNullColumns, column, row[columnIndex])) {
return nullWriteError(column);
for (const columnIndex of options.columns.keys()) {
const sourceColumn = effectiveColumn(options.sourceColumns, options.columns, columnIndex);
if (!sourceColumn) continue;
if (isNullWriteToNotNullColumn(options.databaseType, notNullColumns, sourceColumn, row[columnIndex])) {
return nullWriteError(sourceColumn);
}
}
}
@@ -78,7 +83,8 @@ function validateInsertedPrimaryKeys(options: DataGridSaveValidationOptions): st
const primaryKeys = options.tableMeta?.primaryKeys ?? [];
if (primaryKeys.length === 0 || options.newRows.length === 0) return undefined;
const primaryKeyIndexes = primaryKeys.map((primaryKey) => findColumnIndex(options.columns, primaryKey));
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
const primaryKeyIndexes = primaryKeys.map((primaryKey) => findColumnIndex(saveColumns, primaryKey));
if (primaryKeyIndexes.some((index) => index === -1)) return undefined;
const existingKeys = new Set<string>();
@@ -102,6 +108,7 @@ function validateInsertedPrimaryKeys(options: DataGridSaveValidationOptions): st
export function buildDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
if (options.databaseType === "neo4j") return buildNeo4jDataGridSaveStatements(options);
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
const table = qualifiedTableName({
databaseType: options.databaseType,
@@ -114,27 +121,29 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption
const row = options.rows[rowIndex];
if (!row) continue;
const sets = changes
.filter(([columnIndex]) => !isOracleRowId(options.databaseType, options.columns[columnIndex]))
.filter(([columnIndex]) => !!saveColumns[columnIndex])
.filter(([columnIndex]) => !isOracleRowId(options.databaseType, saveColumns[columnIndex]))
.map(
([columnIndex, value]) =>
`${quoteIdent(options.databaseType, options.columns[columnIndex])} = ${formatGridSqlLiteral(value, options.databaseType)}`,
`${quoteIdent(options.databaseType, saveColumns[columnIndex]!)} = ${formatGridSqlLiteral(value, options.databaseType)}`,
)
.join(", ");
if (!sets) continue;
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, row);
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, saveColumns, row);
statements.push(`UPDATE ${table} SET ${sets} WHERE ${where};`);
}
for (const rowIndex of options.deletedRows) {
const row = options.rows[rowIndex];
if (!row) continue;
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, row);
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, saveColumns, row);
statements.push(`DELETE FROM ${table} WHERE ${where};`);
}
for (const row of options.newRows) {
const insertPairs = options.columns
const insertPairs = saveColumns
.map((column, index) => ({ column, value: row[index] }))
.filter((pair): pair is { column: string; value: GridCellValue } => !!pair.column)
.filter((pair) => !isOracleRowId(options.databaseType, pair.column))
.filter((pair) => pair.value !== null && pair.value !== undefined);
if (!insertPairs.length) continue;
@@ -148,6 +157,7 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption
export function buildDataGridRollbackStatements(options: DataGridSaveStatementOptions): string[] {
if (options.databaseType === "neo4j") return buildNeo4jDataGridRollbackStatements(options);
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
const table = qualifiedTableName({
databaseType: options.databaseType,
@@ -157,15 +167,16 @@ export function buildDataGridRollbackStatements(options: DataGridSaveStatementOp
const statements: string[] = [];
for (const row of options.newRows) {
const where = buildRowWhere(options.databaseType, options.columns, row);
const where = buildRowWhere(options.databaseType, saveColumns, row);
if (where) statements.push(`DELETE FROM ${table} WHERE ${where};`);
}
for (const rowIndex of options.deletedRows) {
const row = options.rows[rowIndex];
if (!row) continue;
const insertPairs = options.columns
const insertPairs = saveColumns
.map((column, index) => ({ column, value: row[index] }))
.filter((pair): pair is { column: string; value: GridCellValue } => !!pair.column)
.filter((pair) => !isOracleRowId(options.databaseType, pair.column));
const columns = insertPairs.map((pair) => quoteIdent(options.databaseType, pair.column)).join(", ");
const values = insertPairs.map((pair) => formatGridSqlLiteral(pair.value, options.databaseType)).join(", ");
@@ -180,19 +191,19 @@ export function buildDataGridRollbackStatements(options: DataGridSaveStatementOp
afterRow[columnIndex] = value;
}
const writableChanges = changes.filter(
([columnIndex]) => !isOracleRowId(options.databaseType, options.columns[columnIndex]),
([columnIndex]) => !!saveColumns[columnIndex] && !isOracleRowId(options.databaseType, saveColumns[columnIndex]),
);
const sets = writableChanges
.map(
([columnIndex]) =>
`${quoteIdent(options.databaseType, options.columns[columnIndex])} = ${formatGridSqlLiteral(row[columnIndex], options.databaseType)}`,
`${quoteIdent(options.databaseType, saveColumns[columnIndex]!)} = ${formatGridSqlLiteral(row[columnIndex], options.databaseType)}`,
)
.join(", ");
if (!sets) continue;
const where = [
buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, afterRow),
buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, saveColumns, afterRow),
...writableChanges.map(([columnIndex, value]) =>
buildColumnPredicate(options.databaseType, options.columns[columnIndex], value),
buildColumnPredicate(options.databaseType, saveColumns[columnIndex]!, value),
),
]
.filter(Boolean)
@@ -203,6 +214,22 @@ export function buildDataGridRollbackStatements(options: DataGridSaveStatementOp
return statements;
}
function effectiveColumns(
sourceColumns: Array<string | undefined> | undefined,
columns: string[],
): Array<string | undefined> {
if (!sourceColumns || sourceColumns.length !== columns.length) return columns;
return sourceColumns;
}
function effectiveColumn(
sourceColumns: Array<string | undefined> | undefined,
columns: string[],
index: number,
): string | undefined {
return effectiveColumns(sourceColumns, columns)[index];
}
export function dataGridSaveExecutionSchema(
databaseType: DatabaseType | undefined,
tableMeta: DataGridTableMeta | undefined,
@@ -233,7 +260,7 @@ export function formatGridSqlLiteral(value: GridCellValue, databaseType?: Databa
function buildPrimaryKeyWhere(
databaseType: DatabaseType | undefined,
primaryKeys: string[],
columns: string[],
columns: Array<string | undefined>,
row: GridCellValue[],
): string {
if (databaseType === "hive" && primaryKeys.length === 0) return buildRowWhere(databaseType, columns, row);
@@ -245,10 +272,14 @@ function buildPrimaryKeyWhere(
.join(" AND ");
}
function buildRowWhere(databaseType: DatabaseType | undefined, columns: string[], row: GridCellValue[]): string {
function buildRowWhere(
databaseType: DatabaseType | undefined,
columns: Array<string | undefined>,
row: GridCellValue[],
): string {
return columns
.map((column, index) =>
isOracleRowId(databaseType, column) ? "" : buildColumnPredicate(databaseType, column, row[index]),
!column || isOracleRowId(databaseType, column) ? "" : buildColumnPredicate(databaseType, column, row[index]),
)
.filter(Boolean)
.join(" AND ");
@@ -283,9 +314,9 @@ function isNullWriteToNotNullColumn(
return value === null && notNullColumns.has(normalizeColumnName(column));
}
function findColumnIndex(columns: string[], target: string): number {
function findColumnIndex(columns: Array<string | undefined>, target: string): number {
const normalizedTarget = normalizeColumnName(target);
return columns.findIndex((column) => normalizeColumnName(column) === normalizedTarget);
return columns.findIndex((column) => (column ? normalizeColumnName(column) : "") === normalizedTarget);
}
function primaryKeyValueKey(primaryKeyIndexes: number[], row: GridCellValue[]): string | undefined {
+279 -81
View File
@@ -21,10 +21,34 @@ export function isBinaryType(dataType: string): boolean {
export interface EditableQueryInfo {
schema: string | undefined;
tableName: string;
tableAlias?: string;
selectStar: boolean;
columns: string[]; // empty array if SELECT *
columns: EditableQueryColumn[]; // empty array if SELECT *
}
export interface EditableQueryColumn {
sourceName?: string;
resultName: string;
expression: string;
}
export type QueryEditabilityReason =
| "not-select"
| "cte"
| "set-operation"
| "aggregation"
| "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 };
/**
* Parse a SELECT statement to determine if it's editable.
* Only simple single-table SELECT queries are considered editable:
@@ -33,122 +57,296 @@ export interface EditableQueryInfo {
* - WHERE, ORDER BY, LIMIT are allowed
*/
export function analyzeEditableQuery(sql: string): EditableQueryInfo | null {
const trimmed = sql.trim();
const result = analyzeEditableQueryEditability(sql);
return result.editable ? result.analysis : null;
}
// Strip trailing semicolons and whitespace
const cleaned = trimmed.replace(/;+\s*$/, "").trim();
// Must start with SELECT (case-insensitive)
if (!/^SELECT\b/i.test(cleaned)) return null;
// Reject CTEs (WITH clause before SELECT)
if (/^\s*WITH\b/i.test(trimmed)) return null;
// Remove inline comments and block comments
const normalized = cleaned.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
// Reject UNION, INTERSECT, EXCEPT
if (/\b(UNION|INTERSECT|EXCEPT)\b/i.test(normalized)) return null;
// --- Extract SELECT body (columns) ---
// Match everything between SELECT and the first SQL clause keyword (FROM, WHERE, ORDER, etc.)
const selectMatch = normalized.match(
/^SELECT\s+(.+?)(?:\bFROM\b|\bWHERE\b|\bORDER\b|\bLIMIT\b|\bGROUP\b|\bHAVING\b|$)/is,
);
if (!selectMatch) return null;
const selectBody = selectMatch[1].trim();
// Reject DISTINCT
if (/^\s*DISTINCT\b/i.test(selectBody)) return null;
// Check if SELECT *
const selectStar = /^\s*\*\s*$/.test(selectBody);
const columns: string[] = [];
if (!selectStar) {
columns.push(...parseSelectColumns(selectBody));
export function analyzeEditableQueryEditability(sql: string): QueryEditability {
const normalized = stripSqlComments(sql)
.replace(/;+\s*$/, "")
.trim();
if (!normalized) return { editable: false, reason: "not-select" };
if (/^\s*WITH\b/i.test(normalized)) return { editable: false, reason: "cte" };
if (!/^SELECT\b/i.test(normalized)) return { editable: false, reason: "not-select" };
if (hasTopLevelKeyword(normalized, ["UNION", "INTERSECT", "EXCEPT"])) {
return { editable: false, reason: "set-operation" };
}
if (normalized.includes(";")) return { editable: false, reason: "complex-source" };
// --- Extract FROM body ---
const fromMatch = normalized.match(/\bFROM\s+(.+?)(?:\bWHERE\b|\bORDER\b|\bLIMIT\b|\bGROUP\b|\bHAVING\b|$)/is);
if (!fromMatch) return null;
const fromBody = fromMatch[1].trim();
const fromIndex = findTopLevelKeyword(normalized, "FROM", 0);
if (fromIndex < 0) return { editable: false, reason: "no-table" };
// Reject JOIN
if (/\bJOIN\b/i.test(fromBody)) return null;
const selectBody = normalized.slice("SELECT".length, fromIndex).trim();
if (/^DISTINCT\b/i.test(selectBody)) return { editable: false, reason: "aggregation" };
// Reject GROUP BY, HAVING (already partially covered but double-check)
if (/\b(GROUP\s+BY|HAVING)\b/i.test(fromBody)) return null;
const groupIndex = findTopLevelKeyword(normalized, "GROUP", fromIndex + 4);
const havingIndex = findTopLevelKeyword(normalized, "HAVING", fromIndex + 4);
if (groupIndex >= 0 || havingIndex >= 0) return { editable: false, reason: "aggregation" };
// Reject subqueries: if FROM body contains SELECT keyword
if (/\bSELECT\b/i.test(fromBody)) return null;
const fromEnd = firstTopLevelKeywordIndex(normalized, ["WHERE", "ORDER", "LIMIT", "OFFSET", "FETCH"], fromIndex + 4);
const fromBody = normalized.slice(fromIndex + 4, fromEnd < 0 ? normalized.length : fromEnd).trim();
const source = parseFromSource(fromBody);
if (!source) return { editable: false, reason: "complex-source" };
// Also reject if there's a SELECT anywhere after FROM (subquery in WHERE etc.)
const fromIdx = normalized.toUpperCase().indexOf("FROM");
if (fromIdx >= 0) {
const afterFirstFrom = normalized.slice(fromIdx);
// Count SELECT occurrences after FROM — should be 0 (the original SELECT is before FROM)
const selectCount = (afterFirstFrom.match(/\bSELECT\b/g) || []).length;
if (selectCount > 0) return null;
}
// --- Extract table name from FROM clause ---
// Strip all quoting characters (backticks, double quotes, square brackets)
const stripped = fromBody.replace(/[`"[\]]/g, "").trim();
// Match: table OR schema.table
const tableMatch = stripped.match(/^(\w+)(?:\.(\w+))?/);
if (!tableMatch) return null;
const schema = tableMatch[2] ? tableMatch[1] : undefined;
const tableName = tableMatch[2] || tableMatch[1];
const selectStar = isSelectStar(selectBody, source.alias);
const columns = selectStar ? [] : parseSelectColumns(selectBody);
if (!selectStar && columns.length === 0) return { editable: false, reason: "computed-columns" };
return {
schema,
tableName,
selectStar,
columns,
editable: true,
analysis: {
schema: source.schema,
tableName: source.tableName,
tableAlias: source.alias,
selectStar,
columns,
},
};
}
function parseSelectColumns(body: string): string[] {
const cols: string[] = [];
export function queryEditabilityMessageKey(reason: QueryEditabilityReason): string {
return {
"not-select": "grid.queryEditUnsupportedNotSelect",
cte: "grid.queryEditUnsupportedCte",
"set-operation": "grid.queryEditUnsupportedSetOperation",
aggregation: "grid.queryEditUnsupportedAggregation",
"complex-source": "grid.queryEditUnsupportedComplexSource",
"computed-columns": "grid.queryEditUnsupportedComputedColumns",
"no-table": "grid.queryEditUnsupportedNoTable",
"no-primary-key": "grid.queryEditUnsupportedNoPrimaryKey",
"primary-key-not-returned": "grid.queryEditUnsupportedPrimaryKeyNotReturned",
"aliased-columns": "grid.queryEditUnsupportedAliasedColumns",
"metadata-unavailable": "grid.queryEditUnsupportedMetadataUnavailable",
}[reason];
}
function parseSelectColumns(body: string): EditableQueryColumn[] {
const cols: EditableQueryColumn[] = [];
let depth = 0;
let current = "";
let quote: string | null = null;
for (let i = 0; i < body.length; i++) {
const ch = body[i];
if (ch === "(") depth++;
if (quote) {
current += ch;
if (ch === quote || (quote === "]" && ch === "]")) quote = null;
continue;
}
if (ch === "'" || ch === '"' || ch === "`") quote = ch;
else if (ch === "[") quote = "]";
else if (ch === "(") depth++;
else if (ch === ")") depth--;
else if (ch === "," && depth === 0) {
cols.push(extractColumnName(current.trim()));
const col = parseSelectColumn(current.trim());
if (!col) return [];
cols.push(col);
current = "";
continue;
}
current += ch;
}
if (current.trim()) {
cols.push(extractColumnName(current.trim()));
const col = parseSelectColumn(current.trim());
if (!col) return [];
cols.push(col);
}
return cols;
}
function extractColumnName(col: string): string {
// Handle AS alias
const asMatch = col.match(/\bAS\s+(\w+)/i);
if (asMatch) return asMatch[1];
function parseSelectColumn(col: string): EditableQueryColumn | null {
const source = parseQualifiedIdentifier(col);
if (!source) return parseComputedSelectColumn(col);
const alias = parseColumnAlias(source.rest);
if (alias === null) return parseComputedSelectColumn(col);
return {
sourceName: source.parts[source.parts.length - 1],
resultName: alias ?? source.parts[source.parts.length - 1],
expression: col.slice(0, source.end).trim(),
};
}
// Strip quoting characters
const stripped = col.replace(/[`"'[\]]/g, "");
function parseComputedSelectColumn(col: string): EditableQueryColumn | null {
const alias = parseExpressionAlias(col);
if (!alias) return null;
return {
sourceName: undefined,
resultName: alias.resultName,
expression: alias.expression,
};
}
// Take last identifier (handles table.column -> column)
const parts = stripped.split(".");
return parts[parts.length - 1] || stripped;
function parseExpressionAlias(col: string): { expression: string; resultName: string } | null {
const asMatch = col.match(/\bAS\s+((?:"[^"]+")|(?:`[^`]+`)|(?:\[[^\]]+\])|(?:[A-Za-z_][\w$]*))\s*$/i);
if (asMatch?.index === undefined) return null;
const alias = readIdentifier(asMatch[1], 0);
if (!alias || alias.end !== asMatch[1].length) return null;
const expression = col.slice(0, asMatch.index).trim();
return expression ? { expression, resultName: alias.value } : null;
}
function parseColumnAlias(rest: string): string | undefined | null {
const trimmed = rest.trim();
if (!trimmed) return undefined;
const asMatch = trimmed.match(/^AS\s+/i);
const aliasText = asMatch ? trimmed.slice(asMatch[0].length).trim() : trimmed;
const alias = readIdentifier(aliasText, 0);
if (!alias || alias.end !== aliasText.length) return null;
return alias.value;
}
function isSelectStar(body: string, alias: string | undefined): boolean {
const trimmed = body.trim();
if (trimmed === "*") return true;
if (!alias) return false;
return new RegExp(`^${escapeRegExp(alias)}\\s*\\.\\s*\\*$`, "i").test(trimmed);
}
function parseFromSource(body: string): { schema?: string; tableName: string; alias?: string } | null {
if (!body || /[,()]/.test(body) || /\bJOIN\b/i.test(body)) return null;
const ident = parseQualifiedIdentifier(body);
if (!ident || ident.parts.length < 1 || ident.parts.length > 2) return null;
const tail = ident.rest.trim();
let alias: string | undefined;
if (tail) {
const aliasText = tail.replace(/^AS\s+/i, "").trim();
const aliasIdent = readIdentifier(aliasText, 0);
if (!aliasIdent || aliasIdent.end !== aliasText.length) return null;
alias = aliasIdent.value;
}
const tableName = ident.parts[ident.parts.length - 1];
const schema = ident.parts.length === 2 ? ident.parts[0] : undefined;
return { schema, tableName, alias };
}
function parseQualifiedIdentifier(text: string): { parts: string[]; end: number; rest: string; done: boolean } | null {
const parts: string[] = [];
let pos = 0;
while (pos < text.length) {
pos = skipWhitespace(text, pos);
const ident = readIdentifier(text, pos);
if (!ident) break;
parts.push(ident.value);
pos = skipWhitespace(text, ident.end);
if (text[pos] !== ".") break;
pos++;
}
if (parts.length === 0) return null;
return { parts, end: pos, rest: text.slice(pos), done: text.slice(pos).trim() === "" };
}
function readIdentifier(text: string, start: number): { value: string; end: number } | null {
const pos = skipWhitespace(text, start);
const quote = text[pos];
if (quote === '"' || quote === "`" || quote === "[") {
const close = quote === "[" ? "]" : quote;
let value = "";
for (let i = pos + 1; i < text.length; i++) {
if (text[i] === close) return { value, end: i + 1 };
value += text[i];
}
return null;
}
const match = text.slice(pos).match(/^[A-Za-z_][\w$]*/);
return match ? { value: match[0], end: pos + match[0].length } : null;
}
function skipWhitespace(text: string, pos: number): number {
while (pos < text.length && /\s/.test(text[pos])) pos++;
return pos;
}
function stripSqlComments(sql: string): string {
return sql.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
}
function hasTopLevelKeyword(sql: string, keywords: string[]): boolean {
return keywords.some((keyword) => findTopLevelKeyword(sql, keyword, 0) >= 0);
}
function firstTopLevelKeywordIndex(sql: string, keywords: string[], start: number): number {
const indexes = keywords.map((keyword) => findTopLevelKeyword(sql, keyword, start)).filter((index) => index >= 0);
return indexes.length ? Math.min(...indexes) : -1;
}
function findTopLevelKeyword(sql: string, keyword: string, start: number): number {
let depth = 0;
let quote: string | null = null;
const upperKeyword = keyword.toUpperCase();
for (let i = start; i < sql.length; i++) {
const ch = sql[i];
if (quote) {
if (ch === quote || (quote === "]" && ch === "]")) quote = null;
continue;
}
if (ch === "'" || ch === '"' || ch === "`") {
quote = ch;
continue;
}
if (ch === "[") {
quote = "]";
continue;
}
if (ch === "(") {
depth++;
continue;
}
if (ch === ")") {
depth = Math.max(0, depth - 1);
continue;
}
if (depth !== 0) continue;
if (sql.slice(i, i + keyword.length).toUpperCase() !== upperKeyword) continue;
const before = i === 0 ? "" : sql[i - 1];
const after = sql[i + keyword.length] ?? "";
if (!isIdentifierChar(before) && !isIdentifierChar(after)) return i;
}
return -1;
}
function isIdentifierChar(ch: string): boolean {
return /[\w$]/.test(ch);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Check if all primary key columns are present in the result set columns.
* Comparison is case-insensitive.
*/
export function allPrimaryKeysPresent(primaryKeys: string[], resultColumns: string[]): boolean {
export function allPrimaryKeysPresent(
primaryKeys: string[],
resultColumns: string[],
analysis?: EditableQueryInfo,
): boolean {
if (analysis && !analysis.selectStar) {
const sourceColumns = new Set(
analysis.columns.flatMap((column) => (column.sourceName ? [column.sourceName.toLowerCase()] : [])),
);
return primaryKeys.every((pk) => sourceColumns.has(pk.toLowerCase()));
}
const colSet = new Set(resultColumns.map((c) => c.toLowerCase()));
return primaryKeys.every((pk) => colSet.has(pk.toLowerCase()));
}
export function allEditableColumnsWriteable(analysis: EditableQueryInfo, resultColumns: string[]): boolean {
if (analysis.selectStar) return true;
const sourceByResult = new Map(analysis.columns.map((column) => [column.resultName.toLowerCase(), column]));
return resultColumns.every((resultColumn) => {
const source = sourceByResult.get(resultColumn.toLowerCase());
return !!source;
});
}
export function sourceColumnsForResult(
analysis: EditableQueryInfo,
resultColumns: string[],
): Array<string | undefined> | undefined {
if (analysis.selectStar) return undefined;
const sourceByResult = new Map(analysis.columns.map((column) => [column.resultName.toLowerCase(), column]));
const sourceColumns = resultColumns.map((resultColumn) => sourceByResult.get(resultColumn.toLowerCase())?.sourceName);
return resultColumns.every((resultColumn) => sourceByResult.has(resultColumn.toLowerCase()))
? sourceColumns
: undefined;
}