uql-orm 0.43.0 → 0.45.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/README.md +3 -2
  2. package/dist/browser/querier/httpQuerier.d.ts +6 -0
  3. package/dist/browser/querier/httpQuerier.js +1 -1
  4. package/dist/browser/uql-browser.min.js +2 -2
  5. package/dist/browser/uql-browser.min.js.map +4 -4
  6. package/dist/dialect/abstractDialect.js +5 -6
  7. package/dist/dialect/abstractSqlDialect.d.ts +2 -2
  8. package/dist/dialect/abstractSqlDialect.js +1 -1
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +8 -1
  10. package/dist/dialect/mysqlLikeSqlDialect.js +8 -1
  11. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  12. package/dist/dialect/pgLikeSqlDialect.js +1 -1
  13. package/dist/entity/decorator/bag.d.ts +3 -0
  14. package/dist/entity/index.d.ts +1 -1
  15. package/dist/entity/index.js +1 -1
  16. package/dist/entity/metadata/definition.d.ts +15 -13
  17. package/dist/entity/metadata/definition.js +47 -16
  18. package/dist/http/handler.d.ts +8 -0
  19. package/dist/http/handler.js +5 -5
  20. package/dist/migrate/builder/migrationBuilder.js +1 -2
  21. package/dist/migrate/builder/tableBuilder.d.ts +1 -0
  22. package/dist/migrate/builder/tableBuilder.js +9 -10
  23. package/dist/migrate/builder/types.d.ts +3 -19
  24. package/dist/migrate/cli.d.ts +5 -0
  25. package/dist/migrate/cli.js +33 -16
  26. package/dist/migrate/codegen/entityTypes.d.ts +7 -0
  27. package/dist/migrate/codegen/entityTypes.js +69 -0
  28. package/dist/migrate/codegen/index.d.ts +1 -0
  29. package/dist/migrate/codegen/index.js +1 -0
  30. package/dist/migrate/generator/definitionToNode.js +2 -2
  31. package/dist/migrate/index.d.ts +1 -1
  32. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +2 -5
  33. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -0
  34. package/dist/migrate/introspection/baseSqlIntrospector.js +4 -3
  35. package/dist/migrate/introspection/mysqlIntrospector.js +1 -2
  36. package/dist/migrate/introspection/postgresIntrospector.js +1 -2
  37. package/dist/migrate/introspection/sqliteIntrospector.js +1 -2
  38. package/dist/migrate/migrator.d.ts +34 -20
  39. package/dist/migrate/migrator.js +90 -35
  40. package/dist/migrate/schemaGenerator.d.ts +20 -5
  41. package/dist/migrate/schemaGenerator.js +105 -30
  42. package/dist/schema/schemaASTBuilder.d.ts +23 -2
  43. package/dist/schema/schemaASTBuilder.js +3 -2
  44. package/dist/schema/schemaASTDiffer.d.ts +11 -1
  45. package/dist/schema/schemaASTDiffer.js +23 -7
  46. package/dist/schema/types.d.ts +19 -4
  47. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  48. package/dist/sqlite/sqliteDialect.js +1 -1
  49. package/dist/type/entity.d.ts +14 -1
  50. package/dist/type/migration.d.ts +55 -9
  51. package/dist/type/queryWhere.d.ts +13 -3
  52. package/dist/util/field.util.d.ts +7 -2
  53. package/dist/util/field.util.js +9 -12
  54. package/dist/util/object.util.d.ts +3 -3
  55. package/dist/util/object.util.js +3 -3
  56. package/package.json +3 -3
@@ -282,12 +282,16 @@ export class Migrator {
282
282
  throw new TypeError('Schema generator and introspector must be set');
283
283
  }
284
284
  const ast = await this.introspectClaimedSchemas();
285
+ // Both sides built once: the database's above, the entities' here. Left to `diffSchema`, each
286
+ // entity would rebuild the whole AST, which is quadratic in the number of entities. Absent on a
287
+ // generator that compares no schema of its own - MongoDB, which reads only indexes.
288
+ const desiredAst = this.schemaGenerator.buildAST?.(this.entities);
285
289
  const diffs = [];
286
290
  for (const entity of this.entities) {
287
291
  const meta = getMeta(entity);
288
292
  const tableName = this.schemaGenerator.resolveTableName(meta);
289
293
  const currentTable = ast.getTable(tableName);
290
- const diff = this.schemaGenerator.diffSchema(entity, currentTable);
294
+ const diff = this.schemaGenerator.diffSchema(entity, currentTable, desiredAst);
291
295
  if (diff) {
292
296
  diffs.push(diff);
293
297
  }
@@ -305,7 +309,7 @@ export class Migrator {
305
309
  const claimed = new Set(this.entities.map((entity) => this.pool.dialect.resolveSchema(getMeta(entity))));
306
310
  const merged = new SchemaAST();
307
311
  for (const schema of claimed) {
308
- const introspector = schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
312
+ const introspector = this.introspectorFor(schema);
309
313
  if (!introspector) {
310
314
  continue;
311
315
  }
@@ -330,52 +334,92 @@ export class Migrator {
330
334
  return undefined;
331
335
  }
332
336
  /**
333
- * Sync schema directly (for development only - not for production!)
337
+ * Applies the entity schema to the database: every registered entity, or the one `entity` names.
338
+ *
339
+ * The whole surface is this and {@link planSync}, which answers the same question without running
340
+ * it - `force` and a single entity included, so `--dry-run` means the same thing whatever else was
341
+ * asked for.
334
342
  */
335
343
  async sync(options = {}) {
336
- if (options.force) {
337
- return this.syncForce();
344
+ const statements = await this.planSync(options);
345
+ if (statements.length) {
346
+ await this.executeSyncStatements(statements, options);
347
+ }
348
+ else if (options.logging) {
349
+ this.logger.logSchema('Schema is already in sync.');
338
350
  }
339
- return this.autoSync({ safe: true });
340
351
  }
341
352
  /**
342
- * Drops and recreates all tables (Development only!)
353
+ * Every table dropped and recreated.
354
+ *
355
+ * Both directions span the whole entity set rather than looping an entity at a time. A per-entity
356
+ * AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
357
+ * with no referential integrity; and the old drop loop went in reverse *declaration* order, which
358
+ * says nothing about the relation graph and is rejected as soon as the constraints are really there.
343
359
  */
344
- async syncForce() {
345
- await this.ensureSchemaGenerator();
346
- // Both directions span the whole entity set rather than looping an entity at a time. A per-entity
347
- // AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
348
- // with no referential integrity; and the old drop loop went in reverse *declaration* order, which
349
- // says nothing about the relation graph and is rejected as soon as the constraints are really there.
350
- const statements = [
360
+ forceStatements() {
361
+ return [
351
362
  ...this.generator.generateDropSchema(this.entities, { ifExists: true, cascade: true }),
352
363
  ...this.generator.generateCreateSchema(this.entities),
353
364
  ];
354
- await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(async () => {
355
- for (const sql of statements) {
356
- this.logger.logSchema(`Executing: ${sql}`);
357
- await querier.run(sql);
358
- }
359
- }));
360
- this.logger.logSchema('Schema sync (force) completed');
361
365
  }
362
366
  /**
363
- * Safely synchronizes the schema by only adding missing tables and columns.
367
+ * Sync one entity, for a schema that grows while the process runs: a content type an admin just
368
+ * created is one table to add, where the whole set would read the catalogue to work that out.
369
+ *
370
+ * The new-table case costs one existence check and creates with `IF NOT EXISTS`, so instances racing
371
+ * the same admin save settle instead of colliding. An existing table still pays for introspection,
372
+ * since a column diff needs the columns.
364
373
  */
365
- async autoSync(options = {}) {
366
- const statements = await this.planSync(options);
367
- if (statements.length === 0) {
368
- if (options.logging)
369
- this.logger.logSchema('Schema is already in sync.');
370
- return;
374
+ /** The DDL for one entity: {@link planSync} narrowed to the table it names. */
375
+ async planEntity(entity, options) {
376
+ const meta = getMeta(entity);
377
+ // Before anything reads `this.generator`, whose own failure names neither the entity nor the
378
+ // dialect that has no support.
379
+ const introspector = this.introspectorFor(this.pool.dialect.resolveSchema(meta));
380
+ if (!introspector) {
381
+ throw new TypeError(`No introspector for '${meta.entity.name}' on '${this.dialectName}'`);
371
382
  }
372
- await this.executeSyncStatements(statements, options);
383
+ const tableName = this.generator.resolveTableName(meta);
384
+ return (await introspector.tableExists(tableName))
385
+ ? this.alterFromEntity(entity, (await introspectSchema(introspector)).getTable(tableName), options)
386
+ : // Spanning the whole set, so a foreign key resolves against the tables it points at, and
387
+ // always including this entity: pinned to an explicit `entities` list, a sync of one outside
388
+ // it emitted nothing at all. `only` is what keeps the statements to this table.
389
+ this.generator.generateCreateSchema(this.entitiesWith(entity), { only: [tableName], ifNotExists: true });
390
+ }
391
+ /** An alter diff as statements, narrowed to what the caller allows. */
392
+ alterFromDiff(diff, options) {
393
+ return this.generator.generateAlterTable(this.filterDiff(diff, options));
394
+ }
395
+ /** The same for one entity against the table it already has, and nothing where the two agree. */
396
+ alterFromEntity(entity, table, options) {
397
+ // Spanning the set for the reason `planEntity` spells out: a foreign key needs the table it
398
+ // points at, which a sync of one entity outside the configured list would not otherwise have.
399
+ const diff = this.generator.diffSchema(entity, table, this.generator.buildAST?.(this.entitiesWith(entity)));
400
+ return diff?.type === 'alter' ? this.alterFromDiff(diff, options) : [];
401
+ }
402
+ /** The configured entities, with `entity` among them however the migrator was built. */
403
+ entitiesWith(entity) {
404
+ const entities = this.entities;
405
+ return entities.includes(entity) ? entities : [...entities, entity];
406
+ }
407
+ /** The introspector for a claimed schema, which is the connection's own where none is claimed. */
408
+ introspectorFor(schema) {
409
+ return schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
373
410
  }
374
411
  /**
375
- * The DDL {@link autoSync} would run, without running it. Separate so `--dry-run` shows the real
412
+ * The DDL {@link sync} would run, without running it. Separate so `--dry-run` shows the real
376
413
  * statements rather than a summary of a second, differently-computed diff.
377
414
  */
378
415
  async planSync(options = {}) {
416
+ await this.ensureSchemaGenerator();
417
+ if (options.force) {
418
+ return this.forceStatements();
419
+ }
420
+ if (options.entity) {
421
+ return this.planEntity(options.entity, options);
422
+ }
379
423
  const creating = [];
380
424
  const altering = [];
381
425
  for (const { diff, entity } of await this.pendingDiffs()) {
@@ -384,7 +428,7 @@ export class Migrator {
384
428
  creating.push(diff.tableName);
385
429
  }
386
430
  else if (diff.type === 'alter') {
387
- altering.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
431
+ altering.push(...this.alterFromDiff(diff, options));
388
432
  }
389
433
  }
390
434
  return [...this.createSchema(creating), ...altering];
@@ -442,6 +486,12 @@ export class Migrator {
442
486
  this.logger.logSkippedMigration(`[AutoSync] Skipped changing the primary key of '${diff.tableName}' from (${filteredDiff.primaryKey.from.join(', ')}) to (${filteredDiff.primaryKey.to.join(', ')}) (safe mode active). Use a migration or { safe: false } to apply.`);
443
487
  delete filteredDiff.primaryKey;
444
488
  }
489
+ if (filteredDiff.foreignKeysToAlter?.length) {
490
+ // Altering one is dropping it and adding it back, so letting the add through while the drop
491
+ // is held would emit `ADD CONSTRAINT` for a constraint the table still has.
492
+ this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.foreignKeysToAlter.length} foreign keys in table '${diff.tableName}': ${filteredDiff.foreignKeysToAlter.map((fk) => fk.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
493
+ delete filteredDiff.foreignKeysToAlter;
494
+ }
445
495
  delete filteredDiff.indexesToDrop;
446
496
  delete filteredDiff.foreignKeysToDrop;
447
497
  }
@@ -452,10 +502,15 @@ export class Migrator {
452
502
  return filteredDiff;
453
503
  }
454
504
  async executeSyncStatements(statements, options) {
455
- // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one.
456
- await withQuerierForMigrations(this.pool, (querier) => this.dialectName === 'mongodb'
457
- ? this.executeMongoSyncStatements(statements, options, querier)
458
- : querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
505
+ // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one -
506
+ // and asks for a SQL querier before it opens it, since `transaction` is what a Mongo one lacks and
507
+ // reaching for it first reports that instead of which querier the dialect needs.
508
+ if (this.dialectName === 'mongodb') {
509
+ await withQuerierForMigrations(this.pool, (querier) => this.executeMongoSyncStatements(statements, options, querier));
510
+ }
511
+ else {
512
+ await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
513
+ }
459
514
  if (options.logging)
460
515
  this.logger.logSchema('Schema synchronization completed');
461
516
  }
@@ -2,8 +2,8 @@ import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { SchemaAST } from '../schema/schemaAST.js';
3
3
  import { type DiffOptions } from '../schema/schemaASTDiffer.js';
4
4
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
5
- import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
6
- import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
5
+ import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
6
+ import type { FullColumnDefinition, TableDefinition } from './builder/types.js';
7
7
  import { type IndexDdl } from './ddl/index.js';
8
8
  /**
9
9
  * Unified SQL schema generator.
@@ -26,8 +26,19 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
26
26
  /**
27
27
  * Primary key type for auto-increment integer IDs
28
28
  */
29
- protected get serialType(): string;
29
+ /**
30
+ * How an auto-increment key of `type` is spelled: the type as any other column renders it, plus what
31
+ * the engine appends to make it generated.
32
+ *
33
+ * Derived rather than a fixed string per dialect, because a foreign key column takes its type from
34
+ * the key it points at, resolved through the same canonical type. A key whose spelling ignored that
35
+ * type could never be referenced: `@Id({ columnType: 'int' })` emitted `BIGINT` while the column
36
+ * pointing at it emitted `INT`, and every engine refuses that constraint.
37
+ */
38
+ protected serialType(type: CanonicalType): string;
30
39
  protected canonicalTypeToSql(type: CanonicalType): string;
40
+ /** The entity side as an AST, carrying this generator's default referential action. */
41
+ buildAST(entities: readonly Type<unknown>[]): SchemaAST;
31
42
  /**
32
43
  * Every `CREATE TABLE` for `entities`, then their foreign keys.
33
44
  *
@@ -53,6 +64,10 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
53
64
  private orderedTables;
54
65
  generateDropTable(tableName: string, options?: DropSchemaOptions): string;
55
66
  generateAlterTable(diff: SchemaDiff): string[];
67
+ /** `ADD CONSTRAINT` for each of `foreignKeys`. */
68
+ private addForeignKeyStatements;
69
+ /** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
70
+ private dropForeignKeyStatements;
56
71
  generateAlterTableDown(diff: SchemaDiff): string[];
57
72
  generateCreateIndex(tableName: string, index: IndexSchema, options?: {
58
73
  ifNotExists?: boolean;
@@ -100,7 +115,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
100
115
  * longer disagree about what has changed. Only two things are this side's own: the entity becomes a
101
116
  * table node first, and types are compared as the *engine* would store them - see `normalizeType`.
102
117
  */
103
- diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
118
+ diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
104
119
  /**
105
120
  * What the shared differ needs from a dialect: a type as this engine would actually store it.
106
121
  *
@@ -144,7 +159,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
144
159
  generateAlterColumnSql(tableName: string, columnName: string, column: FullColumnDefinition): string;
145
160
  generateDropColumnSql(tableName: string, columnName: string): string;
146
161
  generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
147
- generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string;
162
+ generateAddForeignKeySql(tableName: string, foreignKey: ForeignKeySchema): string;
148
163
  generateDropForeignKeySql(tableName: string, constraintName: string): string;
149
164
  /**
150
165
  * `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
@@ -3,7 +3,7 @@ import { getMeta, soleIdOf } from '../entity/index.js';
3
3
  import { areTypesEqual, canonicalToSql, engineType, fieldOptionsToCanonical, isVectorCategory, } from '../schema/canonicalType.js';
4
4
  import { indexSignature } from '../schema/indexDifferences.js';
5
5
  import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
6
- import { diffTable } from '../schema/schemaASTDiffer.js';
6
+ import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
7
7
  import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/index.js';
8
8
  import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
9
9
  import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
@@ -49,12 +49,25 @@ export class SqlSchemaGenerator {
49
49
  /**
50
50
  * Primary key type for auto-increment integer IDs
51
51
  */
52
- get serialType() {
53
- return this.dialect.serialType;
52
+ /**
53
+ * How an auto-increment key of `type` is spelled: the type as any other column renders it, plus what
54
+ * the engine appends to make it generated.
55
+ *
56
+ * Derived rather than a fixed string per dialect, because a foreign key column takes its type from
57
+ * the key it points at, resolved through the same canonical type. A key whose spelling ignored that
58
+ * type could never be referenced: `@Id({ columnType: 'int' })` emitted `BIGINT` while the column
59
+ * pointing at it emitted `INT`, and every engine refuses that constraint.
60
+ */
61
+ serialType(type) {
62
+ return `${this.canonicalTypeToSql(type)} ${this.dialect.autoIncrementSuffix}`;
54
63
  }
55
64
  canonicalTypeToSql(type) {
56
65
  return canonicalToSql(type, this.dialect);
57
66
  }
67
+ /** The entity side as an AST, carrying this generator's default referential action. */
68
+ buildAST(entities) {
69
+ return buildEntityAST(this, entities, this.defaultForeignKeyAction);
70
+ }
58
71
  /**
59
72
  * Every `CREATE TABLE` for `entities`, then their foreign keys.
60
73
  *
@@ -76,16 +89,7 @@ export class SqlSchemaGenerator {
76
89
  statements.push(...tables.flatMap((table) => this.generateCreateTableFromNode(inline ? table : { ...table, outgoingRelations: [] }, options)));
77
90
  if (withForeignKeys && !inline) {
78
91
  for (const table of tables) {
79
- for (const rel of table.outgoingRelations) {
80
- statements.push(this.generateAddForeignKeySql(qualifyName(table.name, table.schema), {
81
- name: rel.name,
82
- columns: rel.from.columns.map((c) => c.name),
83
- referencesTable: qualifyName(rel.to.table.name, rel.to.table.schema),
84
- referencesColumns: rel.to.columns.map((c) => c.name),
85
- onDelete: rel.onDelete ?? this.defaultForeignKeyAction,
86
- onUpdate: rel.onUpdate ?? this.defaultForeignKeyAction,
87
- }));
88
- }
92
+ statements.push(...this.addForeignKeyStatements(qualifyName(table.name, table.schema), table.outgoingRelations.map(foreignKeyOf)));
89
93
  }
90
94
  }
91
95
  return statements;
@@ -129,6 +133,12 @@ export class SqlSchemaGenerator {
129
133
  if (diff.primaryKey?.from.length) {
130
134
  statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
131
135
  }
136
+ // Before the columns: a constraint holds its columns down, so one the entity dropped cannot go
137
+ // while a foreign key still names it. An alter is a drop and an add, and this is its drop half.
138
+ statements.push(...this.dropForeignKeyStatements(diff.tableName, [
139
+ ...(diff.foreignKeysToDrop ?? []),
140
+ ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
141
+ ]));
132
142
  // Add new columns
133
143
  if (diff.columnsToAdd?.length) {
134
144
  for (const column of diff.columnsToAdd) {
@@ -166,14 +176,34 @@ export class SqlSchemaGenerator {
166
176
  if (diff.primaryKey?.to.length) {
167
177
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
168
178
  }
179
+ // After the columns, for the same reason the key is: a constraint cannot name one that is not
180
+ // there yet. The add half of an alter rides along, its drop having gone out above.
181
+ statements.push(...this.addForeignKeyStatements(diff.tableName, [
182
+ ...(diff.foreignKeysToAdd ?? []),
183
+ ...(diff.foreignKeysToAlter ?? []).map((it) => it.to),
184
+ ]));
169
185
  return statements;
170
186
  }
187
+ /** `ADD CONSTRAINT` for each of `foreignKeys`. */
188
+ addForeignKeyStatements(tableName, foreignKeys) {
189
+ return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
190
+ }
191
+ /** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
192
+ dropForeignKeyStatements(tableName, constraintNames) {
193
+ return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
194
+ }
171
195
  generateAlterTableDown(diff) {
172
196
  const statements = [];
173
197
  const tableName = this.escapeId(diff.tableName);
174
- // The key first, mirroring the up direction: a column the up added cannot be dropped below while
175
- // the new key still names it. Restored under the name the database gave it, which is what the
176
- // table had before, rather than a derived one that was never on it.
198
+ // Constraints first, mirroring the up direction: the up added them last, so the down drops them
199
+ // first, and a column it is about to drop is then free of anything naming it.
200
+ statements.push(...this.dropForeignKeyStatements(diff.tableName, [
201
+ ...(diff.foreignKeysToAdd ?? []).map((it) => constraintNameOf(diff.tableName, it)),
202
+ ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.to)),
203
+ ]));
204
+ // The key next, for the same reason: a column the up added cannot be dropped below while the new
205
+ // key still names it. Restored under the name the database gave it, which is what the table had
206
+ // before, rather than a derived one that was never on it.
177
207
  if (diff.primaryKey?.to.length) {
178
208
  statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
179
209
  }
@@ -200,8 +230,11 @@ export class SqlSchemaGenerator {
200
230
  if (diff.primaryKey?.from.length) {
201
231
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
202
232
  }
203
- if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
204
- statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes`);
233
+ // The constraint the up replaced, back under the name the database had for it. A foreign key the
234
+ // up *dropped* is not restored: only its name survived the diff, never what it pointed at.
235
+ statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
236
+ if (diff.columnsToDrop?.length || diff.indexesToDrop?.length || diff.foreignKeysToDrop?.length) {
237
+ statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes/foreign keys`);
205
238
  }
206
239
  return statements;
207
240
  }
@@ -283,7 +316,7 @@ export class SqlSchemaGenerator {
283
316
  const canonical = fieldOptionsToCanonical(field);
284
317
  // Special case for serial primary keys
285
318
  if (isAutoIncrement(field, field.isId === true)) {
286
- return this.dialect.serialType;
319
+ return this.serialType(canonical);
287
320
  }
288
321
  return this.canonicalTypeToSql(canonical);
289
322
  }
@@ -337,16 +370,16 @@ export class SqlSchemaGenerator {
337
370
  * longer disagree about what has changed. Only two things are this side's own: the entity becomes a
338
371
  * table node first, and types are compared as the *engine* would store them - see `normalizeType`.
339
372
  */
340
- diffSchema(entity, currentTable) {
373
+ diffSchema(entity, currentTable, desiredAst) {
341
374
  const meta = getMeta(entity);
342
375
  const tableName = this.resolveTableName(meta);
343
376
  const schema = this.resolveSchema(meta);
344
377
  if (!currentTable) {
345
378
  return { tableName, schema, type: 'create' };
346
379
  }
347
- // Keyed by the qualified name this generator resolves, which is the key the AST it just built
348
- // stores the table under.
349
- const desired = buildEntityAST(this, [entity]).getTable(tableName);
380
+ // Keyed by the qualified name this generator resolves, which is the key the AST stores the table
381
+ // under.
382
+ const desired = (desiredAst ?? buildEntityAST(this, [entity], this.defaultForeignKeyAction)).getTable(tableName);
350
383
  if (!desired) {
351
384
  return undefined;
352
385
  }
@@ -367,10 +400,26 @@ export class SqlSchemaGenerator {
367
400
  to: tableDiff.primaryKeyDiff.expected,
368
401
  fromName: tableDiff.primaryKeyDiff.actualName,
369
402
  };
403
+ // This table's own constraints, which is what `outgoingRelations` holds on both sides: the
404
+ // entity's as the AST derived them, the database's as the introspector read them back.
405
+ //
406
+ // None at all where the engine cannot alter one: SQLite resolves foreign keys lazily and keeps
407
+ // them inline at CREATE time, and its only way to change one afterwards is the twelve-step table
408
+ // rebuild, which a sync does not do. Reporting a difference nothing can apply would throw on
409
+ // every sync of an entity that has a relation. `drift:check` still names it.
410
+ const relationDiffs = this.features.foreignKeyAlter
411
+ ? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
412
+ : [];
413
+ const foreignKeysToAdd = relationDiffs.flatMap((it) => (it.type === 'create' ? [foreignKeyOf(it.expected)] : []));
414
+ const foreignKeysToDrop = relationDiffs.flatMap((it) => (it.type === 'drop' ? [it.name] : []));
415
+ const foreignKeysToAlter = relationDiffs.flatMap((it) => it.type === 'alter' ? [{ from: foreignKeyOf(it.actual), to: foreignKeyOf(it.expected) }] : []);
370
416
  if (!columnsToAdd.length &&
371
417
  !columnsToAlter.length &&
372
418
  !columnsToDrop.length &&
373
419
  !indexesToAdd.length &&
420
+ !foreignKeysToAdd.length &&
421
+ !foreignKeysToDrop.length &&
422
+ !foreignKeysToAlter.length &&
374
423
  !primaryKey) {
375
424
  return undefined;
376
425
  }
@@ -383,6 +432,9 @@ export class SqlSchemaGenerator {
383
432
  columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
384
433
  columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
385
434
  indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
435
+ foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
436
+ foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
437
+ foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
386
438
  };
387
439
  }
388
440
  /**
@@ -413,7 +465,7 @@ export class SqlSchemaGenerator {
413
465
  name: col.name,
414
466
  // The same rule `generateColumnFromNode` renders by, so a column added to an existing table
415
467
  // gets the type it would have had if the table were created from scratch.
416
- type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
468
+ type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
417
469
  nullable: col.nullable,
418
470
  defaultValue: col.defaultValue,
419
471
  isPrimaryKey: col.isPrimaryKey,
@@ -518,7 +570,7 @@ export class SqlSchemaGenerator {
518
570
  generateColumnFromNode(col) {
519
571
  return this.renderColumn({
520
572
  ...col,
521
- type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
573
+ type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
522
574
  });
523
575
  }
524
576
  /**
@@ -555,15 +607,13 @@ export class SqlSchemaGenerator {
555
607
  }
556
608
  generateAddForeignKeySql(tableName, foreignKey) {
557
609
  const fkCols = foreignKey.columns.map((c) => this.escapeId(c)).join(', ');
558
- const refCols = foreignKey.referencesColumns.map((c) => this.escapeId(c)).join(', ');
559
- const constraintName = foreignKey.name
560
- ? this.escapeId(foreignKey.name)
561
- : this.escapeId(derivedForeignKeyName(tableName, foreignKey.columns));
610
+ const refCols = foreignKey.references.columns.map((c) => this.escapeId(c)).join(', ');
611
+ const constraintName = this.escapeId(constraintNameOf(tableName, foreignKey));
562
612
  if (!this.features.foreignKeyAlter) {
563
613
  throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
564
614
  }
565
615
  return (`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
566
- `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.referencesTable)} (${refCols}) ` +
616
+ `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.references.table)} (${refCols}) ` +
567
617
  `ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction};`);
568
618
  }
569
619
  generateDropForeignKeySql(tableName, constraintName) {
@@ -608,6 +658,31 @@ export class SqlSchemaGenerator {
608
658
  'for it. Recreate the table in a written migration.');
609
659
  }
610
660
  }
661
+ /**
662
+ * What a constraint is called: its own name, or one derived from its columns where nothing named it.
663
+ * Shared by the add and the drop so a `DROP CONSTRAINT` names exactly what an `ADD CONSTRAINT` made.
664
+ */
665
+ function constraintNameOf(tableName, foreignKey) {
666
+ return foreignKey.name ?? derivedForeignKeyName(tableName, foreignKey.columns);
667
+ }
668
+ /**
669
+ * A relationship node as the migration's own `ForeignKeySchema`. The node's `name` is kept rather
670
+ * than derived: on the database's side it is the only name a `DROP` can use, and on the entity's it
671
+ * is already the derived one. An unset action stays unset - the default belongs to the one place
672
+ * that spends it, `addForeignKeyStatements`.
673
+ */
674
+ function foreignKeyOf(relation) {
675
+ return {
676
+ name: relation.name,
677
+ columns: relation.from.columns.map((column) => column.name),
678
+ references: {
679
+ table: qualifyName(relation.to.table.name, relation.to.table.schema),
680
+ columns: relation.to.columns.map((column) => column.name),
681
+ },
682
+ onDelete: relation.onDelete,
683
+ onUpdate: relation.onUpdate,
684
+ };
685
+ }
611
686
  /**
612
687
  * The entities as an AST, named the way `generator` names things.
613
688
  *
@@ -5,10 +5,11 @@
5
5
  * - Entity metadata (decorator-based entities)
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
- import type { EntityMeta, FieldOptions, Type } from '../type/index.js';
8
+ import type { EntityGetter } from '../type/entity.js';
9
+ import type { EntityMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
9
10
  import type { NamingStrategy } from '../type/namingStrategy.js';
10
11
  import { SchemaAST } from './schemaAST.js';
11
- import { type ForeignKeyAction } from './types.js';
12
+ import { type CanonicalType, type ForeignKeyAction } from './types.js';
12
13
  /**
13
14
  * Options for building SchemaAST from entities.
14
15
  */
@@ -31,3 +32,23 @@ export interface BuildSchemaASTOptions {
31
32
  * resolves against a table another entity declares, and an index against the columns of its own.
32
33
  */
33
34
  export declare function buildSchemaAST(entities: readonly Type<unknown>[], options?: BuildSchemaASTOptions): SchemaAST;
35
+ /**
36
+ * Resolve the canonical type for a field, inheriting from the referenced
37
+ * entity's primary key when the field is a foreign-key reference
38
+ * (`@Field({ references: () => SomeEntity })`) with no explicit type of its
39
+ * own.
40
+ *
41
+ * Without this, a field like `creatorId?: UUID` (a bare TypeScript alias for
42
+ * `string`, erased at runtime) falls back to the generic string inference in
43
+ * {@link fieldOptionsToCanonical} and gets typed as TEXT/VARCHAR - producing a
44
+ * foreign key column whose type doesn't match the UUID primary key it
45
+ * references, which Postgres (and most databases) reject outright.
46
+ *
47
+ * `field.typeFromReference` (set by `defineField`, see entity/metadata/definition.ts)
48
+ * is what distinguishes "no type was given" from "the decorator explicitly set
49
+ * a type" - including explicit constructor overrides like `type: BigInt`, which
50
+ * a value-based check (e.g. `typeof field.type === 'string'`) would miss since
51
+ * reflection also produces constructor values like `String`/`Number`.
52
+ * `columnType` remains the unambiguous, always-respected explicit override.
53
+ */
54
+ export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
9
9
  import { isSoleIdField } from '../util/field.util.js';
10
+ import { isAutoIncrement } from '../util/field.util.js';
10
11
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
11
12
  import { fieldOptionsToCanonical } from './canonicalType.js';
12
13
  import { createTableNode, SchemaAST } from './schemaAST.js';
@@ -53,7 +54,7 @@ export function buildSchemaAST(entities, options = {}) {
53
54
  * reflection also produces constructor values like `String`/`Number`.
54
55
  * `columnType` remains the unambiguous, always-respected explicit override.
55
56
  */
56
- function resolveColumnCanonicalType(field, seen = new Set()) {
57
+ export function resolveColumnCanonicalType(field, seen = new Set()) {
57
58
  const hasExplicitType = !!field.columnType || !field.typeFromReference;
58
59
  if (!hasExplicitType && field.references && !seen.has(field.references)) {
59
60
  seen.add(field.references);
@@ -96,7 +97,7 @@ function addTableFromEntity(ctx, meta) {
96
97
  nullable: isPrimaryKey ? false : (field.nullable ?? true),
97
98
  defaultValue: field.defaultValue,
98
99
  isPrimaryKey,
99
- isAutoIncrement: field.autoIncrement ?? (isSoleKey && type.category === 'integer'),
100
+ isAutoIncrement: isAutoIncrement(field, isSoleKey),
100
101
  isUnique: field.unique ?? false,
101
102
  comment: field.comment,
102
103
  enum: field.enum,
@@ -10,7 +10,7 @@
10
10
  import { type IndexFacet } from './indexDifferences.js';
11
11
  import type { SchemaAST } from './schemaAST.js';
12
12
  import type { CanonicalType } from './types.js';
13
- import type { SchemaDiffResult, TableDiff, TableNode } from './types.js';
13
+ import type { RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
14
14
  /**
15
15
  * Options for schema diffing.
16
16
  */
@@ -60,3 +60,13 @@ export declare function diffSchemas(source: SchemaAST, target: SchemaAST, option
60
60
  * comparison serves both, so drift and migrations can no longer disagree about what has changed.
61
61
  */
62
62
  export declare function diffTable(source: TableNode, target: TableNode, options?: DiffOptions): TableDiff | undefined;
63
+ /**
64
+ * Compare two lists of relationships.
65
+ *
66
+ * Lists rather than whole schemas, because a migration diffs one table: its two sides are that
67
+ * table's `outgoingRelations`, where `diffSchemas` passes the schema's every relationship.
68
+ *
69
+ * Matched by columns, never by name - the engine named every constraint that already exists, so
70
+ * pairing on names would report every hand-named one as a drop and an add.
71
+ */
72
+ export declare function diffRelationshipNodes(source: readonly RelationshipNode[], target: readonly RelationshipNode[], opts?: DiffOptions): RelationshipDiff[];