uql-orm 0.41.1 → 0.42.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 (69) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  2. package/dist/browser/querier/httpQuerier.js +21 -6
  3. package/dist/browser/type/clientQuerier.d.ts +3 -3
  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/abstractSqlDialect.d.ts +32 -3
  7. package/dist/dialect/abstractSqlDialect.js +58 -22
  8. package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -2
  9. package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
  10. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  11. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  12. package/dist/entity/decorator/entity.d.ts +1 -1
  13. package/dist/entity/decorator/entity.js +1 -1
  14. package/dist/entity/decorator/members.d.ts +10 -2
  15. package/dist/entity/index.d.ts +1 -1
  16. package/dist/entity/index.js +1 -1
  17. package/dist/entity/metadata/definition.d.ts +18 -1
  18. package/dist/entity/metadata/definition.js +91 -36
  19. package/dist/http/handler.js +4 -4
  20. package/dist/maria/mariaDialect.d.ts +2 -2
  21. package/dist/maria/mariaDialect.js +3 -2
  22. package/dist/migrate/builder/tableBuilder.js +5 -4
  23. package/dist/migrate/drift/driftDetector.js +16 -0
  24. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
  25. package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
  26. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
  27. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
  28. package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
  29. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  30. package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
  31. package/dist/migrate/migrator.js +6 -0
  32. package/dist/migrate/schemaGenerator.d.ts +44 -34
  33. package/dist/migrate/schemaGenerator.js +158 -141
  34. package/dist/mongo/mongoDialect.js +39 -14
  35. package/dist/mongo/mongodbQuerier.js +6 -3
  36. package/dist/postgres/postgresDialect.js +1 -1
  37. package/dist/querier/abstractQuerier.d.ts +16 -12
  38. package/dist/querier/abstractQuerier.js +88 -40
  39. package/dist/querier/abstractQuerierPool.d.ts +8 -8
  40. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  41. package/dist/querier/abstractSqlQuerier.js +12 -7
  42. package/dist/querier/relationCount.js +44 -33
  43. package/dist/schema/indexDifferences.d.ts +28 -0
  44. package/dist/schema/indexDifferences.js +46 -0
  45. package/dist/schema/schemaASTBuilder.js +38 -31
  46. package/dist/schema/schemaASTDiffer.d.ts +27 -1
  47. package/dist/schema/schemaASTDiffer.js +54 -18
  48. package/dist/schema/types.d.ts +46 -7
  49. package/dist/sqlite/sqliteDialect.d.ts +2 -1
  50. package/dist/sqlite/sqliteDialect.js +4 -1
  51. package/dist/type/dialect.d.ts +6 -0
  52. package/dist/type/entity.d.ts +57 -14
  53. package/dist/type/migration.d.ts +19 -0
  54. package/dist/type/queryWhere.d.ts +7 -2
  55. package/dist/type/universalQuerier.d.ts +8 -8
  56. package/dist/util/dialect.util.js +15 -14
  57. package/dist/util/field.util.d.ts +11 -1
  58. package/dist/util/field.util.js +12 -0
  59. package/dist/util/index.d.ts +1 -0
  60. package/dist/util/index.js +1 -0
  61. package/dist/util/object.util.d.ts +6 -0
  62. package/dist/util/object.util.js +18 -0
  63. package/dist/util/relationQuery.util.d.ts +48 -7
  64. package/dist/util/relationQuery.util.js +70 -9
  65. package/dist/util/rowKey.util.d.ts +9 -0
  66. package/dist/util/rowKey.util.js +24 -0
  67. package/dist/util/sql.util.d.ts +24 -7
  68. package/dist/util/sql.util.js +75 -10
  69. package/package.json +1 -1
@@ -64,10 +64,13 @@ export class BaseSqlIntrospector {
64
64
  referencedBy: [],
65
65
  };
66
66
  columns.set(col.name, column);
67
- if (col.isPrimaryKey) {
68
- table.primaryKey.push(column);
69
- }
70
67
  }
68
+ // From the ordered list the query returned, not from the per-column flags: `(a, b)` is a
69
+ // different key from `(b, a)`, and a flag says only that a column is *in* the key. Falls back
70
+ // to the flags for an introspector that reports no key of its own.
71
+ const keyColumns = schema.primaryKey ?? schema.columns.filter((col) => col.isPrimaryKey).map((col) => col.name);
72
+ table.primaryKey.push(...keyColumns.flatMap((name) => columns.get(name) ?? []));
73
+ table.primaryKeyName = schema.primaryKeyName;
71
74
  tableNodes.set(schema.name, table);
72
75
  ast.addTable(table);
73
76
  }
@@ -160,7 +160,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
160
160
  }
161
161
  getPrimaryKeyQuery(_tableName) {
162
162
  return /*sql*/ `
163
- SELECT kcu.column_name
163
+ SELECT kcu.column_name, tc.constraint_name
164
164
  FROM information_schema.table_constraints tc
165
165
  JOIN information_schema.key_column_usage kcu
166
166
  ON tc.constraint_name = kcu.constraint_name
@@ -1,3 +1,4 @@
1
+ import { derivedForeignKeyName } from '../../util/sql.util.js';
1
2
  import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
2
3
  /**
3
4
  * SQLite schema introspector
@@ -104,11 +105,14 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
104
105
  existing.push(row);
105
106
  grouped.set(id, existing);
106
107
  }
107
- return Array.from(grouped.entries()).map(([id, rows]) => {
108
+ return Array.from(grouped.entries()).map(([, rows]) => {
108
109
  const first = rows[0];
110
+ const columns = rows.map((r) => r.from);
109
111
  return {
110
- name: `fk_${tableName}_${id}`,
111
- columns: rows.map((r) => r.from),
112
+ // `PRAGMA foreign_key_list` reports no name, so one is derived the same way the entity side
113
+ // derives it. Seeded from the columns, not the PRAGMA's row id, which nothing else knows.
114
+ name: derivedForeignKeyName(tableName, columns),
115
+ columns,
112
116
  referencedTable: first.table,
113
117
  referencedColumns: rows.map((r) => r.to),
114
118
  onDelete: this.normalizeReferentialAction(first.on_delete),
@@ -436,6 +436,12 @@ export class Migrator {
436
436
  this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.columnsToAlter.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToAlter.map((c) => c.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
437
437
  delete filteredDiff.columnsToAlter;
438
438
  }
439
+ if (filteredDiff.primaryKey) {
440
+ // Rewriting a key drops a constraint and rebuilds an index over the whole table, and fails
441
+ // outright where the new columns are null on rows that already exist. Firmly not additive.
442
+ 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
+ delete filteredDiff.primaryKey;
444
+ }
439
445
  delete filteredDiff.indexesToDrop;
440
446
  delete filteredDiff.foreignKeysToDrop;
441
447
  }
@@ -1,7 +1,8 @@
1
1
  import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { SchemaAST } from '../schema/schemaAST.js';
3
+ import { type DiffOptions } from '../schema/schemaASTDiffer.js';
3
4
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
4
- import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
5
+ import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
5
6
  import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
6
7
  import { type IndexDdl } from './ddl/index.js';
7
8
  /**
@@ -25,7 +26,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
25
26
  /**
26
27
  * Primary key type for auto-increment integer IDs
27
28
  */
28
- protected get serialPrimaryKeyType(): string;
29
+ protected get serialType(): string;
29
30
  /**
30
31
  * Convert FieldOptions to CanonicalType using the unified type system.
31
32
  */
@@ -66,13 +67,15 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
66
67
  */
67
68
  generateDropIndex(tableName: string, indexName: string, schema?: string): string;
68
69
  /**
69
- * Generate a column definition from a {@link ColumnSchema}, whose type is the engine's own spelling
70
- * and may already carry its precision (or even `PRIMARY KEY`, for a serial).
70
+ * A column definition from a {@link ColumnSchema}, whose type is already the engine's own spelling
71
+ * and may carry its own size.
72
+ *
73
+ * Kept apart from {@link generateColumnFromNode} rather than folded into it: a `ColumnSchema` has no
74
+ * `enum`, because introspection reads one back as a `CHECK` constraint and not as a property of the
75
+ * column, so only the node knows enough to emit that clause. Both spell the definition through
76
+ * {@link renderColumn}, which is the part that must not be written twice.
71
77
  */
72
- generateColumnDefinitionFromSchema(column: ColumnSchema, options?: {
73
- includePrimaryKey?: boolean;
74
- includeUnique?: boolean;
75
- }): string;
78
+ generateColumnDefinitionFromSchema(column: ColumnSchema): string;
76
79
  /**
77
80
  * The one place a column definition is spelled. Both callers reach it - the `ColumnSchema` path above
78
81
  * and the `ColumnNode` path below - because the clause order and the "no NOT NULL on a primary key"
@@ -82,7 +85,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
82
85
  /** ` DEFAULT <sql>`, or nothing where the column declares none. Empty rather than `DEFAULT NULL`
83
86
  * so an absent default stays absent - `defaultValue: null` is the way to ask for one. */
84
87
  private defaultClause;
85
- getSqlType(field: FieldOptions, fieldType?: unknown): string;
88
+ getSqlType(field: FieldMeta, fieldType?: unknown, isSoleKey?: boolean): string;
86
89
  /**
87
90
  * Generate ALTER COLUMN statements (database-specific)
88
91
  */
@@ -94,39 +97,30 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
94
97
  /**
95
98
  * Compare an entity with a database table node and return the differences.
96
99
  */
97
- diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
98
100
  /**
99
- * Indexes the entity declares that the table does not have, matched by name and built the same way
100
- * `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
101
- * up rather than waiting for the table to be created from scratch somewhere else.
101
+ * How this entity differs from the table the database reported, as the migrator's `SchemaDiff`.
102
102
  *
103
- * Only ever additive. An index the entity does not name is left alone: it may well have been
104
- * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
105
- *
106
- * A vector index is one of these like any other: MariaDB's `CREATE VECTOR INDEX ... ON t (col)`
107
- * adds one to a table that already exists, which the inline `CREATE TABLE` form it also has cannot.
103
+ * The comparison itself is {@link diffTable}, the same one drift detection runs, so the two can no
104
+ * longer disagree about what has changed. Only two things are this side's own: the entity becomes a
105
+ * table node first, and types are compared as the *engine* would store them - see `normalizeType`.
108
106
  */
109
- private missingIndexes;
110
- private columnNodeToSchema;
111
- /**
112
- * Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it
113
- * would create for this field, against what it reported for the existing column.
114
- */
115
- protected fieldToColumnSchema<E>(fieldKey: string, field: FieldOptions, meta: EntityMeta<E>): ColumnSchema;
107
+ diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
116
108
  /**
117
- * Check if two columns differ enough to require alteration
109
+ * What the shared differ needs from a dialect: a type as this engine would actually store it.
110
+ *
111
+ * `boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite, so two canonical types that differ on
112
+ * paper can be one column in the database. Round-tripping through the engine's own spelling is what
113
+ * stops every such column reporting an alteration on every sync.
118
114
  */
119
- protected columnsNeedAlteration(current: ColumnSchema, desired: ColumnSchema): boolean;
120
115
  /**
121
- * Whether two column types are the same *as this engine stores them*.
116
+ * Indexes the entity declares that the table does not already have, in any shape.
122
117
  *
123
- * Both sides are SQL spellings, parsed back to canonical so that `INT` and `INTEGER`, or `DATETIME`
124
- * and `TIMESTAMP`, do not read as a change. Do not "simplify" this into comparing the entity's
125
- * canonical type against the column's: several canonical types share one storage type per engine
126
- * (`boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite), so that comparison reports an
127
- * alteration for those columns on every single sync.
118
+ * Additive only: an index the entity does not name may well have been created deliberately outside
119
+ * the ORM, and dropping it is a decision for a reviewed migration.
128
120
  */
129
- protected isTypeEqual(current: ColumnSchema, desired: ColumnSchema): boolean;
121
+ private missingIndexes;
122
+ protected diffOptions(): DiffOptions;
123
+ private columnNodeToSchema;
130
124
  /**
131
125
  * Compare two default values for equality
132
126
  */
@@ -156,6 +150,22 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
156
150
  generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
157
151
  generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string;
158
152
  generateDropForeignKeySql(tableName: string, constraintName: string): string;
153
+ /**
154
+ * `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
155
+ * {@link generateDropPrimaryKeySql}. Refused where the engine cannot alter a key at all, by name,
156
+ * rather than emitting DDL it will reject.
157
+ */
158
+ generateAddPrimaryKeySql(tableName: string, columns: readonly string[], name?: string): string;
159
+ /**
160
+ * Drops whatever key the table has.
161
+ *
162
+ * `constraintName` has to be what the constraint is *actually* called: the name introspection
163
+ * reported for a key the database already had, or the derived one for a key this generator itself
164
+ * added, which is what reversing a migration drops. Guessing either way names nothing. MySQL takes
165
+ * no name at all - a table's key is always `PRIMARY` there.
166
+ */
167
+ generateDropPrimaryKeySql(tableName: string, constraintName?: string): string;
168
+ private assertPrimaryKeyAlterable;
159
169
  }
160
170
  /**
161
171
  * The entities as an AST, named the way `generator` names things.
@@ -1,9 +1,11 @@
1
1
  import { AbstractSqlDialect } from '../dialect/index.js';
2
- import { getMeta } from '../entity/index.js';
2
+ import { getMeta, soleIdOf } from '../entity/index.js';
3
3
  import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory, sqlToCanonical, } from '../schema/canonicalType.js';
4
+ import { indexSignature } from '../schema/indexDifferences.js';
4
5
  import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
5
- import { getKeys, isAutoIncrement, qualifyName } from '../util/index.js';
6
- import { derivedCheckName, derivedForeignKeyName } from '../util/sql.util.js';
6
+ import { diffTable } from '../schema/schemaASTDiffer.js';
7
+ import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/index.js';
8
+ import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
7
9
  import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
8
10
  import { indexDdlFor } from './ddl/index.js';
9
11
  import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
@@ -47,8 +49,8 @@ export class SqlSchemaGenerator {
47
49
  /**
48
50
  * Primary key type for auto-increment integer IDs
49
51
  */
50
- get serialPrimaryKeyType() {
51
- return this.dialect.serialPrimaryKey;
52
+ get serialType() {
53
+ return this.dialect.serialType;
52
54
  }
53
55
  /**
54
56
  * Convert FieldOptions to CanonicalType using the unified type system.
@@ -128,6 +130,11 @@ export class SqlSchemaGenerator {
128
130
  generateAlterTable(diff) {
129
131
  const statements = [];
130
132
  const tableName = this.escapeId(diff.tableName);
133
+ // Before the columns, because a key column being added cannot be part of the old key, and after
134
+ // it is dropped the table is free to take the new one below.
135
+ if (diff.primaryKey?.from.length) {
136
+ statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
137
+ }
131
138
  // Add new columns
132
139
  if (diff.columnsToAdd?.length) {
133
140
  for (const column of diff.columnsToAdd) {
@@ -161,11 +168,21 @@ export class SqlSchemaGenerator {
161
168
  statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
162
169
  }
163
170
  }
171
+ // Last, so every column it names exists by now.
172
+ if (diff.primaryKey?.to.length) {
173
+ statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
174
+ }
164
175
  return statements;
165
176
  }
166
177
  generateAlterTableDown(diff) {
167
178
  const statements = [];
168
179
  const tableName = this.escapeId(diff.tableName);
180
+ // The key first, mirroring the up direction: a column the up added cannot be dropped below while
181
+ // the new key still names it. Restored under the name the database gave it, which is what the
182
+ // table had before, rather than a derived one that was never on it.
183
+ if (diff.primaryKey?.to.length) {
184
+ statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
185
+ }
169
186
  // Reverse column additions by dropping them
170
187
  if (diff.columnsToAdd?.length) {
171
188
  for (const column of diff.columnsToAdd) {
@@ -175,7 +192,7 @@ export class SqlSchemaGenerator {
175
192
  // Reverse column alterations by restoring original schema
176
193
  if (diff.columnsToAlter?.length) {
177
194
  for (const { from } of diff.columnsToAlter) {
178
- const colDef = this.generateColumnDefinitionFromSchema(from, { includePrimaryKey: false });
195
+ const colDef = this.generateColumnDefinitionFromSchema(from);
179
196
  const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
180
197
  statements.push(...colStatements);
181
198
  }
@@ -186,6 +203,9 @@ export class SqlSchemaGenerator {
186
203
  statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
187
204
  }
188
205
  }
206
+ if (diff.primaryKey?.from.length) {
207
+ statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
208
+ }
189
209
  if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
190
210
  statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes`);
191
211
  }
@@ -205,11 +225,15 @@ export class SqlSchemaGenerator {
205
225
  return `DROP INDEX IF EXISTS ${this.dialect.escapeQualifiedId(indexName, schema)};`;
206
226
  }
207
227
  /**
208
- * Generate a column definition from a {@link ColumnSchema}, whose type is the engine's own spelling
209
- * and may already carry its precision (or even `PRIMARY KEY`, for a serial).
228
+ * A column definition from a {@link ColumnSchema}, whose type is already the engine's own spelling
229
+ * and may carry its own size.
230
+ *
231
+ * Kept apart from {@link generateColumnFromNode} rather than folded into it: a `ColumnSchema` has no
232
+ * `enum`, because introspection reads one back as a `CHECK` constraint and not as a property of the
233
+ * column, so only the node knows enough to emit that clause. Both spell the definition through
234
+ * {@link renderColumn}, which is the part that must not be written twice.
210
235
  */
211
- generateColumnDefinitionFromSchema(column, options = {}) {
212
- const { includePrimaryKey = true, includeUnique = true } = options;
236
+ generateColumnDefinitionFromSchema(column) {
213
237
  let type = column.type;
214
238
  if (!type.includes('(')) {
215
239
  if (column.precision !== undefined) {
@@ -219,17 +243,7 @@ export class SqlSchemaGenerator {
219
243
  type += `(${column.length})`;
220
244
  }
221
245
  }
222
- if (!includePrimaryKey) {
223
- type = type.replace(/\s+PRIMARY\s+KEY/i, '');
224
- }
225
- // `includePrimaryKey: false` suppresses the keyword, not the fact: the column is still the primary
226
- // key, so it must not pick up `NOT NULL` (implied) or `UNIQUE` (redundant) on the way out.
227
- return this.renderColumn({
228
- ...column,
229
- type,
230
- isUnique: column.isUnique && includeUnique,
231
- declaresPrimaryKey: includePrimaryKey && column.isPrimaryKey,
232
- });
246
+ return this.renderColumn({ ...column, type });
233
247
  }
234
248
  /**
235
249
  * The one place a column definition is spelled. Both callers reach it - the `ColumnSchema` path above
@@ -238,9 +252,6 @@ export class SqlSchemaGenerator {
238
252
  */
239
253
  renderColumn(column) {
240
254
  let def = `${this.escapeId(column.name)} ${column.type}`;
241
- if (column.declaresPrimaryKey && !column.type.includes('PRIMARY KEY')) {
242
- def += ' PRIMARY KEY';
243
- }
244
255
  if (!column.nullable && !column.isPrimaryKey) {
245
256
  def += ' NOT NULL';
246
257
  }
@@ -264,19 +275,19 @@ export class SqlSchemaGenerator {
264
275
  ? ''
265
276
  : ` DEFAULT ${formatDefaultValue(column.defaultValue, this.dialect, column.type)}`;
266
277
  }
267
- getSqlType(field, fieldType) {
278
+ getSqlType(field, fieldType, isSoleKey = field.isId === true) {
268
279
  // If field has a reference, inherit type from the target primary key
269
280
  if (field.references) {
270
281
  const refEntity = field.references();
271
282
  const refMeta = getMeta(refEntity);
272
- const refIdField = refMeta.fields[refMeta.id];
283
+ const refIdField = refMeta.fields[field.referencedKey ?? soleIdOf(refMeta, 'a foreign key')];
273
284
  return this.getSqlType({ ...refIdField, references: undefined, isId: undefined, autoIncrement: false }, refIdField.type);
274
285
  }
275
286
  // Get canonical type and convert to SQL
276
287
  const canonical = this.getCanonicalType(field, fieldType);
277
288
  // Special case for serial primary keys
278
- if (isAutoIncrement(field, field.isId === true)) {
279
- return this.dialect.serialPrimaryKey;
289
+ if (isAutoIncrement(field, isSoleKey)) {
290
+ return this.dialect.serialType;
280
291
  }
281
292
  return this.canonicalTypeToSql(canonical);
282
293
  }
@@ -323,80 +334,90 @@ export class SqlSchemaGenerator {
323
334
  /**
324
335
  * Compare an entity with a database table node and return the differences.
325
336
  */
337
+ /**
338
+ * How this entity differs from the table the database reported, as the migrator's `SchemaDiff`.
339
+ *
340
+ * The comparison itself is {@link diffTable}, the same one drift detection runs, so the two can no
341
+ * longer disagree about what has changed. Only two things are this side's own: the entity becomes a
342
+ * table node first, and types are compared as the *engine* would store them - see `normalizeType`.
343
+ */
326
344
  diffSchema(entity, currentTable) {
327
345
  const meta = getMeta(entity);
346
+ const tableName = this.resolveTableName(meta);
347
+ const schema = this.resolveSchema(meta);
328
348
  if (!currentTable) {
329
- return {
330
- tableName: this.resolveTableName(meta),
331
- schema: this.resolveSchema(meta),
332
- type: 'create',
333
- };
334
- }
335
- const columnsToAdd = [];
336
- const columnsToAlter = [];
337
- const columnsToDrop = [];
338
- const currentColumns = new Map(currentTable.columns);
339
- const fieldKeys = getKeys(meta.fields);
340
- for (const key of fieldKeys) {
341
- const field = meta.fields[key];
342
- if (!field || field.virtual)
343
- continue;
344
- const columnName = this.dialect.resolveColumnName(key, field);
345
- const currentColumn = currentColumns.get(columnName);
346
- if (!currentColumn) {
347
- columnsToAdd.push(this.fieldToColumnSchema(key, field, meta));
348
- }
349
- else {
350
- const desiredColumn = this.fieldToColumnSchema(key, field, meta);
351
- const currentColumnSchema = this.columnNodeToSchema(currentColumn);
352
- if (this.columnsNeedAlteration(currentColumnSchema, desiredColumn)) {
353
- columnsToAlter.push({ from: currentColumnSchema, to: desiredColumn });
354
- }
355
- }
356
- currentColumns.delete(columnName);
349
+ return { tableName, schema, type: 'create' };
357
350
  }
358
- for (const [name] of currentColumns) {
359
- columnsToDrop.push(name);
351
+ // Keyed by the qualified name this generator resolves, which is the key the AST it just built
352
+ // stores the table under.
353
+ const desired = buildEntityAST(this, [entity]).getTable(tableName);
354
+ if (!desired) {
355
+ return undefined;
360
356
  }
361
- const indexesToAdd = this.missingIndexes(entity, currentTable);
362
- if (columnsToAdd.length === 0 &&
363
- columnsToAlter.length === 0 &&
364
- columnsToDrop.length === 0 &&
365
- indexesToAdd.length === 0) {
357
+ // Indexes are matched here rather than by the differ, which pairs them by name so that a changed
358
+ // one reads as one index that altered. A migration needs the opposite: an index already in the
359
+ // table, under whatever name, must not be created again, and one whose shape differs is a
360
+ // separate index rather than a change - no engine alters an index's columns or uniqueness.
361
+ const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
362
+ const indexesToAdd = this.missingIndexes(desired, currentTable);
363
+ const columnDiffs = tableDiff?.columnDiffs ?? [];
364
+ const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
365
+ const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
366
+ const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
367
+ ? [{ from: this.columnNodeToSchema(it.actual), to: this.columnNodeToSchema(it.expected) }]
368
+ : []);
369
+ const primaryKey = tableDiff?.primaryKeyDiff && {
370
+ from: tableDiff.primaryKeyDiff.actual,
371
+ to: tableDiff.primaryKeyDiff.expected,
372
+ fromName: tableDiff.primaryKeyDiff.actualName,
373
+ };
374
+ if (!columnsToAdd.length &&
375
+ !columnsToAlter.length &&
376
+ !columnsToDrop.length &&
377
+ !indexesToAdd.length &&
378
+ !primaryKey) {
366
379
  return undefined;
367
380
  }
368
381
  return {
369
- tableName: this.resolveTableName(meta),
370
- schema: this.resolveSchema(meta),
382
+ tableName,
383
+ schema,
371
384
  type: 'alter',
372
- columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
373
- columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
374
- columnsToDrop: columnsToDrop.length > 0 ? columnsToDrop : undefined,
375
- indexesToAdd: indexesToAdd.length > 0 ? indexesToAdd : undefined,
385
+ primaryKey,
386
+ columnsToAdd: columnsToAdd.length ? columnsToAdd : undefined,
387
+ columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
388
+ columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
389
+ indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
376
390
  };
377
391
  }
378
392
  /**
379
- * Indexes the entity declares that the table does not have, matched by name and built the same way
380
- * `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
381
- * up rather than waiting for the table to be created from scratch somewhere else.
393
+ * What the shared differ needs from a dialect: a type as this engine would actually store it.
382
394
  *
383
- * Only ever additive. An index the entity does not name is left alone: it may well have been
384
- * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
395
+ * `boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite, so two canonical types that differ on
396
+ * paper can be one column in the database. Round-tripping through the engine's own spelling is what
397
+ * stops every such column reporting an alteration on every sync.
398
+ */
399
+ /**
400
+ * Indexes the entity declares that the table does not already have, in any shape.
385
401
  *
386
- * A vector index is one of these like any other: MariaDB's `CREATE VECTOR INDEX ... ON t (col)`
387
- * adds one to a table that already exists, which the inline `CREATE TABLE` form it also has cannot.
402
+ * Additive only: an index the entity does not name may well have been created deliberately outside
403
+ * the ORM, and dropping it is a decision for a reviewed migration.
388
404
  */
389
- missingIndexes(entity, currentTable) {
390
- // Keyed by the qualified name, so a table in a schema finds itself rather than reporting that
391
- // the entity declares no indexes at all.
392
- const desired = buildEntityAST(this, [entity]).getTable(qualifyName(currentTable.name, currentTable.schema))?.indexes ?? [];
393
- const present = new Set(currentTable.indexes.map((index) => index.name));
394
- return desired.filter((index) => !present.has(index.name)).map(indexNodeToSchema);
405
+ missingIndexes(desired, currentTable) {
406
+ const present = new Set(currentTable.indexes.map(indexSignature));
407
+ return desired.indexes.filter((index) => !present.has(indexSignature(index))).map(indexNodeToSchema);
408
+ }
409
+ diffOptions() {
410
+ return {
411
+ normalizeType: (type) => sqlToCanonical(this.canonicalTypeToSql(type)),
412
+ defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
413
+ };
395
414
  }
396
415
  columnNodeToSchema(col) {
397
416
  return {
398
417
  name: col.name,
399
- type: this.canonicalTypeToSql(col.type),
418
+ // The same rule `generateColumnFromNode` renders by, so a column added to an existing table
419
+ // gets the type it would have had if the table were created from scratch.
420
+ type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
400
421
  nullable: col.nullable,
401
422
  defaultValue: col.defaultValue,
402
423
  isPrimaryKey: col.isPrimaryKey,
@@ -405,65 +426,17 @@ export class SqlSchemaGenerator {
405
426
  comment: col.comment,
406
427
  };
407
428
  }
408
- /**
409
- * Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it
410
- * would create for this field, against what it reported for the existing column.
411
- */
412
- fieldToColumnSchema(fieldKey, field, meta) {
413
- const isPrimaryKey = field.isId === true && meta.id === fieldKey;
414
- return {
415
- name: this.dialect.resolveColumnName(fieldKey, field),
416
- type: this.getSqlType(field, field.type),
417
- nullable: field.nullable ?? !isPrimaryKey,
418
- defaultValue: field.defaultValue,
419
- isPrimaryKey,
420
- isAutoIncrement: isAutoIncrement(field, isPrimaryKey),
421
- isUnique: field.unique ?? false,
422
- length: field.length,
423
- precision: field.precision,
424
- scale: field.scale,
425
- comment: field.comment,
426
- };
427
- }
428
- /**
429
- * Check if two columns differ enough to require alteration
430
- */
431
- columnsNeedAlteration(current, desired) {
432
- if (current.isPrimaryKey && desired.isPrimaryKey) {
433
- return false;
434
- }
435
- if (current.isPrimaryKey !== desired.isPrimaryKey)
436
- return true;
437
- if (current.nullable !== desired.nullable)
438
- return true;
439
- if (current.isUnique !== desired.isUnique)
440
- return true;
441
- if (!this.isTypeEqual(current, desired))
442
- return true;
443
- if (!this.isDefaultValueEqual(current.defaultValue, desired.defaultValue))
444
- return true;
445
- return false;
446
- }
447
- /**
448
- * Whether two column types are the same *as this engine stores them*.
449
- *
450
- * Both sides are SQL spellings, parsed back to canonical so that `INT` and `INTEGER`, or `DATETIME`
451
- * and `TIMESTAMP`, do not read as a change. Do not "simplify" this into comparing the entity's
452
- * canonical type against the column's: several canonical types share one storage type per engine
453
- * (`boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite), so that comparison reports an
454
- * alteration for those columns on every single sync.
455
- */
456
- isTypeEqual(current, desired) {
457
- return areTypesEqual(sqlToCanonical(current.type), sqlToCanonical(desired.type));
458
- }
459
429
  /**
460
430
  * Compare two default values for equality
461
431
  */
462
432
  isDefaultValueEqual(current, desired) {
463
433
  if (current === desired)
464
434
  return true;
465
- if (current === undefined || desired === undefined)
466
- return current === desired;
435
+ // Both spellings of "no default" are the same fact, and engines disagree on which they report:
436
+ // MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
437
+ // `MODIFY` every nullable column, on every sync, forever.
438
+ if (current == null || desired == null)
439
+ return current == null && desired == null;
467
440
  const normalize = (value) => {
468
441
  if (value === null)
469
442
  return 'null';
@@ -493,9 +466,16 @@ export class SqlSchemaGenerator {
493
466
  const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
494
467
  columns.push(colDef);
495
468
  }
496
- if (table.primaryKey.length > 1) {
497
- const pkCols = table.primaryKey.map((c) => this.escapeId(c.name)).join(', ');
498
- constraints.push(`PRIMARY KEY (${pkCols})`);
469
+ // Every key, of any width, as one named constraint beside the checks and foreign keys - so a
470
+ // later `DROP` has something to name. The exception is a dialect whose serial type states the key
471
+ // itself (SQLite's `INTEGER PRIMARY KEY AUTOINCREMENT`, which cannot be split): there the column
472
+ // has already declared it, and saying it again is a second primary key.
473
+ const declaredByColumn = this.dialect.serialDeclaresPrimaryKey && table.primaryKey.length === 1 && table.primaryKey[0].isAutoIncrement;
474
+ if (table.primaryKey.length && !declaredByColumn) {
475
+ const pkColumns = table.primaryKey.map((c) => c.name);
476
+ const pkName = table.primaryKeyName ?? derivedPrimaryKeyName(table.name, pkColumns);
477
+ const pkCols = pkColumns.map((c) => this.escapeId(c)).join(', ');
478
+ constraints.push(`CONSTRAINT ${this.escapeId(pkName)} PRIMARY KEY (${pkCols})`);
499
479
  }
500
480
  (table.checks ?? []).forEach((check, i) => {
501
481
  const name = check.name ?? derivedCheckName(table.name, i + 1);
@@ -542,8 +522,7 @@ export class SqlSchemaGenerator {
542
522
  generateColumnFromNode(col) {
543
523
  return this.renderColumn({
544
524
  ...col,
545
- type: col.isPrimaryKey && col.isAutoIncrement ? this.serialPrimaryKeyType : this.canonicalTypeToSql(col.type),
546
- declaresPrimaryKey: col.isPrimaryKey && col.table.primaryKey.length === 1,
525
+ type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
547
526
  });
548
527
  }
549
528
  /**
@@ -594,6 +573,44 @@ export class SqlSchemaGenerator {
594
573
  generateDropForeignKeySql(tableName, constraintName) {
595
574
  return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
596
575
  }
576
+ /**
577
+ * `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
578
+ * {@link generateDropPrimaryKeySql}. Refused where the engine cannot alter a key at all, by name,
579
+ * rather than emitting DDL it will reject.
580
+ */
581
+ generateAddPrimaryKeySql(tableName, columns, name) {
582
+ this.assertPrimaryKeyAlterable(tableName);
583
+ const constraintName = this.escapeId(name ?? derivedPrimaryKeyName(tableName, columns));
584
+ const pkCols = columns.map((c) => this.escapeId(c)).join(', ');
585
+ return `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} PRIMARY KEY (${pkCols});`;
586
+ }
587
+ /**
588
+ * Drops whatever key the table has.
589
+ *
590
+ * `constraintName` has to be what the constraint is *actually* called: the name introspection
591
+ * reported for a key the database already had, or the derived one for a key this generator itself
592
+ * added, which is what reversing a migration drops. Guessing either way names nothing. MySQL takes
593
+ * no name at all - a table's key is always `PRIMARY` there.
594
+ */
595
+ generateDropPrimaryKeySql(tableName, constraintName) {
596
+ this.assertPrimaryKeyAlterable(tableName);
597
+ const table = this.escapeId(tableName);
598
+ if (this.dialect.dropPrimaryKeySyntax === 'DROP PRIMARY KEY') {
599
+ return `ALTER TABLE ${table} DROP PRIMARY KEY;`;
600
+ }
601
+ if (!constraintName) {
602
+ throw new TypeError(`Cannot drop the primary key of "${tableName}": ${this.dialect} names the constraint, and ` +
603
+ 'introspection did not report a name for it.');
604
+ }
605
+ return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
606
+ }
607
+ assertPrimaryKeyAlterable(tableName) {
608
+ if (this.features.primaryKeyAlter) {
609
+ return;
610
+ }
611
+ throw new TypeError(`${this.dialect}: Cannot change the primary key of "${tableName}" - this database has no ALTER ` +
612
+ 'for it. Recreate the table in a written migration.');
613
+ }
597
614
  }
598
615
  /**
599
616
  * The entities as an AST, named the way `generator` names things.