metal-orm 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +21 -18
  2. package/dist/decorators/index.cjs +317 -34
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +317 -34
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1965 -267
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +273 -23
  11. package/dist/index.d.ts +273 -23
  12. package/dist/index.js +1947 -267
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-654m4qy8.d.cts → select-CCp1oz9p.d.cts} +254 -4
  15. package/dist/{select-654m4qy8.d.ts → select-CCp1oz9p.d.ts} +254 -4
  16. package/package.json +3 -2
  17. package/src/core/ast/query.ts +40 -22
  18. package/src/core/ddl/dialects/base-schema-dialect.ts +48 -0
  19. package/src/core/ddl/dialects/index.ts +5 -0
  20. package/src/core/ddl/dialects/mssql-schema-dialect.ts +97 -0
  21. package/src/core/ddl/dialects/mysql-schema-dialect.ts +109 -0
  22. package/src/core/ddl/dialects/postgres-schema-dialect.ts +99 -0
  23. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +103 -0
  24. package/src/core/ddl/introspect/mssql.ts +149 -0
  25. package/src/core/ddl/introspect/mysql.ts +99 -0
  26. package/src/core/ddl/introspect/postgres.ts +154 -0
  27. package/src/core/ddl/introspect/sqlite.ts +66 -0
  28. package/src/core/ddl/introspect/types.ts +19 -0
  29. package/src/core/ddl/introspect/utils.ts +27 -0
  30. package/src/core/ddl/schema-diff.ts +179 -0
  31. package/src/core/ddl/schema-generator.ts +229 -0
  32. package/src/core/ddl/schema-introspect.ts +32 -0
  33. package/src/core/ddl/schema-types.ts +39 -0
  34. package/src/core/dialect/abstract.ts +122 -37
  35. package/src/core/dialect/base/sql-dialect.ts +204 -0
  36. package/src/core/dialect/mssql/index.ts +125 -80
  37. package/src/core/dialect/mysql/index.ts +18 -112
  38. package/src/core/dialect/postgres/index.ts +29 -126
  39. package/src/core/dialect/sqlite/index.ts +28 -129
  40. package/src/index.ts +4 -0
  41. package/src/orm/execute.ts +25 -16
  42. package/src/orm/orm-context.ts +60 -55
  43. package/src/orm/query-logger.ts +38 -0
  44. package/src/orm/relations/belongs-to.ts +42 -26
  45. package/src/orm/relations/has-many.ts +41 -25
  46. package/src/orm/relations/many-to-many.ts +43 -27
  47. package/src/orm/unit-of-work.ts +60 -23
  48. package/src/query-builder/hydration-manager.ts +229 -25
  49. package/src/query-builder/query-ast-service.ts +27 -12
  50. package/src/query-builder/select-query-state.ts +24 -12
  51. package/src/query-builder/select.ts +58 -14
  52. package/src/schema/column.ts +206 -27
  53. package/src/schema/table.ts +89 -32
  54. package/src/schema/types.ts +8 -5
@@ -0,0 +1,154 @@
1
+ import { SchemaIntrospector, IntrospectOptions } from './types.js';
2
+ import { queryRows, shouldIncludeTable } from './utils.js';
3
+ import { DatabaseSchema, DatabaseTable, DatabaseIndex, DatabaseColumn } from '../schema-types.js';
4
+ import { DbExecutor } from '../../orm/db-executor.js';
5
+
6
+ export const postgresIntrospector: SchemaIntrospector = {
7
+ async introspect(executor: DbExecutor, options: IntrospectOptions): Promise<DatabaseSchema> {
8
+ const schema = options.schema || 'public';
9
+ const tables: DatabaseTable[] = [];
10
+
11
+ const columnRows = await queryRows(
12
+ executor,
13
+ `
14
+ SELECT table_schema, table_name, column_name, data_type, is_nullable, column_default
15
+ FROM information_schema.columns
16
+ WHERE table_schema = $1
17
+ ORDER BY table_name, ordinal_position
18
+ `,
19
+ [schema]
20
+ );
21
+
22
+ const pkRows = await queryRows(
23
+ executor,
24
+ `
25
+ SELECT
26
+ ns.nspname AS table_schema,
27
+ tbl.relname AS table_name,
28
+ array_agg(att.attname ORDER BY arr.idx) AS pk_columns
29
+ FROM pg_index i
30
+ JOIN pg_class tbl ON tbl.oid = i.indrelid
31
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
32
+ JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS arr(attnum, idx) ON TRUE
33
+ LEFT JOIN pg_attribute att ON att.attrelid = tbl.oid AND att.attnum = arr.attnum
34
+ WHERE i.indisprimary AND ns.nspname = $1
35
+ GROUP BY ns.nspname, tbl.relname
36
+ `,
37
+ [schema]
38
+ );
39
+
40
+ const pkMap = new Map<string, string[]>();
41
+ pkRows.forEach(r => {
42
+ pkMap.set(`${r.table_schema}.${r.table_name}`, r.pk_columns || []);
43
+ });
44
+
45
+ const fkRows = await queryRows(
46
+ executor,
47
+ `
48
+ SELECT
49
+ tc.table_schema,
50
+ tc.table_name,
51
+ kcu.column_name,
52
+ ccu.table_schema AS foreign_table_schema,
53
+ ccu.table_name AS foreign_table_name,
54
+ ccu.column_name AS foreign_column_name,
55
+ rc.update_rule AS on_update,
56
+ rc.delete_rule AS on_delete
57
+ FROM information_schema.table_constraints AS tc
58
+ JOIN information_schema.key_column_usage AS kcu
59
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
60
+ JOIN information_schema.constraint_column_usage AS ccu
61
+ ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
62
+ JOIN information_schema.referential_constraints rc
63
+ ON rc.constraint_name = tc.constraint_name AND rc.constraint_schema = tc.table_schema
64
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1
65
+ `,
66
+ [schema]
67
+ );
68
+
69
+ const fkMap = new Map<string, any[]>();
70
+ fkRows.forEach(r => {
71
+ const key = `${r.table_schema}.${r.table_name}.${r.column_name}`;
72
+ fkMap.set(key, [{
73
+ table: `${r.foreign_table_schema}.${r.foreign_table_name}`,
74
+ column: r.foreign_column_name,
75
+ onDelete: r.on_delete?.toUpperCase(),
76
+ onUpdate: r.on_update?.toUpperCase()
77
+ }]);
78
+ });
79
+
80
+ const indexRows = await queryRows(
81
+ executor,
82
+ `
83
+ SELECT
84
+ ns.nspname AS table_schema,
85
+ tbl.relname AS table_name,
86
+ idx.relname AS index_name,
87
+ i.indisunique AS is_unique,
88
+ pg_get_expr(i.indpred, i.indrelid) AS predicate,
89
+ array_agg(att.attname ORDER BY arr.idx) AS column_names
90
+ FROM pg_index i
91
+ JOIN pg_class tbl ON tbl.oid = i.indrelid
92
+ JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
93
+ JOIN pg_class idx ON idx.oid = i.indexrelid
94
+ JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS arr(attnum, idx) ON TRUE
95
+ LEFT JOIN pg_attribute att ON att.attrelid = tbl.oid AND att.attnum = arr.attnum
96
+ WHERE ns.nspname = $1 AND NOT i.indisprimary
97
+ GROUP BY ns.nspname, tbl.relname, idx.relname, i.indisunique, i.indpred
98
+ `,
99
+ [schema]
100
+ );
101
+
102
+ const tablesByKey = new Map<string, DatabaseTable>();
103
+
104
+ columnRows.forEach(r => {
105
+ const key = `${r.table_schema}.${r.table_name}`;
106
+ if (!shouldIncludeTable(r.table_name, options)) {
107
+ return;
108
+ }
109
+ if (!tablesByKey.has(key)) {
110
+ tablesByKey.set(key, {
111
+ name: r.table_name,
112
+ schema: r.table_schema,
113
+ columns: [],
114
+ primaryKey: pkMap.get(key) || [],
115
+ indexes: []
116
+ });
117
+ }
118
+ const cols = tablesByKey.get(key)!;
119
+ const fk = fkMap.get(`${r.table_schema}.${r.table_name}.${r.column_name}`)?.[0];
120
+ const column: DatabaseColumn = {
121
+ name: r.column_name,
122
+ type: r.data_type,
123
+ notNull: r.is_nullable === 'NO',
124
+ default: r.column_default ?? undefined,
125
+ references: fk
126
+ ? {
127
+ table: fk.table,
128
+ column: fk.column,
129
+ onDelete: fk.onDelete,
130
+ onUpdate: fk.onUpdate
131
+ }
132
+ : undefined
133
+ };
134
+ cols.columns.push(column);
135
+ });
136
+
137
+ indexRows.forEach(r => {
138
+ const key = `${r.table_schema}.${r.table_name}`;
139
+ const table = tablesByKey.get(key);
140
+ if (!table) return;
141
+ const idx: DatabaseIndex = {
142
+ name: r.index_name,
143
+ columns: (r.column_names || []).map((c: string) => ({ column: c })),
144
+ unique: !!r.is_unique,
145
+ where: r.predicate || undefined
146
+ };
147
+ table.indexes = table.indexes || [];
148
+ table.indexes.push(idx);
149
+ });
150
+
151
+ tables.push(...tablesByKey.values());
152
+ return { tables };
153
+ }
154
+ };
@@ -0,0 +1,66 @@
1
+ import { SchemaIntrospector, IntrospectOptions } from './types.js';
2
+ import { queryRows, shouldIncludeTable } from './utils.js';
3
+ import { DatabaseSchema, DatabaseTable, DatabaseIndex } from '../schema-types.js';
4
+ import { DbExecutor } from '../../orm/db-executor.js';
5
+
6
+ const escapeSingleQuotes = (name: string) => name.replace(/'/g, "''");
7
+
8
+ export const sqliteIntrospector: SchemaIntrospector = {
9
+ async introspect(executor: DbExecutor, options: IntrospectOptions): Promise<DatabaseSchema> {
10
+ const tables: DatabaseTable[] = [];
11
+ const tableRows = await queryRows(
12
+ executor,
13
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';`
14
+ );
15
+
16
+ for (const row of tableRows) {
17
+ const name = row.name as string;
18
+ if (!shouldIncludeTable(name, options)) continue;
19
+ const table: DatabaseTable = { name, columns: [], primaryKey: [], indexes: [] };
20
+
21
+ const cols = await queryRows(executor, `PRAGMA table_info('${escapeSingleQuotes(name)}');`);
22
+ cols.forEach(c => {
23
+ table.columns.push({
24
+ name: c.name,
25
+ type: c.type,
26
+ notNull: c.notnull === 1,
27
+ default: c.dflt_value ?? undefined,
28
+ autoIncrement: false
29
+ });
30
+ if (c.pk && c.pk > 0) {
31
+ table.primaryKey = table.primaryKey || [];
32
+ table.primaryKey.push(c.name);
33
+ }
34
+ });
35
+
36
+ const fkRows = await queryRows(executor, `PRAGMA foreign_key_list('${escapeSingleQuotes(name)}');`);
37
+ fkRows.forEach(fk => {
38
+ const col = table.columns.find(c => c.name === fk.from);
39
+ if (col) {
40
+ col.references = {
41
+ table: fk.table,
42
+ column: fk.to,
43
+ onDelete: fk.on_delete?.toUpperCase(),
44
+ onUpdate: fk.on_update?.toUpperCase()
45
+ };
46
+ }
47
+ });
48
+
49
+ const idxList = await queryRows(executor, `PRAGMA index_list('${escapeSingleQuotes(name)}');`);
50
+ for (const idx of idxList) {
51
+ const idxName = idx.name as string;
52
+ const columnsInfo = await queryRows(executor, `PRAGMA index_info('${escapeSingleQuotes(idxName)}');`);
53
+ const idxEntry: DatabaseIndex = {
54
+ name: idxName,
55
+ columns: columnsInfo.map(ci => ({ column: ci.name as string })),
56
+ unique: idx.unique === 1
57
+ };
58
+ table.indexes!.push(idxEntry);
59
+ }
60
+
61
+ tables.push(table);
62
+ }
63
+
64
+ return { tables };
65
+ }
66
+ };
@@ -0,0 +1,19 @@
1
+ import type { DbExecutor } from '../../../orm/db-executor.js';
2
+ import { DatabaseSchema } from '../schema-types.js';
3
+
4
+ /**
5
+ * Dialect-agnostic options for schema introspection.
6
+ */
7
+ export interface IntrospectOptions {
8
+ /** Dialect-specific schema/catalog. Postgres: schema; MySQL: database; MSSQL: schema. */
9
+ schema?: string;
10
+ includeTables?: string[];
11
+ excludeTables?: string[];
12
+ }
13
+
14
+ /**
15
+ * Strategy interface implemented per dialect to introspect an existing database schema.
16
+ */
17
+ export interface SchemaIntrospector {
18
+ introspect(executor: DbExecutor, options: IntrospectOptions): Promise<DatabaseSchema>;
19
+ }
@@ -0,0 +1,27 @@
1
+ import { DbExecutor, QueryResult } from '../../orm/db-executor.js';
2
+ import { IntrospectOptions } from './types.js';
3
+
4
+ export const toRows = (result: QueryResult | undefined): Record<string, any>[] => {
5
+ if (!result) return [];
6
+ return result.values.map(row =>
7
+ result.columns.reduce<Record<string, any>>((acc, col, idx) => {
8
+ acc[col] = row[idx];
9
+ return acc;
10
+ }, {})
11
+ );
12
+ };
13
+
14
+ export const queryRows = async (
15
+ executor: DbExecutor,
16
+ sql: string,
17
+ params: unknown[] = []
18
+ ): Promise<Record<string, any>[]> => {
19
+ const [first] = await executor.executeSql(sql, params);
20
+ return toRows(first);
21
+ };
22
+
23
+ export const shouldIncludeTable = (name: string, options: IntrospectOptions): boolean => {
24
+ if (options.includeTables && !options.includeTables.includes(name)) return false;
25
+ if (options.excludeTables && options.excludeTables.includes(name)) return false;
26
+ return true;
27
+ };
@@ -0,0 +1,179 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { DbExecutor } from '../../orm/db-executor.js';
3
+ import {
4
+ SchemaDialect,
5
+ deriveIndexName,
6
+ generateCreateTableSql,
7
+ renderColumnDefinition
8
+ } from './schema-generator.js';
9
+ import { DatabaseSchema, DatabaseTable } from './schema-types.js';
10
+
11
+ export type SchemaChangeKind =
12
+ | 'createTable'
13
+ | 'dropTable'
14
+ | 'addColumn'
15
+ | 'dropColumn'
16
+ | 'alterColumn'
17
+ | 'addIndex'
18
+ | 'dropIndex';
19
+
20
+ export interface SchemaChange {
21
+ kind: SchemaChangeKind;
22
+ table: string;
23
+ description: string;
24
+ statements: string[];
25
+ safe: boolean;
26
+ }
27
+
28
+ export interface SchemaPlan {
29
+ changes: SchemaChange[];
30
+ warnings: string[];
31
+ }
32
+
33
+ export interface SchemaDiffOptions {
34
+ /** Allow destructive operations (drops) */
35
+ allowDestructive?: boolean;
36
+ }
37
+
38
+ const tableKey = (name: string, schema?: string) => (schema ? `${schema}.${name}` : name);
39
+
40
+ const mapTables = (schema: DatabaseSchema) => {
41
+ const map = new Map<string, DatabaseTable>();
42
+ for (const table of schema.tables) {
43
+ map.set(tableKey(table.name, table.schema), table);
44
+ }
45
+ return map;
46
+ };
47
+
48
+ const buildAddColumnSql = (table: TableDef, colName: string, dialect: SchemaDialect): string => {
49
+ const column = table.columns[colName];
50
+ const rendered = renderColumnDefinition(table, column, dialect);
51
+ return `ALTER TABLE ${dialect.formatTableName(table)} ADD ${rendered.sql};`;
52
+ };
53
+
54
+ export const diffSchema = (
55
+ expectedTables: TableDef[],
56
+ actualSchema: DatabaseSchema,
57
+ dialect: SchemaDialect,
58
+ options: SchemaDiffOptions = {}
59
+ ): SchemaPlan => {
60
+ const allowDestructive = options.allowDestructive ?? false;
61
+ const plan: SchemaPlan = { changes: [], warnings: [] };
62
+
63
+ const actualMap = mapTables(actualSchema);
64
+
65
+ // Create missing tables and indexes
66
+ for (const table of expectedTables) {
67
+ const key = tableKey(table.name, table.schema);
68
+ const actual = actualMap.get(key);
69
+ if (!actual) {
70
+ const { tableSql, indexSql } = generateCreateTableSql(table, dialect);
71
+ plan.changes.push({
72
+ kind: 'createTable',
73
+ table: key,
74
+ description: `Create table ${key}`,
75
+ statements: [tableSql, ...indexSql],
76
+ safe: true
77
+ });
78
+ continue;
79
+ }
80
+
81
+ // Columns
82
+ const actualCols = new Map(actual.columns.map(c => [c.name, c]));
83
+ for (const colName of Object.keys(table.columns)) {
84
+ if (!actualCols.has(colName)) {
85
+ plan.changes.push({
86
+ kind: 'addColumn',
87
+ table: key,
88
+ description: `Add column ${colName} to ${key}`,
89
+ statements: [buildAddColumnSql(table, colName, dialect)],
90
+ safe: true
91
+ });
92
+ }
93
+ }
94
+ for (const colName of actualCols.keys()) {
95
+ if (!table.columns[colName]) {
96
+ plan.changes.push({
97
+ kind: 'dropColumn',
98
+ table: key,
99
+ description: `Drop column ${colName} from ${key}`,
100
+ statements: allowDestructive ? dialect.dropColumnSql(actual, colName) : [],
101
+ safe: false
102
+ });
103
+ const warning = dialect.warnDropColumn?.(actual, colName);
104
+ if (warning) plan.warnings.push(warning);
105
+ }
106
+ }
107
+
108
+ // Indexes (naive: based on name or derived name)
109
+ const expectedIndexes = table.indexes ?? [];
110
+ const actualIndexes = actual.indexes ?? [];
111
+ const actualIndexMap = new Map(actualIndexes.map(idx => [idx.name, idx]));
112
+
113
+ for (const idx of expectedIndexes) {
114
+ const name = idx.name || deriveIndexName(table, idx);
115
+ if (!actualIndexMap.has(name)) {
116
+ plan.changes.push({
117
+ kind: 'addIndex',
118
+ table: key,
119
+ description: `Create index ${name} on ${key}`,
120
+ statements: [dialect.renderIndex(table, { ...idx, name })],
121
+ safe: true
122
+ });
123
+ }
124
+ }
125
+
126
+ for (const idx of actualIndexes) {
127
+ if (idx.name && !expectedIndexes.find(expected => (expected.name || deriveIndexName(table, expected)) === idx.name)) {
128
+ plan.changes.push({
129
+ kind: 'dropIndex',
130
+ table: key,
131
+ description: `Drop index ${idx.name} on ${key}`,
132
+ statements: allowDestructive ? dialect.dropIndexSql(actual, idx.name) : [],
133
+ safe: false
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ // Extra tables
140
+ for (const actual of actualSchema.tables) {
141
+ const key = tableKey(actual.name, actual.schema);
142
+ if (!expectedTables.find(t => tableKey(t.name, t.schema) === key)) {
143
+ plan.changes.push({
144
+ kind: 'dropTable',
145
+ table: key,
146
+ description: `Drop table ${key}`,
147
+ statements: allowDestructive ? dialect.dropTableSql(actual) : [],
148
+ safe: false
149
+ });
150
+ }
151
+ }
152
+
153
+ return plan;
154
+ };
155
+
156
+ export interface SynchronizeOptions extends SchemaDiffOptions {
157
+ dryRun?: boolean;
158
+ }
159
+
160
+ export const synchronizeSchema = async (
161
+ expectedTables: TableDef[],
162
+ actualSchema: DatabaseSchema,
163
+ dialect: SchemaDialect,
164
+ executor: DbExecutor,
165
+ options: SynchronizeOptions = {}
166
+ ): Promise<SchemaPlan> => {
167
+ const plan = diffSchema(expectedTables, actualSchema, dialect, options);
168
+ if (options.dryRun) return plan;
169
+
170
+ for (const change of plan.changes) {
171
+ if (!change.statements.length) continue;
172
+ if (!change.safe && !options.allowDestructive) continue;
173
+ for (const stmt of change.statements) {
174
+ if (!stmt.trim()) continue;
175
+ await executor.executeSql(stmt);
176
+ }
177
+ }
178
+ return plan;
179
+ };
@@ -0,0 +1,229 @@
1
+ import { ColumnDef, ForeignKeyReference, RawDefaultValue } from '../../schema/column.js';
2
+ import { IndexDef, IndexColumn, TableDef } from '../../schema/table.js';
3
+ import { DatabaseTable } from './schema-types.js';
4
+ export { BaseSchemaDialect } from './dialects/base-schema-dialect.js';
5
+ export {
6
+ PostgresSchemaDialect,
7
+ MySqlSchemaDialect,
8
+ SQLiteSchemaDialect,
9
+ MSSqlSchemaDialect
10
+ } from './dialects/index.js';
11
+
12
+ export type DialectName = 'postgres' | 'mysql' | 'sqlite' | 'mssql';
13
+
14
+ export interface SchemaDialect {
15
+ name: DialectName;
16
+ quoteIdentifier(id: string): string;
17
+ formatTableName(table: TableDef | DatabaseTable): string;
18
+ renderColumnType(column: ColumnDef): string;
19
+ renderDefault(value: unknown, column: ColumnDef): string;
20
+ renderAutoIncrement(column: ColumnDef, table: TableDef): string | undefined;
21
+ renderReference(ref: ForeignKeyReference, table: TableDef): string;
22
+ renderIndex(table: TableDef, index: IndexDef): string;
23
+ renderTableOptions(table: TableDef): string | undefined;
24
+ supportsPartialIndexes(): boolean;
25
+ preferInlinePkAutoincrement?(column: ColumnDef, table: TableDef, pk: string[]): boolean;
26
+ dropColumnSql(table: DatabaseTable, column: string): string[];
27
+ dropIndexSql(table: DatabaseTable, index: string): string[];
28
+ dropTableSql(table: DatabaseTable): string[];
29
+ warnDropColumn?(table: DatabaseTable, column: string): string | undefined;
30
+ }
31
+
32
+ export interface SchemaGenerateResult {
33
+ tableSql: string;
34
+ indexSql: string[];
35
+ }
36
+
37
+ export const escapeLiteral = (value: string): string => value.replace(/'/g, "''");
38
+
39
+ const isRawDefault = (value: unknown): value is RawDefaultValue => {
40
+ return !!value && typeof value === 'object' && 'raw' in (value as any) && typeof (value as any).raw === 'string';
41
+ };
42
+
43
+ export const formatLiteral = (value: unknown, dialect: DialectName): string => {
44
+ if (isRawDefault(value)) return value.raw;
45
+ if (value === null) return 'NULL';
46
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'NULL';
47
+ if (typeof value === 'boolean') {
48
+ if (dialect === 'mysql' || dialect === 'sqlite' || dialect === 'mssql') {
49
+ return value ? '1' : '0';
50
+ }
51
+ return value ? 'TRUE' : 'FALSE';
52
+ }
53
+ if (value instanceof Date) return `'${escapeLiteral(value.toISOString())}'`;
54
+ if (typeof value === 'string') return `'${escapeLiteral(value)}'`;
55
+ return `'${escapeLiteral(JSON.stringify(value))}'`;
56
+ };
57
+
58
+ export const resolvePrimaryKey = (table: TableDef): string[] => {
59
+ if (table.primaryKey && table.primaryKey.length > 0) {
60
+ return table.primaryKey;
61
+ }
62
+ const cols = Object.values(table.columns);
63
+ return cols.filter(c => c.primary).map(c => c.name);
64
+ };
65
+
66
+ export const quoteQualified = (dialect: SchemaDialect, identifier: string): string => {
67
+ if (identifier.includes('.')) {
68
+ return identifier
69
+ .split('.')
70
+ .map(part => dialect.quoteIdentifier(part))
71
+ .join('.');
72
+ }
73
+ return dialect.quoteIdentifier(identifier);
74
+ };
75
+
76
+ export const renderIndexColumns = (dialect: SchemaDialect, columns: (string | IndexColumn)[]) => {
77
+ return columns
78
+ .map(col => {
79
+ if (typeof col === 'string') return dialect.quoteIdentifier(col);
80
+ const parts = [dialect.quoteIdentifier(col.column)];
81
+ if (col.order) parts.push(col.order);
82
+ if (col.nulls) parts.push(`NULLS ${col.nulls}`);
83
+ return parts.join(' ');
84
+ })
85
+ .join(', ');
86
+ };
87
+
88
+ export const deriveIndexName = (table: TableDef, index: IndexDef): string => {
89
+ const base = (index.columns || [])
90
+ .map(col => (typeof col === 'string' ? col : col.column))
91
+ .join('_');
92
+ const suffix = index.unique ? 'uniq' : 'idx';
93
+ return `${table.name}_${base}_${suffix}`;
94
+ };
95
+
96
+ export interface RenderColumnOptions {
97
+ includePrimary?: boolean;
98
+ }
99
+
100
+ export const renderColumnDefinition = (
101
+ table: TableDef,
102
+ col: ColumnDef,
103
+ dialect: SchemaDialect,
104
+ options: RenderColumnOptions = {}
105
+ ): { sql: string; inlinePrimary: boolean } => {
106
+ const parts: string[] = [];
107
+ parts.push(dialect.quoteIdentifier(col.name));
108
+ parts.push(dialect.renderColumnType(col));
109
+
110
+ const autoInc = dialect.renderAutoIncrement(col, table);
111
+ if (autoInc) parts.push(autoInc);
112
+
113
+ if (col.notNull) parts.push('NOT NULL');
114
+ if (col.unique) parts.push('UNIQUE');
115
+ if (col.default !== undefined) {
116
+ parts.push(`DEFAULT ${dialect.renderDefault(col.default, col)}`);
117
+ }
118
+ if (options.includePrimary && col.primary) {
119
+ parts.push('PRIMARY KEY');
120
+ }
121
+ if (col.check) {
122
+ parts.push(`CHECK (${col.check})`);
123
+ }
124
+ if (col.references) {
125
+ parts.push(dialect.renderReference(col.references, table));
126
+ }
127
+
128
+ return { sql: parts.join(' '), inlinePrimary: !!(options.includePrimary && col.primary) };
129
+ };
130
+
131
+ export const generateCreateTableSql = (
132
+ table: TableDef,
133
+ dialect: SchemaDialect
134
+ ): SchemaGenerateResult => {
135
+ const pk = resolvePrimaryKey(table);
136
+ const inlinePkColumns = new Set<string>();
137
+
138
+ const columnLines = Object.values(table.columns).map(col => {
139
+ const includePk = dialect.preferInlinePkAutoincrement?.(col, table, pk) && pk.includes(col.name);
140
+ if (includePk) {
141
+ inlinePkColumns.add(col.name);
142
+ }
143
+ return renderColumnDefinition(table, col, dialect, { includePrimary: includePk }).sql;
144
+ });
145
+
146
+ const constraintLines: string[] = [];
147
+
148
+ if (pk.length > 0 && !(pk.length === 1 && inlinePkColumns.has(pk[0]))) {
149
+ const cols = pk.map(c => dialect.quoteIdentifier(c)).join(', ');
150
+ constraintLines.push(`PRIMARY KEY (${cols})`);
151
+ }
152
+
153
+ if (table.checks) {
154
+ table.checks.forEach(check => {
155
+ const name = check.name ? `${dialect.quoteIdentifier(check.name)} ` : '';
156
+ constraintLines.push(`CONSTRAINT ${name}CHECK (${check.expression})`);
157
+ });
158
+ }
159
+
160
+ const allLines = [...columnLines, ...constraintLines];
161
+ const body = allLines.map(line => ` ${line}`).join(',\n');
162
+ const tableOptions = dialect.renderTableOptions(table);
163
+ const tableSql = `CREATE TABLE ${dialect.formatTableName(table)} (\n${body}\n)${tableOptions ? ' ' + tableOptions : ''};`;
164
+
165
+ const indexSql: string[] = [];
166
+ if (table.indexes && table.indexes.length > 0) {
167
+ for (const idx of table.indexes) {
168
+ if (idx.where && !dialect.supportsPartialIndexes()) {
169
+ throw new Error(`Dialect ${dialect.name} does not support partial/filtered indexes (${idx.name || idx.columns.join('_')}).`);
170
+ }
171
+ indexSql.push(dialect.renderIndex(table, idx));
172
+ }
173
+ }
174
+
175
+ return { tableSql, indexSql };
176
+ };
177
+
178
+ export const generateSchemaSql = (
179
+ tables: TableDef[],
180
+ dialect: SchemaDialect
181
+ ): string[] => {
182
+ const ordered = orderTablesByDependencies(tables);
183
+ const statements: string[] = [];
184
+ ordered.forEach(table => {
185
+ const { tableSql, indexSql } = generateCreateTableSql(table, dialect);
186
+ statements.push(tableSql, ...indexSql);
187
+ });
188
+ return statements;
189
+ };
190
+
191
+ const orderTablesByDependencies = (tables: TableDef[]): TableDef[] => {
192
+ const map = new Map<string, TableDef>();
193
+ tables.forEach(t => map.set(t.name, t));
194
+
195
+ const deps = new Map<string, Set<string>>();
196
+ for (const table of tables) {
197
+ const refTables = new Set<string>();
198
+ Object.values(table.columns).forEach(col => {
199
+ if (col.references?.table) {
200
+ refTables.add(col.references.table);
201
+ }
202
+ });
203
+ deps.set(table.name, refTables);
204
+ }
205
+
206
+ const visited = new Set<string>();
207
+ const ordered: TableDef[] = [];
208
+
209
+ const visit = (name: string, stack: Set<string>) => {
210
+ if (visited.has(name)) return;
211
+ const table = map.get(name);
212
+ if (!table) return;
213
+ if (stack.has(name)) {
214
+ ordered.push(table);
215
+ visited.add(name);
216
+ return;
217
+ }
218
+ stack.add(name);
219
+ for (const dep of deps.get(name) || []) {
220
+ visit(dep, stack);
221
+ }
222
+ stack.delete(name);
223
+ visited.add(name);
224
+ ordered.push(table);
225
+ };
226
+
227
+ tables.forEach(t => visit(t.name, new Set()));
228
+ return ordered;
229
+ };