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.
- package/README.md +9 -8
- package/dist/browser/uql-browser.min.js.map +1 -1
- package/dist/cockroachdb/cockroachDialect.js +1 -1
- package/dist/dialect/indexSqlDialect.d.ts +3 -2
- package/dist/dialect/indexSqlDialect.js +10 -8
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -7
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/maria/mariaDialect.js +1 -1
- package/dist/migrate/builder/migrationBuilder.js +1 -1
- package/dist/migrate/builder/tableBuilder.js +2 -2
- package/dist/migrate/cli.js +4 -6
- package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
- package/dist/migrate/codegen/entityCodeGenerator.js +22 -25
- package/dist/migrate/codegen/fieldOptionsSource.d.ts +1 -1
- package/dist/migrate/codegen/fieldOptionsSource.js +6 -1
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +14 -0
- package/dist/migrate/codegen/indexDecoratorSource.js +105 -0
- package/dist/migrate/drift/driftDetector.d.ts +5 -50
- package/dist/migrate/drift/driftDetector.js +215 -224
- package/dist/migrate/drift/index.d.ts +1 -1
- package/dist/migrate/drift/index.js +1 -1
- package/dist/migrate/generator/definitionToNode.d.ts +9 -0
- package/dist/migrate/generator/definitionToNode.js +79 -0
- package/dist/migrate/generator/indexNodeToSchema.js +4 -2
- package/dist/migrate/generator/mongoSchemaGenerator.js +3 -3
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +3 -0
- package/dist/migrate/introspection/baseSqlIntrospector.js +13 -5
- package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -0
- package/dist/migrate/introspection/mongoIntrospector.js +5 -10
- package/dist/migrate/introspection/mysqlIntrospector.js +1 -1
- package/dist/migrate/introspection/postgresIntrospector.d.ts +57 -5
- package/dist/migrate/introspection/postgresIntrospector.js +93 -10
- package/dist/migrate/introspection/sqliteIntrospector.js +1 -1
- package/dist/migrate/migrator.js +3 -2
- package/dist/migrate/schemaGenerator.d.ts +26 -3
- package/dist/migrate/schemaGenerator.js +53 -92
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/index.js +2 -2
- package/dist/schema/indexColumns.d.ts +10 -0
- package/dist/schema/indexColumns.js +11 -0
- package/dist/schema/indexDifferences.d.ts +22 -0
- package/dist/schema/indexDifferences.js +49 -0
- package/dist/schema/schemaAST.js +2 -8
- package/dist/schema/schemaASTBuilder.d.ts +6 -59
- package/dist/schema/schemaASTBuilder.js +208 -236
- package/dist/schema/schemaASTDiffer.d.ts +9 -56
- package/dist/schema/schemaASTDiffer.js +229 -393
- package/dist/schema/types.d.ts +5 -12
- package/dist/schema/types.js +15 -0
- package/dist/type/dialect.d.ts +7 -2
- package/dist/type/dialect.js +1 -0
- package/dist/type/migration.d.ts +14 -1
- package/dist/util/string.util.js +6 -1
- 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
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
*
|
|
425
|
-
* Convenience function for one-off comparisons.
|
|
253
|
+
* Normalize default values for comparison.
|
|
426
254
|
*/
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
}
|