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.
Files changed (56) hide show
  1. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  2. package/dist/dialect/vectorSqlDialect.js +4 -0
  3. package/dist/migrate/builder/expressions.d.ts +2 -0
  4. package/dist/migrate/builder/expressions.js +24 -0
  5. package/dist/migrate/cli.js +1 -1
  6. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  7. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  8. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  9. package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
  10. package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
  11. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  12. package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
  13. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  14. package/dist/migrate/ddl/pgIndexDdl.js +8 -6
  15. package/dist/migrate/ddl/tableDdl.d.ts +3 -2
  16. package/dist/migrate/ddl/tableDdl.js +9 -7
  17. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  18. package/dist/migrate/drift/driftDetector.js +21 -21
  19. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  20. package/dist/migrate/generator/definitionToNode.js +9 -20
  21. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  22. package/dist/migrate/generator/mongoSchemaGenerator.js +11 -19
  23. package/dist/migrate/index.d.ts +2 -1
  24. package/dist/migrate/index.js +1 -0
  25. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
  26. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -18
  27. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  28. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  29. package/dist/migrate/introspection/mongoIntrospector.js +3 -3
  30. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  31. package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
  32. package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
  33. package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
  34. package/dist/migrate/introspection/postgresIntrospector.js +63 -46
  35. package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
  36. package/dist/migrate/migrator.d.ts +5 -0
  37. package/dist/migrate/migrator.js +33 -44
  38. package/dist/migrate/schemaChange.d.ts +18 -0
  39. package/dist/migrate/schemaChange.js +37 -0
  40. package/dist/migrate/schemaGenerator.d.ts +13 -14
  41. package/dist/migrate/schemaGenerator.js +83 -177
  42. package/dist/schema/indexDifferences.d.ts +22 -6
  43. package/dist/schema/indexDifferences.js +23 -8
  44. package/dist/schema/matchByKey.d.ts +10 -0
  45. package/dist/schema/matchByKey.js +18 -0
  46. package/dist/schema/schemaAST.d.ts +6 -2
  47. package/dist/schema/schemaAST.js +7 -3
  48. package/dist/schema/schemaASTBuilder.d.ts +2 -0
  49. package/dist/schema/schemaASTBuilder.js +15 -10
  50. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  51. package/dist/schema/schemaASTDiffer.js +15 -36
  52. package/dist/schema/types.d.ts +14 -15
  53. package/dist/type/migration.d.ts +32 -50
  54. package/dist/util/ddlExpression.util.d.ts +5 -1
  55. package/dist/util/ddlExpression.util.js +6 -2
  56. 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 { formatDefaultValue, SqlExpression } from './builder/expressions.js';
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 statements = [];
178
- // Before the columns, because a key column being added cannot be part of the old key, and after
179
- // it is dropped the table is free to take the new one below.
180
- if (diff.primaryKey?.from.length) {
181
- statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
182
- }
183
- // Before the columns: a constraint holds its columns down, so one the entity dropped cannot go
184
- // while a foreign key still names it. An alter is a drop and an add, and this is its drop half.
185
- statements.push(...this.dropForeignKeyStatements(diff.tableName, [
186
- ...(diff.foreignKeysToDrop ?? []),
187
- ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
188
- ]));
189
- // Before the adds, which may reuse a dropped index's name, and before the columns: some engines
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
- /** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
234
- dropIndexStatements(tableName, indexes = [], schema) {
235
- return indexes.map((index) => this.generateDropIndex(tableName, index.name, schema));
236
- }
237
- generateAlterTableDown(diff) {
238
- const statements = [];
239
- // Constraints first, mirroring the up direction: the up added them last, so the down drops them
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` is left to the key. An
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
- const indexesToAdd = indexes.toAdd.map(indexNodeToSchema);
377
- const indexesToDrop = indexes.toDrop.map(indexNodeToSchema);
378
- const columnDiffs = tableDiff?.columnDiffs ?? [];
379
- const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
380
- const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
381
- // Without its values: an alter restates the whole column, and MySQL answers a restated `CHECK` by
382
- // adding a *second* constraint rather than replacing the first, so the column would accumulate one
383
- // per alter. An enum's values reach the database with the column and are never restated - which is
384
- // also why changing them is a hand-written migration. See architecture/roadmap.md.
385
- const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
386
- ? [
387
- {
388
- from: this.columnNodeToSchema(it.actual),
389
- to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
390
- },
391
- ]
392
- : []);
393
- const primaryKey = tableDiff?.primaryKeyDiff && {
394
- from: tableDiff.primaryKeyDiff.actual,
395
- to: tableDiff.primaryKeyDiff.expected,
396
- fromName: tableDiff.primaryKeyDiff.actualName,
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 foreignKeysToAdd = relationDiffs.flatMap((it) => (it.type === 'create' ? [foreignKeyOf(it.expected)] : []));
404
- const foreignKeysToDrop = relationDiffs.flatMap((it) => (it.type === 'drop' ? [it.name] : []));
405
- const foreignKeysToAlter = relationDiffs.flatMap((it) => it.type === 'alter' ? [{ from: foreignKeyOf(it.actual), to: foreignKeyOf(it.expected) }] : []);
406
- if (!columnsToAdd.length &&
407
- !columnsToAlter.length &&
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
- columnsToAdd: columnsToAdd.length ? columnsToAdd : undefined,
423
- columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
424
- columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
425
- indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
426
- indexesToDrop: indexesToDrop.length ? indexesToDrop : undefined,
427
- foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
428
- foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
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: (expected, actual) => this.isDefaultValueEqual(actual, expected),
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
- * Compare two default values for equality
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
- const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
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
- table.primaryKey.length === 1 &&
487
- table.primaryKey[0].isAutoIncrement;
488
- if (table.primaryKey.length && !declaredByColumn) {
489
- const pkColumns = table.primaryKey.map((c) => c.name);
490
- const pkName = table.primaryKeyName ?? derivedPrimaryKeyName(table.name, pkColumns);
491
- const pkCols = pkColumns.map((c) => this.escapeId(c)).join(', ');
492
- constraints.push(`CONSTRAINT ${this.escapeId(pkName)} PRIMARY KEY (${pkCols})`);
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
- * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
21
- * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
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 indexChanges<I extends ComparableIndex>(table: string, declared: readonly I[], current: readonly IndexNode[], keyOf?: (index: ComparableIndex) => string): {
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: IndexNode, target: IndexNode, facets: ReadonlySet<IndexFacet>): string[];
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
- * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
30
- * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
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 indexChanges(table, declared, current, keyOf = indexSignature) {
33
- const present = new Set(current.map(keyOf));
34
- const wanted = new Set(declared.map(keyOf));
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: declared.filter((index) => !present.has(keyOf(index))),
39
- toDrop: current.filter((index) => !wanted.has(keyOf(index)) && owned(index)),
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 { IndexNode, RelationshipNode, TableNode } from './types.js';
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, comment?: string): TableNode;
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>;
@@ -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, comment) {
4
+ export function createTableNode(name, schema, indexFacets = new Set()) {
5
5
  return {
6
6
  name,
7
7
  schema,
8
- comment,
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, primaryKey } = table;
48
- table.checks?.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
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, a unique column or an index already leads with `columns`, which is all a lookup needs. */
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.map((column) => column.name)) ||
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. */