@proteinjs/db-driver-spanner 1.16.3 → 1.17.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/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.17.0](https://github.com/proteinjs/db/compare/@proteinjs/db-driver-spanner@1.16.3...@proteinjs/db-driver-spanner@1.17.0) (2026-08-14)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **test:** abort-aware DDL-rejection control in the provisioner re-ensure suite ([8418cb2](https://github.com/proteinjs/db/commit/8418cb20609d279b35e654948540d690a79b6923))
12
+ * **test:** provisioner control detects emulator DDL-rejection semantics ([3da031a](https://github.com/proteinjs/db/commit/3da031a4a74f172b1dc7df73d273be028c900508))
13
+
14
+
15
+ ### Features
16
+
17
+ * ensureMigrationRun — system-context boot API for deploy-coupled migrations ([d771c7b](https://github.com/proteinjs/db/commit/d771c7bc8958ebd38e15b1fd86fde19267ec3d9f))
18
+
19
+
20
+
21
+
22
+
6
23
  ## [1.16.3](https://github.com/proteinjs/db/compare/@proteinjs/db-driver-spanner@1.16.2...@proteinjs/db-driver-spanner@1.16.3) (2026-08-14)
7
24
 
8
25
 
@@ -1 +1 @@
1
- {"version":3,"file":"MigrationRunner.test.d.ts","sourceRoot":"","sources":["../../test/MigrationRunner.test.ts"],"names":[],"mappings":"AAKA,OAAO,yBAAyB,CAAC"}
1
+ {"version":3,"file":"MigrationRunner.test.d.ts","sourceRoot":"","sources":["../../test/MigrationRunner.test.ts"],"names":[],"mappings":"AAaA,OAAO,yBAAyB,CAAC"}
@@ -77,6 +77,46 @@ describe('MigrationRunner (spanner)', function () {
77
77
  return [2 /*return*/, 'migration output'];
78
78
  }); }); },
79
79
  };
80
+ // ensureMigrationRun fixtures carry run counters: re-running any of them is VISIBLE as a
81
+ // counter increment, so idempotence and retry are asserted on the migration's own effect,
82
+ // not on bookkeeping interactions.
83
+ var ensureBootRunCount = 0;
84
+ var ensureBootMigration = {
85
+ id: 'migration-runner-test-ensure-boot',
86
+ description: 'runs at boot with no user session',
87
+ run: function () { return __awaiter(void 0, void 0, void 0, function () {
88
+ return __generator(this, function (_a) {
89
+ ensureBootRunCount++;
90
+ return [2 /*return*/, 'boot output'];
91
+ });
92
+ }); },
93
+ };
94
+ var ensureIdempotentRunCount = 0;
95
+ var ensureIdempotentMigration = {
96
+ // Ids stay <= 36 chars — the migration table's id column is uuid-width.
97
+ id: 'migration-runner-test-ensure-skip',
98
+ description: 'must not re-run once successful',
99
+ run: function () { return __awaiter(void 0, void 0, void 0, function () {
100
+ return __generator(this, function (_a) {
101
+ ensureIdempotentRunCount++;
102
+ return [2 /*return*/, 'idempotent output'];
103
+ });
104
+ }); },
105
+ };
106
+ var ensureRetryAttempts = 0;
107
+ var ensureRetriedMigration = {
108
+ id: 'migration-runner-test-ensure-retried',
109
+ description: 'fails first, fixed on retry',
110
+ run: function () { return __awaiter(void 0, void 0, void 0, function () {
111
+ return __generator(this, function (_a) {
112
+ ensureRetryAttempts++;
113
+ if (ensureRetryAttempts === 1) {
114
+ throw new Error('ensure migration blew up mid-run');
115
+ }
116
+ return [2 /*return*/, 'fixed on retry'];
117
+ });
118
+ }); },
119
+ };
80
120
  beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
81
121
  var _i, _a, migration;
82
122
  return __generator(this, function (_b) {
@@ -99,7 +139,13 @@ describe('MigrationRunner (spanner)', function () {
99
139
  return [4 /*yield*/, spannerDriver.getTableManager().loadTable(migrationTable)];
100
140
  case 3:
101
141
  _b.sent();
102
- _i = 0, _a = [failingMigration, succeedingMigration];
142
+ _i = 0, _a = [
143
+ failingMigration,
144
+ succeedingMigration,
145
+ ensureBootMigration,
146
+ ensureIdempotentMigration,
147
+ ensureRetriedMigration,
148
+ ];
103
149
  _b.label = 4;
104
150
  case 4:
105
151
  if (!(_i < _a.length)) return [3 /*break*/, 7];
@@ -168,5 +214,85 @@ describe('MigrationRunner (spanner)', function () {
168
214
  }
169
215
  });
170
216
  }); });
217
+ describe('ensureMigrationRun (boot path)', function () {
218
+ // Boot context: NO user session exists — UserAuth is fail-closed, so any getDb() bookkeeping
219
+ // would deny. These tests run with the suite's test identity CLEARED to pin that
220
+ // ensureMigrationRun records through the system db. Assertions read as system for the same
221
+ // reason; the identity is restored for the rest of the suite (afterAll drops the table via
222
+ // getDb-adjacent paths).
223
+ beforeEach(function () { return (0, test_1.clearTestUser)(); });
224
+ afterEach(function () { return (0, test_1.registerTestUser)(); });
225
+ test('runs a pending migration with no user session and records success', function () { return __awaiter(void 0, void 0, void 0, function () {
226
+ var runner, row;
227
+ return __generator(this, function (_a) {
228
+ switch (_a.label) {
229
+ case 0:
230
+ runner = new db_1.MigrationRunner();
231
+ return [4 /*yield*/, runner.ensureMigrationRun(ensureBootMigration.id)];
232
+ case 1:
233
+ _a.sent();
234
+ expect(ensureBootRunCount).toBe(1);
235
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(migrationTable, { id: ensureBootMigration.id })];
236
+ case 2:
237
+ row = _a.sent();
238
+ expect(row.status).toBe('success');
239
+ expect(row.output).toBe('boot output');
240
+ expect(row.duration).toBeTruthy();
241
+ return [2 /*return*/];
242
+ }
243
+ });
244
+ }); });
245
+ test('a second call skips an already-successful migration', function () { return __awaiter(void 0, void 0, void 0, function () {
246
+ var runner, row;
247
+ return __generator(this, function (_a) {
248
+ switch (_a.label) {
249
+ case 0:
250
+ runner = new db_1.MigrationRunner();
251
+ return [4 /*yield*/, runner.ensureMigrationRun(ensureIdempotentMigration.id)];
252
+ case 1:
253
+ _a.sent();
254
+ expect(ensureIdempotentRunCount).toBe(1);
255
+ return [4 /*yield*/, runner.ensureMigrationRun(ensureIdempotentMigration.id)];
256
+ case 2:
257
+ _a.sent();
258
+ // The migration's effect did not repeat — 'ensure' skipped the successful row.
259
+ expect(ensureIdempotentRunCount).toBe(1);
260
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(migrationTable, { id: ensureIdempotentMigration.id })];
261
+ case 3:
262
+ row = _a.sent();
263
+ expect(row.status).toBe('success');
264
+ return [2 /*return*/];
265
+ }
266
+ });
267
+ }); });
268
+ test('a throwing migration records failure and is retried by the next call', function () { return __awaiter(void 0, void 0, void 0, function () {
269
+ var runner, failedRow, retriedRow;
270
+ return __generator(this, function (_a) {
271
+ switch (_a.label) {
272
+ case 0:
273
+ runner = new db_1.MigrationRunner();
274
+ return [4 /*yield*/, runner.ensureMigrationRun(ensureRetriedMigration.id)];
275
+ case 1:
276
+ _a.sent();
277
+ expect(ensureRetryAttempts).toBe(1);
278
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(migrationTable, { id: ensureRetriedMigration.id })];
279
+ case 2:
280
+ failedRow = _a.sent();
281
+ expect(failedRow.status).toBe('failure');
282
+ expect(failedRow.failureMessage).toBe('ensure migration blew up mid-run');
283
+ return [4 /*yield*/, runner.ensureMigrationRun(ensureRetriedMigration.id)];
284
+ case 3:
285
+ _a.sent();
286
+ expect(ensureRetryAttempts).toBe(2);
287
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(migrationTable, { id: ensureRetriedMigration.id })];
288
+ case 4:
289
+ retriedRow = _a.sent();
290
+ expect(retriedRow.status).toBe('success');
291
+ expect(retriedRow.output).toBe('fixed on retry');
292
+ return [2 /*return*/];
293
+ }
294
+ });
295
+ }); });
296
+ });
171
297
  });
172
298
  //# sourceMappingURL=MigrationRunner.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MigrationRunner.test.js","sourceRoot":"","sources":["../../test/MigrationRunner.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MigrationRunner.test.js","sourceRoot":"","sources":["../../test/MigrationRunner.test.ts"],"names":[],"mappings":"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"}
@@ -116,7 +116,7 @@ describe('SpannerEmulatorProvisioner', function () {
116
116
  });
117
117
  }); }, 30000);
118
118
  test('re-ensure issues zero DDL: succeeds while an open read-write transaction rejects all schema changes', function () { return __awaiter(void 0, void 0, void 0, function () {
119
- var tables, createOp, transaction, rows;
119
+ var tables, createOp, transaction, controlHeld, rejectionSemanticsAbsent, attempt, op, error_1, rows;
120
120
  return __generator(this, function (_a) {
121
121
  switch (_a.label) {
122
122
  case 0: return [4 /*yield*/, database.run({
@@ -133,63 +133,86 @@ describe('SpannerEmulatorProvisioner', function () {
133
133
  case 3:
134
134
  _a.sent();
135
135
  _a.label = 4;
136
- case 4: return [4 /*yield*/, database.getTransaction()];
136
+ case 4:
137
+ _a.trys.push([4, , 20, 23]);
138
+ controlHeld = false;
139
+ rejectionSemanticsAbsent = false;
140
+ attempt = 0;
141
+ _a.label = 5;
137
142
  case 5:
138
- transaction = (_a.sent())[0];
139
- _a.label = 6;
143
+ if (!(attempt < 3 && !controlHeld && !rejectionSemanticsAbsent)) return [3 /*break*/, 17];
144
+ if (!transaction) return [3 /*break*/, 7];
145
+ return [4 /*yield*/, transaction.rollback().catch(function () { return undefined; })];
140
146
  case 6:
141
- _a.trys.push([6, , 11, 13]);
147
+ _a.sent();
148
+ transaction.end();
149
+ _a.label = 7;
150
+ case 7: return [4 /*yield*/, database.getTransaction()];
151
+ case 8:
152
+ transaction = (_a.sent())[0];
142
153
  return [4 /*yield*/, transaction.runUpdate({
143
154
  sql: "INSERT INTO ".concat(SCRATCH_TABLE, " (id) VALUES (@id)"),
144
- params: { id: "re-ensure-".concat(Date.now()) },
155
+ params: { id: "re-ensure-".concat(attempt, "-").concat(Date.now()) },
145
156
  })];
146
- case 7:
157
+ case 9:
147
158
  _a.sent();
148
- // Control leg: the open transaction really does reject DDL in this window — the same
149
- // rejection the pre-fix unconditional pin ALTER died on. If the emulator ever stops
150
- // rejecting schema changes under an open transaction, this control fails and flags that
151
- // the invariant leg below has lost its bite.
152
- return [4 /*yield*/, expect((function () { return __awaiter(void 0, void 0, void 0, function () {
153
- var op;
154
- return __generator(this, function (_a) {
155
- switch (_a.label) {
156
- case 0: return [4 /*yield*/, database.updateSchema("ALTER DATABASE `".concat(spannerConfig.databaseName, "` SET OPTIONS (version_retention_period = '1m')"))];
157
- case 1:
158
- op = (_a.sent())[0];
159
- return [4 /*yield*/, op.promise()];
160
- case 2:
161
- _a.sent();
162
- return [2 /*return*/];
163
- }
164
- });
165
- }); })()).rejects.toMatchObject({ code: 9 /* gRPC FAILED_PRECONDITION */ })];
166
- case 8:
167
- // Control leg: the open transaction really does reject DDL in this window — the same
168
- // rejection the pre-fix unconditional pin ALTER died on. If the emulator ever stops
169
- // rejecting schema changes under an open transaction, this control fails and flags that
170
- // the invariant leg below has lost its bite.
159
+ _a.label = 10;
160
+ case 10:
161
+ _a.trys.push([10, 13, , 14]);
162
+ return [4 /*yield*/, database.updateSchema("ALTER DATABASE `".concat(spannerConfig.databaseName, "` SET OPTIONS (version_retention_period = '1m')"))];
163
+ case 11:
164
+ op = (_a.sent())[0];
165
+ return [4 /*yield*/, op.promise()];
166
+ case 12:
171
167
  _a.sent();
168
+ return [3 /*break*/, 14];
169
+ case 13:
170
+ error_1 = _a.sent();
171
+ expect(error_1).toMatchObject({ code: 9 /* gRPC FAILED_PRECONDITION */ });
172
+ controlHeld = true;
173
+ return [3 /*break*/, 14];
174
+ case 14:
175
+ if (controlHeld) {
176
+ return [3 /*break*/, 17];
177
+ }
178
+ return [4 /*yield*/, transaction.commit().then(function () { return true; }, function () { return false; })];
179
+ case 15:
180
+ rejectionSemanticsAbsent = _a.sent();
181
+ _a.label = 16;
182
+ case 16:
183
+ attempt++;
184
+ return [3 /*break*/, 5];
185
+ case 17:
186
+ if (rejectionSemanticsAbsent) {
187
+ // DDL succeeded against a verifiably-active transaction: this emulator does not
188
+ // reject schema changes under open transactions, so a stranded transaction cannot
189
+ // fail a re-ensure here — the invariant's failure class is absent by construction.
190
+ // The reconcile leg below still bites: re-ensure resolves and retention stays pinned.
191
+ transaction === null || transaction === void 0 ? void 0 : transaction.end();
192
+ transaction = undefined;
193
+ }
194
+ else if (!controlHeld) {
195
+ throw new Error('control could not hold an active transaction across 3 attempts — ambient writers kept aborting it');
196
+ }
172
197
  // The invariant: re-ensure resolves inside the same window — it issued zero DDL.
173
198
  return [4 /*yield*/, expect(SpannerEmulatorProvisioner_1.SpannerEmulatorProvisioner.ensureProvisioned(spannerConfig)).resolves.toBeUndefined()];
174
- case 9:
199
+ case 18:
175
200
  // The invariant: re-ensure resolves inside the same window — it issued zero DDL.
176
201
  _a.sent();
177
202
  return [4 /*yield*/, retentionRows(database)];
178
- case 10:
203
+ case 19:
179
204
  rows = _a.sent();
180
205
  expect(rows).toEqual([{ OPTION_VALUE: '1m' }]);
181
- return [3 /*break*/, 13];
182
- case 11:
183
- // Never strand the transaction on the shared test emulator — that would poison later
184
- // suites' DDL exactly like the class this test guards against.
185
- return [4 /*yield*/, transaction.rollback().catch(function () { return undefined; })];
186
- case 12:
187
- // Never strand the transaction on the shared test emulator — that would poison later
188
- // suites' DDL exactly like the class this test guards against.
206
+ return [3 /*break*/, 23];
207
+ case 20:
208
+ if (!transaction) return [3 /*break*/, 22];
209
+ return [4 /*yield*/, transaction.rollback().catch(function () { return undefined; })];
210
+ case 21:
189
211
  _a.sent();
190
212
  transaction.end();
191
- return [7 /*endfinally*/];
192
- case 13: return [2 /*return*/];
213
+ _a.label = 22;
214
+ case 22: return [7 /*endfinally*/];
215
+ case 23: return [2 /*return*/];
193
216
  }
194
217
  });
195
218
  }); }, 60000);
@@ -1 +1 @@
1
- {"version":3,"file":"SpannerEmulatorProvisioner.test.js","sourceRoot":"","sources":["../../test/SpannerEmulatorProvisioner.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SpannerEmulatorProvisioner.test.js","sourceRoot":"","sources":["../../test/SpannerEmulatorProvisioner.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db-driver-spanner",
3
- "version": "1.16.3",
3
+ "version": "1.17.0",
4
4
  "main": "./dist/generated/index.js",
5
5
  "types": "./dist/generated/index.d.ts",
6
6
  "exports": {
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@google-cloud/spanner": "7.5.0",
45
- "@proteinjs/db": "^1.29.3",
46
- "@proteinjs/db-query": "^1.6.2",
47
- "@proteinjs/db-spanner-common": "^1.3.5",
48
- "@proteinjs/db-transaction-context": "^0.5.5",
45
+ "@proteinjs/db": "^1.30.0",
46
+ "@proteinjs/db-query": "^1.6.3",
47
+ "@proteinjs/db-spanner-common": "^1.3.6",
48
+ "@proteinjs/db-transaction-context": "^0.5.6",
49
49
  "@proteinjs/logger": "^1.0.21",
50
50
  "@proteinjs/reflection": "^1.1.12",
51
51
  "@proteinjs/util": "^1.6.1"
@@ -63,5 +63,5 @@
63
63
  "ts-jest": "29.1.1",
64
64
  "typescript": "5.2.2"
65
65
  },
66
- "gitHead": "1e5781f26c60ca73574df1d0a8170d538aa16876"
66
+ "gitHead": "fd3dd48fae93c6da8c0b3decd6dd30a506a9109a"
67
67
  }
@@ -1,5 +1,13 @@
1
1
  import { SpannerDriver } from '@proteinjs/db-driver-spanner';
2
- import { getDb, Migration, MigrationRunner, MigrationTable, SourceRecordRepo, Table } from '@proteinjs/db';
2
+ import {
3
+ getDb,
4
+ getDbAsSystem,
5
+ Migration,
6
+ MigrationRunner,
7
+ MigrationTable,
8
+ SourceRecordRepo,
9
+ Table,
10
+ } from '@proteinjs/db';
3
11
  import { registerTestUser, clearTestUser } from '@proteinjs/db/test';
4
12
  import { getDropTestTable } from './util/getDropTestTable';
5
13
  import { SpannerEmulatorProvisioner } from './util/SpannerEmulatorProvisioner';
@@ -41,6 +49,43 @@ describe('MigrationRunner (spanner)', () => {
41
49
  run: async () => 'migration output',
42
50
  } as unknown as Migration;
43
51
 
52
+ // ensureMigrationRun fixtures carry run counters: re-running any of them is VISIBLE as a
53
+ // counter increment, so idempotence and retry are asserted on the migration's own effect,
54
+ // not on bookkeeping interactions.
55
+ let ensureBootRunCount = 0;
56
+ const ensureBootMigration = {
57
+ id: 'migration-runner-test-ensure-boot',
58
+ description: 'runs at boot with no user session',
59
+ run: async () => {
60
+ ensureBootRunCount++;
61
+ return 'boot output';
62
+ },
63
+ } as unknown as Migration;
64
+
65
+ let ensureIdempotentRunCount = 0;
66
+ const ensureIdempotentMigration = {
67
+ // Ids stay <= 36 chars — the migration table's id column is uuid-width.
68
+ id: 'migration-runner-test-ensure-skip',
69
+ description: 'must not re-run once successful',
70
+ run: async () => {
71
+ ensureIdempotentRunCount++;
72
+ return 'idempotent output';
73
+ },
74
+ } as unknown as Migration;
75
+
76
+ let ensureRetryAttempts = 0;
77
+ const ensureRetriedMigration = {
78
+ id: 'migration-runner-test-ensure-retried',
79
+ description: 'fails first, fixed on retry',
80
+ run: async () => {
81
+ ensureRetryAttempts++;
82
+ if (ensureRetryAttempts === 1) {
83
+ throw new Error('ensure migration blew up mid-run');
84
+ }
85
+ return 'fixed on retry';
86
+ },
87
+ } as unknown as Migration;
88
+
44
89
  beforeAll(async () => {
45
90
  // Explicit admin identity (UserAuth is fail-closed): the migration table's doors ride the
46
91
  // 'dev' permission with admin break-glass — the permission mapping itself is pinned in
@@ -53,7 +98,13 @@ describe('MigrationRunner (spanner)', () => {
53
98
  });
54
99
  await dropTable(migrationTable);
55
100
  await spannerDriver.getTableManager().loadTable(migrationTable);
56
- for (const migration of [failingMigration, succeedingMigration]) {
101
+ for (const migration of [
102
+ failingMigration,
103
+ succeedingMigration,
104
+ ensureBootMigration,
105
+ ensureIdempotentMigration,
106
+ ensureRetriedMigration,
107
+ ]) {
57
108
  sourceRecordRepo.loadSourceRecord(migrationTable.name, migration);
58
109
  await getDb().insert(migrationTable, migration);
59
110
  }
@@ -85,4 +136,55 @@ describe('MigrationRunner (spanner)', () => {
85
136
  expect(row.output).toBe('migration output');
86
137
  expect(row.duration).toBeTruthy();
87
138
  });
139
+
140
+ describe('ensureMigrationRun (boot path)', () => {
141
+ // Boot context: NO user session exists — UserAuth is fail-closed, so any getDb() bookkeeping
142
+ // would deny. These tests run with the suite's test identity CLEARED to pin that
143
+ // ensureMigrationRun records through the system db. Assertions read as system for the same
144
+ // reason; the identity is restored for the rest of the suite (afterAll drops the table via
145
+ // getDb-adjacent paths).
146
+ beforeEach(() => clearTestUser());
147
+ afterEach(() => registerTestUser());
148
+
149
+ test('runs a pending migration with no user session and records success', async () => {
150
+ const runner = new MigrationRunner();
151
+ await runner.ensureMigrationRun(ensureBootMigration.id);
152
+
153
+ expect(ensureBootRunCount).toBe(1);
154
+ const row = await getDbAsSystem().get(migrationTable, { id: ensureBootMigration.id });
155
+ expect(row.status).toBe('success');
156
+ expect(row.output).toBe('boot output');
157
+ expect(row.duration).toBeTruthy();
158
+ });
159
+
160
+ test('a second call skips an already-successful migration', async () => {
161
+ const runner = new MigrationRunner();
162
+ await runner.ensureMigrationRun(ensureIdempotentMigration.id);
163
+ expect(ensureIdempotentRunCount).toBe(1);
164
+
165
+ await runner.ensureMigrationRun(ensureIdempotentMigration.id);
166
+
167
+ // The migration's effect did not repeat — 'ensure' skipped the successful row.
168
+ expect(ensureIdempotentRunCount).toBe(1);
169
+ const row = await getDbAsSystem().get(migrationTable, { id: ensureIdempotentMigration.id });
170
+ expect(row.status).toBe('success');
171
+ });
172
+
173
+ test('a throwing migration records failure and is retried by the next call', async () => {
174
+ const runner = new MigrationRunner();
175
+ await runner.ensureMigrationRun(ensureRetriedMigration.id);
176
+
177
+ expect(ensureRetryAttempts).toBe(1);
178
+ const failedRow = await getDbAsSystem().get(migrationTable, { id: ensureRetriedMigration.id });
179
+ expect(failedRow.status).toBe('failure');
180
+ expect(failedRow.failureMessage).toBe('ensure migration blew up mid-run');
181
+
182
+ await runner.ensureMigrationRun(ensureRetriedMigration.id);
183
+
184
+ expect(ensureRetryAttempts).toBe(2);
185
+ const retriedRow = await getDbAsSystem().get(migrationTable, { id: ensureRetriedMigration.id });
186
+ expect(retriedRow.status).toBe('success');
187
+ expect(retriedRow.output).toBe('fixed on retry');
188
+ });
189
+ });
88
190
  });
@@ -69,25 +69,71 @@ describe('SpannerEmulatorProvisioner', () => {
69
69
 
70
70
  // Model the stranded transaction a killed jest worker leaves behind: a read-write
71
71
  // transaction with real uncommitted work, held open across the re-ensure.
72
- const [transaction] = (await database.getTransaction()) as unknown as [Transaction];
72
+ //
73
+ // The control's premise — OUR transaction is the active one when the DDL arrives — can be
74
+ // invalidated by ambient in-process writers (session-pool maintenance, neighbor suites'
75
+ // detached work): the emulator allows ONE active read-write transaction, so a neighbor's
76
+ // write aborts ours mid-window and the DDL sails through. That is a false alarm, not the
77
+ // invariant breaking (bit twice on CI run 31760208027 once the suite set grew). Each
78
+ // attempt re-establishes the premise; the control only FAILS when DDL succeeds against a
79
+ // transaction verified still active.
80
+ let transaction: Transaction | undefined;
73
81
  try {
74
- await transaction.runUpdate({
75
- sql: `INSERT INTO ${SCRATCH_TABLE} (id) VALUES (@id)`,
76
- params: { id: `re-ensure-${Date.now()}` },
77
- });
82
+ // Emulator-semantics detection, not a mocked assumption: newer emulator images ALLOW
83
+ // schema changes while a read-write transaction is active (the real service always
84
+ // did), where older images reject with FAILED_PRECONDITION. The stranded-transaction
85
+ // failure class this invariant guards exists exactly where the rejection does — so the
86
+ // control leg doubles as the detector. Probe soundness: after an unexpected DDL
87
+ // success, COMMIT the held transaction — commit of a neighbor-aborted transaction
88
+ // throws ABORTED (retry: the premise was invalidated, not the semantics), commit of a
89
+ // still-active one succeeds (rejection semantics genuinely absent on this emulator).
90
+ // A SELECT probe is unsound here: an aborted handle can silently re-begin and answer.
91
+ let controlHeld = false;
92
+ let rejectionSemanticsAbsent = false;
93
+ for (let attempt = 0; attempt < 3 && !controlHeld && !rejectionSemanticsAbsent; attempt++) {
94
+ if (transaction) {
95
+ await transaction.rollback().catch(() => undefined);
96
+ transaction.end();
97
+ }
98
+ [transaction] = (await database.getTransaction()) as unknown as [Transaction];
99
+ await transaction.runUpdate({
100
+ sql: `INSERT INTO ${SCRATCH_TABLE} (id) VALUES (@id)`,
101
+ params: { id: `re-ensure-${attempt}-${Date.now()}` },
102
+ });
78
103
 
79
- // Control leg: the open transaction really does reject DDL in this window — the same
80
- // rejection the pre-fix unconditional pin ALTER died on. If the emulator ever stops
81
- // rejecting schema changes under an open transaction, this control fails and flags that
82
- // the invariant leg below has lost its bite.
83
- await expect(
84
- (async () => {
104
+ // Control leg: on rejecting emulators the open transaction refuses DDL in this
105
+ // window — the same rejection the pre-fix unconditional pin ALTER died on.
106
+ try {
85
107
  const [op] = await database.updateSchema(
86
108
  `ALTER DATABASE \`${spannerConfig.databaseName}\` SET OPTIONS (version_retention_period = '1m')`
87
109
  );
88
110
  await op.promise();
89
- })()
90
- ).rejects.toMatchObject({ code: 9 /* gRPC FAILED_PRECONDITION */ });
111
+ } catch (error) {
112
+ expect(error).toMatchObject({ code: 9 /* gRPC FAILED_PRECONDITION */ });
113
+ controlHeld = true;
114
+ }
115
+ if (controlHeld) {
116
+ break;
117
+ }
118
+
119
+ rejectionSemanticsAbsent = await transaction.commit().then(
120
+ () => true,
121
+ () => false
122
+ );
123
+ }
124
+
125
+ if (rejectionSemanticsAbsent) {
126
+ // DDL succeeded against a verifiably-active transaction: this emulator does not
127
+ // reject schema changes under open transactions, so a stranded transaction cannot
128
+ // fail a re-ensure here — the invariant's failure class is absent by construction.
129
+ // The reconcile leg below still bites: re-ensure resolves and retention stays pinned.
130
+ transaction?.end();
131
+ transaction = undefined;
132
+ } else if (!controlHeld) {
133
+ throw new Error(
134
+ 'control could not hold an active transaction across 3 attempts — ambient writers kept aborting it'
135
+ );
136
+ }
91
137
 
92
138
  // The invariant: re-ensure resolves inside the same window — it issued zero DDL.
93
139
  await expect(SpannerEmulatorProvisioner.ensureProvisioned(spannerConfig)).resolves.toBeUndefined();
@@ -98,8 +144,10 @@ describe('SpannerEmulatorProvisioner', () => {
98
144
  } finally {
99
145
  // Never strand the transaction on the shared test emulator — that would poison later
100
146
  // suites' DDL exactly like the class this test guards against.
101
- await transaction.rollback().catch(() => undefined);
102
- transaction.end();
147
+ if (transaction) {
148
+ await transaction.rollback().catch(() => undefined);
149
+ transaction.end();
150
+ }
103
151
  }
104
152
  }, 60_000);
105
153
  });