fix(oracle): 修复表数据加载与编辑

This commit is contained in:
t8y2
2026-05-11 16:35:59 +08:00
parent 79277c660f
commit cab0e79dad
17 changed files with 610 additions and 90 deletions
+102 -9
View File
@@ -1,4 +1,5 @@
import type { DatabaseType } from "@/types/database";
import { DBX_ROWID_COLUMN } from "./tableEditing.ts";
import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts";
export type GridCellValue = string | number | boolean | null;
@@ -9,6 +10,11 @@ export interface DataGridTableMeta {
primaryKeys: string[];
}
export interface DataGridColumnInfo {
name: string;
is_nullable: boolean;
}
export interface DataGridSaveStatementOptions {
databaseType?: DatabaseType;
tableMeta: DataGridTableMeta;
@@ -19,6 +25,42 @@ export interface DataGridSaveStatementOptions {
newRows: GridCellValue[][];
}
export interface DataGridSaveValidationOptions {
databaseType?: DatabaseType;
columns: string[];
columnInfo?: DataGridColumnInfo[];
dirtyRows: Array<[number, Array<[number, GridCellValue]>]>;
newRows: GridCellValue[][];
}
export function validateDataGridSave(options: DataGridSaveValidationOptions): string | undefined {
const notNullColumns = new Set(
(options.columnInfo ?? [])
.filter((column) => !column.is_nullable && !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 column = options.columns[columnIndex];
if (isNullWriteToNotNullColumn(options.databaseType, notNullColumns, column, value)) {
return nullWriteError(column);
}
}
}
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);
}
}
}
return undefined;
}
export function buildDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
const table = qualifiedTableName({
databaseType: options.databaseType,
@@ -31,11 +73,13 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption
const row = options.rows[rowIndex];
if (!row) continue;
const sets = changes
.filter(([columnIndex]) => !isOracleRowId(options.databaseType, options.columns[columnIndex]))
.map(
([columnIndex, value]) =>
`${quoteIdent(options.databaseType, options.columns[columnIndex])} = ${formatGridSqlLiteral(value, options.databaseType)}`,
)
.join(", ");
if (!sets) continue;
const where = buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, row);
statements.push(`UPDATE ${table} SET ${sets} WHERE ${where};`);
}
@@ -48,8 +92,11 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption
}
for (const row of options.newRows) {
const columns = options.columns.map((column) => quoteIdent(options.databaseType, column)).join(", ");
const values = row.map((v) => formatGridSqlLiteral(v, options.databaseType)).join(", ");
const insertPairs = options.columns
.map((column, index) => ({ column, value: row[index] }))
.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(", ");
statements.push(`INSERT INTO ${table} (${columns}) VALUES (${values});`);
}
@@ -72,8 +119,11 @@ export function buildDataGridRollbackStatements(options: DataGridSaveStatementOp
for (const rowIndex of options.deletedRows) {
const row = options.rows[rowIndex];
if (!row) continue;
const columns = options.columns.map((column) => quoteIdent(options.databaseType, column)).join(", ");
const values = row.map((v) => formatGridSqlLiteral(v, options.databaseType)).join(", ");
const insertPairs = options.columns
.map((column, index) => ({ column, value: row[index] }))
.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(", ");
statements.push(`INSERT INTO ${table} (${columns}) VALUES (${values});`);
}
@@ -84,15 +134,19 @@ export function buildDataGridRollbackStatements(options: DataGridSaveStatementOp
for (const [columnIndex, value] of changes) {
afterRow[columnIndex] = value;
}
const sets = changes
const writableChanges = changes.filter(
([columnIndex]) => !isOracleRowId(options.databaseType, options.columns[columnIndex]),
);
const sets = writableChanges
.map(
([columnIndex]) =>
`${quoteIdent(options.databaseType, options.columns[columnIndex])} = ${formatGridSqlLiteral(row[columnIndex], options.databaseType)}`,
)
.join(", ");
if (!sets) continue;
const where = [
buildPrimaryKeyWhere(options.databaseType, options.tableMeta.primaryKeys, options.columns, afterRow),
...changes.map(([columnIndex, value]) =>
...writableChanges.map(([columnIndex, value]) =>
buildColumnPredicate(options.databaseType, options.columns[columnIndex], value),
),
]
@@ -104,6 +158,14 @@ export function buildDataGridRollbackStatements(options: DataGridSaveStatementOp
return statements;
}
export function dataGridSaveExecutionSchema(
databaseType: DatabaseType | undefined,
tableMeta: DataGridTableMeta | undefined,
): string | undefined {
if (databaseType === "oracle") return undefined;
return tableMeta?.schema;
}
export function formatGridSqlLiteral(value: GridCellValue, databaseType?: DatabaseType): string {
if (value === null || value === undefined) return "NULL";
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
@@ -123,21 +185,52 @@ function buildPrimaryKeyWhere(
return primaryKeys
.map((primaryKey) => {
const value = row[columns.indexOf(primaryKey)];
return `${quoteIdent(databaseType, primaryKey)} = ${formatGridSqlLiteral(value, databaseType)}`;
return `${predicateIdent(databaseType, primaryKey)} = ${formatGridSqlLiteral(value, databaseType)}`;
})
.join(" AND ");
}
function buildRowWhere(databaseType: DatabaseType | undefined, columns: string[], row: GridCellValue[]): string {
return columns.map((column, index) => buildColumnPredicate(databaseType, column, row[index])).join(" AND ");
return columns
.map((column, index) =>
isOracleRowId(databaseType, column) ? "" : buildColumnPredicate(databaseType, column, row[index]),
)
.filter(Boolean)
.join(" AND ");
}
function buildColumnPredicate(databaseType: DatabaseType | undefined, column: string, value: GridCellValue): string {
const ident = quoteIdent(databaseType, column);
const ident = predicateIdent(databaseType, column);
if (value === null || value === undefined) return `${ident} IS NULL`;
return `${ident} = ${formatGridSqlLiteral(value, databaseType)}`;
}
function isOracleRowId(databaseType: DatabaseType | undefined, name: string | undefined): boolean {
return databaseType === "oracle" && name?.toUpperCase() === DBX_ROWID_COLUMN;
}
function isNullWriteToNotNullColumn(
databaseType: DatabaseType | undefined,
notNullColumns: Set<string>,
column: string | undefined,
value: GridCellValue,
): boolean {
if (!column || isOracleRowId(databaseType, column)) return false;
return value === null && notNullColumns.has(normalizeColumnName(column));
}
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);
}
function quoteIdent(databaseType: DatabaseType | undefined, name: string): string {
return quoteTableIdentifier(databaseType, name);
}
+21
View File
@@ -0,0 +1,21 @@
import type { ColumnInfo, DatabaseType } from "@/types/database";
export const DBX_ROWID_COLUMN = "__DBX_ROWID";
export function editablePrimaryKeys(databaseType: DatabaseType | undefined, columns: ColumnInfo[]): string[] {
const primaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name);
if (databaseType === "oracle" && primaryKeys.length === 0) return [DBX_ROWID_COLUMN];
return primaryKeys;
}
export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean {
return databaseType === "oracle" && primaryKeys.length === 1 && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN;
}
export function isHiddenGridColumn(
databaseType: DatabaseType | undefined,
column: string,
primaryKeys: string[],
): boolean {
return usesSyntheticRowIdKey(databaseType, primaryKeys) && column.toUpperCase() === DBX_ROWID_COLUMN;
}
+18 -2
View File
@@ -1,5 +1,6 @@
import type { DatabaseType } from "../types/database.ts";
import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts";
import { DBX_ROWID_COLUMN } from "./tableEditing.ts";
export interface BuildTableSelectSqlOptions {
databaseType?: DatabaseType;
@@ -11,6 +12,7 @@ export interface BuildTableSelectSqlOptions {
limit?: number;
offset?: number;
whereInput?: string;
includeRowId?: boolean;
}
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
@@ -19,6 +21,15 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
return `"${name.replace(/"/g, '""')}"`;
}
function isOracleRowId(databaseType: DatabaseType | undefined, name: string): boolean {
return databaseType === "oracle" && name.toUpperCase() === DBX_ROWID_COLUMN;
}
function quoteOrderIdentifier(databaseType: DatabaseType | undefined, name: string, tableAlias?: string): string {
if (isOracleRowId(databaseType, name)) return tableAlias ? `${tableAlias}.ROWID` : "ROWID";
return quoteTableIdentifier(databaseType, name);
}
export function qualifiedTableName(
options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName">,
): string {
@@ -40,17 +51,22 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
const table = qualifiedTableName(options);
const predicate = normalizeWhereInput(options.whereInput);
const where = predicate ? ` WHERE (${predicate})` : "";
const rowIdAlias = options.includeRowId && databaseType === "oracle" ? "t" : undefined;
const defaultOrderBy = options.primaryKeys?.length
? options.primaryKeys.map((pk) => `${quoteTableIdentifier(databaseType, pk)} ASC`).join(", ")
? options.primaryKeys.map((pk) => `${quoteOrderIdentifier(databaseType, pk, rowIdAlias)} ASC`).join(", ")
: options.fallbackOrderColumns?.length
? options.fallbackOrderColumns.map((column) => `${quoteTableIdentifier(databaseType, column)} ASC`).join(", ")
: undefined;
const orderBy = options.orderBy ?? defaultOrderBy;
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
const selectColumns =
options.includeRowId && databaseType === "oracle" ? `ROWIDTOCHAR(t.ROWID) AS "${DBX_ROWID_COLUMN}", t.*` : "*";
const tableAlias = options.includeRowId && usesFetchFirst(databaseType) ? `${table} t` : table;
if (usesFetchFirst(databaseType)) {
const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : "";
return `SELECT * FROM ${table}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
return `SELECT ${selectColumns} FROM ${tableAlias}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
}
if (databaseType === "sqlserver") {