metal-orm 1.1.26 → 1.1.28

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.
@@ -1,12 +1,11 @@
1
- import { TableDef } from '../../schema/table.js';
2
- import { ColumnDef } from '../../schema/column-types.js';
1
+ import type { TableDef } from '../../schema/table.js';
2
+ import type { ColumnDef } from '../../schema/column-types.js';
3
3
  import type { DbExecutor } from '../execution/db-executor.js';
4
- import { SchemaDialect } from './schema-dialect.js';
4
+ import type { SchemaDialect } from './schema-dialect.js';
5
5
  import { deriveIndexName } from './naming-strategy.js';
6
6
  import { generateCreateTableSql, renderColumnDefinition } from './schema-generator.js';
7
- import { ColumnDiff, DatabaseColumn, DatabaseSchema, DatabaseTable } from './schema-types.js';
7
+ import type { ColumnDiff, DatabaseColumn, DatabaseSchema, DatabaseTable } from './schema-types.js';
8
8
 
9
- /** The kind of schema change. */
10
9
  export type SchemaChangeKind =
11
10
  | 'createTable'
12
11
  | 'dropTable'
@@ -16,7 +15,6 @@ export type SchemaChangeKind =
16
15
  | 'addIndex'
17
16
  | 'dropIndex';
18
17
 
19
- /** Represents a single schema change. */
20
18
  export interface SchemaChange {
21
19
  kind: SchemaChangeKind;
22
20
  table: string;
@@ -25,45 +23,47 @@ export interface SchemaChange {
25
23
  safe: boolean;
26
24
  }
27
25
 
28
- /** Represents a plan of schema changes. */
29
26
  export interface SchemaPlan {
30
27
  changes: SchemaChange[];
31
28
  warnings: string[];
32
29
  }
33
30
 
34
- /** Options for schema diffing. */
35
31
  export interface SchemaDiffOptions {
36
- /** Allow destructive operations (drops) */
37
32
  allowDestructive?: boolean;
38
33
  }
39
34
 
40
- const tableKey = (name: string, schema?: string) => (schema ? `${schema}.${name}` : name);
35
+ const tableKey = (name: string, schema?: string): string => schema ? `${schema}.${name}` : name;
41
36
 
42
- const mapTables = (schema: DatabaseSchema) => {
37
+ const mapTables = (schema: DatabaseSchema): Map<string, DatabaseTable> => {
43
38
  const map = new Map<string, DatabaseTable>();
44
- for (const table of schema.tables) {
45
- map.set(tableKey(table.name, table.schema), table);
46
- }
39
+ for (const table of schema.tables) map.set(tableKey(table.name, table.schema), table);
47
40
  return map;
48
41
  };
49
42
 
50
- const buildAddColumnSql = (table: TableDef, colName: string, dialect: SchemaDialect): string => {
51
- const column = table.columns[colName];
43
+ const buildAddColumnSql = (table: TableDef, columnName: string, dialect: SchemaDialect): string => {
44
+ const column = table.columns[columnName];
52
45
  const rendered = renderColumnDefinition(table, column, dialect);
53
46
  return `ALTER TABLE ${dialect.formatTableName(table)} ADD ${rendered.sql};`;
54
47
  };
55
48
 
56
- const normalizeType = (value: string | undefined): string => (value || '').toLowerCase().replace(/\s+/g, ' ').trim();
49
+ const normalizeType = (value: string | undefined): string =>
50
+ (value || '').toLowerCase().replace(/\s+/g, ' ').trim();
51
+
57
52
  const normalizeDefault = (value: unknown): string | undefined => {
58
53
  if (value === undefined || value === null) return undefined;
59
54
  return String(value).trim();
60
55
  };
61
56
 
62
- const diffColumn = (expected: ColumnDef, actual: DatabaseColumn, dialect: SchemaDialect): ColumnDiff => {
57
+ const diffColumn = (
58
+ expected: ColumnDef,
59
+ actual: DatabaseColumn,
60
+ dialect: SchemaDialect
61
+ ): ColumnDiff => {
63
62
  const expectedType = normalizeType(dialect.renderColumnType(expected));
64
63
  const actualType = normalizeType(actual.type);
65
- const expectedDefault =
66
- expected.default !== undefined ? normalizeDefault(dialect.renderDefault(expected.default, expected)) : undefined;
64
+ const expectedDefault = expected.default !== undefined
65
+ ? normalizeDefault(dialect.renderDefault(expected.default, expected))
66
+ : undefined;
67
67
  const actualDefault = normalizeDefault(actual.default);
68
68
  return {
69
69
  typeChanged: expectedType !== actualType,
@@ -73,14 +73,13 @@ const diffColumn = (expected: ColumnDef, actual: DatabaseColumn, dialect: Schema
73
73
  };
74
74
  };
75
75
 
76
- /**
77
- * Computes the differences between expected and actual database schemas.
78
- * @param expectedTables - The expected table definitions.
79
- * @param actualSchema - The actual database schema.
80
- * @param dialect - The schema dialect.
81
- * @param options - Options for the diff.
82
- * @returns The schema plan with changes and warnings.
83
- */
76
+ const unsupportedMutationWarning = (
77
+ dialect: SchemaDialect,
78
+ operation: string,
79
+ target: string
80
+ ): string =>
81
+ `Dialect "${dialect.name}" does not provide the ${operation} capability for ${target}; manual migration is required.`;
82
+
84
83
  export const diffSchema = (
85
84
  expectedTables: TableDef[],
86
85
  actualSchema: DatabaseSchema,
@@ -89,10 +88,8 @@ export const diffSchema = (
89
88
  ): SchemaPlan => {
90
89
  const allowDestructive = options.allowDestructive ?? false;
91
90
  const plan: SchemaPlan = { changes: [], warnings: [] };
92
-
93
91
  const actualMap = mapTables(actualSchema);
94
92
 
95
- // Create missing tables and indexes
96
93
  for (const table of expectedTables) {
97
94
  const key = tableKey(table.name, table.schema);
98
95
  const actual = actualMap.get(key);
@@ -108,115 +105,148 @@ export const diffSchema = (
108
105
  continue;
109
106
  }
110
107
 
111
- // Columns
112
- const actualCols = new Map(actual.columns.map(c => [c.name, c]));
113
- for (const colName of Object.keys(table.columns)) {
114
- if (!actualCols.has(colName)) {
108
+ const actualColumns = new Map(actual.columns.map(column => [column.name, column]));
109
+ for (const columnName of Object.keys(table.columns)) {
110
+ if (!actualColumns.has(columnName)) {
115
111
  plan.changes.push({
116
112
  kind: 'addColumn',
117
113
  table: key,
118
- description: `Add column ${colName} to ${key}`,
119
- statements: [buildAddColumnSql(table, colName, dialect)],
114
+ description: `Add column ${columnName} to ${key}`,
115
+ statements: [buildAddColumnSql(table, columnName, dialect)],
120
116
  safe: true
121
117
  });
122
- } else {
123
- const expectedCol = table.columns[colName];
124
- const actualCol = actualCols.get(colName)!;
125
- const colDiff = diffColumn(expectedCol, actualCol, dialect);
126
- const shouldAlter =
127
- colDiff.typeChanged || colDiff.nullabilityChanged || colDiff.defaultChanged || colDiff.autoIncrementChanged;
128
- if (shouldAlter) {
129
- const statements = dialect.alterColumnSql?.(table, expectedCol, actualCol, colDiff) ?? [];
118
+ continue;
119
+ }
120
+
121
+ const expectedColumn = table.columns[columnName];
122
+ const actualColumn = actualColumns.get(columnName)!;
123
+ const columnDiff = diffColumn(expectedColumn, actualColumn, dialect);
124
+ const shouldAlter =
125
+ columnDiff.typeChanged
126
+ || columnDiff.nullabilityChanged
127
+ || columnDiff.defaultChanged
128
+ || columnDiff.autoIncrementChanged;
129
+
130
+ if (shouldAlter) {
131
+ const capability = dialect.mutations.alterColumn;
132
+ if (capability) {
133
+ const statements = capability.compile(table, expectedColumn, actualColumn, columnDiff);
130
134
  if (statements.length > 0) {
131
135
  plan.changes.push({
132
136
  kind: 'alterColumn',
133
137
  table: key,
134
- description: `Alter column ${colName} on ${key}`,
138
+ description: `Alter column ${columnName} on ${key}`,
135
139
  statements,
136
140
  safe: true
137
141
  });
138
142
  }
139
- const warning = dialect.warnAlterColumn?.(table, expectedCol, actualCol, colDiff);
143
+ const warning = capability.warning?.(table, expectedColumn, actualColumn, columnDiff);
140
144
  if (warning) plan.warnings.push(warning);
145
+ } else {
146
+ plan.warnings.push(
147
+ unsupportedMutationWarning(dialect, 'ALTER COLUMN', `${key}.${columnName}`)
148
+ );
141
149
  }
142
150
  }
143
151
  }
144
- for (const colName of actualCols.keys()) {
145
- if (!table.columns[colName]) {
146
- plan.changes.push({
147
- kind: 'dropColumn',
148
- table: key,
149
- description: `Drop column ${colName} from ${key}`,
150
- statements: allowDestructive ? dialect.dropColumnSql(actual, colName) : [],
151
- safe: false
152
- });
153
- const warning = dialect.warnDropColumn?.(actual, colName);
152
+
153
+ for (const columnName of actualColumns.keys()) {
154
+ if (table.columns[columnName]) continue;
155
+ const capability = dialect.mutations.dropColumn;
156
+ const statements = allowDestructive && capability
157
+ ? capability.compile(actual, columnName)
158
+ : [];
159
+ plan.changes.push({
160
+ kind: 'dropColumn',
161
+ table: key,
162
+ description: `Drop column ${columnName} from ${key}`,
163
+ statements,
164
+ safe: false
165
+ });
166
+ if (!capability) {
167
+ plan.warnings.push(
168
+ unsupportedMutationWarning(dialect, 'DROP COLUMN', `${key}.${columnName}`)
169
+ );
170
+ } else {
171
+ const warning = capability.warning?.(actual, columnName);
154
172
  if (warning) plan.warnings.push(warning);
155
173
  }
156
174
  }
157
175
 
158
- // Indexes (naive: based on name or derived name)
159
176
  const expectedIndexes = table.indexes ?? [];
160
177
  const actualIndexes = actual.indexes ?? [];
161
- const actualIndexMap = new Map(actualIndexes.map(idx => [idx.name, idx]));
178
+ const actualIndexMap = new Map(actualIndexes.map(index => [index.name, index]));
162
179
 
163
- for (const idx of expectedIndexes) {
164
- const name = idx.name || deriveIndexName(table, idx);
180
+ for (const index of expectedIndexes) {
181
+ const name = index.name || deriveIndexName(table, index);
165
182
  if (!actualIndexMap.has(name)) {
166
183
  plan.changes.push({
167
184
  kind: 'addIndex',
168
185
  table: key,
169
186
  description: `Create index ${name} on ${key}`,
170
- statements: [dialect.renderIndex(table, { ...idx, name })],
187
+ statements: [dialect.renderIndex(table, { ...index, name })],
171
188
  safe: true
172
189
  });
173
190
  }
174
191
  }
175
192
 
176
- for (const idx of actualIndexes) {
177
- if (idx.name && !expectedIndexes.find(expected => (expected.name || deriveIndexName(table, expected)) === idx.name)) {
178
- plan.changes.push({
179
- kind: 'dropIndex',
180
- table: key,
181
- description: `Drop index ${idx.name} on ${key}`,
182
- statements: allowDestructive ? dialect.dropIndexSql(actual, idx.name) : [],
183
- safe: false
184
- });
193
+ for (const index of actualIndexes) {
194
+ if (!index.name) continue;
195
+ const expected = expectedIndexes.find(
196
+ candidate => (candidate.name || deriveIndexName(table, candidate)) === index.name
197
+ );
198
+ if (expected) continue;
199
+
200
+ const capability = dialect.mutations.dropIndex;
201
+ const statements = allowDestructive && capability
202
+ ? capability.compile(actual, index.name)
203
+ : [];
204
+ plan.changes.push({
205
+ kind: 'dropIndex',
206
+ table: key,
207
+ description: `Drop index ${index.name} on ${key}`,
208
+ statements,
209
+ safe: false
210
+ });
211
+ if (!capability) {
212
+ plan.warnings.push(
213
+ unsupportedMutationWarning(dialect, 'DROP INDEX', `${key}.${index.name}`)
214
+ );
215
+ } else {
216
+ const warning = capability.warning?.(actual, index.name);
217
+ if (warning) plan.warnings.push(warning);
185
218
  }
186
219
  }
187
220
  }
188
221
 
189
- // Extra tables
190
222
  for (const actual of actualSchema.tables) {
191
223
  const key = tableKey(actual.name, actual.schema);
192
- if (!expectedTables.find(t => tableKey(t.name, t.schema) === key)) {
193
- plan.changes.push({
194
- kind: 'dropTable',
195
- table: key,
196
- description: `Drop table ${key}`,
197
- statements: allowDestructive ? dialect.dropTableSql(actual) : [],
198
- safe: false
199
- });
224
+ if (expectedTables.find(table => tableKey(table.name, table.schema) === key)) continue;
225
+
226
+ const capability = dialect.mutations.dropTable;
227
+ const statements = allowDestructive && capability ? capability.compile(actual) : [];
228
+ plan.changes.push({
229
+ kind: 'dropTable',
230
+ table: key,
231
+ description: `Drop table ${key}`,
232
+ statements,
233
+ safe: false
234
+ });
235
+ if (!capability) {
236
+ plan.warnings.push(unsupportedMutationWarning(dialect, 'DROP TABLE', key));
237
+ } else {
238
+ const warning = capability.warning?.(actual);
239
+ if (warning) plan.warnings.push(warning);
200
240
  }
201
241
  }
202
242
 
203
243
  return plan;
204
244
  };
205
245
 
206
- /** Options for schema synchronization. */
207
246
  export interface SynchronizeOptions extends SchemaDiffOptions {
208
247
  dryRun?: boolean;
209
248
  }
210
249
 
211
- /**
212
- * Synchronizes the database schema with the expected tables.
213
- * @param expectedTables - The expected table definitions.
214
- * @param actualSchema - The actual database schema.
215
- * @param dialect - The schema dialect.
216
- * @param executor - The database executor.
217
- * @param options - Options for synchronization.
218
- * @returns The schema plan with changes and warnings.
219
- */
220
250
  export const synchronizeSchema = async (
221
251
  expectedTables: TableDef[],
222
252
  actualSchema: DatabaseSchema,
@@ -231,4 +261,3 @@ export const synchronizeSchema = async (
231
261
  }
232
262
  return plan;
233
263
  };
234
-
@@ -1,20 +1,18 @@
1
- import { DatabaseDriver } from './database-driver.js';
2
- import { SqlServerDialect } from '../dialect/mssql/index.js';
3
- import { MSSqlSchemaDialect } from '../ddl/dialects/mssql-schema-dialect.js';
1
+ import type { DatabaseDriver } from './database-driver.js';
2
+ import { createSqlServerDialect } from '../dialect/mssql/index.js';
3
+ import { createMssqlSchemaDialect } from '../ddl/dialects/mssql-schema-dialect.js';
4
4
  import { mssqlIntrospector } from '../ddl/introspect/mssql.js';
5
5
 
6
- /**
7
- * Database driver for Microsoft SQL Server.
8
- */
6
+ /** Database driver for Microsoft SQL Server. */
9
7
  export class MssqlDriver implements DatabaseDriver {
10
8
  readonly name = 'mssql';
11
9
 
12
10
  createDialect() {
13
- return new SqlServerDialect();
11
+ return createSqlServerDialect();
14
12
  }
15
13
 
16
14
  createSchemaDialect() {
17
- return new MSSqlSchemaDialect();
15
+ return createMssqlSchemaDialect();
18
16
  }
19
17
 
20
18
  createIntrospector() {
@@ -1,20 +1,18 @@
1
- import { DatabaseDriver } from './database-driver.js';
2
- import { MySqlDialect } from '../dialect/mysql/index.js';
3
- import { MySqlSchemaDialect } from '../ddl/dialects/mysql-schema-dialect.js';
1
+ import type { DatabaseDriver } from './database-driver.js';
2
+ import { createMySqlDialect } from '../dialect/mysql/index.js';
3
+ import { createMySqlSchemaDialect } from '../ddl/dialects/mysql-schema-dialect.js';
4
4
  import { mysqlIntrospector } from '../ddl/introspect/mysql.js';
5
5
 
6
- /**
7
- * Database driver for MySQL.
8
- */
6
+ /** Database driver for MySQL. */
9
7
  export class MySqlDriver implements DatabaseDriver {
10
8
  readonly name = 'mysql';
11
9
 
12
10
  createDialect() {
13
- return new MySqlDialect();
11
+ return createMySqlDialect();
14
12
  }
15
13
 
16
14
  createSchemaDialect() {
17
- return new MySqlSchemaDialect();
15
+ return createMySqlSchemaDialect();
18
16
  }
19
17
 
20
18
  createIntrospector() {
@@ -1,20 +1,18 @@
1
- import { DatabaseDriver } from './database-driver.js';
2
- import { PostgresDialect } from '../dialect/postgres/index.js';
3
- import { PostgresSchemaDialect } from '../ddl/dialects/postgres-schema-dialect.js';
1
+ import type { DatabaseDriver } from './database-driver.js';
2
+ import { createPostgresDialect } from '../dialect/postgres/index.js';
3
+ import { createPostgresSchemaDialect } from '../ddl/dialects/postgres-schema-dialect.js';
4
4
  import { postgresIntrospector } from '../ddl/introspect/postgres.js';
5
5
 
6
- /**
7
- * Database driver for PostgreSQL.
8
- */
6
+ /** Database driver for PostgreSQL. */
9
7
  export class PostgresDriver implements DatabaseDriver {
10
8
  readonly name = 'postgres';
11
9
 
12
10
  createDialect() {
13
- return new PostgresDialect();
11
+ return createPostgresDialect();
14
12
  }
15
13
 
16
14
  createSchemaDialect() {
17
- return new PostgresSchemaDialect();
15
+ return createPostgresSchemaDialect();
18
16
  }
19
17
 
20
18
  createIntrospector() {
@@ -1,20 +1,18 @@
1
- import { DatabaseDriver } from './database-driver.js';
2
- import { SqliteDialect } from '../dialect/sqlite/index.js';
3
- import { SQLiteSchemaDialect } from '../ddl/dialects/sqlite-schema-dialect.js';
1
+ import type { DatabaseDriver } from './database-driver.js';
2
+ import { createSqliteDialect } from '../dialect/sqlite/index.js';
3
+ import { createSqliteSchemaDialect } from '../ddl/dialects/sqlite-schema-dialect.js';
4
4
  import { sqliteIntrospector } from '../ddl/introspect/sqlite.js';
5
5
 
6
- /**
7
- * Database driver for SQLite.
8
- */
6
+ /** Database driver for SQLite. */
9
7
  export class SqliteDriver implements DatabaseDriver {
10
8
  readonly name = 'sqlite';
11
9
 
12
10
  createDialect() {
13
- return new SqliteDialect();
11
+ return createSqliteDialect();
14
12
  }
15
13
 
16
14
  createSchemaDialect() {
17
- return new SQLiteSchemaDialect();
15
+ return createSqliteSchemaDialect();
18
16
  }
19
17
 
20
18
  createIntrospector() {
package/src/index.ts CHANGED
@@ -55,6 +55,18 @@ export * from './core/ddl/schema-generator.js';
55
55
  export * from './core/ddl/schema-types.js';
56
56
  export * from './core/ddl/schema-diff.js';
57
57
  export * from './core/ddl/schema-introspect.js';
58
+ export * from './core/ddl/schema-dialect-composer.js';
59
+ export * from './core/ddl/dialects/index.js';
60
+ export { createLiteralFormatter } from './core/ddl/sql-writing.js';
61
+ export type { LiteralFormatter, LiteralFormatOptions } from './core/ddl/sql-writing.js';
62
+ export type {
63
+ SchemaDialect,
64
+ SchemaMutationCapabilities,
65
+ DropTableCapability,
66
+ DropColumnCapability,
67
+ DropIndexCapability,
68
+ AlterColumnCapability
69
+ } from './core/ddl/schema-dialect.js';
58
70
  export * from './core/ddl/introspect/registry.js';
59
71
  export * from './core/functions/text.js';
60
72
  export * from './core/functions/numeric.js';
@@ -1,96 +0,0 @@
1
- import { SchemaDialect, DialectName } from '../schema-dialect.js';
2
- import { formatLiteral, quoteQualified, LiteralFormatter } from '../sql-writing.js';
3
- import { ColumnDef, ForeignKeyReference } from '../../../schema/column-types.js';
4
- import { IndexDef, TableDef } from '../../../schema/table.js';
5
- import { DatabaseTable, DatabaseColumn, ColumnDiff } from '../schema-types.js';
6
-
7
- /** A table-like object with name and optional schema. */
8
- type TableLike = { name: string; schema?: string };
9
-
10
- /**
11
- * Common behavior for schema dialects (DDL).
12
- * Concrete dialects only override the small surface area instead of reimplementing everything.
13
- */
14
- export abstract class BaseSchemaDialect implements SchemaDialect {
15
- /** The name of the dialect. */
16
- abstract readonly name: DialectName;
17
- /** Quotes an identifier for use in SQL. */
18
- abstract quoteIdentifier(id: string): string;
19
- /** Renders the column type for SQL. */
20
- abstract renderColumnType(column: ColumnDef): string;
21
- /** Renders the auto-increment clause for SQL. */
22
- abstract renderAutoIncrement(column: ColumnDef, table: TableDef): string | undefined;
23
- /** Renders an index for SQL. */
24
- abstract renderIndex(table: TableDef, index: IndexDef): string;
25
- supportsPartialIndexes(): boolean {
26
- return false;
27
- }
28
- formatTableName(table: TableLike): string {
29
- if (table.schema) {
30
- return `${this.quoteIdentifier(table.schema)}.${this.quoteIdentifier(table.name)}`;
31
- }
32
- return this.quoteIdentifier(table.name);
33
- }
34
- /** The literal formatter for the dialect. */
35
- abstract get literalFormatter(): LiteralFormatter;
36
-
37
- renderDefault(value: unknown, _column: ColumnDef): string {
38
- void _column;
39
- return formatLiteral(this.literalFormatter, value);
40
- }
41
- renderReference(ref: ForeignKeyReference, _table: TableDef): string {
42
- void _table;
43
- const parts = ['REFERENCES', quoteQualified(this, ref.table), `(${this.quoteIdentifier(ref.column)})`];
44
- if (ref.onDelete) parts.push('ON DELETE', ref.onDelete);
45
- if (ref.onUpdate) parts.push('ON UPDATE', ref.onUpdate);
46
- const suffix = this.renderReferenceSuffix(ref, _table);
47
- if (suffix) parts.push(suffix);
48
- return parts.join(' ');
49
- }
50
-
51
- protected renderReferenceSuffix(ref: ForeignKeyReference, _table: TableDef): string | undefined {
52
- void ref;
53
- void _table;
54
- return undefined;
55
- }
56
- renderTableOptions(_table: TableDef): string | undefined {
57
- void _table;
58
- return undefined;
59
- }
60
- dropTableSql(table: DatabaseTable): string[] {
61
- return [`DROP TABLE IF EXISTS ${this.formatTableName(table)};`];
62
- }
63
- dropColumnSql(table: DatabaseTable, column: string): string[] {
64
- return [`ALTER TABLE ${this.formatTableName(table)} DROP COLUMN ${this.quoteIdentifier(column)};`];
65
- }
66
- dropIndexSql(table: DatabaseTable, index: string): string[] {
67
- return [`DROP INDEX ${this.quoteIdentifier(index)};`];
68
- }
69
- warnDropColumn(_table: DatabaseTable, _column: string): string | undefined {
70
- void _table;
71
- void _column;
72
- return undefined;
73
- }
74
- alterColumnSql?(_table: TableDef, _column: ColumnDef, _actualColumn: DatabaseColumn, _diff: ColumnDiff): string[] {
75
- void _table;
76
- void _column;
77
- void _actualColumn;
78
- void _diff;
79
- return [];
80
- }
81
- warnAlterColumn?(_table: TableDef, _column: ColumnDef, _actual: DatabaseColumn, _diff: ColumnDiff): string | undefined {
82
- void _table;
83
- void _column;
84
- void _actual;
85
- void _diff;
86
- return undefined;
87
- }
88
-
89
- preferInlinePkAutoincrement(_column: ColumnDef, _table: TableDef, _pk: string[]): boolean {
90
- void _column;
91
- void _table;
92
- void _pk;
93
- return false;
94
- }
95
- }
96
-