@spinajs/orm 2.0.485 → 2.0.487

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 (115) hide show
  1. package/lib/cjs/config/orm.d.ts +24 -0
  2. package/lib/cjs/config/orm.d.ts.map +1 -0
  3. package/lib/cjs/config/orm.js +26 -0
  4. package/lib/cjs/config/orm.js.map +1 -0
  5. package/lib/cjs/decorators.d.ts +3 -2
  6. package/lib/cjs/decorators.d.ts.map +1 -1
  7. package/lib/cjs/decorators.js +19 -2
  8. package/lib/cjs/decorators.js.map +1 -1
  9. package/lib/cjs/descriptor.d.ts +29 -1
  10. package/lib/cjs/descriptor.d.ts.map +1 -1
  11. package/lib/cjs/descriptor.js +37 -0
  12. package/lib/cjs/descriptor.js.map +1 -1
  13. package/lib/cjs/index.d.ts +6 -0
  14. package/lib/cjs/index.d.ts.map +1 -1
  15. package/lib/cjs/index.js +6 -0
  16. package/lib/cjs/index.js.map +1 -1
  17. package/lib/cjs/interfaces.d.ts +52 -2
  18. package/lib/cjs/interfaces.d.ts.map +1 -1
  19. package/lib/cjs/interfaces.js +5 -0
  20. package/lib/cjs/interfaces.js.map +1 -1
  21. package/lib/cjs/migration-environment.d.ts +70 -0
  22. package/lib/cjs/migration-environment.d.ts.map +1 -0
  23. package/lib/cjs/migration-environment.js +134 -0
  24. package/lib/cjs/migration-environment.js.map +1 -0
  25. package/lib/cjs/migration-runner.d.ts +108 -0
  26. package/lib/cjs/migration-runner.d.ts.map +1 -0
  27. package/lib/cjs/migration-runner.js +216 -0
  28. package/lib/cjs/migration-runner.js.map +1 -0
  29. package/lib/cjs/migration-service.d.ts +300 -0
  30. package/lib/cjs/migration-service.d.ts.map +1 -0
  31. package/lib/cjs/migration-service.js +829 -0
  32. package/lib/cjs/migration-service.js.map +1 -0
  33. package/lib/cjs/migration-sources.d.ts +85 -0
  34. package/lib/cjs/migration-sources.d.ts.map +1 -0
  35. package/lib/cjs/migration-sources.js +277 -0
  36. package/lib/cjs/migration-sources.js.map +1 -0
  37. package/lib/cjs/orm.d.ts +77 -15
  38. package/lib/cjs/orm.d.ts.map +1 -1
  39. package/lib/cjs/orm.js +156 -150
  40. package/lib/cjs/orm.js.map +1 -1
  41. package/lib/cjs/quoting.d.ts +33 -0
  42. package/lib/cjs/quoting.d.ts.map +1 -0
  43. package/lib/cjs/quoting.js +48 -0
  44. package/lib/cjs/quoting.js.map +1 -0
  45. package/lib/cjs/source-file.d.ts +10 -0
  46. package/lib/cjs/source-file.d.ts.map +1 -0
  47. package/lib/cjs/source-file.js +41 -0
  48. package/lib/cjs/source-file.js.map +1 -0
  49. package/lib/cjs/statements.d.ts +51 -1
  50. package/lib/cjs/statements.d.ts.map +1 -1
  51. package/lib/cjs/statements.js +73 -1
  52. package/lib/cjs/statements.js.map +1 -1
  53. package/lib/cjs/symbols.d.ts +11 -0
  54. package/lib/cjs/symbols.d.ts.map +1 -1
  55. package/lib/cjs/symbols.js +12 -1
  56. package/lib/cjs/symbols.js.map +1 -1
  57. package/lib/mjs/config/orm.d.ts +24 -0
  58. package/lib/mjs/config/orm.d.ts.map +1 -0
  59. package/lib/mjs/config/orm.js +24 -0
  60. package/lib/mjs/config/orm.js.map +1 -0
  61. package/lib/mjs/decorators.d.ts +3 -2
  62. package/lib/mjs/decorators.d.ts.map +1 -1
  63. package/lib/mjs/decorators.js +19 -2
  64. package/lib/mjs/decorators.js.map +1 -1
  65. package/lib/mjs/descriptor.d.ts +29 -1
  66. package/lib/mjs/descriptor.d.ts.map +1 -1
  67. package/lib/mjs/descriptor.js +36 -1
  68. package/lib/mjs/descriptor.js.map +1 -1
  69. package/lib/mjs/index.d.ts +6 -0
  70. package/lib/mjs/index.d.ts.map +1 -1
  71. package/lib/mjs/index.js +6 -0
  72. package/lib/mjs/index.js.map +1 -1
  73. package/lib/mjs/interfaces.d.ts +52 -2
  74. package/lib/mjs/interfaces.d.ts.map +1 -1
  75. package/lib/mjs/interfaces.js +5 -0
  76. package/lib/mjs/interfaces.js.map +1 -1
  77. package/lib/mjs/migration-environment.d.ts +70 -0
  78. package/lib/mjs/migration-environment.d.ts.map +1 -0
  79. package/lib/mjs/migration-environment.js +128 -0
  80. package/lib/mjs/migration-environment.js.map +1 -0
  81. package/lib/mjs/migration-runner.d.ts +108 -0
  82. package/lib/mjs/migration-runner.d.ts.map +1 -0
  83. package/lib/mjs/migration-runner.js +211 -0
  84. package/lib/mjs/migration-runner.js.map +1 -0
  85. package/lib/mjs/migration-service.d.ts +300 -0
  86. package/lib/mjs/migration-service.d.ts.map +1 -0
  87. package/lib/mjs/migration-service.js +824 -0
  88. package/lib/mjs/migration-service.js.map +1 -0
  89. package/lib/mjs/migration-sources.d.ts +85 -0
  90. package/lib/mjs/migration-sources.d.ts.map +1 -0
  91. package/lib/mjs/migration-sources.js +236 -0
  92. package/lib/mjs/migration-sources.js.map +1 -0
  93. package/lib/mjs/orm.d.ts +77 -15
  94. package/lib/mjs/orm.d.ts.map +1 -1
  95. package/lib/mjs/orm.js +158 -152
  96. package/lib/mjs/orm.js.map +1 -1
  97. package/lib/mjs/quoting.d.ts +33 -0
  98. package/lib/mjs/quoting.d.ts.map +1 -0
  99. package/lib/mjs/quoting.js +45 -0
  100. package/lib/mjs/quoting.js.map +1 -0
  101. package/lib/mjs/source-file.d.ts +10 -0
  102. package/lib/mjs/source-file.d.ts.map +1 -0
  103. package/lib/mjs/source-file.js +38 -0
  104. package/lib/mjs/source-file.js.map +1 -0
  105. package/lib/mjs/statements.d.ts +51 -1
  106. package/lib/mjs/statements.d.ts.map +1 -1
  107. package/lib/mjs/statements.js +71 -0
  108. package/lib/mjs/statements.js.map +1 -1
  109. package/lib/mjs/symbols.d.ts +11 -0
  110. package/lib/mjs/symbols.d.ts.map +1 -1
  111. package/lib/mjs/symbols.js +11 -0
  112. package/lib/mjs/symbols.js.map +1 -1
  113. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  114. package/lib/tsconfig.mjs.tsbuildinfo +1 -1
  115. package/package.json +7 -7
@@ -0,0 +1,824 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { NewInstance } from '@spinajs/di';
11
+ import { Log, Logger } from '@spinajs/log-common';
12
+ import { createHash } from 'node:crypto';
13
+ import { hostname } from 'node:os';
14
+ import { OrmDriver } from './driver.js';
15
+ import { MigrationTransactionMode } from './interfaces.js';
16
+ import { OrmException } from './exceptions.js';
17
+ export const MIGRATION_TABLE_NAME = 'spinajs_migration';
18
+ export const MIGRATION_LOCK_POLL_INTERVAL = 500;
19
+ export const MIGRATION_LOCK_TIMEOUT = 30_000;
20
+ export const MIGRATION_LOCK_STALE_AFTER = 600_000;
21
+ /**
22
+ * How many times one `acquireLock()` call may remove a lock row it judged stale. A steal is
23
+ * not proof the row is gone - a DELETE can succeed and remove nothing - so without a cap the
24
+ * stale branch is free to warn and retry forever.
25
+ */
26
+ export const MIGRATION_LOCK_MAX_STEALS = 3;
27
+ /**
28
+ * Fingerprint of a migration's source, used to detect a migration that was edited
29
+ * after it had already been applied.
30
+ */
31
+ export function migrationChecksum(type) {
32
+ return createHash('sha256').update(type.toString()).digest('hex');
33
+ }
34
+ /**
35
+ * Per-connection migration execution contract. Configure an alternative
36
+ * implementation with db.Connections[n].Migration.Service (DI token).
37
+ */
38
+ let OrmMigrationService = class OrmMigrationService {
39
+ constructor(driver) {
40
+ this.driver = driver;
41
+ }
42
+ };
43
+ OrmMigrationService = __decorate([
44
+ NewInstance(),
45
+ __metadata("design:paramtypes", [OrmDriver])
46
+ ], OrmMigrationService);
47
+ export { OrmMigrationService };
48
+ export class DefaultMigrationService extends OrmMigrationService {
49
+ get table() {
50
+ return this.driver.Options.Migration?.Table ?? MIGRATION_TABLE_NAME;
51
+ }
52
+ get lockTable() {
53
+ return `${this.table}_lock`;
54
+ }
55
+ /**
56
+ * Creates `name` unless it is already there, tolerating a second process that creates it in
57
+ * the window between the probe and the CREATE.
58
+ *
59
+ * That window cannot be closed with a lock: the lock table is one of the tables being created
60
+ * here, so it cannot guard its own creation. Two processes booting together therefore both see
61
+ * "absent" and both issue a CREATE, and the loser must not take the whole boot down with it.
62
+ * Only a table that really is present afterwards excuses the failure - anything else ( no
63
+ * permission, bad DDL, dead connection ) is a genuine error and is rethrown.
64
+ *
65
+ * Returns true when the table was *absent at probe time* - which is not the same as "this
66
+ * process created it", since the lost-race path returns true too. Callers use it to skip the
67
+ * legacy upgrade path: a table that appeared inside the race window was created by a peer
68
+ * running this same DDL, so it already carries the current shape.
69
+ */
70
+ async createTableIfAbsent(name, columns) {
71
+ // a builder executes at most once, so every statement needs a fresh SchemaQueryBuilder
72
+ const db = this.driver.Options.Database;
73
+ if (await this.driver.schema().tableExists(name, db)) {
74
+ return false;
75
+ }
76
+ try {
77
+ await this.driver.schema().createTable(name, columns);
78
+ }
79
+ catch (err) {
80
+ if (!(await this.driver.schema().tableExists(name, db))) {
81
+ throw new OrmException(`Could not create migration table ${name} on connection ${this.driver.Options.Name}: ${err.message}`, undefined, undefined, undefined, err);
82
+ }
83
+ this.Log.trace(`Migration table ${name} on connection ${this.driver.Options.Name} was created concurrently by another process - continuing`);
84
+ }
85
+ return true;
86
+ }
87
+ async ensureStorage() {
88
+ // a builder executes at most once, so every statement needs a fresh SchemaQueryBuilder
89
+ const schema = () => this.driver.schema();
90
+ const db = this.driver.Options.Database;
91
+ // "was absent when we probed", not "we created it" - a lost race reports absent too
92
+ const wasAbsent = await this.createTableIfAbsent(this.table, (t) => {
93
+ t.string('Migration').unique().notNull();
94
+ t.dateTime('CreatedAt').notNull();
95
+ t.dateTime('StartedAt').notNull();
96
+ t.dateTime('FinishedAt');
97
+ t.dateTime('RolledBackAt');
98
+ t.text('Logs');
99
+ t.string('Checksum', 64);
100
+ t.int('Batch').notNull().default().value(1);
101
+ });
102
+ if (!wasAbsent) {
103
+ const cols = (await this.driver.tableInfo(this.table, db)) ?? [];
104
+ const has = (n) => cols.some((c) => c.Name === n);
105
+ if (!has('StartedAt') || !has('FinishedAt') || !has('RolledBackAt') || !has('Logs') || !has('Checksum') || !has('Batch')) {
106
+ await schema().alterTable(this.table, (t) => {
107
+ if (!has('StartedAt'))
108
+ t.dateTime('StartedAt').addColumn();
109
+ if (!has('FinishedAt'))
110
+ t.dateTime('FinishedAt').addColumn();
111
+ if (!has('RolledBackAt'))
112
+ t.dateTime('RolledBackAt').addColumn();
113
+ if (!has('Logs'))
114
+ t.text('Logs').addColumn();
115
+ if (!has('Checksum'))
116
+ t.string('Checksum', 64).addColumn();
117
+ if (!has('Batch')) {
118
+ // added nullable and backfilled below - a NOT NULL column cannot be bolted
119
+ // onto a table that already has rows
120
+ const batch = t.int('Batch');
121
+ batch.default().value(1);
122
+ batch.addColumn();
123
+ }
124
+ });
125
+ await this.backfillLegacyRows();
126
+ }
127
+ }
128
+ await this.createTableIfAbsent(this.lockTable, (t) => {
129
+ t.int('Id').unique().notNull();
130
+ t.dateTime('AcquiredAt').notNull();
131
+ t.string('Owner', 255).notNull();
132
+ });
133
+ }
134
+ async records() {
135
+ return ((await this.driver.select().from(this.table)) ?? []);
136
+ }
137
+ /**
138
+ * Fills the columns the upgrade above has just added. A row written before they existed carries
139
+ * nothing but `CreatedAt`, and a NULL `FinishedAt` reads as "never applied" - so without this
140
+ * every migration the deployment ran years ago would run again over a schema that already has
141
+ * it. `CreatedAt` is the only timestamp such a row has, so it is treated as both start and
142
+ * finish.
143
+ *
144
+ * Row by row through the update builder rather than as three set-based `UPDATE`s, and that is
145
+ * the point of the method: a set-based statement has to name the table itself, and the only
146
+ * way to do that here is raw SQL. `Migration.Table` is configuration - a name that needs
147
+ * quoting ( a reserved word, a dot, a space ) would then break this path alone, and only on a
148
+ * deployment that already has rows, which is the least reachable corner in the file. The
149
+ * builder quotes it exactly as every other statement in this class does. The cost is one UPDATE
150
+ * per legacy row, on the single boot that performs the upgrade and never again.
151
+ */
152
+ async backfillLegacyRows() {
153
+ for (const r of await this.records()) {
154
+ const patch = {};
155
+ // `== null` on purpose - the column is either absent from the row object ( undefined ) or
156
+ // present and NULL, depending on how the driver hydrates a freshly added column
157
+ if (r.StartedAt == null) {
158
+ patch.StartedAt = r.CreatedAt;
159
+ }
160
+ // `Logs` set with a NULL `FinishedAt` is the FAILED state. It cannot occur on a table this
161
+ // old - there was no Logs column to write - but stamping a finish over one would silently
162
+ // unblock a half-applied migration, so the guard is stated rather than assumed away.
163
+ if (r.FinishedAt == null && !r.Logs) {
164
+ patch.FinishedAt = r.CreatedAt;
165
+ }
166
+ if (r.Batch == null) {
167
+ patch.Batch = 1;
168
+ }
169
+ if (Object.keys(patch).length === 0) {
170
+ continue;
171
+ }
172
+ await this.driver.update().in(this.table).update(patch).where({ Migration: r.Migration });
173
+ }
174
+ }
175
+ /**
176
+ * Migrations that finished successfully and were not rolled back - the raw rows, unmerged with
177
+ * the registry.
178
+ *
179
+ * A convenience on this class rather than part of `OrmMigrationService`: nothing in the ORM,
180
+ * the runner or the CLI calls it, because they all need the registry merged in and go through
181
+ * `status()`. It is kept because a subclass, a script or a health check reaching for "what does
182
+ * this connection think it has applied?" should not have to reimplement the applied-gate, and
183
+ * getting that gate subtly wrong ( "a row exists" rather than the FinishedAt NOT NULL and
184
+ * RolledBackAt NULL pair ) is the classic way to re-run a migration.
185
+ */
186
+ async applied() {
187
+ return (await this.records()).filter((r) => r.FinishedAt !== null && r.FinishedAt !== undefined && !r.RolledBackAt);
188
+ }
189
+ /**
190
+ * Opens a migration's row: a fresh one, or a reset of whatever a previous failed or
191
+ * rolled-back attempt left behind.
192
+ */
193
+ async upsertStart(name, existing) {
194
+ const now = new Date();
195
+ if (existing) {
196
+ await this.driver.update().in(this.table).update({ StartedAt: now, FinishedAt: null, RolledBackAt: null, Logs: null }).where({ Migration: name });
197
+ }
198
+ else {
199
+ await this.driver.insert().into(this.table).values({ Migration: name, CreatedAt: now, StartedAt: now, FinishedAt: null, RolledBackAt: null, Logs: null, Checksum: null, Batch: 0 });
200
+ }
201
+ }
202
+ /**
203
+ * Closes a migration's row as applied. The batch number is stamped here rather than at
204
+ * insert time, so a row that never finishes carries no batch to be rolled back later.
205
+ */
206
+ async markFinished(name, batch, checksum) {
207
+ await this.driver.update().in(this.table).update({ FinishedAt: new Date(), Batch: batch, Checksum: checksum }).where({ Migration: name });
208
+ }
209
+ /**
210
+ * Records why a migration died. Failed state is `FinishedAt` NULL *and* `Logs` set - the pair
211
+ * `assertNoFailed` matches on - so this write establishes both rather than assuming the row
212
+ * already carries a NULL `FinishedAt`.
213
+ *
214
+ * It cannot assume it: a migration that was applied and later rolled back is pending again
215
+ * while still holding the old `FinishedAt`/`RolledBackAt` timestamps, and the reset
216
+ * `upsertStart` issued for the retry is inside the transaction that just unwound. Writing only
217
+ * `Logs` would leave `FinishedAt` set, and a half-applied migration would slip past the block.
218
+ */
219
+ async markFailed(name, err) {
220
+ await this.driver
221
+ .update()
222
+ .in(this.table)
223
+ .update({ Logs: `${err.message}\n${err.stack ?? ''}`, FinishedAt: null, RolledBackAt: null })
224
+ .where({ Migration: name });
225
+ }
226
+ /**
227
+ * A half-applied migration means the database is in a state nobody described. Refuse to
228
+ * pile more schema changes on top of it.
229
+ */
230
+ assertNoFailed(records) {
231
+ const failed = records.find((r) => !r.FinishedAt && r.Logs);
232
+ if (failed) {
233
+ throw new OrmException(`Migration ${failed.Migration} on connection ${this.driver.Options.Name} failed previously and blocks migration runs. Inspect Logs column, fix the database manually, then run orm.Migration.resolve('${failed.Migration}', 'applied') or ('rolled-back'). That call reaches registered migrations only - if this one's class is gone, remove its row from ${this.table} by hand instead.`);
234
+ }
235
+ }
236
+ /**
237
+ * The shape of a row whose run never reached an outcome: `StartedAt`, written by `upsertStart`,
238
+ * and neither of the two writes that close it - `markFinished`'s `FinishedAt` or `markFailed`'s
239
+ * `Logs`. Nothing in this class produces it deliberately; a process killed between the start and
240
+ * the outcome does.
241
+ *
242
+ * `RolledBackAt` is excluded on purpose. `resolve('rolled-back')` also leaves `FinishedAt` and
243
+ * `Logs` NULL with `StartedAt` set, and that row is pending because somebody said so - not
244
+ * abandoned.
245
+ *
246
+ * The predicate says nothing about how much of the migration reached the database. It says only
247
+ * that nobody recorded the answer, which is exactly why it is worth surfacing.
248
+ */
249
+ isInterrupted(rec) {
250
+ return !!rec.StartedAt && !rec.FinishedAt && !rec.Logs && !rec.RolledBackAt;
251
+ }
252
+ /**
253
+ * Is a migration run in flight on this connection right now? Read, never acquired: the caller is
254
+ * `status()`, which must not block behind the run it is reporting on.
255
+ *
256
+ * The lock row is the only honest signal available, and it is judged exactly as `acquireLock`
257
+ * judges it - a row younger than `StaleAfter` means somebody is inside a run, an older one means
258
+ * the holder is presumed dead. Freshness rather than mere presence is what makes this usable
259
+ * here: a process killed mid-migration leaves BOTH its open tracking row and its lock row
260
+ * behind, so "a lock row exists" would hide every crash this is meant to surface, permanently.
261
+ *
262
+ * Two deliberate consequences. For `StaleAfter` after a crash the answer is "running" and the
263
+ * open row is not yet reported as interrupted - the same window in which `acquireLock` still
264
+ * waits for the holder, and with the same client-clock caveat documented there. And
265
+ * `Lock.Enabled: false` removes the signal altogether, so the answer is "not running": an open
266
+ * row then always reads as interrupted, which is right for the crash and wrong only for a report
267
+ * taken while a run is genuinely in progress.
268
+ */
269
+ async runInProgress() {
270
+ const { enabled, staleAfter } = this.lockOptions();
271
+ if (!enabled) {
272
+ return false;
273
+ }
274
+ const rows = ((await this.driver.select().from(this.lockTable).where({ Id: 1 })) ?? []);
275
+ const holder = rows[0];
276
+ if (!holder) {
277
+ return false;
278
+ }
279
+ const acquiredAt = holder.AcquiredAt instanceof Date ? holder.AcquiredAt : new Date(holder.AcquiredAt);
280
+ // NaN from an unreadable timestamp compares false, which reports "not running" - the same
281
+ // bias as everywhere else here: a warning an operator can dismiss beats a silence they cannot
282
+ return Date.now() - acquiredAt.getTime() <= staleAfter;
283
+ }
284
+ /**
285
+ * Warns about every migration this run is about to re-run whose row says a previous attempt was
286
+ * started and never closed. No lock check is needed here, unlike in `status()`: this runs inside
287
+ * `withLock`, so the only run in flight on this connection is this one.
288
+ *
289
+ * It warns rather than blocks, and that is a judgement call worth stating. The row records that
290
+ * a run STARTED, not that anything reached the database, so blocking would escalate "unknown" to
291
+ * "refuse to migrate" - and it would do so for the common, harmless shapes too: an idempotent
292
+ * `CREATE TABLE` that had not run yet, or any migration on a `PerMigration` / `PerRun`
293
+ * connection, whose transaction unwound the partial work when the process died. In those cases
294
+ * re-running from the top is exactly right, and a block would turn every OOM kill during a long
295
+ * migration into an operator ticket.
296
+ *
297
+ * The case that is genuinely dangerous is `Transaction.Mode: None` ( the default ) plus
298
+ * non-idempotent DML: half the INSERTs are already in, nothing recorded which half, and the
299
+ * re-run applies them again. Non-idempotent DDL is the recoverable version of the same thing -
300
+ * it fails, and the failed row then blocks properly. Neither is detectable from here, so the
301
+ * warning describes them and leaves the decision with the operator, who is also the only party
302
+ * that can look at the data.
303
+ */
304
+ warnOnInterrupted(records, pending) {
305
+ for (const u of pending) {
306
+ const rec = records.find((r) => r.Migration === u.name);
307
+ if (!rec || !this.isInterrupted(rec)) {
308
+ continue;
309
+ }
310
+ // rendered without `new Date(...)`: a dialect that hands timestamps back as strings would
311
+ // make that an Invalid Date on some formats, and `toISOString()` on one THROWS - out of a
312
+ // warning, which would replace the diagnosis with a crash
313
+ const started = rec.StartedAt instanceof Date ? rec.StartedAt.toISOString() : String(rec.StartedAt);
314
+ this.Log.warn(`Migration ${u.name} on connection ${this.driver.Options.Name} was STARTED and never finished - its tracking row carries StartedAt with neither FinishedAt nor Logs, so the process running it was killed mid-migration ( started ${started} ). It is being RE-RUN from the top and nothing recorded how far the first attempt got. Under Migration.Transaction.Mode None ( the default ) whatever it had already applied is still in the database: non-idempotent DDL will fail and land in the failed state, non-idempotent data changes will be applied a second time and nothing will say so. Check what the first attempt left behind, or record the truth with orm.Migration.resolve('${u.name}', 'applied') or ('rolled-back') before running again.`);
315
+ }
316
+ }
317
+ transactionMode() {
318
+ return this.driver.Options.Migration?.Transaction?.Mode ?? MigrationTransactionMode.None;
319
+ }
320
+ /**
321
+ * True when this migration must run outside any wrapping transaction ( TypeORM parity:
322
+ * `public transaction = false` on the migration class - needed for DDL that cannot be
323
+ * transacted, such as MySQL index rebuilds ).
324
+ *
325
+ * That declaration is an *instance* field, assigned in the constructor, so it never reaches
326
+ * the prototype - the resolved instance is the only place it can be read from. A prototype
327
+ * getter or a static property is honoured too, so a migration may also opt out without
328
+ * being constructed.
329
+ */
330
+ optedOutOfTransaction(u, instance) {
331
+ return instance?.transaction === false || u.type.prototype?.transaction === false || u.type?.transaction === false;
332
+ }
333
+ /**
334
+ * Advisory only: transpilation differences move the checksum as readily as an edit does,
335
+ * so this warns and never blocks.
336
+ */
337
+ warnOnChecksumDrift(u, records) {
338
+ const rec = records.find((r) => r.Migration === u.name);
339
+ if (rec?.Checksum && rec.Checksum !== migrationChecksum(u.type)) {
340
+ this.Log.warn(`Migration ${u.name} source changed since it was applied (checksum mismatch). This is advisory - transpilation differences also change the checksum.`);
341
+ }
342
+ }
343
+ lockOptions() {
344
+ const cfg = this.driver.Options.Migration?.Lock;
345
+ return {
346
+ enabled: cfg?.Enabled ?? true,
347
+ timeout: cfg?.Timeout ?? MIGRATION_LOCK_TIMEOUT,
348
+ staleAfter: cfg?.StaleAfter ?? MIGRATION_LOCK_STALE_AFTER,
349
+ };
350
+ }
351
+ /**
352
+ * Identity written into the lock row. It exists to answer "who is holding this?" when a run
353
+ * blocks, so it has to survive being read on another machine.
354
+ */
355
+ lockOwner() {
356
+ return `${hostname()}:${process.pid}`;
357
+ }
358
+ /**
359
+ * Takes the single row of the lock table, waiting for whoever has it.
360
+ *
361
+ * The row is claimed by INSERT rather than by "SELECT then INSERT": `Id` is unique, so the
362
+ * database decides the winner in one statement and two processes racing here cannot both
363
+ * succeed. A refused insert is therefore read as "somebody else holds it" - which is also why
364
+ * the holder is re-read afterwards rather than guessed at.
365
+ *
366
+ * Staleness is judged against the *client* clock: `AcquiredAt` is written here as
367
+ * `new Date()` and compared to this host's `Date.now()`. That is sound for the case this
368
+ * lock is built for - one process migrating, crashing, and restarting to find its own
369
+ * abandoned row - but on hosts whose clocks disagree the window is off by the skew, which
370
+ * shows up as stealing too early or waiting too long. Stamping `AcquiredAt` from the
371
+ * database ( a driver-level `CURRENT_TIMESTAMP` default and a server-side comparison ) would
372
+ * remove the assumption; it needs dialect support that does not exist here yet.
373
+ */
374
+ async acquireLock() {
375
+ const { timeout, staleAfter } = this.lockOptions();
376
+ const owner = this.lockOwner();
377
+ const start = Date.now();
378
+ const expired = () => Date.now() - start > timeout;
379
+ let steals = 0;
380
+ for (;;) {
381
+ try {
382
+ await this.driver.insert().into(this.lockTable).values({ Id: 1, AcquiredAt: new Date(), Owner: owner });
383
+ return;
384
+ }
385
+ catch (err) {
386
+ // deliberately unguarded: a select that dies here means the connection is gone, and that
387
+ // has to surface as itself rather than as a lock timeout thirty seconds later
388
+ const rows = ((await this.driver.select().from(this.lockTable).where({ Id: 1 })) ?? []);
389
+ const holder = rows[0];
390
+ if (holder) {
391
+ const acquiredAt = holder.AcquiredAt instanceof Date ? holder.AcquiredAt : new Date(holder.AcquiredAt);
392
+ const stale = Date.now() - acquiredAt.getTime() > staleAfter;
393
+ // A process that died mid-run leaves its row behind and nothing else will ever clear
394
+ // it. The attempts are capped because a steal is not proof the row is gone - a DELETE
395
+ // can succeed and remove nothing, and the read that follows then finds the same row.
396
+ // Uncapped, that is a sleepless loop warning once per turn and never timing out.
397
+ if (stale && steals < MIGRATION_LOCK_MAX_STEALS) {
398
+ steals++;
399
+ // Loud, because the alternative reading is a live run that outlasted StaleAfter - in
400
+ // which case two migration runs are now in flight and somebody has to know
401
+ this.Log.warn(`Stealing stale migration lock on connection ${this.driver.Options.Name}, held by ${holder.Owner} since ${acquiredAt.toISOString()} ( older than ${staleAfter}ms ). If that process is still alive, two migration runs are now in flight - raise Migration.Lock.StaleAfter above the longest run.`);
402
+ await this.driver.del().from(this.lockTable).where({ Id: 1 });
403
+ // straight back to the INSERT: the row should be free now, and sleeping a poll
404
+ // interval for a lock nobody holds is pure boot latency. Only when the deadline has
405
+ // already passed does this fall through to the throw below
406
+ if (!expired()) {
407
+ continue;
408
+ }
409
+ }
410
+ if (expired()) {
411
+ throw new OrmException(`Could not acquire migration lock on connection ${this.driver.Options.Name} within ${timeout}ms - held by ${holder.Owner} since ${acquiredAt.toISOString()}${steals > 0 ? `, and ${steals} attempt(s) to remove it as stale left it in place - delete the row from ${this.lockTable} by hand` : ''}`);
412
+ }
413
+ }
414
+ else if (expired()) {
415
+ // no row to blame, so the insert is failing for its own reasons ( missing table, lost
416
+ // connection ). Carrying that message is the only thing that makes this diagnosable
417
+ throw new OrmException(`Could not acquire migration lock on connection ${this.driver.Options.Name} within ${timeout}ms - no lock row is present, the last insert failed with: ${err.message}`, undefined, undefined, undefined, err);
418
+ }
419
+ await new Promise((r) => setTimeout(r, MIGRATION_LOCK_POLL_INTERVAL));
420
+ }
421
+ }
422
+ }
423
+ /**
424
+ * Drops the lock row unconditionally rather than only the row this process wrote. A run whose
425
+ * lock was stolen as stale would otherwise have nothing to release, and the alternative -
426
+ * deleting only `Owner = ours` - leaves the table holding a row nobody will clear if the owner
427
+ * string ever changes underneath a run. Losing a stolen lock is the lesser harm: the thief
428
+ * already assumed the run was dead.
429
+ */
430
+ async releaseLock() {
431
+ await this.driver.del().from(this.lockTable).where({ Id: 1 });
432
+ }
433
+ /**
434
+ * Concurrency guard around a whole run: one migration run per connection at a time, across
435
+ * processes. Note the release is `finally` - a run that throws must not leave the connection
436
+ * locked until the staleness window expires.
437
+ */
438
+ async withLock(fn) {
439
+ if (!this.lockOptions().enabled) {
440
+ return fn();
441
+ }
442
+ await this.acquireLock();
443
+ try {
444
+ return await fn();
445
+ }
446
+ finally {
447
+ try {
448
+ await this.releaseLock();
449
+ }
450
+ catch (err) {
451
+ // a throw out of `finally` replaces whatever the run was already throwing, and the
452
+ // migration error is the one the operator actually needs - the likeliest reason the
453
+ // release died is the same dead connection that killed the run. The lock is left behind
454
+ // instead: it goes stale and the next run steals it, which is what StaleAfter is for.
455
+ this.Log.error(`Could not release the migration lock on connection ${this.driver.Options.Name}: ${err.message}. It will block further runs until it goes stale ( Migration.Lock.StaleAfter ) or its row is deleted from ${this.lockTable} by hand.`);
456
+ }
457
+ }
458
+ }
459
+ async up(units, options) {
460
+ await this.ensureStorage();
461
+ return await this.withLock(async () => {
462
+ const records = await this.records();
463
+ this.assertNoFailed(records);
464
+ const isApplied = (n) => records.some((r) => r.Migration === n && r.FinishedAt && !r.RolledBackAt);
465
+ const pending = units.filter((u) => !isApplied(u.name));
466
+ if (pending.length === 0) {
467
+ return [];
468
+ }
469
+ // before the fake branch as well as the real one: `fake: true` on a row nobody closed
470
+ // records "applied" over a migration whose actual effect is unknown, which is the outcome
471
+ // most worth being told about
472
+ this.warnOnInterrupted(records, pending);
473
+ // Resolved up front, once each. `transaction = false` only exists on a constructed
474
+ // migration, so the segmenting below cannot be decided without instances - and resolving
475
+ // a second time at execution would build every migration twice.
476
+ const instances = new Map();
477
+ for (const u of pending) {
478
+ instances.set(u.name, await this.driver.Container.resolve(u.type, [this.driver]));
479
+ }
480
+ const instanceOf = (u) => instances.get(u.name);
481
+ const optedOut = (u) => this.optedOutOfTransaction(u, instances.get(u.name));
482
+ const batch = Math.max(0, ...records.filter((r) => r.FinishedAt && !r.RolledBackAt).map((r) => r.Batch ?? 0)) + 1;
483
+ const executed = [];
484
+ if (options?.fake) {
485
+ const stamp = new Date();
486
+ for (const u of pending) {
487
+ const existing = records.find((r) => r.Migration === u.name);
488
+ if (existing) {
489
+ await this.driver
490
+ .update()
491
+ .in(this.table)
492
+ .update({ StartedAt: stamp, FinishedAt: stamp, RolledBackAt: null, Logs: null, Batch: batch, Checksum: migrationChecksum(u.type) })
493
+ .where({ Migration: u.name });
494
+ }
495
+ else {
496
+ await this.driver
497
+ .insert()
498
+ .into(this.table)
499
+ .values({ Migration: u.name, CreatedAt: stamp, StartedAt: stamp, FinishedAt: stamp, RolledBackAt: null, Logs: null, Checksum: migrationChecksum(u.type), Batch: batch });
500
+ }
501
+ executed.push(instanceOf(u));
502
+ this.Log.info(`Migration ${u.name}: faked (recorded without executing)`);
503
+ }
504
+ return executed;
505
+ }
506
+ const startAndRun = async (u) => {
507
+ const migration = instanceOf(u);
508
+ // the snapshot taken at the top of the run is still accurate here: a unit appears at
509
+ // most once in `pending`, and nothing writes its row before this point
510
+ const existing = records.find((r) => r.Migration === u.name);
511
+ await this.upsertStart(u.name, existing);
512
+ this.warnOnChecksumDrift(u, records);
513
+ await migration.up(this.driver);
514
+ await this.markFinished(u.name, batch, migrationChecksum(u.type));
515
+ executed.push(migration);
516
+ this.Log.info(`Migration ${u.name}:up() success !`);
517
+ };
518
+ /**
519
+ * Only ever called once the transaction that was running the migration has unwound - a
520
+ * failure row written inside it would be rolled back with everything else, leaving no
521
+ * trace of what broke.
522
+ */
523
+ const recordFailure = async (name, err) => {
524
+ try {
525
+ // deliberately re-read rather than reuse the run's snapshot: this has to observe
526
+ // post-rollback state, which may have lost the row `upsertStart` inserted
527
+ const fresh = (await this.records()).find((r) => r.Migration === name);
528
+ if (!fresh) {
529
+ await this.upsertStart(name, undefined);
530
+ }
531
+ await this.markFailed(name, err);
532
+ }
533
+ catch (bookkeeping) {
534
+ // the likeliest reason up() died is a connection that is now gone, which kills this
535
+ // write too. The migration error is what the caller needs; losing it to a secondary
536
+ // failure would hide both the migration name and the root cause.
537
+ this.Log.error(`Migration ${name} on connection ${this.driver.Options.Name} failed, and the failure row could not be written: ${bookkeeping.message}. The tracking table may not reflect that this migration is half-applied - verify it by hand.`);
538
+ }
539
+ };
540
+ const failure = (name, err) => new OrmException(`Migration ${name} failed on connection ${this.driver.Options.Name}: ${err.message}`, undefined, undefined, undefined, err);
541
+ const execute = async (u, wrap) => {
542
+ try {
543
+ if (wrap) {
544
+ await this.driver.transaction(async () => {
545
+ await startAndRun(u);
546
+ });
547
+ }
548
+ else {
549
+ await startAndRun(u);
550
+ }
551
+ }
552
+ catch (err) {
553
+ await recordFailure(u.name, err);
554
+ throw failure(u.name, err);
555
+ }
556
+ };
557
+ const mode = this.transactionMode();
558
+ if (mode === MigrationTransactionMode.PerRun) {
559
+ // One transaction per run of consecutive non-opted-out migrations. An opted-out one
560
+ // splits the run rather than being silently dragged into the shared transaction.
561
+ const queue = [...pending];
562
+ while (queue.length > 0) {
563
+ const head = queue.shift();
564
+ if (optedOut(head)) {
565
+ await execute(head, false);
566
+ continue;
567
+ }
568
+ const segment = [head];
569
+ while (queue.length > 0 && !optedOut(queue[0])) {
570
+ segment.push(queue.shift());
571
+ }
572
+ let current = head;
573
+ try {
574
+ await this.driver.transaction(async () => {
575
+ for (const u of segment) {
576
+ current = u;
577
+ await startAndRun(u);
578
+ }
579
+ });
580
+ }
581
+ catch (err) {
582
+ await recordFailure(current.name, err);
583
+ throw failure(current.name, err);
584
+ }
585
+ }
586
+ }
587
+ else if (mode === MigrationTransactionMode.PerMigration) {
588
+ for (const u of pending) {
589
+ await execute(u, !optedOut(u));
590
+ }
591
+ }
592
+ else {
593
+ for (const u of pending) {
594
+ await execute(u, false);
595
+ }
596
+ }
597
+ return executed;
598
+ });
599
+ }
600
+ async down(units, options) {
601
+ await this.ensureStorage();
602
+ return await this.withLock(async () => {
603
+ const records = await this.records();
604
+ const appliedRows = records.filter((r) => r.FinishedAt && !r.RolledBackAt);
605
+ // a failed row is not applied, so it is simply absent from the set below. Deliberate -
606
+ // down() is a recovery path and blocking it would leave operators with only resolve() -
607
+ // but silently stepping around it hides that the connection stays blocked afterwards
608
+ const failedRows = records.filter((r) => !r.FinishedAt && r.Logs);
609
+ if (failedRows.length > 0) {
610
+ // every one of them has to be cleared: assertNoFailed blocks on the first failed row it
611
+ // finds, so resolving one of two leaves the connection just as blocked as before
612
+ this.Log.warn(`Migration(s) ${failedRows.map((r) => r.Migration).join(', ')} on connection ${this.driver.Options.Name} are in failed state and are skipped by this rollback - the schema may end up reverted while every later up() stays blocked. Clear each of them with orm.Migration.resolve('${failedRows[0].Migration}', 'applied') or ('rolled-back').`);
613
+ }
614
+ if (appliedRows.length === 0) {
615
+ return [];
616
+ }
617
+ // default is the last batch alone: one `up` run is one unit of work, so one `down`
618
+ // undoes exactly that run rather than the whole history
619
+ let target = appliedRows;
620
+ if (!options?.all) {
621
+ const lastBatch = Math.max(...appliedRows.map((r) => r.Batch ?? 0));
622
+ target = appliedRows.filter((r) => (r.Batch ?? 0) === lastBatch);
623
+ }
624
+ // a row with no registered unit can never be rolled back - its class is gone, so there is
625
+ // no down() to run and dropping the row alone would lie about the schema. Nothing can be
626
+ // done about it here, but returning [] without a word leaves it applied forever
627
+ const orphans = target.filter((r) => !units.some((u) => u.name === r.Migration));
628
+ if (orphans.length > 0) {
629
+ this.Log.warn(`Migration(s) ${orphans.map((r) => r.Migration).join(', ')} on connection ${this.driver.Options.Name} are recorded as applied but no registered migration matches them (file deleted or renamed). They cannot be rolled back and stay applied - restore the migration file, or remove the row by hand once the schema is undone.`);
630
+ }
631
+ // newest first - a migration has to be undone before the one it was built on top of.
632
+ // Same-timestamp migrations fall back to reverse name, mirroring the forward order
633
+ const toRun = units.filter((u) => target.some((r) => r.Migration === u.name)).sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : b.name.localeCompare(a.name)));
634
+ if (toRun.length === 0) {
635
+ return [];
636
+ }
637
+ // resolved up front, once each - `transaction = false` is an instance field, so the
638
+ // segmentation below cannot be decided without instances, and resolving again at
639
+ // execution time would construct every migration twice
640
+ const instances = new Map();
641
+ for (const u of toRun) {
642
+ instances.set(u.name, await this.driver.Container.resolve(u.type, [this.driver]));
643
+ }
644
+ const optedOut = (u) => this.optedOutOfTransaction(u, instances.get(u.name));
645
+ const executed = [];
646
+ const runOne = async (u) => {
647
+ const migration = instances.get(u.name);
648
+ if (!options?.fake) {
649
+ await migration.down(this.driver);
650
+ }
651
+ try {
652
+ // the row is dropped rather than stamped RolledBackAt: a deleted row and a
653
+ // rolled-back one are both "pending" to `up()`, and dropping keeps the table
654
+ // holding only migrations that are actually present in the database
655
+ await this.driver.del().from(this.table).where({ Migration: u.name });
656
+ }
657
+ catch (err) {
658
+ // down() already succeeded, so outside a transaction the schema is reverted while
659
+ // the row still claims the migration is applied - the one state nothing downstream
660
+ // can detect. Inside a transaction the wrapper unwinds both, so it is not a lie there
661
+ if (!options?.fake && this.driver.CurrentTransaction === undefined) {
662
+ this.Log.error(`Migration ${u.name} on connection ${this.driver.Options.Name}: down() completed but its tracking row could not be removed (${err.message}). The schema is reverted while the table still reports this migration as applied - delete the row from ${this.table} by hand, or every later up() will skip it.`);
663
+ }
664
+ throw err;
665
+ }
666
+ executed.push(migration);
667
+ this.Log.info(`Migration ${u.name}: ${options?.fake ? 'faked (record removed without executing)' : 'down() success !'}`);
668
+ };
669
+ /**
670
+ * No `Logs` row is written on a failed rollback, unlike `up()`: `Logs` set with a NULL
671
+ * `FinishedAt` is the state `assertNoFailed` blocks on, and stamping it here would take a
672
+ * connection whose only remaining recovery path is `down()` and lock that path shut too.
673
+ * The name and connection are carried on the exception instead.
674
+ */
675
+ const failure = (name, err) => new OrmException(`Migration ${name} failed to roll back on connection ${this.driver.Options.Name}: ${err.message}`, undefined, undefined, undefined, err);
676
+ const execute = async (u, wrap) => {
677
+ try {
678
+ if (wrap) {
679
+ await this.driver.transaction(async () => {
680
+ await runOne(u);
681
+ });
682
+ }
683
+ else {
684
+ await runOne(u);
685
+ }
686
+ }
687
+ catch (err) {
688
+ throw failure(u.name, err);
689
+ }
690
+ };
691
+ const mode = this.transactionMode();
692
+ if (options?.fake) {
693
+ // nothing is executed, so there is no schema change worth wrapping
694
+ for (const u of toRun) {
695
+ await execute(u, false);
696
+ }
697
+ }
698
+ else if (mode === MigrationTransactionMode.PerRun) {
699
+ // one transaction per stretch of consecutive non-opted-out migrations, exactly as `up()`
700
+ // segments. Refusing the whole rollback instead would leave a PerRun connection holding
701
+ // one `transaction = false` migration able to up() but never able to down() as a whole
702
+ const queue = [...toRun];
703
+ while (queue.length > 0) {
704
+ const head = queue.shift();
705
+ if (optedOut(head)) {
706
+ await execute(head, false);
707
+ continue;
708
+ }
709
+ const segment = [head];
710
+ while (queue.length > 0 && !optedOut(queue[0])) {
711
+ segment.push(queue.shift());
712
+ }
713
+ let current = head;
714
+ try {
715
+ await this.driver.transaction(async () => {
716
+ for (const u of segment) {
717
+ current = u;
718
+ await runOne(u);
719
+ }
720
+ });
721
+ }
722
+ catch (err) {
723
+ throw failure(current.name, err);
724
+ }
725
+ }
726
+ }
727
+ else if (mode === MigrationTransactionMode.PerMigration) {
728
+ for (const u of toRun) {
729
+ await execute(u, !optedOut(u));
730
+ }
731
+ }
732
+ else {
733
+ for (const u of toRun) {
734
+ await execute(u, false);
735
+ }
736
+ }
737
+ return executed;
738
+ });
739
+ }
740
+ async status(units) {
741
+ await this.ensureStorage();
742
+ // deliberately unlocked, unlike up()/down(): this is a read-only report and taking the lock
743
+ // would make it block for the whole Timeout behind any running migration - exactly when
744
+ // somebody is asking what is going on
745
+ const records = await this.records();
746
+ // read once for the whole report rather than per entry: every row on this connection is
747
+ // judged against the same "is anything running right now?", and asking N times would let the
748
+ // answer change halfway down a single report
749
+ const running = await this.runInProgress();
750
+ return units.map((u) => {
751
+ const rec = records.find((r) => r.Migration === u.name);
752
+ const applied = !!(rec?.FinishedAt && !rec.RolledBackAt);
753
+ const failed = !!(rec && !rec.FinishedAt && rec.Logs);
754
+ return {
755
+ name: u.name,
756
+ connection: this.driver.Options.Name,
757
+ applied,
758
+ failed,
759
+ rolledBack: !!rec?.RolledBackAt,
760
+ pending: !applied && !failed,
761
+ // a live run legitimately holds an open row, so it is only "interrupted" once nobody is
762
+ // there to close it
763
+ interrupted: !!rec && !running && this.isInterrupted(rec),
764
+ batch: rec?.Batch ?? null,
765
+ startedAt: rec?.StartedAt ?? null,
766
+ finishedAt: rec?.FinishedAt ?? null,
767
+ checksumMismatch: !!(rec?.Checksum && rec.Checksum !== migrationChecksum(u.type)),
768
+ };
769
+ });
770
+ }
771
+ async resolve(name, action, unit) {
772
+ await this.ensureStorage();
773
+ // deliberately unlocked, unlike up()/down(): this is the recovery path for a run that died,
774
+ // quite possibly while holding the lock. Blocking it behind that lock would shut the only
775
+ // door left open
776
+ const records = await this.records();
777
+ const rec = records.find((r) => r.Migration === name);
778
+ // Two shapes may be forced, and they are the two whose real outcome nobody recorded: FAILED
779
+ // ( FinishedAt NULL, Logs set ) and INTERRUPTED ( StartedAt set, neither FinishedAt nor Logs -
780
+ // a run killed before it could write either ). Anything else is healthy, rolled back or
781
+ // absent, and rewriting it would destroy state nobody asked to lose.
782
+ //
783
+ // Deliberately without a lock check, exactly as the method comment above says: this is the
784
+ // recovery path for a run that died, quite possibly still holding the lock. The cost is that
785
+ // an operator who forces a row while a run is genuinely in progress overwrites it - in a
786
+ // deployment running one migration process at a time, which is the supported shape, the run
787
+ // in progress is the operator's own.
788
+ if (!rec || (!(!rec.FinishedAt && rec.Logs) && !this.isInterrupted(rec))) {
789
+ throw new OrmException(`Migration ${name} is not in failed state on connection ${this.driver.Options.Name}, and was not interrupted either - nothing to resolve`);
790
+ }
791
+ if (action === 'applied') {
792
+ // A batch has to be stamped here, exactly as `markFinished` would have: `upsertStart`
793
+ // inserts `Batch: 0` and a first-time failure never reached `markFinished`, so a row
794
+ // resolved without this stays at 0. `down()`'s default target is max(Batch) among applied
795
+ // rows, so a batch-0 row is silently excluded from every default rollback the moment any
796
+ // other row carries a real batch - reachable only via `{ all: true }`. Same rule as `up()`:
797
+ // one past the highest applied batch, which also puts it in the next default rollback.
798
+ const batch = Math.max(0, ...records.filter((r) => r.FinishedAt && !r.RolledBackAt).map((r) => r.Batch ?? 0)) + 1;
799
+ const patch = { FinishedAt: new Date(), Batch: batch };
800
+ // The checksum can only come from the migration class, which this call does not have
801
+ // unless the caller supplies it. Without it the column stays NULL and drift can never be
802
+ // reported for this migration - a NULL is preferable to inventing a fingerprint for
803
+ // source that was never verified against the database.
804
+ if (unit) {
805
+ patch.Checksum = migrationChecksum(unit.type);
806
+ }
807
+ await this.driver.update().in(this.table).update(patch).where({ Migration: name });
808
+ this.Log.info(`Migration ${name} resolved as applied (batch ${batch})${unit ? '' : ' - Checksum left NULL, drift cannot be detected for it'}`);
809
+ }
810
+ else {
811
+ // Logs is cleared, not merely annotated with RolledBackAt. Failed state is the pair
812
+ // `FinishedAt` NULL *and* `Logs` set, and a rolled-back resolution leaves FinishedAt
813
+ // NULL - so a row that kept its Logs would still trip `assertNoFailed` and block every
814
+ // later run, which is the exact thing this call exists to undo.
815
+ await this.driver.update().in(this.table).update({ RolledBackAt: new Date(), Logs: null }).where({ Migration: name });
816
+ this.Log.info(`Migration ${name} resolved as rolled-back (pending again)`);
817
+ }
818
+ }
819
+ }
820
+ __decorate([
821
+ Logger('ORM'),
822
+ __metadata("design:type", Log)
823
+ ], DefaultMigrationService.prototype, "Log", void 0);
824
+ //# sourceMappingURL=migration-service.js.map