import { Field, rateScale } from './dsl.js'; import { ForeignKey, Index, TableSchema } from './db.js'; // MySQL driver: converts a TableSchema into a CREATE TABLE statement. /** Default value constant for created_at columns. */ export const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP'; /** Default value constant for updated_at columns (with auto-update). */ export const CURRENT_TIMESTAMP_ON_UPDATE = 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'; function columnType(field: Field): string { switch (field.type) { case 'string': if (!field.maxLength) throw new Error(`field ${field.name} (string) requires maxLength`); return `VARCHAR(${field.maxLength})`; case 'text': return 'TEXT'; case 'integer': return 'INT'; case 'bigint': return 'BIGINT'; case 'decimal': return `DECIMAL(${field.precision}, ${field.scale})`; case 'rate': return `DECIMAL(5, ${rateScale(field.unit)})`; case 'boolean': return 'TINYINT(1)'; case 'date': return 'DATE'; case 'time': return 'TIME'; case 'datetime': return 'DATETIME'; case 'enum': // Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT. return field.enum.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)'; case 'json': return 'JSON'; case 'array': case 'object': // Nested fields are wire-format only (third-party messages); table columns cannot nest. throw new Error(`field ${field.name} (${field.type}): nested fields are not supported on table columns`); case 'aggregate': // Aggregate fields are query outputs, never table columns. throw new Error(`field ${field.name} (aggregate): aggregate fields are not supported on table columns`); } } function renderDefault(field: Field): string { if (field.default === undefined) return ''; // MySQL keyword expressions (CURRENT_TIMESTAMP / CURRENT_TIMESTAMP ON UPDATE // CURRENT_TIMESTAMP) render bare; any other value is a literal and gets quoted. if (field.default.startsWith(CURRENT_TIMESTAMP)) return ` DEFAULT ${field.default}`; // Numeric columns take a bare literal, not a quoted one. if (field.type === 'integer' || field.type === 'bigint' || field.type === 'decimal' || field.type === 'rate' || field.type === 'boolean') { return ` DEFAULT ${field.default}`; } return ` DEFAULT '${field.default}'`; } function columnDef(field: Field, autoIncrement?: Field): string { const parts = [field.name, columnType(field)]; if (field.optional === false) parts.push('NOT NULL'); parts.push(renderDefault(field)); if (field === autoIncrement) parts.push('AUTO_INCREMENT'); if (field.description) parts.push(`COMMENT '${field.description.replace(/'/g, "\\'")}'`); return parts.join(' '); } function primaryKeyClause(schema: TableSchema): string | null { if (!schema.primaryKey) return null; const fields = Array.isArray(schema.primaryKey) ? schema.primaryKey : [schema.primaryKey]; return `PRIMARY KEY (${fields.map((f) => f.name).join(', ')})`; } function indexClause(index: Index): string { const fields = Array.isArray(index.columns) ? index.columns : [index.columns]; const kind = index.unique ? 'UNIQUE KEY' : 'KEY'; const name = index.name ?? fields.map((f) => f.name).join('_'); return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`; } function foreignKeyClause(name: string, fk: ForeignKey): string { const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns]; const refs = Array.isArray(fk.references) ? fk.references : [fk.references]; const refTable = refs[0].schema?.name; if (!refTable) throw new Error(`foreign key ${name}: references field has no schema`); const fkCols = fields.map((f) => f.name).join(', '); const refCols = refs.map((r) => r.name).join(', '); return `CONSTRAINT \`${name}\` FOREIGN KEY (${fkCols}) REFERENCES \`${refTable}\` (${refCols})`; } export interface BuildCreateTableSqlOptions { /** 是否生成外键约束,默认 false(不生成) */ generateForeignKeys?: boolean; } export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTableSqlOptions = {}): string { const lines = Object.values(schema.columns).map((field) => columnDef(field, schema.autoIncrement)); const pk = primaryKeyClause(schema); if (pk) lines.push(pk); for (const index of schema.indexes ?? []) lines.push(indexClause(index)); if (options.generateForeignKeys) { for (const [name, fk] of Object.entries(schema.foreignKeys ?? {})) { lines.push(foreignKeyClause(name, fk)); } } return `CREATE TABLE IF NOT EXISTS \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`; }