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