mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
- 合并 DuckDB 约束与索引元数据,恢复唯一索引表的可编辑判定 - 修复 attach 多库场景下 catalog/schema/table 定位混乱问题 - 统一前后端 qualified name 解析,支持带点和带引号对象名 - 补充 DuckDB 元数据与编辑链路回归测试
881 lines
38 KiB
TypeScript
881 lines
38 KiB
TypeScript
import {
|
|
isBacktickIdentifierDialect,
|
|
isMysqlFamilyDialect,
|
|
isOracleLikeDialect,
|
|
isPgLikeDialect,
|
|
isSqlServerDialect,
|
|
quoteSqlIdentifierPart,
|
|
quoteSqlIdentifierPath,
|
|
resolveSqlDialect,
|
|
unquoteSqlIdentifierPart,
|
|
unquoteSqlIdentifierPath,
|
|
} from '../utils/sqlDialect';
|
|
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
|
|
|
export interface EditableColumnSnapshot {
|
|
_key: string;
|
|
name: string;
|
|
type: string;
|
|
nullable: string;
|
|
default?: string | null;
|
|
extra?: string;
|
|
comment?: string;
|
|
key?: string;
|
|
isAutoIncrement?: boolean;
|
|
}
|
|
|
|
export interface BuildAlterTablePreviewInput {
|
|
dbType: string;
|
|
tableName: string;
|
|
originalColumns: EditableColumnSnapshot[];
|
|
columns: EditableColumnSnapshot[];
|
|
}
|
|
|
|
export interface BuildCreateTablePreviewInput {
|
|
dbType: string;
|
|
tableName: string;
|
|
columns: EditableColumnSnapshot[];
|
|
charset?: string;
|
|
collation?: string;
|
|
starRocksOptions?: StarRocksCreateTableOptions;
|
|
}
|
|
|
|
export type StarRocksTableKind = 'olap' | 'external';
|
|
export type StarRocksKeyModel = 'DUPLICATE' | 'PRIMARY' | 'UNIQUE' | 'AGGREGATE';
|
|
export type StarRocksDistributionType = 'HASH' | 'RANDOM' | 'NONE';
|
|
|
|
export interface StarRocksRollupOption {
|
|
name: string;
|
|
columnNames: string[];
|
|
fromIndexName?: string;
|
|
properties?: string;
|
|
}
|
|
|
|
export interface StarRocksCreateTableOptions {
|
|
tableKind?: StarRocksTableKind;
|
|
keyModel?: StarRocksKeyModel;
|
|
keyColumnNames?: string[];
|
|
partitionClause?: string;
|
|
distributionType?: StarRocksDistributionType;
|
|
distributionColumnNames?: string[];
|
|
bucketMode?: 'AUTO' | 'NUMBER';
|
|
bucketCount?: number;
|
|
properties?: string;
|
|
rollups?: StarRocksRollupOption[];
|
|
externalEngine?: string;
|
|
externalProperties?: string;
|
|
}
|
|
|
|
export interface BuildStarRocksMaterializedViewPreviewInput {
|
|
name: string;
|
|
query: string;
|
|
async?: boolean;
|
|
comment?: string;
|
|
distributionColumnNames?: string[];
|
|
bucketCount?: number;
|
|
refreshClause?: string;
|
|
partitionClause?: string;
|
|
orderByColumnNames?: string[];
|
|
properties?: string;
|
|
}
|
|
|
|
const escapeSqlString = (value: string) => String(value || '').replace(/'/g, "''");
|
|
|
|
const stripIdentifierQuotes = unquoteSqlIdentifierPart;
|
|
|
|
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
|
const parsed = splitQualifiedNameLast(qualifiedName);
|
|
return {
|
|
schemaName: parsed.parentPath,
|
|
objectName: parsed.objectName,
|
|
};
|
|
};
|
|
|
|
const quoteIdentifierPart = (part: string, dbType: string): string => quoteSqlIdentifierPart(dbType, part);
|
|
|
|
const quoteIdentifierPath = (path: string, dbType: string): string => quoteSqlIdentifierPath(dbType, path);
|
|
|
|
const normalizeDefaultText = (value: unknown): string => String(value ?? '').trim();
|
|
|
|
const isKnownDefaultExpression = (trimmed: string): boolean => {
|
|
if (!trimmed) return false;
|
|
if (/^N?'.*'$/i.test(trimmed)) return true;
|
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return true;
|
|
if (/^(true|false|null)$/i.test(trimmed)) return true;
|
|
if (/^(current_timestamp|current_date|current_time|localtimestamp|sysdate|systimestamp)$/i.test(trimmed)) return true;
|
|
if (/^(now|uuid|newid|sysdatetime)\s*\(\s*\)$/i.test(trimmed)) return true;
|
|
if (/^nextval\s*\(/i.test(trimmed) || /::/.test(trimmed)) return true;
|
|
return false;
|
|
};
|
|
|
|
const formatDefaultExpression = (value: unknown, dbType: string): string => {
|
|
const trimmed = normalizeDefaultText(value);
|
|
if (!trimmed) return '';
|
|
if (isKnownDefaultExpression(trimmed)) {
|
|
if (/^(true|false|null)$/i.test(trimmed)) return trimmed.toUpperCase();
|
|
if (/^(current_timestamp|current_date|current_time|localtimestamp|sysdate|systimestamp)$/i.test(trimmed)) {
|
|
return trimmed.toUpperCase();
|
|
}
|
|
return trimmed;
|
|
}
|
|
const prefix = isSqlServerDialect(dbType) ? 'N' : '';
|
|
return `${prefix}'${escapeSqlString(trimmed)}'`;
|
|
};
|
|
|
|
const buildDefaultSql = (value: unknown, dbType: string): string => {
|
|
const defaultValue = normalizeDefaultText(value);
|
|
if (!defaultValue) return '';
|
|
return `DEFAULT ${formatDefaultExpression(defaultValue, dbType)}`;
|
|
};
|
|
|
|
const definitionChanged = (curr: EditableColumnSnapshot, orig: EditableColumnSnapshot): boolean => (
|
|
curr.type !== orig.type ||
|
|
curr.nullable !== orig.nullable ||
|
|
normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default) ||
|
|
(curr.comment || '') !== (orig.comment || '') ||
|
|
Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)
|
|
);
|
|
|
|
const physicalDefinitionChanged = (curr: EditableColumnSnapshot, orig: EditableColumnSnapshot): boolean => (
|
|
curr.type !== orig.type ||
|
|
curr.nullable !== orig.nullable ||
|
|
normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default) ||
|
|
Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)
|
|
);
|
|
|
|
const buildMySqlColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
|
let extra = String(column.extra || '').trim();
|
|
if (column.isAutoIncrement) {
|
|
if (!extra.toLowerCase().includes('auto_increment')) {
|
|
extra = `${extra} AUTO_INCREMENT`.trim();
|
|
}
|
|
} else {
|
|
extra = extra.replace(/auto_increment/gi, '').trim();
|
|
}
|
|
const defaultSql = buildDefaultSql(column.default, dbType);
|
|
return [
|
|
quoteIdentifierPart(column.name, dbType),
|
|
String(column.type || '').trim(),
|
|
column.nullable === 'NO' ? 'NOT NULL' : 'NULL',
|
|
defaultSql,
|
|
extra,
|
|
`COMMENT '${escapeSqlString(column.comment || '')}'`,
|
|
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
const DORIS_AGG_TYPES = new Set([
|
|
'SUM',
|
|
'MIN',
|
|
'MAX',
|
|
'REPLACE',
|
|
'REPLACE_IF_NOT_NULL',
|
|
'HLL_UNION',
|
|
'BITMAP_UNION',
|
|
'QUANTILE_UNION',
|
|
'GENERIC',
|
|
]);
|
|
|
|
const buildDorisColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
|
const defaultSql = buildDefaultSql(column.default, dbType);
|
|
const autoIncrementSql = column.isAutoIncrement ? 'AUTO_INCREMENT' : '';
|
|
const keyText = String(column.key || '').trim().toUpperCase();
|
|
const extraText = String(column.extra || '').trim().toUpperCase();
|
|
const keyOrAggSql = ['PRI', 'KEY', 'TRUE'].includes(keyText)
|
|
? 'KEY'
|
|
: (DORIS_AGG_TYPES.has(extraText) ? extraText : '');
|
|
return [
|
|
quoteIdentifierPart(column.name, dbType),
|
|
String(column.type || '').trim(),
|
|
keyOrAggSql,
|
|
column.nullable === 'NO' ? 'NOT NULL' : 'NULL',
|
|
defaultSql,
|
|
autoIncrementSql,
|
|
`COMMENT '${escapeSqlString(column.comment || '')}'`,
|
|
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
const buildStarRocksColumnDefinition = (column: EditableColumnSnapshot): string => {
|
|
const defaultSql = buildDefaultSql(column.default, 'starrocks');
|
|
const extraText = String(column.extra || '').trim().toUpperCase();
|
|
const aggregateSql = DORIS_AGG_TYPES.has(extraText) ? extraText : '';
|
|
return [
|
|
quoteIdentifierPart(column.name, 'starrocks'),
|
|
String(column.type || '').trim(),
|
|
aggregateSql,
|
|
column.nullable === 'NO' ? 'NOT NULL' : 'NULL',
|
|
defaultSql,
|
|
`COMMENT '${escapeSqlString(column.comment || '')}'`,
|
|
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
const buildStandardColumnDefinition = (
|
|
column: EditableColumnSnapshot,
|
|
dbType: string,
|
|
options: { includeNull?: boolean; includeIdentity?: boolean } = {},
|
|
): string => {
|
|
const parts = [quoteIdentifierPart(column.name, dbType), String(column.type || '').trim()];
|
|
if (options.includeIdentity && column.isAutoIncrement) {
|
|
if (isSqlServerDialect(dbType)) {
|
|
parts.push('IDENTITY(1,1)');
|
|
} else if (isOracleLikeDialect(dbType)) {
|
|
parts.push('GENERATED BY DEFAULT AS IDENTITY');
|
|
}
|
|
}
|
|
const defaultSql = buildDefaultSql(column.default, dbType);
|
|
if (defaultSql) parts.push(defaultSql);
|
|
if (column.nullable === 'NO') {
|
|
parts.push('NOT NULL');
|
|
} else if (options.includeNull) {
|
|
parts.push('NULL');
|
|
}
|
|
return parts.filter(Boolean).join(' ').trim();
|
|
};
|
|
|
|
const buildPgLikeColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
|
const parts = [quoteIdentifierPart(column.name, dbType), String(column.type || '').trim()];
|
|
const defaultSql = buildDefaultSql(column.default, dbType);
|
|
if (defaultSql) parts.push(defaultSql);
|
|
if (column.nullable === 'NO') parts.push('NOT NULL');
|
|
return parts.join(' ').trim();
|
|
};
|
|
|
|
const buildColumnCommentSql = (tableRef: string, columnName: string, comment: string, dbType: string): string => {
|
|
const columnRef = `${tableRef}.${quoteIdentifierPart(columnName, dbType)}`;
|
|
const trimmed = String(comment || '').trim();
|
|
if (!trimmed && isPgLikeDialect(dbType)) {
|
|
return `COMMENT ON COLUMN ${columnRef} IS NULL;`;
|
|
}
|
|
return `COMMENT ON COLUMN ${columnRef} IS '${escapeSqlString(trimmed)}';`;
|
|
};
|
|
|
|
const buildSqlServerColumnCommentSql = (
|
|
tableName: string,
|
|
columnName: string,
|
|
comment: string,
|
|
): string => {
|
|
const { schemaName, objectName } = splitQualifiedName(tableName);
|
|
const schema = escapeSqlString(schemaName || 'dbo');
|
|
const table = escapeSqlString(objectName || tableName);
|
|
const column = escapeSqlString(columnName);
|
|
const value = escapeSqlString(comment || '');
|
|
return `IF EXISTS (SELECT 1 FROM sys.extended_properties ep JOIN sys.tables t ON ep.major_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id JOIN sys.columns c ON ep.major_id = c.object_id AND ep.minor_id = c.column_id WHERE ep.name = N'MS_Description' AND s.name = N'${schema}' AND t.name = N'${table}' AND c.name = N'${column}') BEGIN EXEC sp_updateextendedproperty @name = N'MS_Description', @value = N'${value}', @level0type = N'SCHEMA', @level0name = N'${schema}', @level1type = N'TABLE', @level1name = N'${table}', @level2type = N'COLUMN', @level2name = N'${column}' END ELSE BEGIN EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'${value}', @level0type = N'SCHEMA', @level0name = N'${schema}', @level1type = N'TABLE', @level1name = N'${table}', @level2type = N'COLUMN', @level2name = N'${column}' END;`;
|
|
};
|
|
|
|
const buildMySqlAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string): string => {
|
|
const tableName = quoteIdentifierPath(input.tableName, dbType);
|
|
const alters: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
alters.push(`DROP COLUMN ${quoteIdentifierPart(orig.name, dbType)}`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr, index) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
const prevCol = index > 0 ? input.columns[index - 1] : null;
|
|
const positionSql = prevCol ? `AFTER ${quoteIdentifierPart(prevCol.name, dbType)}` : 'FIRST';
|
|
const colDef = buildMySqlColumnDefinition(curr, dbType);
|
|
|
|
if (!orig) {
|
|
alters.push(`ADD COLUMN ${colDef} ${positionSql}`.trim());
|
|
return;
|
|
}
|
|
|
|
if (curr.name !== orig.name) {
|
|
alters.push(`CHANGE COLUMN ${quoteIdentifierPart(orig.name, dbType)} ${colDef} ${positionSql}`.trim());
|
|
return;
|
|
}
|
|
|
|
if (definitionChanged(curr, orig)) {
|
|
alters.push(`MODIFY COLUMN ${colDef} ${positionSql}`.trim());
|
|
}
|
|
});
|
|
|
|
const origPKKeys = input.originalColumns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const newPKKeys = input.columns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key));
|
|
if (keysChanged) {
|
|
if (origPKKeys.length > 0) alters.push('DROP PRIMARY KEY');
|
|
if (newPKKeys.length > 0) {
|
|
const pkNames = input.columns
|
|
.filter((col) => col.key === 'PRI')
|
|
.map((col) => quoteIdentifierPart(col.name, dbType))
|
|
.join(', ');
|
|
alters.push(`ADD PRIMARY KEY (${pkNames})`);
|
|
}
|
|
}
|
|
|
|
return alters.length === 0 ? '' : `ALTER TABLE ${tableName}\n${alters.join(',\n')};`;
|
|
};
|
|
|
|
const buildDorisAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string): string => {
|
|
const tableName = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableName}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableName}\nADD COLUMN ${buildDorisColumnDefinition(curr, dbType)};`);
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
statements.push(`ALTER TABLE ${tableName}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} ${quoteIdentifierPart(curr.name, dbType)};`);
|
|
currentName = curr.name;
|
|
}
|
|
|
|
if (definitionChanged(curr, orig)) {
|
|
statements.push(`ALTER TABLE ${tableName}\nMODIFY COLUMN ${buildDorisColumnDefinition({ ...curr, name: currentName }, dbType)};`);
|
|
}
|
|
});
|
|
|
|
const origPKKeys = input.originalColumns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const newPKKeys = input.columns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key));
|
|
if (keysChanged) {
|
|
statements.push('-- Doris 修改主键/Key 模型需要按表模型手工迁移,已避免生成 MySQL 专属的 DROP/ADD PRIMARY KEY。');
|
|
}
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
const buildPgLikeAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string): string => {
|
|
const tableParts = splitQualifiedName(input.tableName);
|
|
const baseTableName = tableParts.objectName || stripIdentifierQuotes(input.tableName);
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD COLUMN ${buildPgLikeColumnDefinition(curr, dbType)};`);
|
|
if (String(curr.comment || '').trim()) statements.push(buildColumnCommentSql(tableRef, curr.name, curr.comment || '', dbType));
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} TO ${quoteIdentifierPart(curr.name, dbType)};`);
|
|
currentName = curr.name;
|
|
}
|
|
|
|
if (curr.type !== orig.type) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} TYPE ${curr.type};`);
|
|
}
|
|
|
|
const currDefault = normalizeDefaultText(curr.default);
|
|
const origDefault = normalizeDefaultText(orig.default);
|
|
if (currDefault !== origDefault) {
|
|
if (currDefault) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DEFAULT ${formatDefaultExpression(currDefault, dbType)};`);
|
|
} else {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} DROP DEFAULT;`);
|
|
}
|
|
}
|
|
|
|
if (curr.nullable !== orig.nullable) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} ${curr.nullable === 'NO' ? 'SET NOT NULL' : 'DROP NOT NULL'};`);
|
|
}
|
|
|
|
if ((curr.comment || '') !== (orig.comment || '')) {
|
|
statements.push(buildColumnCommentSql(tableRef, currentName, curr.comment || '', dbType));
|
|
}
|
|
});
|
|
|
|
const origPKKeys = input.originalColumns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const newPKKeys = input.columns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key));
|
|
if (keysChanged) {
|
|
if (origPKKeys.length > 0) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP CONSTRAINT IF EXISTS ${quoteIdentifierPart(`${baseTableName}_pkey`, dbType)};`);
|
|
}
|
|
if (newPKKeys.length > 0) {
|
|
const pkNames = input.columns
|
|
.filter((col) => col.key === 'PRI')
|
|
.map((col) => quoteIdentifierPart(col.name, dbType))
|
|
.join(', ');
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD PRIMARY KEY (${pkNames});`);
|
|
}
|
|
}
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
const buildOracleLikeAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string): string => {
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD (${buildStandardColumnDefinition(curr, dbType, { includeIdentity: true })});`);
|
|
if (String(curr.comment || '').trim()) statements.push(buildColumnCommentSql(tableRef, curr.name, curr.comment || '', dbType));
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} TO ${quoteIdentifierPart(curr.name, dbType)};`);
|
|
currentName = curr.name;
|
|
}
|
|
|
|
if (physicalDefinitionChanged(curr, orig)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nMODIFY (${buildStandardColumnDefinition({ ...curr, name: currentName }, dbType, { includeIdentity: true })});`);
|
|
}
|
|
|
|
if ((curr.comment || '') !== (orig.comment || '')) {
|
|
statements.push(buildColumnCommentSql(tableRef, currentName, curr.comment || '', dbType));
|
|
}
|
|
});
|
|
|
|
const origPKKeys = input.originalColumns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const newPKKeys = input.columns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key));
|
|
if (keysChanged) {
|
|
if (origPKKeys.length > 0) statements.push(`ALTER TABLE ${tableRef}\nDROP PRIMARY KEY;`);
|
|
if (newPKKeys.length > 0) {
|
|
const pkNames = input.columns.filter((col) => col.key === 'PRI').map((col) => quoteIdentifierPart(col.name, dbType)).join(', ');
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD PRIMARY KEY (${pkNames});`);
|
|
}
|
|
}
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
const buildSqlServerDefaultDropBatch = (tableName: string, columnName: string): string => {
|
|
const { schemaName, objectName } = splitQualifiedName(tableName);
|
|
const schema = escapeSqlString(schemaName || 'dbo');
|
|
const table = escapeSqlString(objectName || tableName);
|
|
const column = escapeSqlString(columnName);
|
|
const tableRef = quoteIdentifierPath(`${schemaName || 'dbo'}.${objectName || tableName}`, 'sqlserver');
|
|
return `DECLARE @gonavi_df nvarchar(128); SELECT @gonavi_df = dc.name FROM sys.default_constraints dc JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id JOIN sys.tables t ON c.object_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = N'${schema}' AND t.name = N'${table}' AND c.name = N'${column}'; IF @gonavi_df IS NOT NULL EXEC(N'ALTER TABLE ${tableRef} DROP CONSTRAINT ' + QUOTENAME(@gonavi_df));`;
|
|
};
|
|
|
|
const buildSqlServerAlterPreviewSql = (input: BuildAlterTablePreviewInput): string => {
|
|
const dbType = 'sqlserver';
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD ${buildStandardColumnDefinition(curr, dbType, { includeNull: true, includeIdentity: true })};`);
|
|
if (String(curr.comment || '').trim()) statements.push(buildSqlServerColumnCommentSql(input.tableName, curr.name, curr.comment || ''));
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
const plainTablePath = unquoteSqlIdentifierPath(input.tableName);
|
|
statements.push(`EXEC sp_rename '${escapeSqlString(`${plainTablePath}.${orig.name}`)}', '${escapeSqlString(curr.name)}', 'COLUMN';`);
|
|
currentName = curr.name;
|
|
}
|
|
|
|
if (curr.type !== orig.type || curr.nullable !== orig.nullable || Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${buildStandardColumnDefinition({ ...curr, name: currentName, default: '' }, dbType, { includeNull: true, includeIdentity: false })};`);
|
|
}
|
|
|
|
const currDefault = normalizeDefaultText(curr.default);
|
|
const origDefault = normalizeDefaultText(orig.default);
|
|
if (currDefault !== origDefault) {
|
|
statements.push(buildSqlServerDefaultDropBatch(input.tableName, currentName));
|
|
if (currDefault) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD DEFAULT ${formatDefaultExpression(currDefault, dbType)} FOR ${quoteIdentifierPart(currentName, dbType)};`);
|
|
}
|
|
}
|
|
|
|
if ((curr.comment || '') !== (orig.comment || '')) {
|
|
statements.push(buildSqlServerColumnCommentSql(input.tableName, currentName, curr.comment || ''));
|
|
}
|
|
});
|
|
|
|
const origPKKeys = input.originalColumns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const newPKKeys = input.columns.filter((col) => col.key === 'PRI').map((col) => col._key);
|
|
const keysChanged = origPKKeys.length !== newPKKeys.length || !origPKKeys.every((key) => newPKKeys.includes(key));
|
|
if (keysChanged) {
|
|
const { objectName } = splitQualifiedName(input.tableName);
|
|
const constraintName = quoteIdentifierPart(`PK_${objectName || 'table'}`, dbType);
|
|
if (origPKKeys.length > 0) {
|
|
statements.push(`-- SQL Server 删除旧主键需要原约束名;请先在索引页确认后删除。`);
|
|
}
|
|
if (newPKKeys.length > 0) {
|
|
const pkNames = input.columns.filter((col) => col.key === 'PRI').map((col) => quoteIdentifierPart(col.name, dbType)).join(', ');
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD CONSTRAINT ${constraintName} PRIMARY KEY (${pkNames});`);
|
|
}
|
|
}
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
const buildSqliteAlterPreviewSql = (input: BuildAlterTablePreviewInput): string => {
|
|
const dbType = 'sqlite';
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD COLUMN ${buildStandardColumnDefinition(curr, dbType)};`);
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} TO ${quoteIdentifierPart(curr.name, dbType)};`);
|
|
currentName = curr.name;
|
|
}
|
|
if (physicalDefinitionChanged(curr, orig) || (curr.comment || '') !== (orig.comment || '')) {
|
|
statements.push(`-- SQLite 不支持直接修改字段属性,请通过创建新表、迁移数据、替换旧表的方式处理字段 ${currentName}。`);
|
|
}
|
|
});
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
const buildDuckDbAlterPreviewSql = (input: BuildAlterTablePreviewInput): string => {
|
|
const dbType = 'duckdb';
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD COLUMN ${buildStandardColumnDefinition(curr, dbType)};`);
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} TO ${quoteIdentifierPart(curr.name, dbType)};`);
|
|
currentName = curr.name;
|
|
}
|
|
if (curr.type !== orig.type) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DATA TYPE ${curr.type};`);
|
|
}
|
|
const currDefault = normalizeDefaultText(curr.default);
|
|
const origDefault = normalizeDefaultText(orig.default);
|
|
if (currDefault !== origDefault) {
|
|
if (currDefault) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} SET DEFAULT ${formatDefaultExpression(currDefault, dbType)};`);
|
|
} else {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} DROP DEFAULT;`);
|
|
}
|
|
}
|
|
if (curr.nullable !== orig.nullable) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nALTER COLUMN ${quoteIdentifierPart(currentName, dbType)} ${curr.nullable === 'NO' ? 'SET NOT NULL' : 'DROP NOT NULL'};`);
|
|
}
|
|
if ((curr.comment || '') !== (orig.comment || '')) {
|
|
statements.push(`-- DuckDB 不支持通过 COMMENT ON COLUMN 持久化字段备注,字段 ${currentName} 的备注仅保留在设计器预览中。`);
|
|
}
|
|
});
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
const buildLimitedBacktickAlterPreviewSql = (input: BuildAlterTablePreviewInput, dbType: string, label: string): string => {
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const statements: string[] = [];
|
|
|
|
input.originalColumns.forEach((orig) => {
|
|
if (!input.columns.find((col) => col._key === orig._key)) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nDROP COLUMN ${quoteIdentifierPart(orig.name, dbType)};`);
|
|
}
|
|
});
|
|
|
|
input.columns.forEach((curr) => {
|
|
const orig = input.originalColumns.find((col) => col._key === curr._key);
|
|
if (!orig) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nADD COLUMN ${quoteIdentifierPart(curr.name, dbType)} ${curr.type};`);
|
|
if (curr.nullable === 'NO' || normalizeDefaultText(curr.default) || String(curr.comment || '').trim()) {
|
|
statements.push(`-- ${label} 的字段约束/默认值/备注语法与 MySQL 不同,已避免生成 MySQL 专属子句,请按目标库能力补充。`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let currentName = orig.name;
|
|
if (curr.name !== orig.name) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nRENAME COLUMN ${quoteIdentifierPart(orig.name, dbType)} TO ${quoteIdentifierPart(curr.name, dbType)};`);
|
|
currentName = curr.name;
|
|
}
|
|
if (curr.type !== orig.type) {
|
|
statements.push(`ALTER TABLE ${tableRef}\nMODIFY COLUMN ${quoteIdentifierPart(currentName, dbType)} ${curr.type};`);
|
|
}
|
|
if (
|
|
curr.nullable !== orig.nullable ||
|
|
normalizeDefaultText(curr.default) !== normalizeDefaultText(orig.default) ||
|
|
(curr.comment || '') !== (orig.comment || '') ||
|
|
Boolean(curr.isAutoIncrement) !== Boolean(orig.isAutoIncrement)
|
|
) {
|
|
statements.push(`-- ${label} 的字段约束/默认值/备注语法与 MySQL 不同,已避免生成 MySQL 专属子句,请按目标库能力补充。`);
|
|
}
|
|
});
|
|
|
|
return statements.join('\n');
|
|
};
|
|
|
|
export const buildAlterTablePreviewSql = (input: BuildAlterTablePreviewInput): string => {
|
|
const dbType = resolveSqlDialect(input.dbType);
|
|
if (isPgLikeDialect(dbType)) return buildPgLikeAlterPreviewSql({ ...input, dbType }, dbType);
|
|
if (isOracleLikeDialect(dbType)) return buildOracleLikeAlterPreviewSql({ ...input, dbType }, dbType);
|
|
if (isSqlServerDialect(dbType)) return buildSqlServerAlterPreviewSql({ ...input, dbType });
|
|
if (dbType === 'sqlite') return buildSqliteAlterPreviewSql({ ...input, dbType });
|
|
if (dbType === 'duckdb') return buildDuckDbAlterPreviewSql({ ...input, dbType });
|
|
if (dbType === 'diros') return buildDorisAlterPreviewSql({ ...input, dbType }, dbType);
|
|
if (dbType === 'starrocks') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'StarRocks');
|
|
if (dbType === 'clickhouse') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'ClickHouse');
|
|
if (dbType === 'tdengine') return buildLimitedBacktickAlterPreviewSql({ ...input, dbType }, dbType, 'TDengine');
|
|
if (isMysqlFamilyDialect(dbType)) return buildMySqlAlterPreviewSql({ ...input, dbType }, dbType);
|
|
return buildPgLikeAlterPreviewSql({ ...input, dbType }, dbType);
|
|
};
|
|
|
|
export const hasAlterTableDraftChanges = (input: BuildAlterTablePreviewInput): boolean =>
|
|
buildAlterTablePreviewSql(input).trim().length > 0;
|
|
|
|
const buildCreateTableColumnDefinition = (column: EditableColumnSnapshot, dbType: string): string => {
|
|
if (isMysqlFamilyDialect(dbType)) {
|
|
return buildMySqlColumnDefinition(column, dbType);
|
|
}
|
|
if (isOracleLikeDialect(dbType)) {
|
|
return buildStandardColumnDefinition(column, dbType, { includeIdentity: true });
|
|
}
|
|
if (isSqlServerDialect(dbType)) {
|
|
return buildStandardColumnDefinition(column, dbType, { includeNull: true, includeIdentity: true });
|
|
}
|
|
if (dbType === 'clickhouse' || dbType === 'tdengine') {
|
|
return [quoteIdentifierPart(column.name, dbType), String(column.type || '').trim()].join(' ');
|
|
}
|
|
return buildStandardColumnDefinition(column, dbType);
|
|
};
|
|
|
|
const buildCreateColumnComments = (tableRef: string, input: BuildCreateTablePreviewInput, dbType: string): string[] => (
|
|
input.columns
|
|
.filter((column) => String(column.comment || '').trim())
|
|
.map((column) => {
|
|
if (isSqlServerDialect(dbType)) {
|
|
return buildSqlServerColumnCommentSql(input.tableName, column.name, column.comment || '');
|
|
}
|
|
if (isPgLikeDialect(dbType) || isOracleLikeDialect(dbType)) {
|
|
return buildColumnCommentSql(tableRef, column.name, column.comment || '', dbType);
|
|
}
|
|
return '';
|
|
})
|
|
.filter(Boolean)
|
|
);
|
|
|
|
const normalizeStarRocksKeyModel = (value: unknown): StarRocksKeyModel => {
|
|
const normalized = String(value || '').trim().toUpperCase();
|
|
if (normalized === 'PRIMARY' || normalized === 'UNIQUE' || normalized === 'AGGREGATE') return normalized;
|
|
return 'DUPLICATE';
|
|
};
|
|
|
|
const normalizeStarRocksDistributionType = (value: unknown): StarRocksDistributionType => {
|
|
const normalized = String(value || '').trim().toUpperCase();
|
|
if (normalized === 'RANDOM' || normalized === 'NONE') return normalized;
|
|
return 'HASH';
|
|
};
|
|
|
|
const pickStarRocksKeyColumns = (
|
|
input: BuildCreateTablePreviewInput,
|
|
options: StarRocksCreateTableOptions,
|
|
): string[] => {
|
|
const requested = Array.isArray(options.keyColumnNames) ? options.keyColumnNames : [];
|
|
const fallback = input.columns.filter((column) => column.key === 'PRI').map((column) => column.name);
|
|
const source = requested.length > 0 ? requested : (fallback.length > 0 ? fallback : input.columns.slice(0, 1).map((column) => column.name));
|
|
return source.map((columnName) => String(columnName || '').trim()).filter(Boolean);
|
|
};
|
|
|
|
const quoteStarRocksColumnList = (columnNames: string[]): string => (
|
|
columnNames.map((columnName) => quoteIdentifierPart(columnName, 'starrocks')).filter(Boolean).join(', ')
|
|
);
|
|
|
|
const normalizeStarRocksPropertiesBlock = (raw: unknown): string => {
|
|
const lines = String(raw || '')
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim().replace(/,+$/, ''))
|
|
.filter(Boolean);
|
|
if (lines.length === 0) return '';
|
|
return `PROPERTIES (\n ${lines.join(',\n ')}\n)`;
|
|
};
|
|
|
|
const buildStarRocksDistributionSql = (
|
|
input: BuildCreateTablePreviewInput,
|
|
options: StarRocksCreateTableOptions,
|
|
keyColumns: string[],
|
|
): string => {
|
|
const distributionType = normalizeStarRocksDistributionType(options.distributionType);
|
|
if (distributionType === 'NONE') return '';
|
|
if (distributionType === 'RANDOM') {
|
|
return options.bucketMode === 'NUMBER' && Number(options.bucketCount) > 0
|
|
? `DISTRIBUTED BY RANDOM BUCKETS ${Number(options.bucketCount)}`
|
|
: 'DISTRIBUTED BY RANDOM BUCKETS AUTO';
|
|
}
|
|
|
|
const requested = Array.isArray(options.distributionColumnNames) ? options.distributionColumnNames : [];
|
|
const distributionColumns = requested.length > 0 ? requested : keyColumns;
|
|
const columnList = quoteStarRocksColumnList(
|
|
distributionColumns.length > 0 ? distributionColumns : input.columns.slice(0, 1).map((column) => column.name)
|
|
);
|
|
if (!columnList) return '';
|
|
|
|
const bucketSql = options.bucketMode === 'NUMBER' && Number(options.bucketCount) > 0
|
|
? `BUCKETS ${Number(options.bucketCount)}`
|
|
: 'BUCKETS AUTO';
|
|
return `DISTRIBUTED BY HASH(${columnList}) ${bucketSql}`;
|
|
};
|
|
|
|
const buildStarRocksRollupSql = (tableRef: string, rollups: StarRocksRollupOption[] | undefined): string[] => (
|
|
(Array.isArray(rollups) ? rollups : [])
|
|
.map((rollup) => {
|
|
const rollupName = String(rollup?.name || '').trim();
|
|
const columnList = quoteStarRocksColumnList(Array.isArray(rollup?.columnNames) ? rollup.columnNames : []);
|
|
if (!rollupName || !columnList) return '';
|
|
const fromSql = String(rollup.fromIndexName || '').trim()
|
|
? ` FROM ${quoteIdentifierPart(String(rollup.fromIndexName || '').trim(), 'starrocks')}`
|
|
: '';
|
|
const propertiesSql = normalizeStarRocksPropertiesBlock(rollup.properties);
|
|
const suffix = propertiesSql ? `\n${propertiesSql}` : '';
|
|
return `ALTER TABLE ${tableRef}\nADD ROLLUP ${quoteIdentifierPart(rollupName, 'starrocks')} (${columnList})${fromSql}${suffix};`;
|
|
})
|
|
.filter(Boolean)
|
|
);
|
|
|
|
const buildStarRocksCreateTablePreviewSql = (input: BuildCreateTablePreviewInput): string => {
|
|
const options = input.starRocksOptions || {};
|
|
const tableRef = quoteIdentifierPath(input.tableName, 'starrocks');
|
|
const colDefs = input.columns.map((column) => buildStarRocksColumnDefinition(column));
|
|
const createPrefix = options.tableKind === 'external' ? 'CREATE EXTERNAL TABLE' : 'CREATE TABLE';
|
|
const createSql = `${createPrefix} ${tableRef} (\n ${colDefs.join(',\n ')}\n)`;
|
|
|
|
if (options.tableKind === 'external') {
|
|
const engine = String(options.externalEngine || 'hive').trim().toUpperCase();
|
|
const propertiesSql = normalizeStarRocksPropertiesBlock(options.externalProperties || options.properties);
|
|
return `${createSql}\nENGINE=${engine}${propertiesSql ? `\n${propertiesSql}` : ''};`;
|
|
}
|
|
|
|
const keyModel = normalizeStarRocksKeyModel(options.keyModel);
|
|
const keyColumns = pickStarRocksKeyColumns(input, options);
|
|
const keyColumnSql = quoteStarRocksColumnList(keyColumns);
|
|
const keySql = keyColumnSql ? `${keyModel} KEY (${keyColumnSql})` : '';
|
|
const partitionSql = String(options.partitionClause || '').trim().replace(/;+\s*$/, '');
|
|
const distributionSql = buildStarRocksDistributionSql(input, options, keyColumns);
|
|
const propertiesSql = normalizeStarRocksPropertiesBlock(options.properties);
|
|
|
|
const clauses = [
|
|
'ENGINE=OLAP',
|
|
keySql,
|
|
partitionSql,
|
|
distributionSql,
|
|
propertiesSql,
|
|
].filter(Boolean);
|
|
const createStatement = `${createSql}\n${clauses.join('\n')};`;
|
|
const rollupStatements = buildStarRocksRollupSql(tableRef, options.rollups);
|
|
return [createStatement, ...rollupStatements].join('\n');
|
|
};
|
|
|
|
export const buildStarRocksMaterializedViewPreviewSql = (
|
|
input: BuildStarRocksMaterializedViewPreviewInput,
|
|
): string => {
|
|
const name = quoteIdentifierPath(input.name || 'mv_name', 'starrocks');
|
|
const query = String(input.query || '').trim().replace(/;+\s*$/, '') || 'SELECT column1, COUNT(*) AS cnt\nFROM table_name\nGROUP BY column1';
|
|
const commentSql = String(input.comment || '').trim() ? `\nCOMMENT '${escapeSqlString(String(input.comment || '').trim())}'` : '';
|
|
const refreshSql = String(input.refreshClause || '').trim()
|
|
|| (input.async === false ? 'REFRESH MANUAL' : 'REFRESH ASYNC');
|
|
const partitionSql = String(input.partitionClause || '').trim().replace(/;+\s*$/, '');
|
|
const distributionColumns = quoteStarRocksColumnList(Array.isArray(input.distributionColumnNames) ? input.distributionColumnNames : []);
|
|
const distributionSql = distributionColumns
|
|
? `DISTRIBUTED BY HASH(${distributionColumns}) BUCKETS ${Number(input.bucketCount) > 0 ? Number(input.bucketCount) : 'AUTO'}`
|
|
: '';
|
|
const orderByColumns = quoteStarRocksColumnList(Array.isArray(input.orderByColumnNames) ? input.orderByColumnNames : []);
|
|
const orderBySql = orderByColumns ? `ORDER BY (${orderByColumns})` : '';
|
|
const propertiesSql = normalizeStarRocksPropertiesBlock(input.properties);
|
|
return [
|
|
`CREATE MATERIALIZED VIEW ${name}${commentSql}`,
|
|
refreshSql,
|
|
partitionSql,
|
|
distributionSql,
|
|
orderBySql,
|
|
propertiesSql,
|
|
'AS',
|
|
`${query};`,
|
|
].filter(Boolean).join('\n');
|
|
};
|
|
|
|
export const buildCreateTablePreviewSql = (input: BuildCreateTablePreviewInput): string => {
|
|
const dbType = resolveSqlDialect(input.dbType);
|
|
if (dbType === 'starrocks') {
|
|
return buildStarRocksCreateTablePreviewSql({ ...input, dbType });
|
|
}
|
|
|
|
const tableRef = quoteIdentifierPath(input.tableName, dbType);
|
|
const colDefs = input.columns.map((column) => buildCreateTableColumnDefinition(column, dbType));
|
|
const pkColumns = input.columns.filter((column) => column.key === 'PRI');
|
|
if (pkColumns.length > 0) {
|
|
const pkNames = pkColumns.map((column) => quoteIdentifierPart(column.name, dbType)).join(', ');
|
|
colDefs.push(`PRIMARY KEY (${pkNames})`);
|
|
}
|
|
|
|
const createSql = `CREATE TABLE ${tableRef} (\n ${colDefs.join(',\n ')}\n)`;
|
|
const comments = buildCreateColumnComments(tableRef, input, dbType);
|
|
|
|
if (dbType === 'mysql' || dbType === 'mariadb') {
|
|
const charset = String(input.charset || '').trim();
|
|
const collation = String(input.collation || '').trim();
|
|
const charsetSql = charset ? ` DEFAULT CHARSET=${charset}` : '';
|
|
const collationSql = collation ? ` COLLATE=${collation}` : '';
|
|
return `${createSql} ENGINE=InnoDB${charsetSql}${collationSql};`;
|
|
}
|
|
|
|
if (dbType === 'clickhouse') {
|
|
return `${createSql}\nENGINE = MergeTree\nORDER BY tuple();`;
|
|
}
|
|
|
|
const suffixComments = comments.length > 0 ? `\n${comments.join('\n')}` : '';
|
|
if (dbType === 'tdengine' && !input.columns.some((column) => /^timestamp$/i.test(String(column.type || '').trim()))) {
|
|
return `${createSql};\n-- TDengine 普通表通常需要 TIMESTAMP 时间列,执行前请确认表模型。${suffixComments}`;
|
|
}
|
|
|
|
if (isBacktickIdentifierDialect(dbType) && dbType !== 'mysql' && dbType !== 'mariadb') {
|
|
return `${createSql};${suffixComments}`;
|
|
}
|
|
|
|
return `${createSql};${suffixComments}`;
|
|
};
|