feat: lighten ai agent admin frontend

This commit is contained in:
2026-06-11 18:03:43 +08:00
parent ecf29e2278
commit 15229cc5ab
316 changed files with 898 additions and 60944 deletions
-592
View File
@@ -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();
}
-217
View File
@@ -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) {