593 lines
16 KiB
TypeScript
593 lines
16 KiB
TypeScript
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;
|
|
}
|