mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 08:23:34 +00:00
fix(oracle): 修复表数据加载与编辑
This commit is contained in:
+102
-9
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user