uql-orm 0.25.1 → 0.26.0

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 (55) hide show
  1. package/README.md +9 -8
  2. package/dist/browser/uql-browser.min.js.map +1 -1
  3. package/dist/cockroachdb/cockroachDialect.js +1 -1
  4. package/dist/dialect/indexSqlDialect.d.ts +3 -2
  5. package/dist/dialect/indexSqlDialect.js +10 -8
  6. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -2
  7. package/dist/dialect/mysqlLikeSqlDialect.js +0 -7
  8. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  9. package/dist/maria/mariaDialect.js +1 -1
  10. package/dist/migrate/builder/migrationBuilder.js +1 -1
  11. package/dist/migrate/builder/tableBuilder.js +2 -2
  12. package/dist/migrate/cli.js +4 -6
  13. package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
  14. package/dist/migrate/codegen/entityCodeGenerator.js +22 -25
  15. package/dist/migrate/codegen/fieldOptionsSource.d.ts +1 -1
  16. package/dist/migrate/codegen/fieldOptionsSource.js +6 -1
  17. package/dist/migrate/codegen/indexDecoratorSource.d.ts +14 -0
  18. package/dist/migrate/codegen/indexDecoratorSource.js +105 -0
  19. package/dist/migrate/drift/driftDetector.d.ts +5 -50
  20. package/dist/migrate/drift/driftDetector.js +215 -224
  21. package/dist/migrate/drift/index.d.ts +1 -1
  22. package/dist/migrate/drift/index.js +1 -1
  23. package/dist/migrate/generator/definitionToNode.d.ts +9 -0
  24. package/dist/migrate/generator/definitionToNode.js +79 -0
  25. package/dist/migrate/generator/indexNodeToSchema.js +4 -2
  26. package/dist/migrate/generator/mongoSchemaGenerator.js +3 -3
  27. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +3 -0
  28. package/dist/migrate/introspection/baseSqlIntrospector.js +13 -5
  29. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -0
  30. package/dist/migrate/introspection/mongoIntrospector.js +5 -10
  31. package/dist/migrate/introspection/mysqlIntrospector.js +1 -1
  32. package/dist/migrate/introspection/postgresIntrospector.d.ts +57 -5
  33. package/dist/migrate/introspection/postgresIntrospector.js +93 -10
  34. package/dist/migrate/introspection/sqliteIntrospector.js +1 -1
  35. package/dist/migrate/migrator.js +3 -2
  36. package/dist/migrate/schemaGenerator.d.ts +26 -3
  37. package/dist/migrate/schemaGenerator.js +53 -92
  38. package/dist/schema/index.d.ts +3 -3
  39. package/dist/schema/index.js +2 -2
  40. package/dist/schema/indexColumns.d.ts +10 -0
  41. package/dist/schema/indexColumns.js +11 -0
  42. package/dist/schema/indexDifferences.d.ts +22 -0
  43. package/dist/schema/indexDifferences.js +49 -0
  44. package/dist/schema/schemaAST.js +2 -8
  45. package/dist/schema/schemaASTBuilder.d.ts +6 -59
  46. package/dist/schema/schemaASTBuilder.js +208 -236
  47. package/dist/schema/schemaASTDiffer.d.ts +9 -56
  48. package/dist/schema/schemaASTDiffer.js +229 -393
  49. package/dist/schema/types.d.ts +5 -12
  50. package/dist/schema/types.js +15 -0
  51. package/dist/type/dialect.d.ts +7 -2
  52. package/dist/type/dialect.js +1 -0
  53. package/dist/type/migration.d.ts +14 -1
  54. package/dist/util/string.util.js +6 -1
  55. package/package.json +1 -1
@@ -8,423 +8,259 @@
8
8
  * - Schema synchronization
9
9
  */
10
10
  import { areTypesEqual, isBreakingTypeChange } from './canonicalType.js';
11
+ import { describeIndexDifferences } from './indexDifferences.js';
11
12
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
12
13
  /**
13
14
  * Default diff options.
14
15
  */
15
16
  const DEFAULT_OPTIONS = {
16
17
  compareIndexes: true,
18
+ indexFacets: new Set(),
17
19
  compareRelationships: true,
18
20
  ignoreCase: false,
19
21
  excludeTables: [],
20
22
  };
23
+ function nameNormalizer(opts) {
24
+ return opts.ignoreCase ? (name) => name.toLowerCase() : (name) => name;
25
+ }
21
26
  /**
22
- * Compares two SchemaAST instances and produces a detailed diff.
27
+ * The only three ways two keyed collections can differ, which is the shape of every comparison here:
28
+ * tables, columns, indexes and relationships all key by name and then split the same way.
23
29
  */
24
- export class SchemaASTDiffer {
25
- /**
26
- * Compare two schemas and return the differences.
27
- *
28
- * @param source - The "expected" or "desired" schema (e.g., from entities)
29
- * @param target - The "actual" or "current" schema (e.g., from database)
30
- * @param options - Diff options
31
- * @returns Detailed diff result
32
- */
33
- diff(source, target, options = {}) {
34
- const opts = { ...DEFAULT_OPTIONS, ...options };
35
- const normalizeName = opts.ignoreCase ? (n) => n.toLowerCase() : (n) => n;
36
- const tablesToCreate = [];
37
- const tablesToDrop = [];
38
- const tablesToAlter = [];
39
- const columnDiffs = [];
40
- const indexDiffs = [];
41
- const relationshipDiffs = [];
42
- // Build lookup maps
43
- const sourceTableMap = new Map();
44
- const targetTableMap = new Map();
45
- for (const table of source.tables.values()) {
46
- if (!opts.excludeTables.includes(table.name)) {
47
- sourceTableMap.set(normalizeName(table.name), table);
48
- }
49
- }
50
- for (const table of target.tables.values()) {
51
- if (!opts.excludeTables.includes(table.name)) {
52
- targetTableMap.set(normalizeName(table.name), table);
53
- }
54
- }
55
- // Find tables to create (in source but not target)
56
- for (const [name, sourceTable] of sourceTableMap) {
57
- if (!targetTableMap.has(name)) {
58
- tablesToCreate.push(sourceTable);
59
- }
60
- }
61
- // Find tables to drop (in target but not source)
62
- for (const [name, targetTable] of targetTableMap) {
63
- if (!sourceTableMap.has(name)) {
64
- tablesToDrop.push(targetTable);
65
- }
66
- }
67
- // Find tables that need alteration
68
- for (const [name, sourceTable] of sourceTableMap) {
69
- const targetTable = targetTableMap.get(name);
70
- if (!targetTable)
71
- continue;
72
- const tableDiff = this.diffTable(sourceTable, targetTable, opts);
73
- if (tableDiff) {
74
- tablesToAlter.push(tableDiff);
75
- // Collect column diffs
76
- if (tableDiff.columnDiffs) {
77
- columnDiffs.push(...tableDiff.columnDiffs);
78
- }
79
- // Collect index diffs
80
- if (tableDiff.indexDiffs) {
81
- indexDiffs.push(...tableDiff.indexDiffs);
82
- }
83
- }
84
- }
85
- // Compare relationships (global view, not just per-table)
86
- if (opts.compareRelationships) {
87
- const relDiffs = this.diffRelationships(source, target, opts);
88
- relationshipDiffs.push(...relDiffs);
89
- }
90
- // Determine if there are any differences
91
- const hasDifferences = tablesToCreate.length > 0 ||
92
- tablesToDrop.length > 0 ||
93
- tablesToAlter.length > 0 ||
94
- relationshipDiffs.length > 0 ||
95
- indexDiffs.length > 0;
96
- // Determine if any changes are breaking
97
- const hasBreakingChanges = tablesToDrop.length > 0 || columnDiffs.some((d) => d.isBreaking);
98
- return {
99
- tablesToCreate,
100
- tablesToDrop,
101
- tablesToAlter,
102
- columnDiffs,
103
- indexDiffs,
104
- relationshipDiffs,
105
- hasDifferences,
106
- hasBreakingChanges,
107
- };
108
- }
109
- /**
110
- * Compare two tables and return the differences.
111
- */
112
- diffTable(source, target, opts) {
113
- const normalizeName = opts.ignoreCase ? (n) => n.toLowerCase() : (n) => n;
114
- const columnDiffs = this.diffTableColumns(source, target, normalizeName);
115
- const indexDiffs = opts.compareIndexes ? this.diffTableIndexes(source, target, normalizeName) : [];
116
- // If no differences, return undefined
117
- if (columnDiffs.length === 0 && indexDiffs.length === 0) {
118
- return undefined;
119
- }
120
- return {
121
- name: source.name,
122
- type: 'alter',
123
- columnDiffs,
124
- indexDiffs,
125
- };
30
+ function matchByKey(source, target, key) {
31
+ const sourceByKey = new Map([...source].map((item) => [key(item), item]));
32
+ const targetByKey = new Map([...target].map((item) => [key(item), item]));
33
+ return {
34
+ created: [...sourceByKey].filter(([at]) => !targetByKey.has(at)).map(([, item]) => item),
35
+ dropped: [...targetByKey].filter(([at]) => !sourceByKey.has(at)).map(([, item]) => item),
36
+ matched: [...sourceByKey].flatMap(([at, item]) => {
37
+ const counterpart = targetByKey.get(at);
38
+ return counterpart ? [[item, counterpart]] : [];
39
+ }),
40
+ };
41
+ }
42
+ /** How a relationship diff names the pair it is about, whichever way it differs. */
43
+ function relationEnds(relation) {
44
+ return { name: relation.name, fromTable: relation.from.table.name, toTable: relation.to.table.name };
45
+ }
46
+ /**
47
+ * Compare two schemas and return the differences.
48
+ *
49
+ * @param source - The "expected" or "desired" schema (e.g., from entities)
50
+ * @param target - The "actual" or "current" schema (e.g., from database)
51
+ * @param options - Diff options
52
+ * @returns Detailed diff result
53
+ */
54
+ export function diffSchemas(source, target, options = {}) {
55
+ const opts = { ...DEFAULT_OPTIONS, ...options };
56
+ const normalizeName = nameNormalizer(opts);
57
+ const included = (tables) => [...tables].filter((table) => !opts.excludeTables.includes(table.name));
58
+ const { created: tablesToCreate, dropped: tablesToDrop, matched, } = matchByKey(included(source.tables.values()), included(target.tables.values()), (table) => normalizeName(table.name));
59
+ const tablesToAlter = matched
60
+ .map(([sourceTable, targetTable]) => diffTable(sourceTable, targetTable, opts))
61
+ .filter((tableDiff) => tableDiff !== undefined);
62
+ const columnDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.columnDiffs ?? []);
63
+ const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs ?? []);
64
+ // Relationships span tables, so they are compared over the whole schema rather than per table.
65
+ const relationshipDiffs = opts.compareRelationships ? diffRelationships(source, target, opts) : [];
66
+ const hasDifferences = tablesToCreate.length > 0 ||
67
+ tablesToDrop.length > 0 ||
68
+ tablesToAlter.length > 0 ||
69
+ relationshipDiffs.length > 0 ||
70
+ indexDiffs.length > 0;
71
+ const hasBreakingChanges = tablesToDrop.length > 0 || columnDiffs.some((d) => d.isBreaking);
72
+ return {
73
+ tablesToCreate,
74
+ tablesToDrop,
75
+ tablesToAlter,
76
+ columnDiffs,
77
+ indexDiffs,
78
+ relationshipDiffs,
79
+ hasDifferences,
80
+ hasBreakingChanges,
81
+ };
82
+ }
83
+ /**
84
+ * Compare two tables and return the differences.
85
+ */
86
+ function diffTable(source, target, opts) {
87
+ const columnDiffs = diffTableColumns(source, target, opts);
88
+ const indexDiffs = opts.compareIndexes ? diffTableIndexes(source, target, opts) : [];
89
+ if (columnDiffs.length === 0 && indexDiffs.length === 0) {
90
+ return undefined;
126
91
  }
127
- /**
128
- * Compare columns between two tables.
129
- */
130
- diffTableColumns(source, target, normalizeName) {
131
- const columnDiffs = [];
132
- // Build column maps
133
- const sourceColMap = new Map();
134
- const targetColMap = new Map();
135
- for (const col of source.columns.values()) {
136
- sourceColMap.set(normalizeName(col.name), col);
137
- }
138
- for (const col of target.columns.values()) {
139
- targetColMap.set(normalizeName(col.name), col);
140
- }
141
- // Columns to add (in source but not target)
142
- for (const [name, sourceCol] of sourceColMap) {
143
- if (!targetColMap.has(name)) {
144
- columnDiffs.push({
145
- table: source.name,
146
- column: sourceCol.name,
147
- type: 'add',
148
- expected: sourceCol,
149
- description: `Add column "${sourceCol.name}"`,
150
- });
151
- }
152
- }
153
- // Columns to drop (in target but not source)
154
- for (const [name, targetCol] of targetColMap) {
155
- if (!sourceColMap.has(name)) {
156
- columnDiffs.push({
157
- table: target.name,
158
- column: targetCol.name,
159
- type: 'drop',
160
- actual: targetCol,
161
- isBreaking: true,
162
- description: `Drop column "${targetCol.name}"`,
163
- });
164
- }
165
- }
166
- // Columns that might need alteration
167
- for (const [name, sourceCol] of sourceColMap) {
168
- const targetCol = targetColMap.get(name);
169
- if (!targetCol)
170
- continue;
171
- const colDiff = this.diffColumn(source.name, sourceCol, targetCol);
172
- if (colDiff) {
173
- columnDiffs.push(colDiff);
174
- }
175
- }
176
- return columnDiffs;
92
+ return { name: source.name, type: 'alter', columnDiffs, indexDiffs };
93
+ }
94
+ /**
95
+ * Compare columns between two tables.
96
+ */
97
+ function diffTableColumns(source, target, opts) {
98
+ const normalizeName = nameNormalizer(opts);
99
+ const { created, dropped, matched } = matchByKey(source.columns.values(), target.columns.values(), (column) => normalizeName(column.name));
100
+ return [
101
+ ...created.map((column) => ({
102
+ table: source.name,
103
+ column: column.name,
104
+ type: 'add',
105
+ expected: column,
106
+ description: `Add column "${column.name}"`,
107
+ })),
108
+ ...dropped.map((column) => ({
109
+ table: target.name,
110
+ column: column.name,
111
+ type: 'drop',
112
+ actual: column,
113
+ isBreaking: true,
114
+ description: `Drop column "${column.name}"`,
115
+ })),
116
+ ...matched
117
+ .map(([sourceColumn, targetColumn]) => diffColumn(source.name, sourceColumn, targetColumn))
118
+ .filter((diff) => diff !== undefined),
119
+ ];
120
+ }
121
+ /**
122
+ * Compare indexes between two tables.
123
+ */
124
+ function diffTableIndexes(source, target, opts) {
125
+ const normalizeName = nameNormalizer(opts);
126
+ const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(index.name));
127
+ return [
128
+ ...created.map((index) => ({ name: index.name, table: source.name, type: 'create', expected: index })),
129
+ ...dropped.map((index) => ({ name: index.name, table: target.name, type: 'drop', actual: index })),
130
+ ...matched
131
+ .map(([sourceIndex, targetIndex]) => diffIndex(source.name, sourceIndex, targetIndex, opts.indexFacets))
132
+ .filter((diff) => diff !== undefined),
133
+ ];
134
+ }
135
+ /**
136
+ * Compare two columns and return the difference.
137
+ */
138
+ function diffColumn(tableName, source, target) {
139
+ const differences = [];
140
+ // Compare types
141
+ if (!areTypesEqual(source.type, target.type)) {
142
+ differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
177
143
  }
178
- /**
179
- * Compare indexes between two tables.
180
- */
181
- diffTableIndexes(source, target, normalizeName) {
182
- const indexDiffs = [];
183
- const sourceIndexMap = new Map();
184
- const targetIndexMap = new Map();
185
- for (const idx of source.indexes) {
186
- sourceIndexMap.set(normalizeName(idx.name), idx);
187
- }
188
- for (const idx of target.indexes) {
189
- targetIndexMap.set(normalizeName(idx.name), idx);
190
- }
191
- // Indexes to create
192
- for (const [, sourceIdx] of sourceIndexMap) {
193
- const normalizedName = normalizeName(sourceIdx.name);
194
- if (!targetIndexMap.has(normalizedName)) {
195
- indexDiffs.push({
196
- name: sourceIdx.name,
197
- table: source.name,
198
- type: 'create',
199
- expected: sourceIdx,
200
- });
201
- }
202
- }
203
- // Indexes to drop
204
- for (const [, targetIdx] of targetIndexMap) {
205
- const normalizedName = normalizeName(targetIdx.name);
206
- if (!sourceIndexMap.has(normalizedName)) {
207
- indexDiffs.push({
208
- name: targetIdx.name,
209
- table: target.name,
210
- type: 'drop',
211
- actual: targetIdx,
212
- });
213
- }
214
- }
215
- // Indexes that might differ
216
- for (const [name, sourceIdx] of sourceIndexMap) {
217
- const targetIdx = targetIndexMap.get(name);
218
- if (!targetIdx)
219
- continue;
220
- const idxDiff = this.diffIndex(source.name, sourceIdx, targetIdx);
221
- if (idxDiff) {
222
- indexDiffs.push(idxDiff);
223
- }
224
- }
225
- return indexDiffs;
144
+ // Compare nullability
145
+ if (source.nullable !== target.nullable) {
146
+ differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
226
147
  }
227
- /**
228
- * Compare two columns and return the difference.
229
- */
230
- diffColumn(tableName, source, target) {
231
- const differences = [];
232
- // Compare types
233
- if (!areTypesEqual(source.type, target.type)) {
234
- differences.push(`type: ${this.formatType(source.type)} → ${this.formatType(target.type)}`);
235
- }
236
- // Compare nullability
237
- if (source.nullable !== target.nullable) {
238
- differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
239
- }
240
- // Compare unique constraint
241
- if (source.isUnique !== target.isUnique) {
242
- differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
243
- }
244
- // Compare auto-increment
245
- if (source.isAutoIncrement !== target.isAutoIncrement) {
246
- differences.push(`autoIncrement: ${target.isAutoIncrement} → ${source.isAutoIncrement}`);
247
- }
248
- // Compare default values (if both defined)
249
- if (this.normalizeDefault(source.defaultValue) !== this.normalizeDefault(target.defaultValue)) {
250
- differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
251
- }
252
- if (differences.length === 0) {
253
- return undefined;
254
- }
255
- return {
256
- table: tableName,
257
- column: source.name,
258
- type: 'alter',
259
- expected: source,
260
- actual: target,
261
- isBreaking: isBreakingTypeChange(target.type, source.type),
262
- description: differences.join(', '),
263
- };
148
+ // Compare unique constraint
149
+ if (source.isUnique !== target.isUnique) {
150
+ differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
264
151
  }
265
- /**
266
- * Compare two indexes and return the difference.
267
- */
268
- diffIndex(tableName, source, target) {
269
- // Compare columns
270
- const sourceColNames = source.columns.map((c) => c.name).join(',');
271
- const targetColNames = target.columns.map((c) => c.name).join(',');
272
- if (sourceColNames !== targetColNames) {
273
- return {
274
- name: source.name,
275
- table: tableName,
276
- type: 'alter',
277
- expected: source,
278
- actual: target,
279
- };
280
- }
281
- // Compare uniqueness
282
- if (source.unique !== target.unique) {
283
- return {
284
- name: source.name,
285
- table: tableName,
286
- type: 'alter',
287
- expected: source,
288
- actual: target,
289
- };
290
- }
291
- // Compare index type
292
- if (source.type !== target.type) {
293
- return {
294
- name: source.name,
295
- table: tableName,
296
- type: 'alter',
297
- expected: source,
298
- actual: target,
299
- };
300
- }
301
- return undefined;
152
+ // Compare auto-increment
153
+ if (source.isAutoIncrement !== target.isAutoIncrement) {
154
+ differences.push(`autoIncrement: ${target.isAutoIncrement} → ${source.isAutoIncrement}`);
302
155
  }
303
- /**
304
- * Compare relationships at the schema level.
305
- */
306
- diffRelationships(source, target, opts) {
307
- const diffs = [];
308
- const normalizeName = opts.ignoreCase ? (n) => n.toLowerCase() : (n) => n;
309
- // Build relationship maps by a normalized key
310
- const sourceRelMap = new Map();
311
- const targetRelMap = new Map();
312
- for (const rel of source.relationships) {
313
- const key = this.getRelationshipKey(rel, normalizeName);
314
- sourceRelMap.set(key, rel);
315
- }
316
- for (const rel of target.relationships) {
317
- const key = this.getRelationshipKey(rel, normalizeName);
318
- targetRelMap.set(key, rel);
319
- }
320
- // Relationships to create
321
- for (const [key, sourceRel] of sourceRelMap) {
322
- if (!targetRelMap.has(key)) {
323
- diffs.push({
324
- name: sourceRel.name,
325
- fromTable: sourceRel.from.table.name,
326
- toTable: sourceRel.to.table.name,
327
- type: 'create',
328
- expected: sourceRel,
329
- });
330
- }
331
- }
332
- // Relationships to drop
333
- for (const [key, targetRel] of targetRelMap) {
334
- if (!sourceRelMap.has(key)) {
335
- diffs.push({
336
- name: targetRel.name,
337
- fromTable: targetRel.from.table.name,
338
- toTable: targetRel.to.table.name,
339
- type: 'drop',
340
- actual: targetRel,
341
- });
342
- }
343
- }
344
- // Relationships that differ
345
- for (const [key, sourceRel] of sourceRelMap) {
346
- const targetRel = targetRelMap.get(key);
347
- if (!targetRel)
348
- continue;
349
- const relDiff = this.diffRelationship(sourceRel, targetRel);
350
- if (relDiff) {
351
- diffs.push(relDiff);
352
- }
353
- }
354
- return diffs;
156
+ // Compare default values (if both defined)
157
+ if (normalizeDefault(source.defaultValue) !== normalizeDefault(target.defaultValue)) {
158
+ differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
355
159
  }
356
- /**
357
- * Compare two relationships.
358
- */
359
- diffRelationship(source, target) {
360
- // Compare on delete/update actions (normalizing defaults)
361
- const sDelete = source.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION;
362
- const tDelete = target.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION;
363
- const sUpdate = source.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION;
364
- const tUpdate = target.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION;
365
- if (sDelete !== tDelete || sUpdate !== tUpdate) {
366
- return {
367
- name: source.name,
368
- fromTable: source.from.table.name,
369
- toTable: source.to.table.name,
370
- type: 'alter',
371
- expected: source,
372
- actual: target,
373
- };
374
- }
160
+ if (differences.length === 0) {
375
161
  return undefined;
376
162
  }
377
- /**
378
- * Generate a unique key for a relationship based on its structure.
379
- */
380
- getRelationshipKey(rel, normalizeName) {
381
- const fromCols = rel.from.columns
382
- .map((c) => c.name)
383
- .sort()
384
- .join(',');
385
- const toCols = rel.to.columns
386
- .map((c) => c.name)
387
- .sort()
388
- .join(',');
389
- return `${normalizeName(rel.from.table.name)}.${fromCols}->${normalizeName(rel.to.table.name)}.${toCols}`;
163
+ return {
164
+ table: tableName,
165
+ column: source.name,
166
+ type: 'alter',
167
+ expected: source,
168
+ actual: target,
169
+ isBreaking: isBreakingTypeChange(target.type, source.type),
170
+ description: differences.join(', '),
171
+ };
172
+ }
173
+ /**
174
+ * Compare two indexes and return the difference.
175
+ */
176
+ function diffIndex(tableName, source, target, facets) {
177
+ const differences = describeIndexDifferences(source, target, facets);
178
+ if (differences.length === 0) {
179
+ return undefined;
390
180
  }
391
- /**
392
- * Format a canonical type for display.
393
- */
394
- formatType(type) {
395
- let result = type.category;
396
- if (type.size)
397
- result += `(${type.size})`;
398
- if (type.length)
399
- result += `(${type.length})`;
400
- if (type.precision) {
401
- result += type.scale !== undefined ? `(${type.precision},${type.scale})` : `(${type.precision})`;
402
- }
403
- if (type.unsigned)
404
- result += ' unsigned';
405
- return result;
181
+ return {
182
+ name: source.name,
183
+ table: tableName,
184
+ type: 'alter',
185
+ expected: source,
186
+ actual: target,
187
+ description: differences.join(', '),
188
+ };
189
+ }
190
+ /**
191
+ * Compare relationships at the schema level.
192
+ */
193
+ function diffRelationships(source, target, opts) {
194
+ const normalizeName = nameNormalizer(opts);
195
+ const { created, dropped, matched } = matchByKey(source.relationships, target.relationships, (relation) => getRelationshipKey(relation, normalizeName));
196
+ return [
197
+ ...created.map((relation) => ({
198
+ ...relationEnds(relation),
199
+ type: 'create',
200
+ expected: relation,
201
+ })),
202
+ ...dropped.map((relation) => ({ ...relationEnds(relation), type: 'drop', actual: relation })),
203
+ ...matched
204
+ .map(([sourceRelation, targetRelation]) => diffRelationship(sourceRelation, targetRelation))
205
+ .filter((diff) => diff !== undefined),
206
+ ];
207
+ }
208
+ /**
209
+ * Compare two relationships.
210
+ */
211
+ function diffRelationship(source, target) {
212
+ // Compare on delete/update actions (normalizing defaults)
213
+ const sDelete = source.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION;
214
+ const tDelete = target.onDelete ?? DEFAULT_FOREIGN_KEY_ACTION;
215
+ const sUpdate = source.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION;
216
+ const tUpdate = target.onUpdate ?? DEFAULT_FOREIGN_KEY_ACTION;
217
+ if (sDelete !== tDelete || sUpdate !== tUpdate) {
218
+ return { ...relationEnds(source), type: 'alter', expected: source, actual: target };
406
219
  }
407
- /**
408
- * Normalize default values for comparison.
409
- */
410
- normalizeDefault(value) {
411
- if (value === undefined || value === null)
412
- return '';
413
- if (typeof value === 'string') {
414
- // Normalize function calls
415
- const upper = value.toUpperCase();
416
- if (upper.includes('NOW()') || upper.includes('CURRENT_TIMESTAMP')) {
417
- return 'CURRENT_TIMESTAMP';
418
- }
419
- }
420
- return String(value);
220
+ return undefined;
221
+ }
222
+ /**
223
+ * Generate a unique key for a relationship based on its structure.
224
+ */
225
+ function getRelationshipKey(rel, normalizeName) {
226
+ const fromCols = rel.from.columns
227
+ .map((c) => c.name)
228
+ .sort()
229
+ .join(',');
230
+ const toCols = rel.to.columns
231
+ .map((c) => c.name)
232
+ .sort()
233
+ .join(',');
234
+ return `${normalizeName(rel.from.table.name)}.${fromCols}->${normalizeName(rel.to.table.name)}.${toCols}`;
235
+ }
236
+ /**
237
+ * Format a canonical type for display.
238
+ */
239
+ function formatType(type) {
240
+ let result = type.category;
241
+ if (type.size)
242
+ result += `(${type.size})`;
243
+ if (type.length)
244
+ result += `(${type.length})`;
245
+ if (type.precision) {
246
+ result += type.scale !== undefined ? `(${type.precision},${type.scale})` : `(${type.precision})`;
421
247
  }
248
+ if (type.unsigned)
249
+ result += ' unsigned';
250
+ return result;
422
251
  }
423
252
  /**
424
- * Create a differ and run a comparison.
425
- * Convenience function for one-off comparisons.
253
+ * Normalize default values for comparison.
426
254
  */
427
- export function diffSchemas(source, target, options) {
428
- const differ = new SchemaASTDiffer();
429
- return differ.diff(source, target, options);
255
+ function normalizeDefault(value) {
256
+ if (value === undefined || value === null)
257
+ return '';
258
+ if (typeof value === 'string') {
259
+ // Normalize function calls
260
+ const upper = value.toUpperCase();
261
+ if (upper.includes('NOW()') || upper.includes('CURRENT_TIMESTAMP')) {
262
+ return 'CURRENT_TIMESTAMP';
263
+ }
264
+ }
265
+ return String(value);
430
266
  }