uql-orm 0.44.0 → 0.45.1

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 (32) hide show
  1. package/README.md +3 -2
  2. package/dist/dialect/abstractSqlDialect.d.ts +2 -2
  3. package/dist/dialect/abstractSqlDialect.js +1 -1
  4. package/dist/dialect/mysqlLikeSqlDialect.d.ts +8 -1
  5. package/dist/dialect/mysqlLikeSqlDialect.js +8 -1
  6. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  7. package/dist/dialect/pgLikeSqlDialect.js +1 -1
  8. package/dist/migrate/builder/migrationBuilder.js +1 -2
  9. package/dist/migrate/builder/tableBuilder.d.ts +1 -0
  10. package/dist/migrate/builder/tableBuilder.js +9 -10
  11. package/dist/migrate/builder/types.d.ts +3 -19
  12. package/dist/migrate/generator/definitionToNode.js +2 -2
  13. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +2 -5
  14. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -0
  15. package/dist/migrate/introspection/baseSqlIntrospector.js +4 -3
  16. package/dist/migrate/introspection/mysqlIntrospector.js +1 -2
  17. package/dist/migrate/introspection/postgresIntrospector.js +1 -2
  18. package/dist/migrate/introspection/sqliteIntrospector.js +1 -2
  19. package/dist/migrate/migrator.js +14 -2
  20. package/dist/migrate/schemaGenerator.d.ts +20 -5
  21. package/dist/migrate/schemaGenerator.js +117 -33
  22. package/dist/schema/schemaASTBuilder.js +2 -1
  23. package/dist/schema/schemaASTDiffer.d.ts +11 -1
  24. package/dist/schema/schemaASTDiffer.js +23 -7
  25. package/dist/schema/types.d.ts +19 -4
  26. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  27. package/dist/sqlite/sqliteDialect.js +1 -1
  28. package/dist/type/entity.d.ts +1 -1
  29. package/dist/type/migration.d.ts +50 -10
  30. package/dist/util/field.util.d.ts +7 -2
  31. package/dist/util/field.util.js +9 -12
  32. package/package.json +2 -2
package/README.md CHANGED
@@ -7,9 +7,10 @@
7
7
  </picture>
8
8
  </a>
9
9
 
10
- <h3>The JSON-native ORM</h3>
10
+ <h3>The JSON-native TypeScript ORM</h3>
11
11
 
12
- <p>Queries are plain JSON, typed to the leaf. Unified across SQL databases and MongoDB.</p>
12
+ <p>UQL stands for Unified Query Language. With pure (type-safe) JSON queries, complex jobs can be done simply across SQL vendors + MongoDB. It got some inspiration from Mongo's best syntax.
13
+ </p>
13
14
 
14
15
  <p>
15
16
  <a href="https://uql-orm.dev"><b>Website</b></a> ·
@@ -16,9 +16,9 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
16
16
  * declares over its columns as one named constraint. SQLite is the exception - see
17
17
  * {@link serialDeclaresPrimaryKey}.
18
18
  */
19
- abstract readonly serialType: string;
19
+ abstract readonly autoIncrementSuffix: string;
20
20
  /**
21
- * Whether {@link serialType} states `PRIMARY KEY` itself, so the table must not state it again.
21
+ * Whether {@link autoIncrementSuffix} states `PRIMARY KEY` itself, so the table must not state it again.
22
22
  *
23
23
  * True on SQLite alone, where `AUTOINCREMENT` is legal only in the exact phrase
24
24
  * `INTEGER PRIMARY KEY AUTOINCREMENT` - the key cannot be lifted out of the column there.
@@ -11,7 +11,7 @@ import { resolveVectorCast } from './vectorCast.js';
11
11
  import { VectorSqlDialect } from './vectorSqlDialect.js';
12
12
  export class AbstractSqlDialect extends VectorSqlDialect {
13
13
  /**
14
- * Whether {@link serialType} states `PRIMARY KEY` itself, so the table must not state it again.
14
+ * Whether {@link autoIncrementSuffix} states `PRIMARY KEY` itself, so the table must not state it again.
15
15
  *
16
16
  * True on SQLite alone, where `AUTOINCREMENT` is legal only in the exact phrase
17
17
  * `INTEGER PRIMARY KEY AUTOINCREMENT` - the key cannot be lifted out of the column there.
@@ -24,7 +24,14 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
24
24
  estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
25
25
  /** `OFFSET` is only legal after a `LIMIT` here, so a bare `$skip` needs one. */
26
26
  pager(ctx: QueryContext, opts: QueryPager): void;
27
- readonly serialType = "BIGINT UNSIGNED AUTO_INCREMENT";
27
+ /**
28
+ * Signed, though MySQL's own convention is `UNSIGNED`: a foreign key column takes its type from the
29
+ * key it points at, resolved through the *canonical* type, which has no way to know this string said
30
+ * `UNSIGNED`. The two then disagree and the engine refuses the constraint - the same trap knex hit
31
+ * (knex#6129) and MikroORM still carries (mikro-orm#5485). Signed is also the portable half: no
32
+ * other engine here has unsigned integers, so an `@Id` means one range everywhere.
33
+ */
34
+ readonly autoIncrementSuffix = "AUTO_INCREMENT";
28
35
  readonly escapeIdChar = "`";
29
36
  readonly tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
30
37
  readonly beginTransactionCommand = "START TRANSACTION";
@@ -63,7 +63,14 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
63
63
  }
64
64
  super.pager(ctx, opts);
65
65
  }
66
- serialType = 'BIGINT UNSIGNED AUTO_INCREMENT';
66
+ /**
67
+ * Signed, though MySQL's own convention is `UNSIGNED`: a foreign key column takes its type from the
68
+ * key it points at, resolved through the *canonical* type, which has no way to know this string said
69
+ * `UNSIGNED`. The two then disagree and the engine refuses the constraint - the same trap knex hit
70
+ * (knex#6129) and MikroORM still carries (mikro-orm#5485). Signed is also the portable half: no
71
+ * other engine here has unsigned integers, so an `@Id` means one range everywhere.
72
+ */
73
+ autoIncrementSuffix = 'AUTO_INCREMENT';
67
74
  escapeIdChar = '`';
68
75
  tableOptions = 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
69
76
  beginTransactionCommand = 'START TRANSACTION';
@@ -15,7 +15,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
15
15
  /** Default {@link DialectFeatures} for Postgres-wire dialects. */
16
16
  protected readonly featureDefaults: DialectFeatures;
17
17
  readonly escapeIdChar = "\"";
18
- readonly serialType: string;
18
+ readonly autoIncrementSuffix: string;
19
19
  readonly tableOptions = "";
20
20
  readonly beginTransactionCommand = "BEGIN";
21
21
  readonly commitTransactionCommand = "COMMIT";
@@ -40,7 +40,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
40
40
  // under heavy concurrent insert load (writes concentrate on one range); this default still beats
41
41
  // `SERIAL` (CockroachDB's `unique_rowid()`, a ~64-bit value that overflows JS's safe-integer
42
42
  // range). High-throughput CockroachDB users should override this per-entity with a UUID PK.
43
- serialType = 'BIGINT GENERATED BY DEFAULT AS IDENTITY';
43
+ autoIncrementSuffix = 'GENERATED BY DEFAULT AS IDENTITY';
44
44
  tableOptions = '';
45
45
  beginTransactionCommand = 'BEGIN';
46
46
  commitTransactionCommand = 'COMMIT';
@@ -49,8 +49,7 @@ function addForeignKeyOperation(tableName, columns, target, options = {}) {
49
49
  foreignKey: {
50
50
  name: options.name,
51
51
  columns,
52
- referencesTable: target.table,
53
- referencesColumns: target.columns,
52
+ references: { table: target.table, columns: target.columns },
54
53
  onDelete: options.onDelete ?? 'NO ACTION',
55
54
  onUpdate: options.onUpdate ?? 'NO ACTION',
56
55
  },
@@ -16,6 +16,7 @@ export declare class TableBuilder implements ITableBuilder {
16
16
  private _foreignKeyBuilders;
17
17
  private _comment?;
18
18
  constructor(name: string);
19
+ /** Big, matching an entity's `@Id`: a key is spelled from this type, so it has to state the real one. */
19
20
  id(name?: string, options?: BaseColumnOptions): IColumnBuilder;
20
21
  integer(name: string, options?: BaseColumnOptions): IColumnBuilder;
21
22
  smallint(name: string, options?: BaseColumnOptions): IColumnBuilder;
@@ -12,8 +12,8 @@ import { expr } from './expressions.js';
12
12
  */
13
13
  class TableForeignKeyBuilder {
14
14
  _columns;
15
- _referencesTable;
16
- _referencesColumns = [];
15
+ _referencedTable;
16
+ _referencedColumns = [];
17
17
  _onDelete = 'NO ACTION';
18
18
  _onUpdate = 'NO ACTION';
19
19
  _name;
@@ -21,8 +21,8 @@ class TableForeignKeyBuilder {
21
21
  this._columns = columns;
22
22
  }
23
23
  references(table, columns) {
24
- this._referencesTable = table;
25
- this._referencesColumns = columns;
24
+ this._referencedTable = table;
25
+ this._referencedColumns = columns;
26
26
  return this;
27
27
  }
28
28
  onDelete(action) {
@@ -41,13 +41,12 @@ class TableForeignKeyBuilder {
41
41
  * Build the foreign key definition.
42
42
  */
43
43
  build() {
44
- if (!this._referencesTable)
44
+ if (!this._referencedTable)
45
45
  return undefined;
46
46
  return {
47
47
  name: this._name,
48
48
  columns: this._columns,
49
- referencesTable: this._referencesTable,
50
- referencesColumns: this._referencesColumns,
49
+ references: { table: this._referencedTable, columns: this._referencedColumns },
51
50
  onDelete: this._onDelete,
52
51
  onUpdate: this._onUpdate,
53
52
  };
@@ -66,8 +65,9 @@ export class TableBuilder {
66
65
  constructor(name) {
67
66
  this._name = name;
68
67
  }
68
+ /** Big, matching an entity's `@Id`: a key is spelled from this type, so it has to state the real one. */
69
69
  id(name = 'id', options = {}) {
70
- return this.add(name, { category: 'integer' }, { ...options, primaryKey: true, autoIncrement: true });
70
+ return this.add(name, { category: 'integer', size: 'big' }, { ...options, primaryKey: true, autoIncrement: true });
71
71
  }
72
72
  integer(name, options) {
73
73
  return this.add(name, { category: 'integer' }, options);
@@ -217,8 +217,7 @@ export class TableBuilder {
217
217
  foreignKeys.push({
218
218
  name: col.foreignKey.name,
219
219
  columns: [col.name],
220
- referencesTable: col.foreignKey.table,
221
- referencesColumns: col.foreignKey.columns,
220
+ references: { table: col.foreignKey.table, columns: col.foreignKey.columns },
222
221
  onDelete: col.foreignKey.onDelete,
223
222
  onUpdate: col.foreignKey.onUpdate,
224
223
  });
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { CanonicalType, ForeignKeyAction } from '../../schema/types.js';
8
8
  import type { IndexColumnInput, IndexOptions, IndexSchema } from '../../type/index.js';
9
+ import type { ForeignKeySchema } from '../../type/migration.js';
9
10
  /**
10
11
  * Foreign key reference options.
11
12
  */
@@ -124,27 +125,10 @@ export interface TableDefinition {
124
125
  /** Index definitions */
125
126
  indexes: IndexSchema[];
126
127
  /** Foreign key definitions at table level */
127
- foreignKeys: TableForeignKeyDefinition[];
128
+ foreignKeys: ForeignKeySchema[];
128
129
  /** Table comment */
129
130
  comment?: string;
130
131
  }
131
- /**
132
- * Table-level foreign key (for composite FKs).
133
- */
134
- export interface TableForeignKeyDefinition {
135
- /** Constraint name */
136
- name?: string;
137
- /** Local columns */
138
- columns: string[];
139
- /** Referenced table */
140
- referencesTable: string;
141
- /** Referenced columns */
142
- referencesColumns: string[];
143
- /** Action on delete */
144
- onDelete: ForeignKeyAction;
145
- /** Action on update */
146
- onUpdate: ForeignKeyAction;
147
- }
148
132
  /**
149
133
  * Type of migration operation.
150
134
  */
@@ -238,7 +222,7 @@ export interface DropIndexOperation extends MigrationOperation {
238
222
  export interface AddForeignKeyOperation extends MigrationOperation {
239
223
  type: 'addForeignKey';
240
224
  tableName: string;
241
- foreignKey: TableForeignKeyDefinition;
225
+ foreignKey: ForeignKeySchema;
242
226
  }
243
227
  /**
244
228
  * Drop foreign key operation.
@@ -40,8 +40,8 @@ export function tableDefinitionToNode(def) {
40
40
  columns: fkDef.columns.map((name) => columns.get(name)).filter((c) => c !== undefined),
41
41
  },
42
42
  to: {
43
- table: { name: fkDef.referencesTable },
44
- columns: fkDef.referencesColumns.map((name) => ({ name })),
43
+ table: { name: fkDef.references.table },
44
+ columns: fkDef.references.columns.map((name) => ({ name })),
45
45
  },
46
46
  onDelete: fkDef.onDelete,
47
47
  onUpdate: fkDef.onUpdate,
@@ -1,9 +1,6 @@
1
+ import type { ForeignKeyAction } from '../../schema/types.js';
1
2
  import type { ColumnSchema, ForeignKeySchema, IndexSchema, QuerierPool, RawRow, SchemaIntrospector, SqlQuerier, TableSchema } from '../../type/index.js';
2
3
  import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
3
- /**
4
- * Referential action type for foreign key constraints.
5
- */
6
- export type ReferentialAction = 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
7
4
  /**
8
5
  * Reads the rows of one statement while introspecting a table.
9
6
  *
@@ -72,7 +69,7 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
72
69
  /**
73
70
  * Normalize referential action string to standard type.
74
71
  */
75
- protected normalizeReferentialAction(action: string): ReferentialAction | undefined;
72
+ protected normalizeReferentialAction(action: string): ForeignKeyAction | undefined;
76
73
  /**
77
74
  * Convert bigint/null values to number safely.
78
75
  */
@@ -131,6 +131,8 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
131
131
  return 'RESTRICT';
132
132
  case 'NO ACTION':
133
133
  return 'NO ACTION';
134
+ case 'SET DEFAULT':
135
+ return 'SET DEFAULT';
134
136
  default:
135
137
  return undefined;
136
138
  }
@@ -1,6 +1,7 @@
1
1
  import { canonicalColumnType } from '../../schema/canonicalType.js';
2
2
  import { createTableNode, SchemaAST } from '../../schema/schemaAST.js';
3
3
  import { escapeSqlId } from '../../util/index.js';
4
+ import { derivedForeignKeyName } from '../../util/sql.util.js';
4
5
  /**
5
6
  * Base class for SQL introspectors with shared AST building logic.
6
7
  */
@@ -83,14 +84,14 @@ export class BaseSqlIntrospector {
83
84
  if (!fromTable)
84
85
  continue;
85
86
  for (const fk of schema.foreignKeys) {
86
- const toTable = tableNodes.get(fk.referencedTable);
87
+ const toTable = tableNodes.get(fk.references.table);
87
88
  if (!toTable)
88
89
  continue;
89
90
  const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
90
- const toColumns = fk.referencedColumns.flatMap((name) => toTable.columns.get(name) ?? []);
91
+ const toColumns = fk.references.columns.flatMap((name) => toTable.columns.get(name) ?? []);
91
92
  if (fromColumns.length > 0 && toColumns.length > 0) {
92
93
  const rel = {
93
- name: fk.name,
94
+ name: fk.name ?? derivedForeignKeyName(schema.name, fk.columns),
94
95
  type: fromColumns[0].isUnique ? 'OneToOne' : 'ManyToOne',
95
96
  from: { table: fromTable, columns: fromColumns },
96
97
  to: { table: toTable, columns: toColumns },
@@ -123,8 +123,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
123
123
  return results.map((row) => ({
124
124
  name: row.constraint_name,
125
125
  columns: (row.columns || '').split(','),
126
- referencedTable: row.referenced_table,
127
- referencedColumns: (row.referenced_columns || '').split(','),
126
+ references: { table: row.referenced_table, columns: (row.referenced_columns || '').split(',') },
128
127
  onDelete: this.normalizeReferentialAction(row.delete_rule),
129
128
  onUpdate: this.normalizeReferentialAction(row.update_rule),
130
129
  }));
@@ -204,8 +204,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
204
204
  return results.map((row) => ({
205
205
  name: row.constraint_name,
206
206
  columns: row.columns,
207
- referencedTable: row.referenced_table,
208
- referencedColumns: row.referenced_columns,
207
+ references: { table: row.referenced_table, columns: row.referenced_columns },
209
208
  onDelete: this.normalizeReferentialAction(row.delete_rule),
210
209
  onUpdate: this.normalizeReferentialAction(row.update_rule),
211
210
  }));
@@ -113,8 +113,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
113
113
  // derives it. Seeded from the columns, not the PRAGMA's row id, which nothing else knows.
114
114
  name: derivedForeignKeyName(tableName, columns),
115
115
  columns,
116
- referencedTable: first.table,
117
- referencedColumns: rows.map((r) => r.to),
116
+ references: { table: first.table, columns: rows.map((r) => r.to) },
118
117
  onDelete: this.normalizeReferentialAction(first.on_delete),
119
118
  onUpdate: this.normalizeReferentialAction(first.on_update),
120
119
  };
@@ -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
  }
@@ -390,7 +394,9 @@ export class Migrator {
390
394
  }
391
395
  /** The same for one entity against the table it already has, and nothing where the two agree. */
392
396
  alterFromEntity(entity, table, options) {
393
- const diff = this.generator.diffSchema(entity, table);
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)));
394
400
  return diff?.type === 'alter' ? this.alterFromDiff(diff, options) : [];
395
401
  }
396
402
  /** The configured entities, with `entity` among them however the migrator was built. */
@@ -480,6 +486,12 @@ export class Migrator {
480
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.`);
481
487
  delete filteredDiff.primaryKey;
482
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
+ }
483
495
  delete filteredDiff.indexesToDrop;
484
496
  delete filteredDiff.foreignKeysToDrop;
485
497
  }
@@ -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,11 +133,16 @@ 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) {
135
- const colDef = this.generateColumnDefinitionFromSchema(column);
136
- statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${colDef};`);
145
+ statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.generateColumnDefinitionFromSchema(column)};`);
137
146
  }
138
147
  }
139
148
  // Alter existing columns
@@ -166,14 +175,34 @@ export class SqlSchemaGenerator {
166
175
  if (diff.primaryKey?.to.length) {
167
176
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
168
177
  }
178
+ // After the columns, for the same reason the key is: a constraint cannot name one that is not
179
+ // there yet. The add half of an alter rides along, its drop having gone out above.
180
+ statements.push(...this.addForeignKeyStatements(diff.tableName, [
181
+ ...(diff.foreignKeysToAdd ?? []),
182
+ ...(diff.foreignKeysToAlter ?? []).map((it) => it.to),
183
+ ]));
169
184
  return statements;
170
185
  }
186
+ /** `ADD CONSTRAINT` for each of `foreignKeys`. */
187
+ addForeignKeyStatements(tableName, foreignKeys) {
188
+ return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
189
+ }
190
+ /** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
191
+ dropForeignKeyStatements(tableName, constraintNames) {
192
+ return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
193
+ }
171
194
  generateAlterTableDown(diff) {
172
195
  const statements = [];
173
196
  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.
197
+ // Constraints first, mirroring the up direction: the up added them last, so the down drops them
198
+ // first, and a column it is about to drop is then free of anything naming it.
199
+ statements.push(...this.dropForeignKeyStatements(diff.tableName, [
200
+ ...(diff.foreignKeysToAdd ?? []).map((it) => constraintNameOf(diff.tableName, it)),
201
+ ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.to)),
202
+ ]));
203
+ // The key next, for the same reason: a column the up added cannot be dropped below while the new
204
+ // key still names it. Restored under the name the database gave it, which is what the table had
205
+ // before, rather than a derived one that was never on it.
177
206
  if (diff.primaryKey?.to.length) {
178
207
  statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
179
208
  }
@@ -200,8 +229,11 @@ export class SqlSchemaGenerator {
200
229
  if (diff.primaryKey?.from.length) {
201
230
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
202
231
  }
203
- if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
204
- statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes`);
232
+ // The constraint the up replaced, back under the name the database had for it. A foreign key the
233
+ // up *dropped* is not restored: only its name survived the diff, never what it pointed at.
234
+ statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
235
+ if (diff.columnsToDrop?.length || diff.indexesToDrop?.length || diff.foreignKeysToDrop?.length) {
236
+ statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes/foreign keys`);
205
237
  }
206
238
  return statements;
207
239
  }
@@ -283,7 +315,7 @@ export class SqlSchemaGenerator {
283
315
  const canonical = fieldOptionsToCanonical(field);
284
316
  // Special case for serial primary keys
285
317
  if (isAutoIncrement(field, field.isId === true)) {
286
- return this.dialect.serialType;
318
+ return this.serialType(canonical);
287
319
  }
288
320
  return this.canonicalTypeToSql(canonical);
289
321
  }
@@ -337,16 +369,16 @@ export class SqlSchemaGenerator {
337
369
  * longer disagree about what has changed. Only two things are this side's own: the entity becomes a
338
370
  * table node first, and types are compared as the *engine* would store them - see `normalizeType`.
339
371
  */
340
- diffSchema(entity, currentTable) {
372
+ diffSchema(entity, currentTable, desiredAst) {
341
373
  const meta = getMeta(entity);
342
374
  const tableName = this.resolveTableName(meta);
343
375
  const schema = this.resolveSchema(meta);
344
376
  if (!currentTable) {
345
377
  return { tableName, schema, type: 'create' };
346
378
  }
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);
379
+ // Keyed by the qualified name this generator resolves, which is the key the AST stores the table
380
+ // under.
381
+ const desired = (desiredAst ?? buildEntityAST(this, [entity], this.defaultForeignKeyAction)).getTable(tableName);
350
382
  if (!desired) {
351
383
  return undefined;
352
384
  }
@@ -359,18 +391,43 @@ export class SqlSchemaGenerator {
359
391
  const columnDiffs = tableDiff?.columnDiffs ?? [];
360
392
  const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
361
393
  const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
394
+ // Without its values: an alter restates the whole column, and MySQL answers a restated `CHECK` by
395
+ // adding a *second* constraint rather than replacing the first, so the column would accumulate one
396
+ // per alter. An enum's values reach the database with the column and are never restated - which is
397
+ // also why changing them is a hand-written migration. See architecture/roadmap.md.
362
398
  const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
363
- ? [{ from: this.columnNodeToSchema(it.actual), to: this.columnNodeToSchema(it.expected) }]
399
+ ? [
400
+ {
401
+ from: this.columnNodeToSchema(it.actual),
402
+ to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
403
+ },
404
+ ]
364
405
  : []);
365
406
  const primaryKey = tableDiff?.primaryKeyDiff && {
366
407
  from: tableDiff.primaryKeyDiff.actual,
367
408
  to: tableDiff.primaryKeyDiff.expected,
368
409
  fromName: tableDiff.primaryKeyDiff.actualName,
369
410
  };
411
+ // This table's own constraints, which is what `outgoingRelations` holds on both sides: the
412
+ // entity's as the AST derived them, the database's as the introspector read them back.
413
+ //
414
+ // None at all where the engine cannot alter one: SQLite resolves foreign keys lazily and keeps
415
+ // them inline at CREATE time, and its only way to change one afterwards is the twelve-step table
416
+ // rebuild, which a sync does not do. Reporting a difference nothing can apply would throw on
417
+ // every sync of an entity that has a relation. `drift:check` still names it.
418
+ const relationDiffs = this.features.foreignKeyAlter
419
+ ? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
420
+ : [];
421
+ const foreignKeysToAdd = relationDiffs.flatMap((it) => (it.type === 'create' ? [foreignKeyOf(it.expected)] : []));
422
+ const foreignKeysToDrop = relationDiffs.flatMap((it) => (it.type === 'drop' ? [it.name] : []));
423
+ const foreignKeysToAlter = relationDiffs.flatMap((it) => it.type === 'alter' ? [{ from: foreignKeyOf(it.actual), to: foreignKeyOf(it.expected) }] : []);
370
424
  if (!columnsToAdd.length &&
371
425
  !columnsToAlter.length &&
372
426
  !columnsToDrop.length &&
373
427
  !indexesToAdd.length &&
428
+ !foreignKeysToAdd.length &&
429
+ !foreignKeysToDrop.length &&
430
+ !foreignKeysToAlter.length &&
374
431
  !primaryKey) {
375
432
  return undefined;
376
433
  }
@@ -383,6 +440,9 @@ export class SqlSchemaGenerator {
383
440
  columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
384
441
  columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
385
442
  indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
443
+ foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
444
+ foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
445
+ foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
386
446
  };
387
447
  }
388
448
  /**
@@ -413,13 +473,14 @@ export class SqlSchemaGenerator {
413
473
  name: col.name,
414
474
  // The same rule `generateColumnFromNode` renders by, so a column added to an existing table
415
475
  // 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),
476
+ type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
417
477
  nullable: col.nullable,
418
478
  defaultValue: col.defaultValue,
419
479
  isPrimaryKey: col.isPrimaryKey,
420
480
  isAutoIncrement: col.isAutoIncrement,
421
481
  isUnique: col.isUnique,
422
482
  comment: col.comment,
483
+ enum: col.enum,
423
484
  };
424
485
  }
425
486
  /**
@@ -518,7 +579,7 @@ export class SqlSchemaGenerator {
518
579
  generateColumnFromNode(col) {
519
580
  return this.renderColumn({
520
581
  ...col,
521
- type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
582
+ type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
522
583
  });
523
584
  }
524
585
  /**
@@ -555,15 +616,13 @@ export class SqlSchemaGenerator {
555
616
  }
556
617
  generateAddForeignKeySql(tableName, foreignKey) {
557
618
  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));
619
+ const refCols = foreignKey.references.columns.map((c) => this.escapeId(c)).join(', ');
620
+ const constraintName = this.escapeId(constraintNameOf(tableName, foreignKey));
562
621
  if (!this.features.foreignKeyAlter) {
563
622
  throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
564
623
  }
565
624
  return (`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
566
- `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.referencesTable)} (${refCols}) ` +
625
+ `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.references.table)} (${refCols}) ` +
567
626
  `ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction};`);
568
627
  }
569
628
  generateDropForeignKeySql(tableName, constraintName) {
@@ -608,6 +667,31 @@ export class SqlSchemaGenerator {
608
667
  'for it. Recreate the table in a written migration.');
609
668
  }
610
669
  }
670
+ /**
671
+ * What a constraint is called: its own name, or one derived from its columns where nothing named it.
672
+ * Shared by the add and the drop so a `DROP CONSTRAINT` names exactly what an `ADD CONSTRAINT` made.
673
+ */
674
+ function constraintNameOf(tableName, foreignKey) {
675
+ return foreignKey.name ?? derivedForeignKeyName(tableName, foreignKey.columns);
676
+ }
677
+ /**
678
+ * A relationship node as the migration's own `ForeignKeySchema`. The node's `name` is kept rather
679
+ * than derived: on the database's side it is the only name a `DROP` can use, and on the entity's it
680
+ * is already the derived one. An unset action stays unset - the default belongs to the one place
681
+ * that spends it, `addForeignKeyStatements`.
682
+ */
683
+ function foreignKeyOf(relation) {
684
+ return {
685
+ name: relation.name,
686
+ columns: relation.from.columns.map((column) => column.name),
687
+ references: {
688
+ table: qualifyName(relation.to.table.name, relation.to.table.schema),
689
+ columns: relation.to.columns.map((column) => column.name),
690
+ },
691
+ onDelete: relation.onDelete,
692
+ onUpdate: relation.onUpdate,
693
+ };
694
+ }
611
695
  /**
612
696
  * The entities as an AST, named the way `generator` names things.
613
697
  *
@@ -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';
@@ -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[];
@@ -65,7 +65,9 @@ export function diffSchemas(source, target, options = {}) {
65
65
  const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs ?? []);
66
66
  const primaryKeyDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.primaryKeyDiff ?? []);
67
67
  // Relationships span tables, so they are compared over the whole schema rather than per table.
68
- const relationshipDiffs = opts.compareRelationships ? diffRelationships(source, target, opts) : [];
68
+ const relationshipDiffs = opts.compareRelationships
69
+ ? diffRelationshipNodes(source.relationships, target.relationships, opts)
70
+ : [];
69
71
  const hasDifferences = tablesToCreate.length > 0 ||
70
72
  tablesToDrop.length > 0 ||
71
73
  tablesToAlter.length > 0 ||
@@ -167,7 +169,7 @@ function diffColumn(tableName, source, target, opts) {
167
169
  const differences = [];
168
170
  // Two things a key column implies rather than states, and catalogues report inconsistently: its
169
171
  // type, which is the dialect's serial spelling rather than one the entity chose and does not round
170
- // trip (`BIGINT UNSIGNED AUTO_INCREMENT` reads back as `BIGINT(20) UNSIGNED`), and its nullability,
172
+ // trip (`BIGINT AUTO_INCREMENT` reads back as `BIGINT(20)`), and its nullability,
171
173
  // which is NOT NULL in every engine whatever is reported - SQLite's `PRAGMA table_info` says
172
174
  // `notnull: 0` for the `INTEGER PRIMARY KEY` that is the table's own rowid. Comparing either asked
173
175
  // to rewrite the column on every sync, and on SQLite, which cannot alter one at all, failed
@@ -179,6 +181,14 @@ function diffColumn(tableName, source, target, opts) {
179
181
  if (typeChanged) {
180
182
  differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
181
183
  }
184
+ // Signedness is the one thing compared on a generated key, because it is the one part of the serial
185
+ // spelling that does round trip - and a key left unsigned refuses every foreign key pointing at it,
186
+ // since the referencing column takes its type from the canonical one, which is signed. Without this
187
+ // a database created before the serial became signed could never gain a foreign key.
188
+ const signednessChanged = generatedType && !!source.type.unsigned !== !!target.type.unsigned;
189
+ if (signednessChanged) {
190
+ differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
191
+ }
182
192
  if (!impliedNotNull && source.nullable !== target.nullable) {
183
193
  differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
184
194
  }
@@ -206,7 +216,7 @@ function diffColumn(tableName, source, target, opts) {
206
216
  // Only the type this diff actually reports: a column altered for its default carries no data loss,
207
217
  // and a generated key's type - never compared above - reads as unsigned against an entity that
208
218
  // cannot say so.
209
- isBreaking: typeChanged && isBreakingTypeChange(target.type, source.type),
219
+ isBreaking: (typeChanged || signednessChanged) && isBreakingTypeChange(target.type, source.type),
210
220
  description: differences.join(', '),
211
221
  };
212
222
  }
@@ -228,11 +238,17 @@ function diffIndex(tableName, source, target, facets) {
228
238
  };
229
239
  }
230
240
  /**
231
- * Compare relationships at the schema level.
241
+ * Compare two lists of relationships.
242
+ *
243
+ * Lists rather than whole schemas, because a migration diffs one table: its two sides are that
244
+ * table's `outgoingRelations`, where `diffSchemas` passes the schema's every relationship.
245
+ *
246
+ * Matched by columns, never by name - the engine named every constraint that already exists, so
247
+ * pairing on names would report every hand-named one as a drop and an add.
232
248
  */
233
- function diffRelationships(source, target, opts) {
234
- const normalizeName = nameNormalizer(opts);
235
- const { created, dropped, matched } = matchByKey(source.relationships, target.relationships, (relation) => getRelationshipKey(relation, normalizeName));
249
+ export function diffRelationshipNodes(source, target, opts = {}) {
250
+ const normalizeName = nameNormalizer({ ...DEFAULT_OPTIONS, ...opts });
251
+ const { created, dropped, matched } = matchByKey(source, target, (relation) => getRelationshipKey(relation, normalizeName));
236
252
  return [
237
253
  ...created.map((relation) => ({
238
254
  ...relationEnds(relation),
@@ -272,14 +272,29 @@ export interface IndexDiff {
272
272
  /**
273
273
  * Difference between two relationship definitions.
274
274
  */
275
- export interface RelationshipDiff {
275
+ interface RelationshipDiffBase {
276
276
  readonly name: string;
277
277
  readonly fromTable: string;
278
278
  readonly toTable: string;
279
- readonly type: 'create' | 'drop' | 'alter';
280
- readonly expected?: RelationshipNode;
281
- readonly actual?: RelationshipNode;
282
279
  }
280
+ /**
281
+ * Difference between two relationships, shaped like {@link ColumnDiff} and for the same reason: which
282
+ * node is present follows from the kind of difference, so a reader never asserts its way past an
283
+ * `undefined` the kind had already ruled out.
284
+ */
285
+ export type RelationshipDiff = RelationshipDiffBase & ({
286
+ readonly type: 'create';
287
+ readonly expected: RelationshipNode;
288
+ readonly actual?: undefined;
289
+ } | {
290
+ readonly type: 'drop';
291
+ readonly expected?: undefined;
292
+ readonly actual: RelationshipNode;
293
+ } | {
294
+ readonly type: 'alter';
295
+ readonly expected: RelationshipNode;
296
+ readonly actual: RelationshipNode;
297
+ });
283
298
  /**
284
299
  * Complete diff between two schemas.
285
300
  */
@@ -5,7 +5,7 @@ export declare class SqliteDialect extends AbstractSqlDialect {
5
5
  protected readonly featureDefaults: DialectFeatures;
6
6
  readonly dialectName = "sqlite";
7
7
  readonly escapeIdChar = "`";
8
- readonly serialType = "INTEGER PRIMARY KEY AUTOINCREMENT";
8
+ readonly autoIncrementSuffix = "PRIMARY KEY AUTOINCREMENT";
9
9
  readonly serialDeclaresPrimaryKey = true;
10
10
  readonly tableOptions = "";
11
11
  readonly beginTransactionCommand = "BEGIN TRANSACTION";
@@ -22,7 +22,7 @@ export class SqliteDialect extends AbstractSqlDialect {
22
22
  };
23
23
  dialectName = 'sqlite';
24
24
  escapeIdChar = '`';
25
- serialType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
25
+ autoIncrementSuffix = 'PRIMARY KEY AUTOINCREMENT';
26
26
  // `AUTOINCREMENT` is only legal in that exact phrase, so the key cannot be lifted to table level.
27
27
  serialDeclaresPrimaryKey = true;
28
28
  tableOptions = '';
@@ -232,7 +232,7 @@ export type RelationValue<E> = E[RelationKey<E>];
232
232
  /**
233
233
  * SQL numeric column types
234
234
  */
235
- export type NumericColumnType = 'int' | 'integer' | 'tinyint' | 'smallint' | 'bigint' | 'float' | 'float4' | 'float8' | 'double' | 'double precision' | 'decimal' | 'numeric' | 'real' | 'serial' | 'smallserial' | 'bigserial';
235
+ export type NumericColumnType = 'int' | 'integer' | 'tinyint' | 'smallint' | 'bigint' | 'float' | 'float4' | 'float8' | 'double' | 'double precision' | 'decimal' | 'numeric' | 'real';
236
236
  /**
237
237
  * SQL string column types
238
238
  */
@@ -1,8 +1,8 @@
1
1
  import type { VectorCast } from '../dialect/vectorCast.js';
2
- import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from '../migrate/builder/types.js';
2
+ import type { FullColumnDefinition, TableDefinition } from '../migrate/builder/types.js';
3
3
  import type { IndexFacet } from '../schema/indexDifferences.js';
4
4
  import type { SchemaAST } from '../schema/schemaAST.js';
5
- import type { ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
5
+ import type { EnumValues, ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
6
6
  import type { EntityMeta, FieldOptions, IndexColumnSchema, LoggingOptions, SqlQuerier, Type, VectorIndexOptions } from './index.js';
7
7
  /**
8
8
  * Defines a migration using a simple object literal
@@ -116,6 +116,13 @@ export interface ColumnSchema {
116
116
  readonly precision?: number;
117
117
  readonly scale?: number;
118
118
  readonly comment?: string;
119
+ /**
120
+ * The values the column accepts, rendered as an inline `CHECK`. Carried only so a column *added* to
121
+ * an existing table is constrained the way one created with its table is; an alter drops it, since
122
+ * MySQL adds a second check rather than replacing the first. Introspection never sets it: a database
123
+ * reports a check as a constraint, not as a property of the column.
124
+ */
125
+ readonly enum?: EnumValues;
119
126
  }
120
127
  /**
121
128
  * Represents a database table schema
@@ -161,15 +168,25 @@ export interface IndexSchema extends VectorIndexOptions {
161
168
  readonly vectorType?: VectorCast;
162
169
  }
163
170
  /**
164
- * Represents a foreign key constraint
171
+ * A foreign key constraint, wherever one is described: read back by introspection, planned into a
172
+ * {@link SchemaDiff}, or declared through the migration builder. One shape for all three - they
173
+ * differed only in spelling, and the translation between them was pure overhead.
165
174
  */
166
175
  export interface ForeignKeySchema {
167
- readonly name: string;
176
+ /** Absent when nothing named it, which the generator fills in with `derivedForeignKeyName`. */
177
+ readonly name?: string;
168
178
  readonly columns: string[];
169
- readonly referencedTable: string;
170
- readonly referencedColumns: string[];
171
- readonly onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
172
- readonly onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
179
+ /**
180
+ * The far end, as one thing. Same shape and same name as everywhere else a relationship's target is
181
+ * described - `@Field({ references })`, `addForeignKey`'s `target`, a `RelationshipNode`'s `to` -
182
+ * so nothing has to be destructured on the way between them.
183
+ */
184
+ readonly references: {
185
+ readonly table: string;
186
+ readonly columns: string[];
187
+ };
188
+ readonly onDelete?: ForeignKeyAction;
189
+ readonly onUpdate?: ForeignKeyAction;
173
190
  }
174
191
  /**
175
192
  * Represents a difference between current and desired schema
@@ -204,7 +221,17 @@ export interface SchemaDiff {
204
221
  readonly indexesToAdd?: IndexSchema[];
205
222
  readonly indexesToDrop?: string[];
206
223
  readonly foreignKeysToAdd?: ForeignKeySchema[];
224
+ /** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
207
225
  readonly foreignKeysToDrop?: string[];
226
+ /**
227
+ * A constraint whose referential actions changed. Its own field rather than a pair of entries in
228
+ * the two above, because no engine alters an action in place: it is a drop and an add that have to
229
+ * travel together, and safe mode has to hold back both or neither.
230
+ */
231
+ readonly foreignKeysToAlter?: {
232
+ readonly from: ForeignKeySchema;
233
+ readonly to: ForeignKeySchema;
234
+ }[];
208
235
  }
209
236
  /**
210
237
  * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
@@ -278,8 +305,21 @@ export interface SchemaGenerator {
278
305
  getSqlType(fieldOptions: FieldOptions): string;
279
306
  /**
280
307
  * Compare an entity with a database table node and return the differences.
308
+ *
309
+ * `desiredAst` is the entity side, from {@link buildAST}, and must span every entity a foreign key
310
+ * on this table points at: a relation whose target is absent resolves to nothing, so the constraint
311
+ * reads as missing from both sides, which is a match and no statement. Defaults to this entity
312
+ * alone, which is right only where it has no relations.
313
+ */
314
+ diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
315
+ /**
316
+ * The entity side as an AST, to hand to every {@link diffSchema} of one run - building it per
317
+ * entity instead is quadratic in the number of entities.
318
+ *
319
+ * Optional because not every generator compares one: MongoDB has no foreign keys and diffs only
320
+ * indexes, so it neither implements this nor reads the argument.
281
321
  */
282
- diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
322
+ buildAST?(entities: readonly Type<unknown>[]): SchemaAST;
283
323
  /**
284
324
  * The table's key: {@link resolveTableAlias} behind {@link resolveSchema}, which is how a
285
325
  * `SchemaAST` stores it and how a diff finds it again.
@@ -326,7 +366,7 @@ export interface SqlDdlGenerator extends SchemaGenerator {
326
366
  /** Generate RENAME COLUMN statement */
327
367
  generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
328
368
  /** Generate ADD FOREIGN KEY statement */
329
- generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string;
369
+ generateAddForeignKeySql(tableName: string, foreignKey: ForeignKeySchema): string;
330
370
  /** Generate DROP FOREIGN KEY statement */
331
371
  generateDropForeignKeySql(tableName: string, constraintName: string): string;
332
372
  }
@@ -12,7 +12,7 @@ export type ColumnFamily = 'string' | 'numeric' | 'boolean' | 'date' | 'json' |
12
12
  * restates the unions: the compile-time side of the same question reads them directly.
13
13
  */
14
14
  export declare const COLUMN_TYPES_BY_FAMILY: {
15
- readonly numeric: readonly ["int", "integer", "tinyint", "smallint", "bigint", "float", "float4", "float8", "double", "double precision", "decimal", "numeric", "real", "serial", "smallserial", "bigserial"];
15
+ readonly numeric: readonly ["int", "integer", "tinyint", "smallint", "bigint", "float", "float4", "float8", "double", "double precision", "decimal", "numeric", "real"];
16
16
  readonly string: readonly ["char", "varchar", "text", "uuid"];
17
17
  readonly date: readonly ["date", "time", "datetime", "timestamp", "timestamptz"];
18
18
  readonly json: readonly ["json", "jsonb"];
@@ -33,6 +33,11 @@ export declare function columnFamily(type: unknown): ColumnFamily | undefined;
33
33
  */
34
34
  export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOptions): boolean;
35
35
  /**
36
- * Checks if a field should be treated as auto-incrementing.
36
+ * Whether the database generates this column's value.
37
+ *
38
+ * The only answer: the schema AST asked it separately and disagreed on three counts - it ignored
39
+ * `onInsert`, so a key the application generates was still emitted `AUTO_INCREMENT`, and it ignored
40
+ * `columnType`, where this one used to let *any* declared width suppress the whole inference. A key
41
+ * that states its width is still a generated key; one that states how it is filled is not.
37
42
  */
38
43
  export declare function isAutoIncrement(field: FieldOptions, isPrimaryKey: boolean): boolean;
@@ -20,9 +20,6 @@ export const COLUMN_TYPES_BY_FAMILY = {
20
20
  'decimal',
21
21
  'numeric',
22
22
  'real',
23
- 'serial',
24
- 'smallserial',
25
- 'bigserial',
26
23
  ],
27
24
  string: ['char', 'varchar', 'text', 'uuid'],
28
25
  date: ['date', 'time', 'datetime', 'timestamp', 'timestamptz'],
@@ -62,15 +59,15 @@ export function isSoleIdField(meta, field) {
62
59
  return field.isId === true && meta.ids.length === 1;
63
60
  }
64
61
  /**
65
- * Checks if a field should be treated as auto-incrementing.
62
+ * Whether the database generates this column's value.
63
+ *
64
+ * The only answer: the schema AST asked it separately and disagreed on three counts - it ignored
65
+ * `onInsert`, so a key the application generates was still emitted `AUTO_INCREMENT`, and it ignored
66
+ * `columnType`, where this one used to let *any* declared width suppress the whole inference. A key
67
+ * that states its width is still a generated key; one that states how it is filled is not.
66
68
  */
67
69
  export function isAutoIncrement(field, isPrimaryKey) {
68
- if (field.autoIncrement === false)
69
- return false;
70
- if (field.autoIncrement)
71
- return true;
72
- const colType = field.columnType?.toLowerCase();
73
- if (colType === 'serial' || colType === 'smallserial' || colType === 'bigserial')
74
- return true;
75
- return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.columnType;
70
+ if (field.autoIncrement !== undefined)
71
+ return field.autoIncrement;
72
+ return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert;
76
73
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "uql-orm",
3
3
  "homepage": "https://uql-orm.dev",
4
- "description": "JSON-native TypeScript ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
4
+ "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.44.0",
6
+ "version": "0.45.1",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"