uql-orm 0.80.0 → 0.81.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/dist/dialect/vectorSqlDialect.d.ts +2 -0
- package/dist/dialect/vectorSqlDialect.js +4 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +24 -0
- package/dist/migrate/cli.js +1 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
- package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
- package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
- package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
- package/dist/migrate/ddl/pgIndexDdl.js +8 -6
- package/dist/migrate/ddl/tableDdl.d.ts +3 -2
- package/dist/migrate/ddl/tableDdl.js +9 -7
- package/dist/migrate/drift/driftDetector.d.ts +4 -5
- package/dist/migrate/drift/driftDetector.js +21 -21
- package/dist/migrate/generator/definitionToNode.d.ts +1 -1
- package/dist/migrate/generator/definitionToNode.js +9 -20
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +11 -19
- package/dist/migrate/index.d.ts +2 -1
- package/dist/migrate/index.js +1 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -18
- package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +3 -3
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
- package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
- package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
- package/dist/migrate/introspection/postgresIntrospector.js +63 -46
- package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
- package/dist/migrate/migrator.d.ts +5 -0
- package/dist/migrate/migrator.js +33 -44
- package/dist/migrate/schemaChange.d.ts +18 -0
- package/dist/migrate/schemaChange.js +37 -0
- package/dist/migrate/schemaGenerator.d.ts +13 -14
- package/dist/migrate/schemaGenerator.js +83 -177
- package/dist/schema/indexDifferences.d.ts +22 -6
- package/dist/schema/indexDifferences.js +23 -8
- package/dist/schema/matchByKey.d.ts +10 -0
- package/dist/schema/matchByKey.js +18 -0
- package/dist/schema/schemaAST.d.ts +6 -2
- package/dist/schema/schemaAST.js +7 -3
- package/dist/schema/schemaASTBuilder.d.ts +2 -0
- package/dist/schema/schemaASTBuilder.js +15 -10
- package/dist/schema/schemaASTDiffer.d.ts +2 -3
- package/dist/schema/schemaASTDiffer.js +15 -36
- package/dist/schema/types.d.ts +14 -15
- package/dist/type/migration.d.ts +32 -50
- package/dist/util/ddlExpression.util.d.ts +5 -1
- package/dist/util/ddlExpression.util.js +6 -2
- package/package.json +1 -1
|
@@ -5,13 +5,14 @@ import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
|
|
|
5
5
|
import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
|
|
6
6
|
import { isAutoIncrement, qualifyName } from '../util/index.js';
|
|
7
7
|
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName, isOwnedName } from '../util/sql.util.js';
|
|
8
|
-
import {
|
|
8
|
+
import { sameDefault } from './builder/expressions.js';
|
|
9
9
|
import { splitSqlStatements } from './builder/splitSqlStatements.js';
|
|
10
10
|
import { indexDdlFor, tableDdlFor } from './ddl/index.js';
|
|
11
11
|
import { sizedType } from './ddl/tableDdl.js';
|
|
12
12
|
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, renderIndexDefinition, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
13
13
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
14
14
|
import { assertIndexPredicate } from './indexPredicate.js';
|
|
15
|
+
import { added, alterations, dropped, nonEmpty, sides } from './schemaChange.js';
|
|
15
16
|
import { dropTrigger, renderTrigger, stampTriggers } from './triggerSql.js';
|
|
16
17
|
/**
|
|
17
18
|
* Unified SQL schema generator.
|
|
@@ -78,6 +79,7 @@ export class SqlSchemaGenerator {
|
|
|
78
79
|
return buildEntityAST(this, entities, {
|
|
79
80
|
defaultForeignKeyAction: this.defaultForeignKeyAction,
|
|
80
81
|
textScoreIndexes: this.dialect.features.textScoreIndexes,
|
|
82
|
+
vectorIndexRequiresNotNull: this.features.vectorIndexRequiresNotNull,
|
|
81
83
|
});
|
|
82
84
|
}
|
|
83
85
|
/**
|
|
@@ -173,97 +175,45 @@ export class SqlSchemaGenerator {
|
|
|
173
175
|
const cascade = options.cascade && this.features.dropTableCascade ? ' CASCADE' : '';
|
|
174
176
|
return `DROP TABLE ${ifExists}${this.escapeId(tableName)}${cascade};`;
|
|
175
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The statements taking a table through `diff`, in the one order both directions need: whatever holds
|
|
180
|
+
* something down goes before it and comes back after it. A foreign key holds its columns and the key it
|
|
181
|
+
* points at, so it goes first and comes back last; the key holds its columns; and some engines drop an
|
|
182
|
+
* index along with its column, which would leave nothing to name. An alter is its drop, then its add.
|
|
183
|
+
*/
|
|
176
184
|
generateAlterTable(diff) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
...(diff.
|
|
187
|
-
...(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// drop an index along with its column, which would leave nothing here to name.
|
|
191
|
-
statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToDrop, diff.schema));
|
|
192
|
-
for (const column of diff.columnsToAdd ?? []) {
|
|
193
|
-
this.assertColumnAddable(diff.tableName, column);
|
|
194
|
-
statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
|
|
195
|
-
statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
|
|
196
|
-
}
|
|
197
|
-
statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.to)));
|
|
198
|
-
for (const columnName of diff.columnsToDrop ?? []) {
|
|
199
|
-
statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
|
|
200
|
-
}
|
|
201
|
-
statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToAdd));
|
|
202
|
-
// Last, so every column it names exists by now.
|
|
203
|
-
if (diff.primaryKey?.to.length) {
|
|
204
|
-
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
|
|
205
|
-
}
|
|
206
|
-
// After the columns, for the same reason the key is: a constraint cannot name one that is not
|
|
207
|
-
// there yet. The add half of an alter rides along, its drop having gone out above.
|
|
208
|
-
statements.push(...this.addForeignKeyStatements(diff.tableName, [
|
|
209
|
-
...(diff.foreignKeysToAdd ?? []),
|
|
210
|
-
...(diff.foreignKeysToAlter ?? []).map((it) => it.to),
|
|
211
|
-
]));
|
|
212
|
-
return statements;
|
|
185
|
+
const { tableName, schema, primaryKey } = diff;
|
|
186
|
+
const { columns } = diff;
|
|
187
|
+
return [
|
|
188
|
+
...sides(diff.foreignKeys, 'from').map((foreignKey) => this.generateDropForeignKeySql(tableName, constraintNameOf(tableName, foreignKey))),
|
|
189
|
+
...(primaryKey?.from ? [this.generateDropPrimaryKeySql(tableName, primaryKey.from.name)] : []),
|
|
190
|
+
...sides(diff.indexes, 'from').map((index) => this.generateDropIndex(tableName, index.name, schema)),
|
|
191
|
+
...added(columns).flatMap((column) => this.addColumnStatements(tableName, column, schema)),
|
|
192
|
+
...alterations(columns).flatMap(({ from, to }) => this.tableDdl.alterColumn(tableName, to, this.generateColumnDefinitionFromSchema(to), from)),
|
|
193
|
+
...dropped(columns).flatMap((column) => this.tableDdl.dropColumn(tableName, column.name)),
|
|
194
|
+
...this.addIndexStatements(tableName, sides(diff.indexes, 'to')),
|
|
195
|
+
...(primaryKey?.to ? [this.generateAddPrimaryKeySql(tableName, primaryKey.to.columns, primaryKey.to.name)] : []),
|
|
196
|
+
...this.addForeignKeyStatements(tableName, sides(diff.foreignKeys, 'to')),
|
|
197
|
+
];
|
|
213
198
|
}
|
|
214
199
|
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
215
200
|
addForeignKeyStatements(tableName, foreignKeys) {
|
|
216
201
|
return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
|
|
217
202
|
}
|
|
218
|
-
/** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
|
|
219
|
-
dropForeignKeyStatements(tableName, constraintNames) {
|
|
220
|
-
return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
|
|
221
|
-
}
|
|
222
|
-
/** The `ALTER COLUMN` restating each of `columns`. */
|
|
223
|
-
alterColumnStatements(tableName, columns) {
|
|
224
|
-
return columns.flatMap((column) => this.generateAlterColumnStatements(tableName, column, this.generateColumnDefinitionFromSchema(column)));
|
|
225
|
-
}
|
|
226
203
|
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
227
|
-
addIndexStatements(tableName, indexes
|
|
204
|
+
addIndexStatements(tableName, indexes) {
|
|
228
205
|
return indexes.flatMap((index) => [
|
|
229
206
|
this.generateCreateIndex(tableName, index),
|
|
230
207
|
...this.indexDdl.settleStatements(tableName, index),
|
|
231
208
|
]);
|
|
232
209
|
}
|
|
233
|
-
/**
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// first, and a column it is about to drop is then free of anything naming it.
|
|
241
|
-
statements.push(...this.dropForeignKeyStatements(diff.tableName, [
|
|
242
|
-
...(diff.foreignKeysToAdd ?? []).map((it) => constraintNameOf(diff.tableName, it)),
|
|
243
|
-
...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.to)),
|
|
244
|
-
]));
|
|
245
|
-
// The key next, for the same reason: a column the up added cannot be dropped below while the new
|
|
246
|
-
// key still names it. Restored under the name the database gave it, which is what the table had
|
|
247
|
-
// before, rather than a derived one that was never on it.
|
|
248
|
-
if (diff.primaryKey?.to.length) {
|
|
249
|
-
statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
|
|
250
|
-
}
|
|
251
|
-
for (const column of diff.columnsToAdd ?? []) {
|
|
252
|
-
statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
|
|
253
|
-
}
|
|
254
|
-
statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.from)));
|
|
255
|
-
statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToAdd, diff.schema));
|
|
256
|
-
statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToDrop));
|
|
257
|
-
if (diff.primaryKey?.from.length) {
|
|
258
|
-
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
|
|
259
|
-
}
|
|
260
|
-
// The constraint the up replaced, back under the name the database had for it. A foreign key the
|
|
261
|
-
// up *dropped* is not restored: only its name survived the diff, never what it pointed at.
|
|
262
|
-
statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
|
|
263
|
-
if (diff.columnsToDrop?.length || diff.foreignKeysToDrop?.length) {
|
|
264
|
-
statements.push(`-- TODO: Manual reversal needed for dropped columns/foreign keys`);
|
|
265
|
-
}
|
|
266
|
-
return statements;
|
|
210
|
+
/** A column added to a table that exists, and its comment where the engine keeps one apart. */
|
|
211
|
+
addColumnStatements(tableName, column, schema) {
|
|
212
|
+
this.assertColumnAddable(tableName, column);
|
|
213
|
+
return [
|
|
214
|
+
this.tableDdl.addColumn(tableName, this.generateColumnDefinitionFromSchema(column)),
|
|
215
|
+
...this.generateColumnCommentStatement(tableName, column, schema),
|
|
216
|
+
];
|
|
267
217
|
}
|
|
268
218
|
generateCreateIndex(tableName, index, options = {}) {
|
|
269
219
|
return this.indexDdl.getCreateIndexStatement(tableName, index, options);
|
|
@@ -288,8 +238,8 @@ export class SqlSchemaGenerator {
|
|
|
288
238
|
/**
|
|
289
239
|
* The one place a column definition is spelled, so the `ColumnSchema` and `ColumnNode` paths cannot
|
|
290
240
|
* drift. A key column states `NOT NULL` rather than leave it to the key: SQLite lets a key column hold
|
|
291
|
-
* NULL otherwise, and SQL Server adds no key over a nullable column. `UNIQUE
|
|
292
|
-
* enum's `CHECK` comes last, the only place MariaDB takes it.
|
|
241
|
+
* NULL otherwise, and SQL Server adds no key over a nullable column. Never `UNIQUE`: a unique column is
|
|
242
|
+
* a unique index, which the table creates beside it. An enum's `CHECK` comes last, the only place MariaDB takes it.
|
|
293
243
|
*/
|
|
294
244
|
renderColumn(column) {
|
|
295
245
|
const type = column.generatedAs
|
|
@@ -299,9 +249,6 @@ export class SqlSchemaGenerator {
|
|
|
299
249
|
if (!column.nullable) {
|
|
300
250
|
def += ' NOT NULL';
|
|
301
251
|
}
|
|
302
|
-
if (column.isUnique && !column.isPrimaryKey) {
|
|
303
|
-
def += ' UNIQUE';
|
|
304
|
-
}
|
|
305
252
|
def += this.tableDdl.defaultClause(column);
|
|
306
253
|
if (column.comment) {
|
|
307
254
|
def += this.generateColumnComment(column.comment);
|
|
@@ -367,72 +314,61 @@ export class SqlSchemaGenerator {
|
|
|
367
314
|
if (!desired) {
|
|
368
315
|
return undefined;
|
|
369
316
|
}
|
|
370
|
-
// Indexes are matched here rather than by the differ, which pairs them by name so that a changed
|
|
371
|
-
// one reads as one index that altered. A migration needs the opposite: an index already in the
|
|
372
|
-
// table, under whatever name, must not be created again, and one whose shape differs is dropped
|
|
373
|
-
// and created anew - no engine alters an index's columns or uniqueness.
|
|
374
317
|
const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
|
|
375
|
-
const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
318
|
+
const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes, currentTable.indexFacets);
|
|
319
|
+
// An alter's `to` without its values: an alter restates the whole column, and MySQL answers a
|
|
320
|
+
// restated `CHECK` by adding a *second* constraint rather than replacing the first, so the column
|
|
321
|
+
// would accumulate one per alter. An enum's values reach the database with the column and are never
|
|
322
|
+
// restated - which is also why changing them is a hand-written migration. See architecture/roadmap.md.
|
|
323
|
+
const columns = (tableDiff?.columnDiffs ?? []).map((it) => {
|
|
324
|
+
if (it.type === 'add') {
|
|
325
|
+
return { to: this.columnNodeToSchema(it.expected) };
|
|
326
|
+
}
|
|
327
|
+
if (it.type === 'drop') {
|
|
328
|
+
return { from: this.columnNodeToSchema(it.actual) };
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
from: this.columnNodeToSchema(it.actual),
|
|
332
|
+
to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
const keyDiff = tableDiff?.primaryKeyDiff;
|
|
336
|
+
// The key added named as this generator names it, so the rollback can drop it by that name.
|
|
337
|
+
const primaryKey = keyDiff && {
|
|
338
|
+
from: keyDiff.actual,
|
|
339
|
+
to: keyDiff.expected && {
|
|
340
|
+
columns: keyDiff.expected.columns,
|
|
341
|
+
name: derivedPrimaryKeyName(tableName, keyDiff.expected.columns),
|
|
342
|
+
},
|
|
397
343
|
};
|
|
398
344
|
// This table's own foreign keys. None where the engine cannot alter one (SQLite, short of rebuilding
|
|
399
345
|
// the table), since a difference nothing can apply would throw on every sync; `drift:check` names it.
|
|
400
346
|
const relationDiffs = this.features.foreignKeyAlter
|
|
401
347
|
? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
|
|
402
348
|
: [];
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
!columnsToDrop.length &&
|
|
409
|
-
!indexesToAdd.length &&
|
|
410
|
-
!indexesToDrop.length &&
|
|
411
|
-
!foreignKeysToAdd.length &&
|
|
412
|
-
!foreignKeysToDrop.length &&
|
|
413
|
-
!foreignKeysToAlter.length &&
|
|
414
|
-
!primaryKey) {
|
|
415
|
-
return undefined;
|
|
416
|
-
}
|
|
417
|
-
return {
|
|
349
|
+
const foreignKeys = relationDiffs.map(({ actual, expected }) => ({
|
|
350
|
+
from: actual && foreignKeyOf(actual),
|
|
351
|
+
to: expected && foreignKeyOf(expected),
|
|
352
|
+
}));
|
|
353
|
+
const alter = {
|
|
418
354
|
tableName,
|
|
419
355
|
schema,
|
|
420
356
|
type: 'alter',
|
|
421
357
|
primaryKey,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
|
|
358
|
+
columns: nonEmpty(columns),
|
|
359
|
+
indexes: nonEmpty([
|
|
360
|
+
...indexes.toAdd.map((to) => ({ to: indexNodeToSchema(to) })),
|
|
361
|
+
...indexes.toDrop.map((from) => ({ from: indexNodeToSchema(from) })),
|
|
362
|
+
...indexes.toAlter.map(({ from, to }) => ({ from: indexNodeToSchema(from), to: indexNodeToSchema(to) })),
|
|
363
|
+
]),
|
|
364
|
+
foreignKeys: nonEmpty(foreignKeys),
|
|
430
365
|
};
|
|
366
|
+
return alter.primaryKey || alter.columns || alter.indexes || alter.foreignKeys ? alter : undefined;
|
|
431
367
|
}
|
|
432
368
|
diffOptions() {
|
|
433
369
|
return {
|
|
434
370
|
normalizeType: engineType(this.dialect),
|
|
435
|
-
defaultsEqual:
|
|
371
|
+
defaultsEqual: this.defaultsEqual,
|
|
436
372
|
};
|
|
437
373
|
}
|
|
438
374
|
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
@@ -440,58 +376,28 @@ export class SqlSchemaGenerator {
|
|
|
440
376
|
const { table: _table, referencedBy: _referencedBy, references: _references, ...column } = col;
|
|
441
377
|
return { ...column, type: this.columnSqlType(col) };
|
|
442
378
|
}
|
|
443
|
-
/**
|
|
444
|
-
|
|
445
|
-
*/
|
|
446
|
-
isDefaultValueEqual(current, desired) {
|
|
447
|
-
if (current === desired)
|
|
448
|
-
return true;
|
|
449
|
-
// Both spellings of "no default" are the same fact, and engines disagree on which they report:
|
|
450
|
-
// MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
|
|
451
|
-
// `MODIFY` every nullable column, on every sync, forever.
|
|
452
|
-
if (current == null || desired == null)
|
|
453
|
-
return current == null && desired == null;
|
|
454
|
-
const normalize = (value) => {
|
|
455
|
-
// Render first: the desired side may be a symbolic expression, the current side is always the
|
|
456
|
-
// engine's own text, and `{"kind":"now"}` matches no spelling of `CURRENT_TIMESTAMP`.
|
|
457
|
-
const val = SqlExpression.isExpression(value) ? formatDefaultValue(value, this.dialect) : value;
|
|
458
|
-
if (typeof val === 'string') {
|
|
459
|
-
let s = val.replace(/::[a-z_]+(\s+[a-z_]+)*(\[\])?$/i, '');
|
|
460
|
-
s = s.replace(/^'(.*)'$/, '$1');
|
|
461
|
-
if (s.toLowerCase() === 'null')
|
|
462
|
-
return 'null';
|
|
463
|
-
return s;
|
|
464
|
-
}
|
|
465
|
-
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
466
|
-
};
|
|
467
|
-
return normalize(current) === normalize(desired);
|
|
468
|
-
}
|
|
379
|
+
/** Whether a column's stored default is the one the entity declares, as this engine reprints it. */
|
|
380
|
+
defaultsEqual = (desired, current) => sameDefault(desired, current, this.dialect);
|
|
469
381
|
generateCreateTableFromNode(table, options = {}) {
|
|
470
382
|
const columns = [];
|
|
471
383
|
const constraints = [];
|
|
472
|
-
// MariaDB rejects a `VECTOR INDEX` whose column is nullable ("All parts of a VECTOR index must
|
|
473
|
-
// be NOT NULL"), so being indexed decides it rather than the entity's own nullability.
|
|
474
|
-
const indexedVectorColumns = new Set(this.features.vectorIndexRequiresNotNull
|
|
475
|
-
? table.indexes.filter((index) => index.type === 'vector').flatMap((idx) => idx.entries.map((e) => e.column))
|
|
476
|
-
: []);
|
|
477
384
|
for (const col of table.columns.values()) {
|
|
478
|
-
|
|
479
|
-
columns.push(colDef);
|
|
385
|
+
columns.push(this.generateColumnFromNode(col));
|
|
480
386
|
}
|
|
481
387
|
// Every key, of any width, as one named constraint beside the checks and foreign keys - so a
|
|
482
388
|
// later `DROP` has something to name. The exception is a dialect whose serial type states the key
|
|
483
389
|
// itself (SQLite's `INTEGER PRIMARY KEY AUTOINCREMENT`, which cannot be split): there the column
|
|
484
390
|
// has already declared it, and saying it again is a second primary key.
|
|
391
|
+
const key = table.primaryKey;
|
|
485
392
|
const declaredByColumn = this.dialect.features.serialDeclaresPrimaryKey &&
|
|
486
|
-
|
|
487
|
-
table.
|
|
488
|
-
if (
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
(table.checks ?? []).forEach((check, i) => {
|
|
393
|
+
key?.columns.length === 1 &&
|
|
394
|
+
table.columns.get(key.columns[0])?.isAutoIncrement;
|
|
395
|
+
if (key && !declaredByColumn) {
|
|
396
|
+
const name = key.name ?? derivedPrimaryKeyName(table.name, key.columns);
|
|
397
|
+
const columns = key.columns.map((column) => this.escapeId(column)).join(', ');
|
|
398
|
+
constraints.push(`CONSTRAINT ${this.escapeId(name)} PRIMARY KEY (${columns})`);
|
|
399
|
+
}
|
|
400
|
+
table.checks.forEach((check, i) => {
|
|
495
401
|
const name = check.name ?? derivedCheckName(table.name, i + 1);
|
|
496
402
|
constraints.push(`CONSTRAINT ${this.escapeId(name)} CHECK (${check.expression})`);
|
|
497
403
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { IndexSchema } from '../type/index.js';
|
|
1
2
|
import type { IndexNode } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* What an introspector reports about an index, and so all a diff may compare; apart from `IndexFeature`, what an engine emits.
|
|
4
|
-
* `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it
|
|
5
|
+
* `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it;
|
|
6
|
+
* `distance` is the metric a vector index was built for.
|
|
5
7
|
* `textIndex` is a text index's weights and language, kept by an engine that lists its fields in no declared order.
|
|
6
8
|
*/
|
|
7
|
-
export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textIndex';
|
|
9
|
+
export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'distance' | 'textIndex';
|
|
8
10
|
type ComparableIndex = Pick<IndexNode, 'name' | 'entries' | 'unique'>;
|
|
9
11
|
/**
|
|
10
12
|
* Whether the table has this index already, by shape rather than name, uniqueness included. An index
|
|
@@ -17,16 +19,30 @@ export declare function indexSignature(index: ComparableIndex): string;
|
|
|
17
19
|
*/
|
|
18
20
|
export declare function indexNameStem(name: string): string;
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* Pairs by name, then what is left by shape: an index the database has under another name is still
|
|
23
|
+
* the one asked for. What stays unpaired is created or dropped.
|
|
22
24
|
*/
|
|
23
|
-
export declare function
|
|
25
|
+
export declare function pairIndexes<S extends ComparableIndex, T extends ComparableIndex>(source: readonly S[], target: readonly T[], normalizeName?: (name: string) => string): {
|
|
26
|
+
created: S[];
|
|
27
|
+
dropped: T[];
|
|
28
|
+
matched: (readonly [S, T])[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
|
|
32
|
+
* `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
|
|
33
|
+
* dropped: any other may have been made outside the ORM.
|
|
34
|
+
*/
|
|
35
|
+
export declare function indexChanges<I extends IndexSchema>(table: string, declared: readonly I[], current: readonly IndexNode[], facets: ReadonlySet<IndexFacet>): {
|
|
24
36
|
toAdd: I[];
|
|
25
37
|
toDrop: IndexNode[];
|
|
38
|
+
toAlter: {
|
|
39
|
+
from: IndexNode;
|
|
40
|
+
to: I;
|
|
41
|
+
}[];
|
|
26
42
|
};
|
|
27
43
|
/**
|
|
28
44
|
* What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
|
|
29
45
|
* path or a predicate is reprinted by the engine, so never compared.
|
|
30
46
|
*/
|
|
31
|
-
export declare function describeIndexDifferences(source:
|
|
47
|
+
export declare function describeIndexDifferences(source: IndexSchema, target: IndexSchema, facets: ReadonlySet<IndexFacet>): string[];
|
|
32
48
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { isVectorIndexType } from '../type/vector.js';
|
|
1
|
+
import { indexDistance, isVectorIndexType } from '../type/vector.js';
|
|
2
2
|
import { fulltextConfig } from '../util/dialect.util.js';
|
|
3
3
|
import { derivedIndexName } from '../util/sql.util.js';
|
|
4
|
+
import { matchByKey } from './matchByKey.js';
|
|
4
5
|
/** An entry the engine reprints in its own words, so never compared as written. */
|
|
5
6
|
function isReprinted(entry) {
|
|
6
7
|
return Boolean(entry.expression || entry.jsonPath || entry.jsonArray);
|
|
@@ -26,17 +27,27 @@ export function indexNameStem(name) {
|
|
|
26
27
|
return bare.replace(/__/g, '_');
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* Pairs by name, then what is left by shape: an index the database has under another name is still
|
|
31
|
+
* the one asked for. What stays unpaired is created or dropped.
|
|
31
32
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const
|
|
34
|
-
const
|
|
33
|
+
export function pairIndexes(source, target, normalizeName = (name) => name) {
|
|
34
|
+
const byName = matchByKey(source, target, (index) => normalizeName(indexNameStem(index.name)));
|
|
35
|
+
const byShape = matchByKey(byName.created, byName.dropped, indexSignature);
|
|
36
|
+
return { created: byShape.created, dropped: byShape.dropped, matched: [...byName.matched, ...byShape.matched] };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
|
|
40
|
+
* `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
|
|
41
|
+
* dropped: any other may have been made outside the ORM.
|
|
42
|
+
*/
|
|
43
|
+
export function indexChanges(table, declared, current, facets) {
|
|
44
|
+
const { created, dropped, matched } = pairIndexes(declared, current);
|
|
35
45
|
const claimed = new Set(declared.map((index) => index.name));
|
|
36
46
|
const owned = (index) => claimed.has(index.name) || hasDerivedName(table, index);
|
|
37
47
|
return {
|
|
38
|
-
toAdd:
|
|
39
|
-
toDrop:
|
|
48
|
+
toAdd: created,
|
|
49
|
+
toDrop: dropped.filter(owned),
|
|
50
|
+
toAlter: matched.flatMap(([to, from]) => (describeIndexDifferences(to, from, facets).length ? [{ from, to }] : [])),
|
|
40
51
|
};
|
|
41
52
|
}
|
|
42
53
|
/**
|
|
@@ -85,6 +96,10 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
85
96
|
if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
|
|
86
97
|
differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
|
|
87
98
|
}
|
|
99
|
+
const bothVector = isVectorIndexType(source.type) && isVectorIndexType(target.type);
|
|
100
|
+
if (facets.has('distance') && bothVector && indexDistance(source) !== indexDistance(target)) {
|
|
101
|
+
differences.push(`distance: ${indexDistance(target)} -> ${indexDistance(source)}`);
|
|
102
|
+
}
|
|
88
103
|
if (facets.has('vector') && isVectorIndexType(source.type) !== isVectorIndexType(target.type)) {
|
|
89
104
|
const [expected, actual] = [source, target].map((index) => (isVectorIndexType(index.type) ? 'yes' : 'no'));
|
|
90
105
|
differences.push(`vector index: ${actual} -> ${expected}`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only three ways two keyed collections can differ, which is the shape of every comparison in
|
|
3
|
+
* the schema diff: tables, columns, indexes and relationships all key and then split the same way.
|
|
4
|
+
* One counterpart each, so two items sharing a key (a duplicate index) are not collapsed into one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function matchByKey<S, T>(source: Iterable<S>, target: Iterable<T>, key: (item: S | T) => string): {
|
|
7
|
+
created: S[];
|
|
8
|
+
dropped: T[];
|
|
9
|
+
matched: (readonly [S, T])[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only three ways two keyed collections can differ, which is the shape of every comparison in
|
|
3
|
+
* the schema diff: tables, columns, indexes and relationships all key and then split the same way.
|
|
4
|
+
* One counterpart each, so two items sharing a key (a duplicate index) are not collapsed into one.
|
|
5
|
+
*/
|
|
6
|
+
export function matchByKey(source, target, key) {
|
|
7
|
+
const unpaired = Map.groupBy(target, key);
|
|
8
|
+
const created = [];
|
|
9
|
+
const matched = [];
|
|
10
|
+
for (const item of source) {
|
|
11
|
+
const counterpart = unpaired.get(key(item))?.shift();
|
|
12
|
+
if (counterpart === undefined)
|
|
13
|
+
created.push(item);
|
|
14
|
+
else
|
|
15
|
+
matched.push([item, counterpart]);
|
|
16
|
+
}
|
|
17
|
+
return { created, dropped: [...unpaired.values()].flat(), matched };
|
|
18
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PrimaryKeySchema } from '../type/migration.js';
|
|
2
|
+
import type { IndexFacet } from './indexDifferences.js';
|
|
3
|
+
import type { ColumnNode, IndexNode, RelationshipNode, TableNode } from './types.js';
|
|
2
4
|
/** A table node with its collections empty, ready to be filled. */
|
|
3
|
-
export declare function createTableNode(name: string, schema?: string,
|
|
5
|
+
export declare function createTableNode(name: string, schema?: string, indexFacets?: ReadonlySet<IndexFacet>): TableNode;
|
|
6
|
+
/** The key the columns' own flags say, in their order, for a source that reports no key of its own. */
|
|
7
|
+
export declare function keyOfColumns(columns: Iterable<Pick<ColumnNode, 'name' | 'isPrimaryKey'>>): PrimaryKeySchema | undefined;
|
|
4
8
|
/** A database schema as a graph: tables, the foreign keys between them, and their indexes. */
|
|
5
9
|
export declare class SchemaAST {
|
|
6
10
|
readonly tables: Map<string, TableNode>;
|
package/dist/schema/schemaAST.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { qualifyName } from '../util/sql.util.js';
|
|
2
2
|
import { createOrder, dropOrder } from './dependencyGraph.js';
|
|
3
3
|
/** A table node with its collections empty, ready to be filled. */
|
|
4
|
-
export function createTableNode(name, schema,
|
|
4
|
+
export function createTableNode(name, schema, indexFacets = new Set()) {
|
|
5
5
|
return {
|
|
6
6
|
name,
|
|
7
7
|
schema,
|
|
8
|
-
|
|
8
|
+
indexFacets,
|
|
9
9
|
columns: new Map(),
|
|
10
|
-
primaryKey: [],
|
|
11
10
|
indexes: [],
|
|
12
11
|
checks: [],
|
|
13
12
|
incomingRelations: [],
|
|
14
13
|
outgoingRelations: [],
|
|
15
14
|
};
|
|
16
15
|
}
|
|
16
|
+
/** The key the columns' own flags say, in their order, for a source that reports no key of its own. */
|
|
17
|
+
export function keyOfColumns(columns) {
|
|
18
|
+
const keyColumns = [...columns].filter((column) => column.isPrimaryKey).map((column) => column.name);
|
|
19
|
+
return keyColumns.length ? { columns: keyColumns } : undefined;
|
|
20
|
+
}
|
|
17
21
|
/** A database schema as a graph: tables, the foreign keys between them, and their indexes. */
|
|
18
22
|
export class SchemaAST {
|
|
19
23
|
tables = new Map();
|
|
@@ -25,6 +25,8 @@ export interface BuildSchemaASTOptions {
|
|
|
25
25
|
compileIndexPredicate?: (where: EntityWhereMeta<object>, entity: Type<object>, indexName: string) => string;
|
|
26
26
|
/** Whether a weighted fulltext index declares one of its own for each heavier column, as MySQL scores through one. */
|
|
27
27
|
textScoreIndexes?: boolean;
|
|
28
|
+
/** Whether a column a vector index covers is `NOT NULL` whatever the entity declares, as MariaDB demands. */
|
|
29
|
+
vectorIndexRequiresNotNull?: boolean;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
|
|
@@ -5,7 +5,7 @@ import { isAutoIncrement, isInlinedExpression, isSoleIdField } from '../util/fie
|
|
|
5
5
|
import { definedEntries } from '../util/object.util.js';
|
|
6
6
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
7
7
|
import { resolveColumnCanonicalType } from './canonicalType.js';
|
|
8
|
-
import { createTableNode, SchemaAST } from './schemaAST.js';
|
|
8
|
+
import { createTableNode, keyOfColumns, SchemaAST } from './schemaAST.js';
|
|
9
9
|
import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
|
|
10
10
|
/**
|
|
11
11
|
* Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
|
|
@@ -26,6 +26,7 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
26
26
|
compileDdl,
|
|
27
27
|
compileIndexPredicate: options.compileIndexPredicate ?? compileDdl,
|
|
28
28
|
textScoreIndexes: options.textScoreIndexes ?? false,
|
|
29
|
+
vectorIndexRequiresNotNull: options.vectorIndexRequiresNotNull ?? false,
|
|
29
30
|
};
|
|
30
31
|
for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
|
|
31
32
|
for (const entity of entities) {
|
|
@@ -38,14 +39,21 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
38
39
|
function refuseDdl() {
|
|
39
40
|
throw new TypeError('building the schema of an entity that declares SQL (a check, a stored computed column, an index expression or predicate) needs a dialect to render it: pass `compileDdl`, as `buildEntityAST` does');
|
|
40
41
|
}
|
|
42
|
+
/** The entries a vector index of `meta` covers: the members it names, and any expression. */
|
|
43
|
+
function vectorIndexedEntries(meta) {
|
|
44
|
+
return new Set((meta.indexes ?? [])
|
|
45
|
+
.filter((index) => index.type === 'vector')
|
|
46
|
+
.flatMap((index) => index.columns.map((entry) => entry.column)));
|
|
47
|
+
}
|
|
41
48
|
/**
|
|
42
49
|
* Add a table from entity metadata.
|
|
43
50
|
*/
|
|
44
51
|
function addTableFromEntity(ctx, meta) {
|
|
45
52
|
const tableName = ctx.resolveTableName(meta);
|
|
46
53
|
const table = createTableNode(tableName, ctx.resolveSchema(meta));
|
|
47
|
-
const { columns
|
|
48
|
-
table.checks
|
|
54
|
+
const { columns } = table;
|
|
55
|
+
table.checks.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
|
|
56
|
+
const notNull = ctx.vectorIndexRequiresNotNull ? vectorIndexedEntries(meta) : new Set();
|
|
49
57
|
// Add columns from fields
|
|
50
58
|
for (const [key, field] of definedEntries(meta.fields)) {
|
|
51
59
|
// An inlined expression has no column; a stored one is a column like any other.
|
|
@@ -60,7 +68,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
60
68
|
type,
|
|
61
69
|
// A primary key is NOT NULL in every engine, whatever the entity's property says: `id?: number`
|
|
62
70
|
// is optional because the database assigns it, not because the column accepts a null.
|
|
63
|
-
nullable: isPrimaryKey ? false : (field.nullable ?? true),
|
|
71
|
+
nullable: isPrimaryKey || notNull.has(key) ? false : (field.nullable ?? true),
|
|
64
72
|
defaultValue: field.defaultValue,
|
|
65
73
|
isPrimaryKey,
|
|
66
74
|
isAutoIncrement: isAutoIncrement(field, isSoleKey),
|
|
@@ -74,10 +82,8 @@ function addTableFromEntity(ctx, meta) {
|
|
|
74
82
|
references: undefined,
|
|
75
83
|
};
|
|
76
84
|
columns.set(columnName, column);
|
|
77
|
-
if (field.isId) {
|
|
78
|
-
primaryKey.push(column);
|
|
79
|
-
}
|
|
80
85
|
}
|
|
86
|
+
table.primaryKey = keyOfColumns(columns.values());
|
|
81
87
|
ctx.ast.addTable(table);
|
|
82
88
|
}
|
|
83
89
|
/** The node an entity maps to, found under the key {@link SchemaAST} stores it by. */
|
|
@@ -157,11 +163,10 @@ function addForeignKeyIndexes(ctx, meta, table) {
|
|
|
157
163
|
});
|
|
158
164
|
}
|
|
159
165
|
}
|
|
160
|
-
/** Whether the key
|
|
166
|
+
/** Whether the key or an index already leads with `columns`, which is all a lookup needs. */
|
|
161
167
|
function isIndexedBy(table, columns) {
|
|
162
168
|
const leads = (indexed) => columns.every((column, at) => indexed[at] === column);
|
|
163
|
-
return (leads(table.primaryKey
|
|
164
|
-
(columns.length === 1 && table.columns.get(columns[0])?.isUnique === true) ||
|
|
169
|
+
return (leads(table.primaryKey?.columns ?? []) ||
|
|
165
170
|
table.indexes.some((index) => leads(index.entries.map((entry) => entry.expression || entry.jsonPath || entry.jsonArray ? undefined : entry.column))));
|
|
166
171
|
}
|
|
167
172
|
/** An `include` column is named like any other, so a naming strategy has to reach it too. */
|