feat: lighten ai agent admin frontend
This commit is contained in:
@@ -1,592 +0,0 @@
|
||||
import { mapToDbType } from '#/utils/database-types';
|
||||
import {
|
||||
quoteIdentifier,
|
||||
quoteTable,
|
||||
} from '#/views/_core/database-manager/utils/sql-identifier';
|
||||
|
||||
export interface ColumnFieldDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
length?: number;
|
||||
precision?: number;
|
||||
scale?: number;
|
||||
nullable: boolean;
|
||||
default?: string;
|
||||
primaryKey: boolean;
|
||||
unique: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
function normalizeDbType(dbType: string): string {
|
||||
const db = (dbType || 'postgresql').toLowerCase();
|
||||
if (db === 'sql server' || db === 'mssql') {
|
||||
return 'sqlserver';
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
function escapeSqlString(value: string): string {
|
||||
return value.replaceAll("'", "''");
|
||||
}
|
||||
|
||||
function buildTypeDef(field: ColumnFieldDefinition, dbType: string): string {
|
||||
const db = normalizeDbType(dbType);
|
||||
let typeDef = mapToDbType(field.type, dbType);
|
||||
|
||||
const typesWithLength = [
|
||||
'varchar',
|
||||
'char',
|
||||
'nvarchar',
|
||||
'nchar',
|
||||
'varbinary',
|
||||
'binary',
|
||||
];
|
||||
const typesWithPrecision = ['decimal', 'numeric'];
|
||||
const mysqlTypesWithPrecision = ['float'];
|
||||
const lowerType = field.type.toLowerCase();
|
||||
|
||||
if (
|
||||
typesWithLength.includes(lowerType) &&
|
||||
field.length &&
|
||||
field.length > 0
|
||||
) {
|
||||
typeDef += `(${field.length})`;
|
||||
} else if (typesWithPrecision.includes(lowerType) && field.precision) {
|
||||
typeDef += field.scale
|
||||
? `(${field.precision}, ${field.scale})`
|
||||
: `(${field.precision})`;
|
||||
} else if (
|
||||
db === 'mysql' &&
|
||||
mysqlTypesWithPrecision.includes(lowerType) &&
|
||||
field.precision
|
||||
) {
|
||||
typeDef += field.scale
|
||||
? `(${field.precision}, ${field.scale})`
|
||||
: `(${field.precision})`;
|
||||
}
|
||||
|
||||
return typeDef;
|
||||
}
|
||||
|
||||
export function buildColumnDefinition(
|
||||
field: ColumnFieldDefinition,
|
||||
dbType: string,
|
||||
): string {
|
||||
const db = normalizeDbType(dbType);
|
||||
const col = quoteIdentifier(field.name, db);
|
||||
let def = `${col} ${buildTypeDef(field, dbType)}`;
|
||||
|
||||
if (!field.nullable) {
|
||||
def += ' NOT NULL';
|
||||
}
|
||||
|
||||
if (field.default) {
|
||||
def += ` DEFAULT ${field.default}`;
|
||||
}
|
||||
|
||||
if (field.comment && db === 'mysql') {
|
||||
def += ` COMMENT '${escapeSqlString(field.comment)}'`;
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
function buildSqlServerColumnExtendedProperty(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
comment: string,
|
||||
schema: string,
|
||||
isUpdate: boolean,
|
||||
): string {
|
||||
const proc = isUpdate ? 'sp_updateextendedproperty' : 'sp_addextendedproperty';
|
||||
return `EXEC ${proc} @name=N'MS_Description', @value=N'${escapeSqlString(comment)}', @level0type=N'SCHEMA', @level0name=N'${escapeSqlString(schema)}', @level1type=N'TABLE', @level1name=N'${escapeSqlString(tableName)}', @level2type=N'COLUMN', @level2name=N'${escapeSqlString(columnName)}';`;
|
||||
}
|
||||
|
||||
function buildSqlServerDropDefaultSql(
|
||||
tableRef: string,
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
schema: string,
|
||||
): string {
|
||||
const schemaName = escapeSqlString(schema || 'dbo');
|
||||
const table = escapeSqlString(tableName);
|
||||
const column = escapeSqlString(columnName);
|
||||
return `DECLARE @sql NVARCHAR(MAX);
|
||||
SELECT @sql = N'ALTER TABLE ${tableRef} DROP CONSTRAINT ' + QUOTENAME(dc.name)
|
||||
FROM sys.default_constraints dc
|
||||
INNER JOIN sys.columns col ON dc.parent_object_id = col.object_id AND dc.parent_column_id = col.column_id
|
||||
INNER JOIN sys.tables tab ON col.object_id = tab.object_id
|
||||
INNER JOIN sys.schemas sch ON tab.schema_id = sch.schema_id
|
||||
WHERE sch.name = N'${schemaName}' AND tab.name = N'${table}' AND col.name = N'${column}';
|
||||
IF @sql IS NOT NULL EXEC sp_executesql @sql;`;
|
||||
}
|
||||
|
||||
function buildSqlServerTableExtendedProperty(
|
||||
tableName: string,
|
||||
comment: string,
|
||||
schema: string,
|
||||
isUpdate: boolean,
|
||||
): string {
|
||||
const proc = isUpdate ? 'sp_updateextendedproperty' : 'sp_addextendedproperty';
|
||||
return `EXEC ${proc} @name=N'MS_Description', @value=N'${escapeSqlString(comment)}', @level0type=N'SCHEMA', @level0name=N'${escapeSqlString(schema)}', @level1type=N'TABLE', @level1name=N'${escapeSqlString(tableName)}';`;
|
||||
}
|
||||
|
||||
export function buildAddColumnSql(
|
||||
tableRef: string,
|
||||
field: ColumnFieldDefinition,
|
||||
dbType: string,
|
||||
options?: { schema?: string; tableName?: string; hadComment?: boolean },
|
||||
): string[] {
|
||||
const db = normalizeDbType(dbType);
|
||||
const columnDef = buildColumnDefinition(field, dbType);
|
||||
const statements: string[] = [];
|
||||
|
||||
switch (db) {
|
||||
case 'mysql': {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${columnDef};`);
|
||||
break;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD ${columnDef};`);
|
||||
if (field.comment && options?.tableName) {
|
||||
statements.push(
|
||||
buildSqlServerColumnExtendedProperty(
|
||||
options.tableName,
|
||||
field.name,
|
||||
field.comment,
|
||||
options.schema || 'dbo',
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'oracle': {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD (${columnDef});`);
|
||||
if (field.comment) {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${tableRef}.${quoteIdentifier(field.name, db)} IS '${escapeSqlString(field.comment)}';`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'postgresql':
|
||||
default: {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${columnDef};`);
|
||||
if (field.comment) {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${tableRef}.${quoteIdentifier(field.name, db)} IS '${escapeSqlString(field.comment)}';`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
export function buildDropColumnSql(
|
||||
tableRef: string,
|
||||
columnName: string,
|
||||
dbType: string,
|
||||
): string {
|
||||
const db = normalizeDbType(dbType);
|
||||
return `ALTER TABLE ${tableRef} DROP COLUMN ${quoteIdentifier(columnName, db)};`;
|
||||
}
|
||||
|
||||
export function buildTableCommentAlterSql(
|
||||
tableRef: string,
|
||||
tableName: string,
|
||||
comment: string,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
hadComment?: boolean,
|
||||
): string | null {
|
||||
const db = normalizeDbType(dbType);
|
||||
const escaped = escapeSqlString(comment);
|
||||
|
||||
switch (db) {
|
||||
case 'postgresql':
|
||||
case 'oracle': {
|
||||
return `COMMENT ON TABLE ${tableRef} IS '${escaped}';`;
|
||||
}
|
||||
case 'mysql': {
|
||||
return `ALTER TABLE ${tableRef} COMMENT = '${escaped}';`;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
return buildSqlServerTableExtendedProperty(
|
||||
tableName,
|
||||
comment,
|
||||
schema || 'dbo',
|
||||
Boolean(hadComment),
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildColumnCommentAlterSql(
|
||||
tableRef: string,
|
||||
tableName: string,
|
||||
field: ColumnFieldDefinition,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
hadComment?: boolean,
|
||||
): string[] {
|
||||
const db = normalizeDbType(dbType);
|
||||
const col = quoteIdentifier(field.name, db);
|
||||
const comment = field.comment || '';
|
||||
const statements: string[] = [];
|
||||
|
||||
switch (db) {
|
||||
case 'postgresql':
|
||||
case 'oracle': {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${tableRef}.${col} IS '${escapeSqlString(comment)}';`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'mysql': {
|
||||
const columnDef = buildColumnDefinition(field, dbType);
|
||||
statements.push(`ALTER TABLE ${tableRef} MODIFY COLUMN ${columnDef};`);
|
||||
break;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
statements.push(
|
||||
buildSqlServerColumnExtendedProperty(
|
||||
tableName,
|
||||
field.name,
|
||||
comment,
|
||||
schema || 'dbo',
|
||||
Boolean(hadComment),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
export function buildFieldAlterSql(
|
||||
originalFields: ColumnFieldDefinition[],
|
||||
currentFields: ColumnFieldDefinition[],
|
||||
tableRef: string,
|
||||
tableName: string,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
): string[] {
|
||||
const db = normalizeDbType(dbType);
|
||||
const statements: string[] = [];
|
||||
const colRef = (name: string) => quoteIdentifier(name, db);
|
||||
|
||||
for (const origField of originalFields) {
|
||||
if (!currentFields.find((f) => f.name === origField.name)) {
|
||||
statements.push(buildDropColumnSql(tableRef, origField.name, dbType));
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of currentFields) {
|
||||
if (!originalFields.find((f) => f.name === field.name)) {
|
||||
statements.push(
|
||||
...buildAddColumnSql(tableRef, field, dbType, {
|
||||
schema,
|
||||
tableName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of currentFields) {
|
||||
const origField = originalFields.find((f) => f.name === field.name);
|
||||
if (!origField || JSON.stringify(field) === JSON.stringify(origField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const col = colRef(field.name);
|
||||
const typeDef = buildTypeDef(field, dbType);
|
||||
const origTypeDef = buildTypeDef(origField, dbType);
|
||||
|
||||
if (db === 'postgresql') {
|
||||
const typeChanged =
|
||||
field.type !== origField.type ||
|
||||
field.length !== origField.length ||
|
||||
field.precision !== origField.precision ||
|
||||
field.scale !== origField.scale;
|
||||
|
||||
if (typeChanged) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ALTER COLUMN ${col} TYPE ${typeDef} USING ${col}::${typeDef};`,
|
||||
);
|
||||
}
|
||||
if (field.nullable !== origField.nullable) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ALTER COLUMN ${col} ${field.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'};`,
|
||||
);
|
||||
}
|
||||
if (field.default !== origField.default) {
|
||||
if (field.default) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ALTER COLUMN ${col} SET DEFAULT ${field.default};`,
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ALTER COLUMN ${col} DROP DEFAULT;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (field.unique !== origField.unique) {
|
||||
if (field.unique) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ADD CONSTRAINT ${colRef(`${tableName}_${field.name}_key`)} UNIQUE (${col});`,
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP CONSTRAINT IF EXISTS ${colRef(`${tableName}_${field.name}_key`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (field.primaryKey !== origField.primaryKey) {
|
||||
if (field.primaryKey) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ADD PRIMARY KEY (${col});`,
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP CONSTRAINT IF EXISTS ${colRef(`${tableName}_pkey`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (field.comment !== origField.comment) {
|
||||
statements.push(
|
||||
...buildColumnCommentAlterSql(
|
||||
tableRef,
|
||||
tableName,
|
||||
field,
|
||||
dbType,
|
||||
schema,
|
||||
Boolean(origField.comment),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (db === 'mysql') {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} MODIFY COLUMN ${buildColumnDefinition(field, dbType)};`,
|
||||
);
|
||||
|
||||
if (field.unique !== origField.unique) {
|
||||
if (field.unique) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ADD UNIQUE INDEX ${colRef(`${tableName}_${field.name}_key`)} (${col});`,
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP INDEX IF EXISTS ${colRef(`${tableName}_${field.name}_key`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (field.primaryKey !== origField.primaryKey) {
|
||||
if (field.primaryKey) {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD PRIMARY KEY (${col});`);
|
||||
} else {
|
||||
statements.push(`ALTER TABLE ${tableRef} DROP PRIMARY KEY;`);
|
||||
}
|
||||
}
|
||||
} else if (db === 'sqlserver') {
|
||||
if (typeDef !== origTypeDef) {
|
||||
let alterDef = `${col} ${typeDef}`;
|
||||
alterDef += field.nullable ? ' NULL' : ' NOT NULL';
|
||||
statements.push(`ALTER TABLE ${tableRef} ALTER COLUMN ${alterDef};`);
|
||||
} else if (field.nullable !== origField.nullable) {
|
||||
let alterDef = `${col} ${typeDef}`;
|
||||
alterDef += field.nullable ? ' NULL' : ' NOT NULL';
|
||||
statements.push(`ALTER TABLE ${tableRef} ALTER COLUMN ${alterDef};`);
|
||||
}
|
||||
|
||||
if (field.default !== origField.default) {
|
||||
if (field.default) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ADD DEFAULT ${field.default} FOR ${col};`,
|
||||
);
|
||||
} else if (origField.default) {
|
||||
statements.push(
|
||||
buildSqlServerDropDefaultSql(
|
||||
tableRef,
|
||||
tableName,
|
||||
field.name,
|
||||
schema || 'dbo',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.comment !== origField.comment) {
|
||||
statements.push(
|
||||
...buildColumnCommentAlterSql(
|
||||
tableRef,
|
||||
tableName,
|
||||
field,
|
||||
dbType,
|
||||
schema,
|
||||
Boolean(origField.comment),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (field.unique !== origField.unique) {
|
||||
if (field.unique) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ADD CONSTRAINT ${colRef(`${tableName}_${field.name}_key`)} UNIQUE (${col});`,
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP CONSTRAINT ${colRef(`${tableName}_${field.name}_key`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (field.primaryKey !== origField.primaryKey) {
|
||||
if (field.primaryKey) {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD PRIMARY KEY (${col});`);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP CONSTRAINT ${colRef(`${tableName}_pkey`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (db === 'oracle') {
|
||||
const structureChanged =
|
||||
typeDef !== origTypeDef ||
|
||||
field.nullable !== origField.nullable ||
|
||||
field.default !== origField.default;
|
||||
|
||||
if (structureChanged) {
|
||||
let modifyDef = `${col} ${typeDef}`;
|
||||
modifyDef += field.nullable ? ' NULL' : ' NOT NULL';
|
||||
if (field.default !== origField.default) {
|
||||
modifyDef += field.default
|
||||
? ` DEFAULT ${field.default}`
|
||||
: ' DEFAULT NULL';
|
||||
}
|
||||
statements.push(`ALTER TABLE ${tableRef} MODIFY (${modifyDef});`);
|
||||
}
|
||||
|
||||
if (field.comment !== origField.comment) {
|
||||
statements.push(
|
||||
...buildColumnCommentAlterSql(
|
||||
tableRef,
|
||||
tableName,
|
||||
field,
|
||||
dbType,
|
||||
schema,
|
||||
Boolean(origField.comment),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (field.unique !== origField.unique) {
|
||||
if (field.unique) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} ADD CONSTRAINT ${colRef(`${tableName}_${field.name}_key`)} UNIQUE (${col});`,
|
||||
);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP CONSTRAINT ${colRef(`${tableName}_${field.name}_key`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (field.primaryKey !== origField.primaryKey) {
|
||||
if (field.primaryKey) {
|
||||
statements.push(`ALTER TABLE ${tableRef} ADD PRIMARY KEY (${col});`);
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} DROP CONSTRAINT ${colRef(`${tableName}_pk`)};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
/** 建表后追加表/列注释 SQL */
|
||||
export function buildCreateTableCommentSql(
|
||||
tableName: string,
|
||||
schema: string | undefined,
|
||||
dbType: string,
|
||||
tableComment: string,
|
||||
fields: ColumnFieldDefinition[],
|
||||
): string[] {
|
||||
const db = normalizeDbType(dbType);
|
||||
const tableRef = quoteTable(schema, tableName, db);
|
||||
const statements: string[] = [];
|
||||
|
||||
if (tableComment) {
|
||||
switch (db) {
|
||||
case 'postgresql':
|
||||
case 'oracle': {
|
||||
statements.push(
|
||||
`COMMENT ON TABLE ${tableRef} IS '${escapeSqlString(tableComment)}';`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'mysql': {
|
||||
statements.push(
|
||||
`ALTER TABLE ${tableRef} COMMENT = '${escapeSqlString(tableComment)}';`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
statements.push(
|
||||
buildSqlServerTableExtendedProperty(
|
||||
tableName,
|
||||
tableComment,
|
||||
schema || 'dbo',
|
||||
false,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (!field.comment) {
|
||||
continue;
|
||||
}
|
||||
switch (db) {
|
||||
case 'postgresql':
|
||||
case 'oracle': {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${tableRef}.${quoteIdentifier(field.name, db)} IS '${escapeSqlString(field.comment)}';`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
statements.push(
|
||||
buildSqlServerColumnExtendedProperty(
|
||||
tableName,
|
||||
field.name,
|
||||
field.comment,
|
||||
schema || 'dbo',
|
||||
false,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
import type { ConstraintInfo } from '#/api/core/database-manager';
|
||||
|
||||
export interface TableConstraintDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
definition?: string;
|
||||
columns?: string[];
|
||||
referencedTable?: string;
|
||||
referencedColumns?: string[];
|
||||
onDelete?: string;
|
||||
onUpdate?: string;
|
||||
}
|
||||
|
||||
export const FOREIGN_KEY_ACTIONS = [
|
||||
'NO ACTION',
|
||||
'RESTRICT',
|
||||
'CASCADE',
|
||||
'SET NULL',
|
||||
'SET DEFAULT',
|
||||
] as const;
|
||||
|
||||
export type ForeignKeyAction = (typeof FOREIGN_KEY_ACTIONS)[number];
|
||||
|
||||
export function normalizeForeignKeyAction(value?: string): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.toUpperCase().replaceAll(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
export function parseForeignKeyActions(definition?: string): {
|
||||
onDelete: string;
|
||||
onUpdate: string;
|
||||
} {
|
||||
const result = { onDelete: '', onUpdate: '' };
|
||||
if (!definition) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const onDeleteMatch = definition.match(
|
||||
/\sON DELETE (NO ACTION|RESTRICT|CASCADE|SET NULL|SET DEFAULT)\b/i,
|
||||
);
|
||||
const onUpdateMatch = definition.match(
|
||||
/\sON UPDATE (NO ACTION|RESTRICT|CASCADE|SET NULL|SET DEFAULT)\b/i,
|
||||
);
|
||||
|
||||
if (onDeleteMatch?.[1]) {
|
||||
result.onDelete = normalizeForeignKeyAction(onDeleteMatch[1]);
|
||||
}
|
||||
if (onUpdateMatch?.[1]) {
|
||||
result.onUpdate = normalizeForeignKeyAction(onUpdateMatch[1]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildForeignKeyActionsSql(
|
||||
constraint: TableConstraintDefinition,
|
||||
dbType?: string,
|
||||
): string {
|
||||
const db = (dbType || 'postgresql').toLowerCase();
|
||||
const supportsOnUpdate = db !== 'sqlserver' && db !== 'oracle' && db !== 'mssql';
|
||||
|
||||
const parts: string[] = [];
|
||||
if (constraint.onDelete) {
|
||||
parts.push(`ON DELETE ${constraint.onDelete}`);
|
||||
}
|
||||
if (supportsOnUpdate && constraint.onUpdate) {
|
||||
parts.push(`ON UPDATE ${constraint.onUpdate}`);
|
||||
}
|
||||
return parts.length ? ` ${parts.join(' ')}` : '';
|
||||
}
|
||||
|
||||
export function normalizeConstraintType(type: string): string {
|
||||
const normalized = type.toLowerCase().replaceAll('_', ' ').trim();
|
||||
if (normalized === 'primary key' || normalized === 'primary') {
|
||||
return 'primary';
|
||||
}
|
||||
if (normalized === 'foreign key' || normalized === 'foreign') {
|
||||
return 'foreign';
|
||||
}
|
||||
if (normalized === 'unique') {
|
||||
return 'unique';
|
||||
}
|
||||
if (normalized === 'check') {
|
||||
return 'check';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function parseForeignKeyFromDefinition(definition?: string): {
|
||||
columns: string[];
|
||||
referencedTable: string;
|
||||
referencedColumns: string[];
|
||||
} {
|
||||
const empty = { columns: [], referencedTable: '', referencedColumns: [] };
|
||||
if (!definition?.trim()) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
const match = definition.match(
|
||||
/FOREIGN KEY\s*\(([^)]+)\)\s*REFERENCES\s*(?:"?([\w$]+)"?\.)?"?([\w$]+)"?\s*\(([^)]+)\)/i,
|
||||
);
|
||||
if (!match) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
const parseColumnList = (value: string) =>
|
||||
value
|
||||
.split(',')
|
||||
.map((column) => column.trim().replaceAll(/^"|"$/g, ''))
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
columns: parseColumnList(match[1]),
|
||||
referencedTable: match[3],
|
||||
referencedColumns: parseColumnList(match[4]),
|
||||
};
|
||||
}
|
||||
|
||||
export function inferForeignKeyLocalColumnFromName(
|
||||
constraintName?: string,
|
||||
): string | undefined {
|
||||
if (!constraintName?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
const match = constraintName.trim().match(/^fk_(.+)$/i);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
export function parseConstraintColumnsFromApi(
|
||||
type: string,
|
||||
definition?: string,
|
||||
constraintName?: string,
|
||||
apiColumns?: string[],
|
||||
): { columns: string[]; definition: string } {
|
||||
const columns = apiColumns?.filter(Boolean) || [];
|
||||
let normalizedDefinition = definition?.trim() || '';
|
||||
|
||||
if (columns.length) {
|
||||
return { columns, definition: normalizedDefinition };
|
||||
}
|
||||
|
||||
if (normalizedDefinition) {
|
||||
const checkNotNullMatch = normalizedDefinition.match(
|
||||
/CHECK\s*\(\(?\s*"?(\w+)"?\s+IS NOT NULL\s*\)?\)?/i,
|
||||
);
|
||||
if (checkNotNullMatch?.[1]) {
|
||||
const column = checkNotNullMatch[1];
|
||||
return {
|
||||
columns: [column],
|
||||
definition: `${column} IS NOT NULL`,
|
||||
};
|
||||
}
|
||||
|
||||
const notNullPrefixMatch = normalizedDefinition.match(/^NOT NULL\s+"?(\w+)"?$/i);
|
||||
if (notNullPrefixMatch?.[1]) {
|
||||
const column = notNullPrefixMatch[1];
|
||||
return {
|
||||
columns: [column],
|
||||
definition: `${column} IS NOT NULL`,
|
||||
};
|
||||
}
|
||||
|
||||
const isNotNullMatch = normalizedDefinition.match(/^"?(\w+)"?\s+IS NOT NULL$/i);
|
||||
if (isNotNullMatch?.[1]) {
|
||||
return {
|
||||
columns: [isNotNullMatch[1]],
|
||||
definition: normalizedDefinition,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizeConstraintType(type) === 'check' && constraintName) {
|
||||
const nameMatch = constraintName.match(/_(\w+)_not_null$/i);
|
||||
if (nameMatch?.[1]) {
|
||||
const column = nameMatch[1];
|
||||
return {
|
||||
columns: [column],
|
||||
definition: normalizedDefinition || `${column} IS NOT NULL`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { columns: [], definition: normalizedDefinition };
|
||||
}
|
||||
|
||||
export function mapConstraintFromApi(
|
||||
con: ConstraintInfo,
|
||||
): TableConstraintDefinition {
|
||||
const type = normalizeConstraintType(con.constraint_type || 'check');
|
||||
const actions =
|
||||
type === 'foreign'
|
||||
? parseForeignKeyActions(con.definition)
|
||||
: { onDelete: '', onUpdate: '' };
|
||||
|
||||
if (type === 'foreign') {
|
||||
const fkFromDef = parseForeignKeyFromDefinition(con.definition);
|
||||
const apiColumns = con.columns?.split(', ').filter(Boolean) || [];
|
||||
const apiReferencedColumns =
|
||||
con.referenced_columns?.split(', ').filter(Boolean) || [];
|
||||
|
||||
let columns = fkFromDef.columns.length ? fkFromDef.columns : apiColumns;
|
||||
let referencedTable = fkFromDef.referencedTable || con.referenced_table || '';
|
||||
let referencedColumns = fkFromDef.referencedColumns.length
|
||||
? fkFromDef.referencedColumns
|
||||
: apiReferencedColumns;
|
||||
|
||||
if (!columns.length) {
|
||||
const inferred = inferForeignKeyLocalColumnFromName(con.constraint_name);
|
||||
if (inferred) {
|
||||
columns = [inferred];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: con.constraint_name,
|
||||
type,
|
||||
definition: con.definition || '',
|
||||
columns,
|
||||
referencedTable,
|
||||
referencedColumns,
|
||||
onDelete: actions.onDelete,
|
||||
onUpdate: actions.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
const apiColumns = con.columns?.split(', ').filter(Boolean) || [];
|
||||
const parsed = parseConstraintColumnsFromApi(
|
||||
type,
|
||||
con.definition,
|
||||
con.constraint_name,
|
||||
apiColumns,
|
||||
);
|
||||
|
||||
return {
|
||||
name: con.constraint_name,
|
||||
type,
|
||||
definition: parsed.definition,
|
||||
columns: parsed.columns,
|
||||
referencedTable: con.referenced_table || '',
|
||||
referencedColumns:
|
||||
con.referenced_columns?.split(', ').filter(Boolean) || [],
|
||||
onDelete: actions.onDelete,
|
||||
onUpdate: actions.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
function quoteIdentifier(name: string, dbType: string): string {
|
||||
if (dbType === 'mysql') {
|
||||
return `\`${name}\``;
|
||||
}
|
||||
if (dbType === 'sqlserver' || dbType === 'sql server') {
|
||||
return `[${name}]`;
|
||||
}
|
||||
if (dbType === 'oracle' || dbType === 'postgresql') {
|
||||
return `"${name.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return `"${name}"`;
|
||||
}
|
||||
|
||||
function quoteTableRef(
|
||||
tableName: string,
|
||||
schema: string | undefined,
|
||||
dbType: string,
|
||||
): string {
|
||||
if (tableName.includes('.')) {
|
||||
const [schemaName, pureTableName] = tableName.split('.', 2);
|
||||
return `${quoteIdentifier(schemaName, dbType)}.${quoteIdentifier(pureTableName, dbType)}`;
|
||||
}
|
||||
if (schema) {
|
||||
return `${quoteIdentifier(schema, dbType)}.${quoteIdentifier(tableName, dbType)}`;
|
||||
}
|
||||
return quoteIdentifier(tableName, dbType);
|
||||
}
|
||||
|
||||
function quoteColumnList(columns: string[] | undefined, dbType: string): string {
|
||||
return (columns || [])
|
||||
.map((column) => quoteIdentifier(column, dbType))
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
export function buildCreateTableConstraintSql(
|
||||
constraint: TableConstraintDefinition,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
): string {
|
||||
const type = normalizeConstraintType(constraint.type);
|
||||
const columns = quoteColumnList(constraint.columns, dbType);
|
||||
let sql = ` CONSTRAINT ${quoteIdentifier(constraint.name, dbType)}`;
|
||||
|
||||
switch (type) {
|
||||
case 'primary': {
|
||||
sql += ` PRIMARY KEY (${columns})`;
|
||||
break;
|
||||
}
|
||||
case 'foreign': {
|
||||
const refTable = quoteTableRef(
|
||||
constraint.referencedTable || '',
|
||||
schema,
|
||||
dbType,
|
||||
);
|
||||
const refColumns = quoteColumnList(constraint.referencedColumns, dbType);
|
||||
sql += ` FOREIGN KEY (${columns}) REFERENCES ${refTable} (${refColumns})${buildForeignKeyActionsSql(constraint, dbType)}`;
|
||||
break;
|
||||
}
|
||||
case 'unique': {
|
||||
sql += ` UNIQUE (${columns})`;
|
||||
break;
|
||||
}
|
||||
case 'check': {
|
||||
sql += ` CHECK (${constraint.definition || ''})`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
export function buildAddConstraintSql(
|
||||
constraint: TableConstraintDefinition,
|
||||
tableRef: string,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
): string {
|
||||
const type = normalizeConstraintType(constraint.type);
|
||||
const columns = quoteColumnList(constraint.columns, dbType);
|
||||
let sql = `ALTER TABLE ${tableRef} ADD CONSTRAINT ${quoteIdentifier(constraint.name, dbType)}`;
|
||||
|
||||
switch (type) {
|
||||
case 'primary': {
|
||||
sql += ` PRIMARY KEY (${columns})`;
|
||||
break;
|
||||
}
|
||||
case 'foreign': {
|
||||
const refTable = quoteTableRef(
|
||||
constraint.referencedTable || '',
|
||||
schema,
|
||||
dbType,
|
||||
);
|
||||
const refColumns = quoteColumnList(constraint.referencedColumns, dbType);
|
||||
sql += ` FOREIGN KEY (${columns}) REFERENCES ${refTable} (${refColumns})${buildForeignKeyActionsSql(constraint, dbType)}`;
|
||||
break;
|
||||
}
|
||||
case 'unique': {
|
||||
sql += ` UNIQUE (${columns})`;
|
||||
break;
|
||||
}
|
||||
case 'check': {
|
||||
sql += ` CHECK (${constraint.definition || ''})`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${sql};`;
|
||||
}
|
||||
|
||||
export function buildDropConstraintSql(
|
||||
constraint: TableConstraintDefinition,
|
||||
tableRef: string,
|
||||
dbType: string,
|
||||
): string {
|
||||
const name = quoteIdentifier(constraint.name, dbType);
|
||||
if (dbType === 'mysql' && normalizeConstraintType(constraint.type) === 'foreign') {
|
||||
return `ALTER TABLE ${tableRef} DROP FOREIGN KEY ${name};`;
|
||||
}
|
||||
return `ALTER TABLE ${tableRef} DROP CONSTRAINT ${name};`;
|
||||
}
|
||||
|
||||
function normalizeConstraintForCompare(
|
||||
constraint: TableConstraintDefinition,
|
||||
): TableConstraintDefinition {
|
||||
return {
|
||||
name: constraint.name.trim(),
|
||||
type: normalizeConstraintType(constraint.type),
|
||||
definition: constraint.definition?.trim() || '',
|
||||
columns: [...(constraint.columns || [])],
|
||||
referencedTable: constraint.referencedTable?.trim() || '',
|
||||
referencedColumns: [...(constraint.referencedColumns || [])],
|
||||
onDelete: normalizeForeignKeyAction(constraint.onDelete),
|
||||
onUpdate: normalizeForeignKeyAction(constraint.onUpdate),
|
||||
};
|
||||
}
|
||||
|
||||
export function constraintEquals(
|
||||
left: TableConstraintDefinition,
|
||||
right: TableConstraintDefinition,
|
||||
): boolean {
|
||||
return (
|
||||
JSON.stringify(normalizeConstraintForCompare(left)) ===
|
||||
JSON.stringify(normalizeConstraintForCompare(right))
|
||||
);
|
||||
}
|
||||
|
||||
export function buildConstraintAlterSql(
|
||||
originalConstraints: TableConstraintDefinition[],
|
||||
currentConstraints: TableConstraintDefinition[],
|
||||
tableRef: string,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
): string[] {
|
||||
const statements: string[] = [];
|
||||
const originalMap = new Map(
|
||||
originalConstraints.map((item) => [item.name, item]),
|
||||
);
|
||||
const currentMap = new Map(currentConstraints.map((item) => [item.name, item]));
|
||||
|
||||
for (const original of originalConstraints) {
|
||||
const current = currentMap.get(original.name);
|
||||
if (!current) {
|
||||
statements.push(buildDropConstraintSql(original, tableRef, dbType));
|
||||
continue;
|
||||
}
|
||||
if (!constraintEquals(original, current)) {
|
||||
statements.push(buildDropConstraintSql(original, tableRef, dbType));
|
||||
statements.push(buildAddConstraintSql(current, tableRef, dbType, schema));
|
||||
}
|
||||
}
|
||||
|
||||
for (const current of currentConstraints) {
|
||||
if (!originalMap.has(current.name)) {
|
||||
statements.push(buildAddConstraintSql(current, tableRef, dbType, schema));
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
export interface ConstraintValidateResult {
|
||||
valid: boolean;
|
||||
errorKey?:
|
||||
| 'constraintNameRequired'
|
||||
| 'constraintColumnsRequired'
|
||||
| 'constraintCheckDefinitionRequired'
|
||||
| 'constraintReferencedTableRequired'
|
||||
| 'constraintReferencedColumnsRequired'
|
||||
| 'constraintReferencedColumnsMismatch';
|
||||
}
|
||||
|
||||
export function validateConstraints(
|
||||
constraints: TableConstraintDefinition[],
|
||||
): ConstraintValidateResult {
|
||||
for (const constraint of constraints) {
|
||||
if (!constraint.name?.trim()) {
|
||||
return { valid: false, errorKey: 'constraintNameRequired' };
|
||||
}
|
||||
|
||||
if (!constraint.columns?.length) {
|
||||
return { valid: false, errorKey: 'constraintColumnsRequired' };
|
||||
}
|
||||
|
||||
const type = normalizeConstraintType(constraint.type);
|
||||
if (type === 'check' && !constraint.definition?.trim()) {
|
||||
return { valid: false, errorKey: 'constraintCheckDefinitionRequired' };
|
||||
}
|
||||
|
||||
if (type === 'foreign') {
|
||||
if (!constraint.referencedTable?.trim()) {
|
||||
return { valid: false, errorKey: 'constraintReferencedTableRequired' };
|
||||
}
|
||||
if (!constraint.referencedColumns?.length) {
|
||||
return {
|
||||
valid: false,
|
||||
errorKey: 'constraintReferencedColumnsRequired',
|
||||
};
|
||||
}
|
||||
if (constraint.columns.length !== constraint.referencedColumns.length) {
|
||||
return {
|
||||
valid: false,
|
||||
errorKey: 'constraintReferencedColumnsMismatch',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
// 数据类型定义
|
||||
export interface DataTypeOption {
|
||||
label: string;
|
||||
value: string;
|
||||
hasLength?: boolean;
|
||||
hasPrecision?: boolean;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用数据类型列表
|
||||
*
|
||||
* 前端统一使用通用类型,后端根据数据库类型自动转换:
|
||||
* - varchar: PostgreSQL=VARCHAR, MySQL=VARCHAR, SQL Server=VARCHAR
|
||||
* - text: PostgreSQL=TEXT, MySQL=TEXT, SQL Server=TEXT
|
||||
* - int: PostgreSQL=INTEGER, MySQL=INT, SQL Server=INT
|
||||
* - bigint: PostgreSQL=BIGINT, MySQL=BIGINT, SQL Server=BIGINT
|
||||
* - smallint: PostgreSQL=SMALLINT, MySQL=SMALLINT, SQL Server=SMALLINT
|
||||
* - decimal: PostgreSQL=DECIMAL, MySQL=DECIMAL, SQL Server=DECIMAL
|
||||
* - float: PostgreSQL=REAL, MySQL=FLOAT, SQL Server=FLOAT
|
||||
* - double: PostgreSQL=DOUBLE PRECISION, MySQL=DOUBLE, SQL Server=FLOAT
|
||||
* - datetime: PostgreSQL=TIMESTAMP, MySQL=DATETIME, SQL Server=DATETIME2
|
||||
* - date: PostgreSQL=DATE, MySQL=DATE, SQL Server=DATE
|
||||
* - time: PostgreSQL=TIME, MySQL=TIME, SQL Server=TIME
|
||||
* - boolean: PostgreSQL=BOOLEAN, MySQL=TINYINT(1), SQL Server=BIT
|
||||
* - json: PostgreSQL=JSON, MySQL=JSON, SQL Server=NVARCHAR(MAX)
|
||||
*/
|
||||
export const commonDataTypes = computed<DataTypeOption[]>(() => [
|
||||
{
|
||||
label: 'VARCHAR',
|
||||
value: 'varchar',
|
||||
hasLength: true,
|
||||
desc: $t('database-manager.dataTypes.varchar'),
|
||||
},
|
||||
{
|
||||
label: 'CHAR',
|
||||
value: 'char',
|
||||
hasLength: true,
|
||||
desc: $t('database-manager.dataTypes.char'),
|
||||
},
|
||||
{ label: 'TEXT', value: 'text', desc: $t('database-manager.dataTypes.text') },
|
||||
{ label: 'INT', value: 'int', desc: $t('database-manager.dataTypes.int') },
|
||||
{
|
||||
label: 'BIGINT',
|
||||
value: 'bigint',
|
||||
desc: $t('database-manager.dataTypes.bigint'),
|
||||
},
|
||||
{
|
||||
label: 'SMALLINT',
|
||||
value: 'smallint',
|
||||
desc: $t('database-manager.dataTypes.smallint'),
|
||||
},
|
||||
{
|
||||
label: 'DECIMAL',
|
||||
value: 'decimal',
|
||||
hasPrecision: true,
|
||||
desc: $t('database-manager.dataTypes.decimal'),
|
||||
},
|
||||
{
|
||||
label: 'NUMERIC',
|
||||
value: 'numeric',
|
||||
hasPrecision: true,
|
||||
desc: $t('database-manager.dataTypes.numeric'),
|
||||
},
|
||||
{
|
||||
label: 'FLOAT',
|
||||
value: 'float',
|
||||
desc: $t('database-manager.dataTypes.float'),
|
||||
},
|
||||
{
|
||||
label: 'DOUBLE',
|
||||
value: 'double',
|
||||
desc: $t('database-manager.dataTypes.double'),
|
||||
},
|
||||
{
|
||||
label: 'DATETIME',
|
||||
value: 'datetime',
|
||||
desc: $t('database-manager.dataTypes.datetime'),
|
||||
},
|
||||
{ label: 'DATE', value: 'date', desc: $t('database-manager.dataTypes.date') },
|
||||
{ label: 'TIME', value: 'time', desc: $t('database-manager.dataTypes.time') },
|
||||
{
|
||||
label: 'BOOLEAN',
|
||||
value: 'boolean',
|
||||
desc: $t('database-manager.dataTypes.boolean'),
|
||||
},
|
||||
{ label: 'JSON', value: 'json', desc: $t('database-manager.dataTypes.json') },
|
||||
]);
|
||||
|
||||
// 保留旧的类型列表以兼容现有代码(已弃用,建议使用 commonDataTypes)
|
||||
export const postgresqlTypes = commonDataTypes;
|
||||
export const mysqlTypes = commonDataTypes;
|
||||
export const sqlserverTypes = commonDataTypes;
|
||||
|
||||
// 根据数据库类型获取数据类型列表(统一返回通用类型)
|
||||
export function getDataTypesByDbType(_dbType: string): DataTypeOption[] {
|
||||
// 统一使用通用类型,后端会根据数据库类型自动转换
|
||||
return commonDataTypes.value;
|
||||
}
|
||||
|
||||
// 判断字段类型是否需要长度
|
||||
export function typeHasLength(types: DataTypeOption[], type: string): boolean {
|
||||
const typeOption = types.find((t) => t.value === type);
|
||||
return typeOption?.hasLength || false;
|
||||
}
|
||||
|
||||
// 判断字段类型是否需要精度(小数位)
|
||||
export function typeHasPrecision(
|
||||
types: DataTypeOption[],
|
||||
type: string,
|
||||
): boolean {
|
||||
const typeOption = types.find((t) => t.value === type);
|
||||
return typeOption?.hasPrecision || false;
|
||||
}
|
||||
|
||||
// NUMERIC 类型默认值
|
||||
export const NUMERIC_DEFAULT_LENGTH = 10;
|
||||
export const NUMERIC_DEFAULT_SCALE = 2;
|
||||
|
||||
export interface DbColumnLike {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
character_maximum_length?: null | number;
|
||||
numeric_precision?: null | number;
|
||||
numeric_scale?: null | number;
|
||||
is_nullable: boolean;
|
||||
column_default?: null | string;
|
||||
is_primary_key: boolean;
|
||||
is_unique: boolean;
|
||||
description?: null | string;
|
||||
}
|
||||
|
||||
export interface MappedTableField {
|
||||
name: string;
|
||||
type: string;
|
||||
length?: number;
|
||||
precision?: number;
|
||||
scale?: number;
|
||||
nullable: boolean;
|
||||
default?: string;
|
||||
primaryKey: boolean;
|
||||
unique: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
/** 将 SQL Server INFORMATION_SCHEMA 默认值格式规范化为可读形式,如 ((0)) -> 0 */
|
||||
export function normalizeColumnDefault(
|
||||
defaultValue?: null | string,
|
||||
dbType?: string,
|
||||
): string | undefined {
|
||||
if (defaultValue == null || defaultValue === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const db = (dbType || '').toLowerCase();
|
||||
if (db !== 'sqlserver' && db !== 'mssql' && db !== 'sql server') {
|
||||
return defaultValue.trim() || undefined;
|
||||
}
|
||||
|
||||
let value = defaultValue.trim();
|
||||
while (value.startsWith('(') && value.endsWith(')')) {
|
||||
value = value.slice(1, -1).trim();
|
||||
}
|
||||
|
||||
if (!value || value.toUpperCase() === 'NULL') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 将数据库列信息映射为表设计器字段(含 NUMERIC 精度兼容) */
|
||||
export function mapDbColumnToField(
|
||||
col: DbColumnLike,
|
||||
dbType?: string,
|
||||
): MappedTableField {
|
||||
const type = normalizeDbType(col.data_type);
|
||||
let length = col.character_maximum_length ?? undefined;
|
||||
let precision = col.numeric_precision ?? undefined;
|
||||
let scale = col.numeric_scale ?? undefined;
|
||||
|
||||
// 兼容旧版 UI 误将精度写入 length 的情况
|
||||
if (
|
||||
(type === 'numeric' || type === 'decimal') &&
|
||||
precision == null &&
|
||||
length != null
|
||||
) {
|
||||
precision = length;
|
||||
length = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: col.column_name,
|
||||
type,
|
||||
length,
|
||||
precision,
|
||||
scale,
|
||||
nullable: col.is_nullable,
|
||||
default: normalizeColumnDefault(col.column_default, dbType),
|
||||
primaryKey: col.is_primary_key,
|
||||
unique: col.is_unique,
|
||||
comment: col.description ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库返回类型到通用类型的映射表
|
||||
* 用于将数据库返回的原生类型(如 INTEGER, TIMESTAMP)标准化为前端通用类型(如 int, datetime)
|
||||
*/
|
||||
const DB_TYPE_TO_COMMON_TYPE: Record<string, string> = {
|
||||
// PostgreSQL 类型映射
|
||||
integer: 'int',
|
||||
int4: 'int',
|
||||
int8: 'bigint',
|
||||
int2: 'smallint',
|
||||
'double precision': 'double',
|
||||
float4: 'float',
|
||||
float8: 'double',
|
||||
real: 'float',
|
||||
timestamp: 'datetime',
|
||||
'timestamp without time zone': 'datetime',
|
||||
'timestamp with time zone': 'datetime',
|
||||
timestamptz: 'datetime',
|
||||
bool: 'boolean',
|
||||
jsonb: 'json',
|
||||
'character varying': 'varchar',
|
||||
character: 'char',
|
||||
|
||||
// MySQL 类型映射
|
||||
tinyint: 'boolean',
|
||||
'tinyint(1)': 'boolean',
|
||||
|
||||
// SQL Server 类型映射
|
||||
datetime2: 'datetime',
|
||||
bit: 'boolean',
|
||||
'nvarchar(max)': 'json',
|
||||
nvarchar: 'varchar',
|
||||
nchar: 'char',
|
||||
|
||||
// Oracle 类型映射
|
||||
varchar2: 'varchar',
|
||||
nvarchar2: 'varchar',
|
||||
number: 'numeric',
|
||||
clob: 'text',
|
||||
nclob: 'text',
|
||||
blob: 'binary',
|
||||
binary_float: 'float',
|
||||
binary_double: 'double',
|
||||
'timestamp(6)': 'datetime',
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数据库返回的类型标准化为前端通用类型
|
||||
* @param dbType 数据库返回的原生类型(如 INTEGER, TIMESTAMP, varchar(255))
|
||||
* @returns 标准化后的通用类型(如 int, datetime, varchar)
|
||||
*/
|
||||
export function normalizeDbType(dbType: string): string {
|
||||
if (!dbType) return 'varchar';
|
||||
|
||||
// 转小写并去除首尾空格
|
||||
const lowerType = dbType.toLowerCase().trim();
|
||||
|
||||
// 先尝试直接匹配
|
||||
if (DB_TYPE_TO_COMMON_TYPE[lowerType]) {
|
||||
return DB_TYPE_TO_COMMON_TYPE[lowerType];
|
||||
}
|
||||
|
||||
// 处理带长度的类型,如 varchar(255) -> varchar, numeric(10,2) -> numeric
|
||||
const baseType = lowerType.replace(/\(.*\)$/, '').trim();
|
||||
|
||||
// 再次尝试匹配基础类型
|
||||
if (DB_TYPE_TO_COMMON_TYPE[baseType]) {
|
||||
return DB_TYPE_TO_COMMON_TYPE[baseType];
|
||||
}
|
||||
|
||||
// 检查是否已经是通用类型
|
||||
const commonTypeValues = commonDataTypes.value.map((t) => t.value);
|
||||
if (commonTypeValues.includes(baseType)) {
|
||||
return baseType;
|
||||
}
|
||||
|
||||
// 默认返回原类型(小写)
|
||||
return baseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用类型到数据库特定类型的映射表
|
||||
* 用于生成 SQL 时将通用类型(如 int, datetime)转换为数据库特定类型(如 INTEGER, TIMESTAMP)
|
||||
*/
|
||||
const COMMON_TYPE_TO_DB_TYPE: Record<string, Record<string, string>> = {
|
||||
postgresql: {
|
||||
int: 'INTEGER',
|
||||
bigint: 'BIGINT',
|
||||
smallint: 'SMALLINT',
|
||||
float: 'REAL',
|
||||
double: 'DOUBLE PRECISION',
|
||||
datetime: 'TIMESTAMP',
|
||||
boolean: 'BOOLEAN',
|
||||
json: 'JSON',
|
||||
varchar: 'VARCHAR',
|
||||
char: 'CHAR',
|
||||
text: 'TEXT',
|
||||
decimal: 'DECIMAL',
|
||||
numeric: 'NUMERIC',
|
||||
date: 'DATE',
|
||||
time: 'TIME',
|
||||
},
|
||||
mysql: {
|
||||
int: 'INT',
|
||||
bigint: 'BIGINT',
|
||||
smallint: 'SMALLINT',
|
||||
float: 'FLOAT',
|
||||
double: 'DOUBLE',
|
||||
datetime: 'DATETIME',
|
||||
boolean: 'TINYINT(1)',
|
||||
json: 'JSON',
|
||||
varchar: 'VARCHAR',
|
||||
char: 'CHAR',
|
||||
text: 'TEXT',
|
||||
decimal: 'DECIMAL',
|
||||
numeric: 'DECIMAL',
|
||||
date: 'DATE',
|
||||
time: 'TIME',
|
||||
},
|
||||
sqlserver: {
|
||||
int: 'INT',
|
||||
bigint: 'BIGINT',
|
||||
smallint: 'SMALLINT',
|
||||
float: 'FLOAT',
|
||||
double: 'FLOAT',
|
||||
datetime: 'DATETIME2',
|
||||
boolean: 'BIT',
|
||||
json: 'NVARCHAR(MAX)',
|
||||
// 中文等 Unicode 必须用 N* 类型,VARCHAR/CHAR/TEXT 会落成问号
|
||||
varchar: 'NVARCHAR',
|
||||
char: 'NCHAR',
|
||||
text: 'NVARCHAR(MAX)',
|
||||
decimal: 'DECIMAL',
|
||||
numeric: 'NUMERIC',
|
||||
date: 'DATE',
|
||||
time: 'TIME',
|
||||
},
|
||||
oracle: {
|
||||
int: 'NUMBER',
|
||||
bigint: 'NUMBER',
|
||||
smallint: 'NUMBER',
|
||||
float: 'BINARY_FLOAT',
|
||||
double: 'BINARY_DOUBLE',
|
||||
datetime: 'TIMESTAMP',
|
||||
boolean: 'NUMBER(1)',
|
||||
json: 'CLOB',
|
||||
varchar: 'VARCHAR2',
|
||||
char: 'CHAR',
|
||||
text: 'CLOB',
|
||||
decimal: 'NUMBER',
|
||||
numeric: 'NUMBER',
|
||||
date: 'DATE',
|
||||
time: 'TIMESTAMP',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 将通用类型转换为数据库特定类型(用于生成 SQL)
|
||||
* @param commonType 通用类型(如 int, datetime, varchar)
|
||||
* @param dbType 数据库类型(postgresql, mysql, sqlserver, oracle)
|
||||
* @returns 数据库特定类型(如 INTEGER, TIMESTAMP, VARCHAR)
|
||||
*/
|
||||
export function mapToDbType(commonType: string, dbType: string): string {
|
||||
if (!commonType) return 'VARCHAR';
|
||||
|
||||
const lowerCommonType = commonType.toLowerCase().trim();
|
||||
let lowerDbType = dbType.toLowerCase().trim();
|
||||
if (lowerDbType === 'mssql' || lowerDbType === 'sql server') {
|
||||
lowerDbType = 'sqlserver';
|
||||
}
|
||||
|
||||
// 获取对应数据库的映射表,默认使用 PostgreSQL
|
||||
const typeMap: Record<string, string> =
|
||||
COMMON_TYPE_TO_DB_TYPE[lowerDbType] ?? COMMON_TYPE_TO_DB_TYPE.postgresql!;
|
||||
|
||||
// 查找映射
|
||||
const mappedType = typeMap[lowerCommonType];
|
||||
if (mappedType) {
|
||||
return mappedType;
|
||||
}
|
||||
|
||||
// 如果没有映射,返回原类型的大写形式
|
||||
return commonType.toUpperCase();
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import {
|
||||
quoteIdentifier,
|
||||
quoteTable,
|
||||
} from '#/views/_core/database-manager/utils/sql-identifier';
|
||||
|
||||
export interface IndexDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
columns: string[];
|
||||
unique: boolean;
|
||||
}
|
||||
|
||||
function normalizeDbType(dbType: string): string {
|
||||
const db = (dbType || 'postgresql').toLowerCase();
|
||||
if (db === 'sql server' || db === 'mssql') {
|
||||
return 'sqlserver';
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
/** 将后端返回的 index_type 规范化为前端枚举值 */
|
||||
export function normalizeIndexType(
|
||||
indexType: string | undefined,
|
||||
dbType: string,
|
||||
): string {
|
||||
const db = normalizeDbType(dbType);
|
||||
const raw = (indexType || '').toLowerCase().replaceAll(/\s+/g, '_');
|
||||
|
||||
if (db === 'postgresql') {
|
||||
if (['btree', 'hash', 'gin', 'gist', 'brin'].includes(raw)) {
|
||||
return raw;
|
||||
}
|
||||
return 'btree';
|
||||
}
|
||||
if (db === 'mysql') {
|
||||
if (raw === 'hash') {
|
||||
return 'hash';
|
||||
}
|
||||
return 'btree';
|
||||
}
|
||||
if (db === 'sqlserver') {
|
||||
if (raw.includes('clustered') && !raw.includes('non')) {
|
||||
return 'clustered';
|
||||
}
|
||||
if (raw === 'hash') {
|
||||
return 'hash';
|
||||
}
|
||||
return 'nonclustered';
|
||||
}
|
||||
if (db === 'oracle') {
|
||||
if (raw.includes('bitmap')) {
|
||||
return 'bitmap';
|
||||
}
|
||||
return 'normal';
|
||||
}
|
||||
return 'btree';
|
||||
}
|
||||
|
||||
function quoteColumnList(columns: string[], dbType: string): string {
|
||||
return columns
|
||||
.map((column) => quoteIdentifier(column, dbType))
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function buildSqlServerIndexTypeClause(indexType: string): string {
|
||||
const type = indexType.toLowerCase();
|
||||
if (type === 'clustered') {
|
||||
return 'CLUSTERED ';
|
||||
}
|
||||
if (type === 'hash') {
|
||||
return 'NONCLUSTERED ';
|
||||
}
|
||||
return 'NONCLUSTERED ';
|
||||
}
|
||||
|
||||
function buildOracleIndexTypeClause(indexType: string): string {
|
||||
if (indexType.toLowerCase() === 'bitmap') {
|
||||
return 'BITMAP ';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function buildCreateIndexSql(
|
||||
index: IndexDefinition,
|
||||
tableRef: string,
|
||||
dbType: string,
|
||||
_schema?: string,
|
||||
): string {
|
||||
const db = normalizeDbType(dbType);
|
||||
const indexName = quoteIdentifier(index.name, db);
|
||||
const indexColumns = quoteColumnList(index.columns, db);
|
||||
const uniquePrefix = index.unique ? 'UNIQUE ' : '';
|
||||
|
||||
switch (db) {
|
||||
case 'mysql': {
|
||||
const indexKeyword = index.unique ? 'UNIQUE INDEX' : 'INDEX';
|
||||
return `CREATE ${indexKeyword} ${indexName} ON ${tableRef} (${indexColumns});`;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
const typeClause = buildSqlServerIndexTypeClause(index.type);
|
||||
return `CREATE ${uniquePrefix}${typeClause}INDEX ${indexName} ON ${tableRef} (${indexColumns});`;
|
||||
}
|
||||
case 'oracle': {
|
||||
const typeClause = buildOracleIndexTypeClause(index.type);
|
||||
return `CREATE ${uniquePrefix}${typeClause}INDEX ${indexName} ON ${tableRef} (${indexColumns});`;
|
||||
}
|
||||
case 'postgresql':
|
||||
default: {
|
||||
const method = (index.type || 'btree').toUpperCase();
|
||||
const indexKeyword = index.unique ? 'UNIQUE INDEX' : 'INDEX';
|
||||
return `CREATE ${indexKeyword} ${indexName} ON ${tableRef} USING ${method} (${indexColumns});`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDropIndexSql(
|
||||
index: IndexDefinition,
|
||||
tableRef: string,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
): string {
|
||||
const db = normalizeDbType(dbType);
|
||||
const indexName = quoteIdentifier(index.name, db);
|
||||
|
||||
switch (db) {
|
||||
case 'mysql': {
|
||||
return `DROP INDEX ${indexName} ON ${tableRef};`;
|
||||
}
|
||||
case 'sqlserver': {
|
||||
return `DROP INDEX ${indexName} ON ${tableRef};`;
|
||||
}
|
||||
case 'oracle': {
|
||||
if (schema) {
|
||||
return `DROP INDEX ${quoteIdentifier(schema, db)}.${indexName};`;
|
||||
}
|
||||
return `DROP INDEX ${indexName};`;
|
||||
}
|
||||
case 'postgresql':
|
||||
default: {
|
||||
if (schema) {
|
||||
return `DROP INDEX IF EXISTS ${quoteIdentifier(schema, db)}.${indexName};`;
|
||||
}
|
||||
return `DROP INDEX IF EXISTS ${indexName};`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeIndexForCompare(index: IndexDefinition): IndexDefinition {
|
||||
return {
|
||||
name: index.name.trim(),
|
||||
type: (index.type || 'btree').toLowerCase(),
|
||||
columns: [...index.columns],
|
||||
unique: index.unique,
|
||||
};
|
||||
}
|
||||
|
||||
export function indexEquals(
|
||||
left: IndexDefinition,
|
||||
right: IndexDefinition,
|
||||
): boolean {
|
||||
return (
|
||||
JSON.stringify(normalizeIndexForCompare(left)) ===
|
||||
JSON.stringify(normalizeIndexForCompare(right))
|
||||
);
|
||||
}
|
||||
|
||||
export function buildIndexAlterSql(
|
||||
originalIndexes: IndexDefinition[],
|
||||
currentIndexes: IndexDefinition[],
|
||||
tableRef: string,
|
||||
dbType: string,
|
||||
schema?: string,
|
||||
): string[] {
|
||||
const statements: string[] = [];
|
||||
const originalMap = new Map(
|
||||
originalIndexes.map((item) => [item.name, item]),
|
||||
);
|
||||
const currentMap = new Map(currentIndexes.map((item) => [item.name, item]));
|
||||
|
||||
for (const original of originalIndexes) {
|
||||
const current = currentMap.get(original.name);
|
||||
if (!current) {
|
||||
statements.push(buildDropIndexSql(original, tableRef, dbType, schema));
|
||||
continue;
|
||||
}
|
||||
if (!indexEquals(original, current)) {
|
||||
statements.push(buildDropIndexSql(original, tableRef, dbType, schema));
|
||||
statements.push(buildCreateIndexSql(current, tableRef, dbType, schema));
|
||||
}
|
||||
}
|
||||
|
||||
for (const current of currentIndexes) {
|
||||
if (!originalMap.has(current.name)) {
|
||||
statements.push(buildCreateIndexSql(current, tableRef, dbType, schema));
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
/** 解析 API 索引数据为编辑器格式 */
|
||||
export function mapIndexFromApi(
|
||||
idx: {
|
||||
index_name: string;
|
||||
index_type?: string;
|
||||
columns?: string;
|
||||
is_unique?: boolean;
|
||||
},
|
||||
dbType: string,
|
||||
): IndexDefinition {
|
||||
return {
|
||||
name: idx.index_name,
|
||||
type: normalizeIndexType(idx.index_type, dbType),
|
||||
columns: idx.columns?.split(', ').filter(Boolean) || [],
|
||||
unique: Boolean(idx.is_unique),
|
||||
};
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import { preferences } from '@vben/preferences';
|
||||
const LEGACY_HOME_PATTERNS = [
|
||||
/^\/app\/[^/]+\/form-render\//,
|
||||
/^\/form-render\//,
|
||||
/^\/online-dev(?:\/|$)/,
|
||||
/^\/online-development(?:\/|$)/,
|
||||
];
|
||||
|
||||
function decodePath(path: string) {
|
||||
|
||||
Reference in New Issue
Block a user