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; }