@mikro-orm/migrations 7.3.0-dev.2 → 7.3.0-dev.20

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.
@@ -9,6 +9,11 @@ export declare class MigrationRunner {
9
9
  protected readonly config: Configuration;
10
10
  constructor(driver: AbstractSqlDriver, options: MigrationsOptions, config: Configuration);
11
11
  run(migration: Migration, method: 'up' | 'down', afterRun?: (tx?: Transaction) => Promise<void>): Promise<void>;
12
+ /** Runs migrations with FK checks off, for drivers that ignore the switch inside a transaction (SQLite). */
13
+ runWithoutForeignKeys<T>(run: () => Promise<T>): Promise<T>;
14
+ private runWithForeignKeys;
15
+ private setForeignKeys;
16
+ private countForeignKeyViolations;
12
17
  private resetSessionSchema;
13
18
  setMasterMigration(trx: Transaction): void;
14
19
  unsetMasterMigration(): void;
@@ -1,4 +1,4 @@
1
- import { Utils } from '@mikro-orm/core';
1
+ import { Utils, ValidationError } from '@mikro-orm/core';
2
2
  /** Executes individual migration files within optional transaction contexts. */
3
3
  export class MigrationRunner {
4
4
  driver;
@@ -8,6 +8,7 @@ export class MigrationRunner {
8
8
  #helper;
9
9
  #masterTransaction;
10
10
  #runSchema;
11
+ #foreignKeysOff = false;
11
12
  constructor(driver, options, config) {
12
13
  this.driver = driver;
13
14
  this.options = options;
@@ -23,16 +24,23 @@ export class MigrationRunner {
23
24
  if (this.#runSchema) {
24
25
  throw new Error('Runtime schema (migrations.schema / migrator.up({ schema })) is only supported with transactional migrations');
25
26
  }
26
- const queries = await this.getQueries(migration, method);
27
- await Utils.runSerial(queries, sql => this.driver.execute(sql));
28
- await afterRun?.();
27
+ await this.runWithForeignKeys(async () => {
28
+ const queries = await this.getQueries(migration, method);
29
+ await Utils.runSerial(queries, sql => this.driver.execute(sql));
30
+ await afterRun?.();
31
+ });
29
32
  }
30
33
  else {
31
34
  await this.#connection.transactional(async (tx) => {
32
35
  migration.setTransactionContext(tx);
36
+ const violations = await this.countForeignKeyViolations(tx);
33
37
  try {
34
38
  const queries = await this.getQueries(migration, method);
35
39
  await Utils.runSerial(queries, sql => this.driver.execute(sql, undefined, 'all', tx));
40
+ const added = (await this.countForeignKeyViolations(tx)) - violations;
41
+ if (added > 0) {
42
+ throw new ValidationError(`Migration ${migration.name ?? migration.constructor.name} left ${added} rows that violate foreign keys`);
43
+ }
36
44
  await afterRun?.(tx);
37
45
  }
38
46
  finally {
@@ -41,6 +49,41 @@ export class MigrationRunner {
41
49
  }, { ctx: this.#masterTransaction });
42
50
  }
43
51
  }
52
+ /** Runs migrations with FK checks off, for drivers that ignore the switch inside a transaction (SQLite). */
53
+ async runWithoutForeignKeys(run) {
54
+ await this.setForeignKeys(false);
55
+ try {
56
+ return await run();
57
+ }
58
+ finally {
59
+ await this.setForeignKeys(true);
60
+ }
61
+ }
62
+ // a migration outside a transaction gets no `foreign_key_check`, so it runs with FK checks on
63
+ async runWithForeignKeys(run) {
64
+ if (!this.#foreignKeysOff) {
65
+ return run();
66
+ }
67
+ await this.setForeignKeys(true);
68
+ try {
69
+ await run();
70
+ }
71
+ finally {
72
+ await this.setForeignKeys(false);
73
+ }
74
+ }
75
+ async setForeignKeys(enabled) {
76
+ await this.driver.execute(enabled ? this.#helper.enableForeignKeysSQL() : this.#helper.disableForeignKeysSQL());
77
+ this.#foreignKeysOff = !enabled;
78
+ }
79
+ async countForeignKeyViolations(ctx) {
80
+ const sql = this.#helper.getForeignKeyCheckSQL();
81
+ // with FK checks on (caller-supplied transaction), the database enforces them itself
82
+ if (!this.#foreignKeysOff || !sql) {
83
+ return 0;
84
+ }
85
+ return (await this.driver.execute(sql, undefined, 'all', ctx)).length;
86
+ }
44
87
  async resetSessionSchema(ctx) {
45
88
  if (!this.#runSchema) {
46
89
  return;
package/Migrator.d.ts CHANGED
@@ -13,6 +13,8 @@ export declare class Migrator extends AbstractMigrator<AbstractSqlDriver> {
13
13
  protected getDefaultGenerator(): IMigrationGenerator;
14
14
  private getSnapshotPath;
15
15
  protected init(): Promise<void>;
16
+ /** Runs the callback while holding the migration lock (if enabled and supported by the driver). */
17
+ private runLocked;
16
18
  /**
17
19
  * @inheritDoc
18
20
  */
@@ -27,6 +29,7 @@ export declare class Migrator extends AbstractMigrator<AbstractSqlDriver> {
27
29
  */
28
30
  createInitial(path?: string, name?: string, blank?: boolean): Promise<MigrationResult>;
29
31
  protected runMigrations(method: 'up' | 'down', options?: string | string[] | MigrateOptions): Promise<MigrationInfo[]>;
32
+ private runWithoutForeignKeys;
30
33
  /** Whether the introspected schema differs from the snapshot in any way the comparator turns into SQL. */
31
34
  private snapshotDiffers;
32
35
  getStorage(): MigrationStorage;
package/Migrator.js CHANGED
@@ -63,6 +63,46 @@ export class Migrator extends AbstractMigrator {
63
63
  await this.storage.ensureTable();
64
64
  }
65
65
  }
66
+ /** Runs the callback while holding the migration lock (if enabled and supported by the driver). */
67
+ async runLocked(cb) {
68
+ const lock = this.options.lock ? this.driver.getPlatform().getSchemaHelper().getMigrationLockSQL() : undefined;
69
+ if (!lock) {
70
+ return cb();
71
+ }
72
+ const poolMax = this.config.get('pool').max;
73
+ if (poolMax != null && poolMax < 2) {
74
+ throw new Error('The migration lock needs at least 2 pooled connections, as it holds one for the whole run');
75
+ }
76
+ // the database has to exist before a connection can be reserved, creating it may reconnect
77
+ await this.#schemaGenerator.ensureDatabase();
78
+ const connection = this.driver.getConnection();
79
+ const name = this.options.tableName;
80
+ const deadline = Date.now() + (this.options.lockTimeout ?? Infinity);
81
+ // a session-level lock on a reserved connection spans all transactions of the run, while its queries keep using the pool
82
+ return connection
83
+ .getClient()
84
+ .connection()
85
+ .execute(async (db) => {
86
+ let waiting = false;
87
+ while (!(await connection.execute(lock.acquire, [name], 'get', db)).locked) {
88
+ if (Date.now() >= deadline) {
89
+ throw new Error(`Timed out after ${this.options.lockTimeout} ms waiting for the migration lock`);
90
+ }
91
+ if (!waiting && !this.options.silent) {
92
+ const logger = this.config.getLogger();
93
+ logger.log('migrator', 'Waiting for the migration lock held by another run', { enabled: true });
94
+ }
95
+ waiting = true;
96
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, deadline - Date.now())));
97
+ }
98
+ try {
99
+ return await cb();
100
+ }
101
+ finally {
102
+ await connection.execute(lock.release, [name], 'run', db);
103
+ }
104
+ });
105
+ }
66
106
  /**
67
107
  * @inheritDoc
68
108
  */
@@ -148,9 +188,9 @@ export class Migrator extends AbstractMigrator {
148
188
  };
149
189
  }
150
190
  async runMigrations(method, options) {
151
- const result = await super.runMigrations(method, options);
191
+ const ctx = Utils.isObject(options) ? options.transaction : undefined;
192
+ const result = await this.runLocked(() => this.runWithoutForeignKeys(ctx, () => super.runMigrations(method, options)));
152
193
  if (result.length > 0 && this.options.snapshot && this.options.snapshotOnMigrate) {
153
- const ctx = Utils.isObject(options) ? options.transaction : undefined;
154
194
  const { skipTables, skipViews } = this.config.get('schemaGenerator');
155
195
  const schema = await DatabaseSchema.create(this.em.getConnection(), this.em.getPlatform(), this.config, undefined, undefined, undefined, skipTables, skipViews, ctx);
156
196
  // keep the snapshot authored by `migration:create` when the migrated DB still matches it
@@ -169,6 +209,14 @@ export class Migrator extends AbstractMigrator {
169
209
  }
170
210
  return result;
171
211
  }
212
+ async runWithoutForeignKeys(ctx, run) {
213
+ // sqlite ignores `pragma foreign_keys` inside a transaction, so a table rebuild would run the FK actions of child rows
214
+ if (ctx || !this.options.transactional || !this.driver.getPlatform().getSchemaHelper().getForeignKeyCheckSQL()) {
215
+ return run();
216
+ }
217
+ await this.init(); // can recreate the runner, which holds the FK state
218
+ return this.runner.runWithoutForeignKeys(run);
219
+ }
172
220
  /** Whether the introspected schema differs from the snapshot in any way the comparator turns into SQL. */
173
221
  snapshotDiffers(snapshot, schema) {
174
222
  const comparator = new SchemaComparator(this.driver.getPlatform());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/migrations",
3
- "version": "7.3.0-dev.2",
3
+ "version": "7.3.0-dev.20",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -47,13 +47,13 @@
47
47
  "copy": "node ../../scripts/copy.mjs"
48
48
  },
49
49
  "dependencies": {
50
- "@mikro-orm/sql": "7.3.0-dev.2"
50
+ "@mikro-orm/sql": "7.3.0-dev.20"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.2.1"
53
+ "@mikro-orm/core": "^7.2.7"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.3.0-dev.2"
56
+ "@mikro-orm/core": "7.3.0-dev.20"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"