|
|
|
@@ -1,10 +1,5 @@
|
|
|
|
|
import type { DatabaseType } from "@/types/database";
|
|
|
|
|
import {
|
|
|
|
|
DBX_NEO4J_ELEMENT_ID_COLUMN,
|
|
|
|
|
DBX_ROWID_COLUMN,
|
|
|
|
|
DBX_TDENGINE_TBNAME_COLUMN,
|
|
|
|
|
usesKeylessRowPredicate,
|
|
|
|
|
} from "./tableEditing.ts";
|
|
|
|
|
import { DBX_ROWID_COLUMN } from "./tableEditing.ts";
|
|
|
|
|
import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts";
|
|
|
|
|
|
|
|
|
|
export type GridCellValue = string | number | boolean | null;
|
|
|
|
@@ -54,155 +49,6 @@ export interface DataGridCopyInsertStatementOptions {
|
|
|
|
|
excludePrimaryKeys?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DataGridSaveValidationOptions {
|
|
|
|
|
databaseType?: DatabaseType;
|
|
|
|
|
tableMeta?: DataGridTableMeta;
|
|
|
|
|
columns: string[];
|
|
|
|
|
sourceColumns?: Array<string | undefined>;
|
|
|
|
|
rows?: GridCellValue[][];
|
|
|
|
|
columnInfo?: DataGridColumnInfo[];
|
|
|
|
|
dirtyRows: Array<[number, Array<[number, GridCellValue]>]>;
|
|
|
|
|
newRows: GridCellValue[][];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function validateDataGridSave(options: DataGridSaveValidationOptions): string | undefined {
|
|
|
|
|
const duplicateKeyError = validateInsertedPrimaryKeys(options);
|
|
|
|
|
if (duplicateKeyError) return duplicateKeyError;
|
|
|
|
|
|
|
|
|
|
const notNullColumns = new Set(
|
|
|
|
|
(options.columnInfo ?? [])
|
|
|
|
|
.filter(
|
|
|
|
|
(column) =>
|
|
|
|
|
!column.is_nullable &&
|
|
|
|
|
!column.column_default &&
|
|
|
|
|
!isAutoGeneratedColumn(column) &&
|
|
|
|
|
!isOracleRowId(options.databaseType, column.name),
|
|
|
|
|
)
|
|
|
|
|
.map((column) => normalizeColumnName(column.name)),
|
|
|
|
|
);
|
|
|
|
|
if (notNullColumns.size === 0) return undefined;
|
|
|
|
|
|
|
|
|
|
for (const [, changes] of options.dirtyRows) {
|
|
|
|
|
for (const [columnIndex, value] of changes) {
|
|
|
|
|
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 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateInsertedPrimaryKeys(options: DataGridSaveValidationOptions): string | undefined {
|
|
|
|
|
const primaryKeys = options.tableMeta?.primaryKeys ?? [];
|
|
|
|
|
if (primaryKeys.length === 0 || options.newRows.length === 0) return undefined;
|
|
|
|
|
|
|
|
|
|
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>();
|
|
|
|
|
for (const row of options.rows ?? []) {
|
|
|
|
|
const key = primaryKeyValueKey(primaryKeyIndexes, row);
|
|
|
|
|
if (key) existingKeys.add(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newKeys = new Set<string>();
|
|
|
|
|
for (const row of options.newRows) {
|
|
|
|
|
const key = primaryKeyValueKey(primaryKeyIndexes, row);
|
|
|
|
|
if (!key) continue;
|
|
|
|
|
if (existingKeys.has(key) || newKeys.has(key)) {
|
|
|
|
|
return duplicatePrimaryKeyError(primaryKeys, primaryKeyIndexes, row, existingKeys.has(key));
|
|
|
|
|
}
|
|
|
|
|
newKeys.add(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
|
|
|
|
|
if (options.databaseType === "neo4j") return buildNeo4jDataGridSaveStatements(options);
|
|
|
|
|
if (options.databaseType === "tdengine") return buildTdengineDataGridSaveStatements(options);
|
|
|
|
|
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
|
|
|
|
const columnInfo = columnInfoByName(options.tableMeta.columns);
|
|
|
|
|
|
|
|
|
|
const table = qualifiedTableName({
|
|
|
|
|
databaseType: options.databaseType,
|
|
|
|
|
schema: options.tableMeta.schema,
|
|
|
|
|
tableName: options.tableMeta.tableName,
|
|
|
|
|
});
|
|
|
|
|
const statements: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const [rowIndex, changes] of options.dirtyRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
const sets = changes
|
|
|
|
|
.filter(([columnIndex]) => !!saveColumns[columnIndex])
|
|
|
|
|
.filter(([columnIndex]) => !isOracleRowId(options.databaseType, saveColumns[columnIndex]))
|
|
|
|
|
.map(
|
|
|
|
|
([columnIndex, value]) =>
|
|
|
|
|
`${quoteIdent(options.databaseType, saveColumns[columnIndex]!)} = ${formatGridSqlLiteral(
|
|
|
|
|
value,
|
|
|
|
|
options.databaseType,
|
|
|
|
|
columnInfo.get(normalizeColumnName(saveColumns[columnIndex]!)),
|
|
|
|
|
)}`,
|
|
|
|
|
)
|
|
|
|
|
.join(", ");
|
|
|
|
|
if (!sets) continue;
|
|
|
|
|
const where = buildPrimaryKeyWhere(
|
|
|
|
|
options.databaseType,
|
|
|
|
|
options.tableMeta.primaryKeys,
|
|
|
|
|
saveColumns,
|
|
|
|
|
row,
|
|
|
|
|
columnInfo,
|
|
|
|
|
);
|
|
|
|
|
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,
|
|
|
|
|
saveColumns,
|
|
|
|
|
row,
|
|
|
|
|
columnInfo,
|
|
|
|
|
);
|
|
|
|
|
statements.push(`DELETE FROM ${table} WHERE ${where};`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of options.newRows) {
|
|
|
|
|
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;
|
|
|
|
|
const columns = insertPairs.map((pair) => quoteIdent(options.databaseType, pair.column)).join(", ");
|
|
|
|
|
const values = insertPairs
|
|
|
|
|
.map((pair) =>
|
|
|
|
|
formatGridSqlLiteral(pair.value, options.databaseType, columnInfo.get(normalizeColumnName(pair.column))),
|
|
|
|
|
)
|
|
|
|
|
.join(", ");
|
|
|
|
|
statements.push(`INSERT INTO ${table} (${columns}) VALUES (${values});`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDataGridCopyUpdateStatements(options: DataGridCopyUpdateStatementOptions): string[] {
|
|
|
|
|
if (options.databaseType === "neo4j" || options.databaseType === "tdengine") return [];
|
|
|
|
|
const primaryKeys = options.tableMeta.primaryKeys;
|
|
|
|
@@ -295,154 +141,6 @@ export function buildDataGridCopyInsertStatement(options: DataGridCopyInsertStat
|
|
|
|
|
return `INSERT INTO ${table} (${columns}) VALUES${valueRows.length === 1 ? " " : "\n"}${valueRows.join(",\n")};`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildTdengineDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
|
|
|
|
|
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
|
|
|
|
const statements: string[] = [];
|
|
|
|
|
const canOverwriteExistingRows = saveColumns.some((column) => column?.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN);
|
|
|
|
|
|
|
|
|
|
if (canOverwriteExistingRows) {
|
|
|
|
|
for (const [rowIndex, changes] of options.dirtyRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
const afterRow = [...row];
|
|
|
|
|
for (const [columnIndex, value] of changes) {
|
|
|
|
|
afterRow[columnIndex] = value;
|
|
|
|
|
}
|
|
|
|
|
const statement = buildTdengineInsertStatement(options, saveColumns, afterRow);
|
|
|
|
|
if (statement) statements.push(statement);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of options.newRows) {
|
|
|
|
|
const statement = buildTdengineInsertStatement(options, saveColumns, row);
|
|
|
|
|
if (statement) statements.push(statement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildTdengineInsertStatement(
|
|
|
|
|
options: DataGridSaveStatementOptions,
|
|
|
|
|
saveColumns: Array<string | undefined>,
|
|
|
|
|
row: GridCellValue[],
|
|
|
|
|
): string | undefined {
|
|
|
|
|
const tbname = tdengineTbnameValue(saveColumns, row);
|
|
|
|
|
const table = qualifiedTableName({
|
|
|
|
|
databaseType: "tdengine",
|
|
|
|
|
schema: options.tableMeta.schema,
|
|
|
|
|
tableName: options.tableMeta.tableName,
|
|
|
|
|
});
|
|
|
|
|
const tagColumns = tdengineTagColumns(options.tableMeta.columns);
|
|
|
|
|
const insertPairs = saveColumns
|
|
|
|
|
.map((column, index) => ({ column, value: row[index] }))
|
|
|
|
|
.filter((pair): pair is { column: string; value: GridCellValue } => !!pair.column)
|
|
|
|
|
.filter((pair) => pair.value !== null && pair.value !== undefined)
|
|
|
|
|
.filter((pair) => tdengineCanInsertColumn(pair.column, options.tableMeta.tableName, tbname, tagColumns));
|
|
|
|
|
if (!insertPairs.length) return undefined;
|
|
|
|
|
const columns = insertPairs.map((pair) => quoteIdent("tdengine", pair.column)).join(", ");
|
|
|
|
|
const values = insertPairs.map((pair) => formatGridSqlLiteral(pair.value, "tdengine")).join(", ");
|
|
|
|
|
return `INSERT INTO ${table} (${columns}) VALUES (${values});`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tdengineTbnameValue(saveColumns: Array<string | undefined>, row: GridCellValue[]): string | undefined {
|
|
|
|
|
const index = saveColumns.findIndex((column) => column?.toLowerCase() === DBX_TDENGINE_TBNAME_COLUMN);
|
|
|
|
|
if (index < 0) return undefined;
|
|
|
|
|
const value = row[index];
|
|
|
|
|
return value === null || value === undefined ? undefined : String(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tdengineCanInsertColumn(
|
|
|
|
|
column: string,
|
|
|
|
|
tableName: string,
|
|
|
|
|
tbname: string | undefined,
|
|
|
|
|
tagColumns: Set<string>,
|
|
|
|
|
): boolean {
|
|
|
|
|
const normalized = column.toLowerCase();
|
|
|
|
|
const targetIsChildTable = !tbname || tbname === tableName;
|
|
|
|
|
if (!targetIsChildTable) return true;
|
|
|
|
|
if (normalized === DBX_TDENGINE_TBNAME_COLUMN) return false;
|
|
|
|
|
return !tagColumns.has(normalized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tdengineTagColumns(columns: DataGridColumnInfo[] | undefined): Set<string> {
|
|
|
|
|
return new Set(
|
|
|
|
|
(columns ?? []).filter((column) => /\btag\b/i.test(column.extra ?? "")).map((column) => column.name.toLowerCase()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDataGridRollbackStatements(options: DataGridSaveStatementOptions): string[] {
|
|
|
|
|
if (options.databaseType === "neo4j") return buildNeo4jDataGridRollbackStatements(options);
|
|
|
|
|
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
|
|
|
|
|
const columnInfo = columnInfoByName(options.tableMeta.columns);
|
|
|
|
|
|
|
|
|
|
const table = qualifiedTableName({
|
|
|
|
|
databaseType: options.databaseType,
|
|
|
|
|
schema: options.tableMeta.schema,
|
|
|
|
|
tableName: options.tableMeta.tableName,
|
|
|
|
|
});
|
|
|
|
|
const statements: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const row of options.newRows) {
|
|
|
|
|
const where = buildRowWhere(options.databaseType, saveColumns, row, columnInfo);
|
|
|
|
|
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 = 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, columnInfo.get(normalizeColumnName(pair.column))),
|
|
|
|
|
)
|
|
|
|
|
.join(", ");
|
|
|
|
|
statements.push(`INSERT INTO ${table} (${columns}) VALUES (${values});`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [rowIndex, changes] of options.dirtyRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
const afterRow = [...row];
|
|
|
|
|
for (const [columnIndex, value] of changes) {
|
|
|
|
|
afterRow[columnIndex] = value;
|
|
|
|
|
}
|
|
|
|
|
const writableChanges = changes.filter(
|
|
|
|
|
([columnIndex]) => !!saveColumns[columnIndex] && !isOracleRowId(options.databaseType, saveColumns[columnIndex]),
|
|
|
|
|
);
|
|
|
|
|
const sets = writableChanges
|
|
|
|
|
.map(
|
|
|
|
|
([columnIndex]) =>
|
|
|
|
|
`${quoteIdent(options.databaseType, saveColumns[columnIndex]!)} = ${formatGridSqlLiteral(
|
|
|
|
|
row[columnIndex],
|
|
|
|
|
options.databaseType,
|
|
|
|
|
columnInfo.get(normalizeColumnName(saveColumns[columnIndex]!)),
|
|
|
|
|
)}`,
|
|
|
|
|
)
|
|
|
|
|
.join(", ");
|
|
|
|
|
if (!sets) continue;
|
|
|
|
|
const where = [
|
|
|
|
|
buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, saveColumns, afterRow, columnInfo),
|
|
|
|
|
...writableChanges.map(([columnIndex, value]) =>
|
|
|
|
|
buildColumnPredicate(
|
|
|
|
|
options.databaseType,
|
|
|
|
|
saveColumns[columnIndex]!,
|
|
|
|
|
value,
|
|
|
|
|
columnInfo.get(normalizeColumnName(saveColumns[columnIndex]!)),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(" AND ");
|
|
|
|
|
statements.push(`UPDATE ${table} SET ${sets} WHERE ${where};`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function effectiveColumns(
|
|
|
|
|
sourceColumns: Array<string | undefined> | undefined,
|
|
|
|
|
columns: string[],
|
|
|
|
@@ -451,27 +149,10 @@ function effectiveColumns(
|
|
|
|
|
return sourceColumns;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function effectiveColumn(
|
|
|
|
|
sourceColumns: Array<string | undefined> | undefined,
|
|
|
|
|
columns: string[],
|
|
|
|
|
index: number,
|
|
|
|
|
): string | undefined {
|
|
|
|
|
return effectiveColumns(sourceColumns, columns)[index];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function columnInfoByName(columns: DataGridColumnInfo[] | undefined): Map<string, DataGridColumnInfo> {
|
|
|
|
|
return new Map((columns ?? []).map((column) => [normalizeColumnName(column.name), column]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function dataGridSaveExecutionSchema(
|
|
|
|
|
databaseType: DatabaseType | undefined,
|
|
|
|
|
tableMeta: DataGridTableMeta | undefined,
|
|
|
|
|
): string | undefined {
|
|
|
|
|
if (databaseType === "neo4j") return undefined;
|
|
|
|
|
if (databaseType === "oracle") return undefined;
|
|
|
|
|
return tableMeta?.schema;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeDataGridSaveError(databaseType: DatabaseType | undefined, error: unknown): string {
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
|
if (databaseType === "hive" && /Attempt to do update or delete|Error 10294/i.test(message)) {
|
|
|
|
@@ -560,43 +241,6 @@ function localTimezoneOffsetSuffix(text: string): string {
|
|
|
|
|
return `${sign}${hours}:${minutes}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildPrimaryKeyWhere(
|
|
|
|
|
databaseType: DatabaseType | undefined,
|
|
|
|
|
primaryKeys: string[],
|
|
|
|
|
columns: Array<string | undefined>,
|
|
|
|
|
row: GridCellValue[],
|
|
|
|
|
columnInfo: Map<string, DataGridColumnInfo> = new Map(),
|
|
|
|
|
): string {
|
|
|
|
|
if (primaryKeys.length === 0 && usesKeylessRowPredicate(databaseType))
|
|
|
|
|
return buildRowWhere(databaseType, columns, row, columnInfo);
|
|
|
|
|
return primaryKeys
|
|
|
|
|
.map((primaryKey) => {
|
|
|
|
|
const value = row[columns.indexOf(primaryKey)];
|
|
|
|
|
return `${predicateIdent(databaseType, primaryKey)} = ${formatGridSqlLiteral(
|
|
|
|
|
value,
|
|
|
|
|
databaseType,
|
|
|
|
|
columnInfo.get(normalizeColumnName(primaryKey)),
|
|
|
|
|
)}`;
|
|
|
|
|
})
|
|
|
|
|
.join(" AND ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRowWhere(
|
|
|
|
|
databaseType: DatabaseType | undefined,
|
|
|
|
|
columns: Array<string | undefined>,
|
|
|
|
|
row: GridCellValue[],
|
|
|
|
|
columnInfo: Map<string, DataGridColumnInfo> = new Map(),
|
|
|
|
|
): string {
|
|
|
|
|
return columns
|
|
|
|
|
.map((column, index) =>
|
|
|
|
|
!column || isOracleRowId(databaseType, column)
|
|
|
|
|
? ""
|
|
|
|
|
: buildColumnPredicate(databaseType, column, row[index], columnInfo.get(normalizeColumnName(column))),
|
|
|
|
|
)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(" AND ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildColumnPredicate(
|
|
|
|
|
databaseType: DatabaseType | undefined,
|
|
|
|
|
column: string,
|
|
|
|
@@ -612,63 +256,15 @@ function isOracleRowId(databaseType: DatabaseType | undefined, name: string | un
|
|
|
|
|
return databaseType === "oracle" && name?.toUpperCase() === DBX_ROWID_COLUMN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isNeo4jElementId(databaseType: DatabaseType | undefined, name: string | undefined): boolean {
|
|
|
|
|
return databaseType === "neo4j" && name === DBX_NEO4J_ELEMENT_ID_COLUMN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isAutoGeneratedColumn(column: DataGridColumnInfo): boolean {
|
|
|
|
|
return /\b(auto_increment|autoincrement|identity)\b/i.test(column.extra ?? "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isNullWriteToNotNullColumn(
|
|
|
|
|
databaseType: DatabaseType | undefined,
|
|
|
|
|
notNullColumns: Set<string>,
|
|
|
|
|
column: string | undefined,
|
|
|
|
|
value: GridCellValue,
|
|
|
|
|
): boolean {
|
|
|
|
|
if (!column || isOracleRowId(databaseType, column)) return false;
|
|
|
|
|
if (isNeo4jElementId(databaseType, column)) return false;
|
|
|
|
|
return value === null && notNullColumns.has(normalizeColumnName(column));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findColumnIndex(columns: Array<string | undefined>, target: string): number {
|
|
|
|
|
const normalizedTarget = normalizeColumnName(target);
|
|
|
|
|
return columns.findIndex((column) => (column ? normalizeColumnName(column) : "") === normalizedTarget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function primaryKeyValueKey(primaryKeyIndexes: number[], row: GridCellValue[]): string | undefined {
|
|
|
|
|
const values = primaryKeyIndexes.map((index) => row[index]);
|
|
|
|
|
if (values.some((value) => value === null || value === undefined)) return undefined;
|
|
|
|
|
return JSON.stringify(values);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function duplicatePrimaryKeyError(
|
|
|
|
|
primaryKeys: string[],
|
|
|
|
|
primaryKeyIndexes: number[],
|
|
|
|
|
row: GridCellValue[],
|
|
|
|
|
matchesExistingRow: boolean,
|
|
|
|
|
): string {
|
|
|
|
|
const keySummary = primaryKeys
|
|
|
|
|
.map((primaryKey, index) => `${primaryKey} = ${formatKeyValueForMessage(row[primaryKeyIndexes[index]])}`)
|
|
|
|
|
.join(", ");
|
|
|
|
|
const source = matchesExistingRow ? "the existing primary key" : "another new row's primary key";
|
|
|
|
|
return `New row duplicates ${source} (${keySummary}). Change the key before saving.`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatKeyValueForMessage(value: GridCellValue): string {
|
|
|
|
|
if (value === null || value === undefined) return "NULL";
|
|
|
|
|
if (typeof value === "string") return `"${value.replace(/"/g, '\\"')}"`;
|
|
|
|
|
return String(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeColumnName(name: string): string {
|
|
|
|
|
return name.toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nullWriteError(column: string): string {
|
|
|
|
|
return `Column "${column}" does not allow NULL.`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function predicateIdent(databaseType: DatabaseType | undefined, name: string): string {
|
|
|
|
|
return isOracleRowId(databaseType, name) ? "ROWIDTOCHAR(ROWID)" : quoteIdent(databaseType, name);
|
|
|
|
|
}
|
|
|
|
@@ -676,98 +272,3 @@ function predicateIdent(databaseType: DatabaseType | undefined, name: string): s
|
|
|
|
|
function quoteIdent(databaseType: DatabaseType | undefined, name: string): string {
|
|
|
|
|
return quoteTableIdentifier(databaseType, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildNeo4jDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
|
|
|
|
|
const label = quoteIdent("neo4j", options.tableMeta.tableName);
|
|
|
|
|
const statements: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const [rowIndex, changes] of options.dirtyRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
const sets = changes
|
|
|
|
|
.filter(([columnIndex]) => !isNeo4jElementId("neo4j", options.columns[columnIndex]))
|
|
|
|
|
.map(
|
|
|
|
|
([columnIndex, value]) =>
|
|
|
|
|
`n.${quoteIdent("neo4j", options.columns[columnIndex])} = ${formatGridSqlLiteral(value, "neo4j")}`,
|
|
|
|
|
)
|
|
|
|
|
.join(", ");
|
|
|
|
|
if (!sets) continue;
|
|
|
|
|
statements.push(`MATCH (n:${label}) WHERE ${neo4jElementIdPredicate(options.columns, row)} SET ${sets};`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const rowIndex of options.deletedRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
statements.push(`MATCH (n:${label}) WHERE ${neo4jElementIdPredicate(options.columns, row)} DETACH DELETE n;`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const row of options.newRows) {
|
|
|
|
|
const props = options.columns
|
|
|
|
|
.map((column, index) => ({ column, value: row[index] }))
|
|
|
|
|
.filter((pair) => !isNeo4jElementId("neo4j", pair.column))
|
|
|
|
|
.filter((pair) => pair.value !== null && pair.value !== undefined)
|
|
|
|
|
.map((pair) => `${quoteIdent("neo4j", pair.column)}: ${formatGridSqlLiteral(pair.value, "neo4j")}`)
|
|
|
|
|
.join(", ");
|
|
|
|
|
statements.push(props ? `CREATE (n:${label} {${props}});` : `CREATE (n:${label});`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildNeo4jDataGridRollbackStatements(options: DataGridSaveStatementOptions): string[] {
|
|
|
|
|
const label = quoteIdent("neo4j", options.tableMeta.tableName);
|
|
|
|
|
const statements: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const row of options.newRows) {
|
|
|
|
|
const where = neo4jRowPropertyWhere(options.columns, row);
|
|
|
|
|
statements.push(
|
|
|
|
|
where ? `MATCH (n:${label}) WHERE ${where} DETACH DELETE n;` : `MATCH (n:${label}) DETACH DELETE n;`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const rowIndex of options.deletedRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
const props = options.columns
|
|
|
|
|
.map((column, index) => ({ column, value: row[index] }))
|
|
|
|
|
.filter((pair) => !isNeo4jElementId("neo4j", pair.column))
|
|
|
|
|
.filter((pair) => pair.value !== null && pair.value !== undefined)
|
|
|
|
|
.map((pair) => `${quoteIdent("neo4j", pair.column)}: ${formatGridSqlLiteral(pair.value, "neo4j")}`)
|
|
|
|
|
.join(", ");
|
|
|
|
|
statements.push(props ? `CREATE (n:${label} {${props}});` : `CREATE (n:${label});`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [rowIndex, changes] of options.dirtyRows) {
|
|
|
|
|
const row = options.rows[rowIndex];
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
const sets = changes
|
|
|
|
|
.filter(([columnIndex]) => !isNeo4jElementId("neo4j", options.columns[columnIndex]))
|
|
|
|
|
.map(
|
|
|
|
|
([columnIndex]) =>
|
|
|
|
|
`n.${quoteIdent("neo4j", options.columns[columnIndex])} = ${formatGridSqlLiteral(row[columnIndex], "neo4j")}`,
|
|
|
|
|
)
|
|
|
|
|
.join(", ");
|
|
|
|
|
if (!sets) continue;
|
|
|
|
|
statements.push(`MATCH (n:${label}) WHERE ${neo4jElementIdPredicate(options.columns, row)} SET ${sets};`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return statements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function neo4jElementIdPredicate(columns: string[], row: GridCellValue[]): string {
|
|
|
|
|
const value = row[columns.indexOf(DBX_NEO4J_ELEMENT_ID_COLUMN)];
|
|
|
|
|
return `elementId(n) = ${formatGridSqlLiteral(value, "neo4j")}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function neo4jRowPropertyWhere(columns: string[], row: GridCellValue[]): string {
|
|
|
|
|
return columns
|
|
|
|
|
.map((column, index) => {
|
|
|
|
|
if (isNeo4jElementId("neo4j", column)) return "";
|
|
|
|
|
const value = row[index];
|
|
|
|
|
const ident = `n.${quoteIdent("neo4j", column)}`;
|
|
|
|
|
if (value === null || value === undefined) return `${ident} IS NULL`;
|
|
|
|
|
return `${ident} = ${formatGridSqlLiteral(value, "neo4j")}`;
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(" AND ");
|
|
|
|
|
}
|
|
|
|
|