@proteinjs/db-driver-spanner 1.20.1 → 1.20.3
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,28 @@
|
|
|
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.20.3](https://github.com/proteinjs/db/compare/@proteinjs/db-driver-spanner@1.20.2...@proteinjs/db-driver-spanner@1.20.3) (2026-08-15)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* reconcile re-read rides out brief INFORMATION_SCHEMA propagation (bounded retry) ([0f4739b](https://github.com/proteinjs/db/commit/0f4739bb2f233d9dff462cf7ec74777f53ea0de6))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [1.20.2](https://github.com/proteinjs/db/compare/@proteinjs/db-driver-spanner@1.20.1...@proteinjs/db-driver-spanner@1.20.2) (2026-08-15)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* schema reconcile logs the concurrent-ALREADY_EXISTS tolerance loudly (WARN) ([8e95c92](https://github.com/proteinjs/db/commit/8e95c92c3757bd577048a269e28a072a2e045208))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [1.20.1](https://github.com/proteinjs/db/compare/@proteinjs/db-driver-spanner@1.20.0...@proteinjs/db-driver-spanner@1.20.1) (2026-08-15)
|
|
7
29
|
|
|
8
30
|
|
|
@@ -143,6 +143,15 @@ var internals = function (tableManager) { return tableManager; };
|
|
|
143
143
|
var classifier = function (tableManager) {
|
|
144
144
|
return tableManager.schemaOperations;
|
|
145
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Spy on THIS TableManager instance's own logger.warn. TableManager (@proteinjs/db) and this test
|
|
148
|
+
* (@proteinjs/db-driver-spanner) resolve separate physical @proteinjs/logger copies, so a
|
|
149
|
+
* Logger.prototype spy here would never intercept the reconcile's warns — spying the instance's
|
|
150
|
+
* method does, regardless of module identity.
|
|
151
|
+
*/
|
|
152
|
+
var spyOnReconcileWarn = function (tableManager) {
|
|
153
|
+
return jest.spyOn(tableManager.logger, 'warn');
|
|
154
|
+
};
|
|
146
155
|
describe('Concurrent schema reconcile', function () {
|
|
147
156
|
var dropTable = (0, getDropTestTable_1.getDropTestTable)(spannerDriver);
|
|
148
157
|
var tableManager = spannerDriver.getTableManager();
|
|
@@ -187,7 +196,7 @@ describe('Concurrent schema reconcile', function () {
|
|
|
187
196
|
jest.restoreAllMocks();
|
|
188
197
|
});
|
|
189
198
|
test('two actors adding the SAME new column both succeed; the column lands once with the intended type', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
190
|
-
var loserTm, realGetColumnMetadata, columnMetadata, runUpdateSchemaSpy;
|
|
199
|
+
var loserTm, realGetColumnMetadata, loserWarnSpy, toleratedWarn, columnMetadata, cleanWarnSpy, runUpdateSchemaSpy;
|
|
191
200
|
return __generator(this, function (_a) {
|
|
192
201
|
switch (_a.label) {
|
|
193
202
|
case 0:
|
|
@@ -213,6 +222,7 @@ describe('Concurrent schema reconcile', function () {
|
|
|
213
222
|
}
|
|
214
223
|
});
|
|
215
224
|
}); });
|
|
225
|
+
loserWarnSpy = spyOnReconcileWarn(loserTm);
|
|
216
226
|
// OUTCOME: the loser's Db.init-equivalent does NOT reject — the duplicate DDL error is
|
|
217
227
|
// reconciled to success. (Pre-fix, this rejects with "Duplicate column name" — the red run.)
|
|
218
228
|
return [4 /*yield*/, expect(loserTm.loadTable(grownTable())).resolves.toBeUndefined()];
|
|
@@ -220,16 +230,27 @@ describe('Concurrent schema reconcile', function () {
|
|
|
220
230
|
// OUTCOME: the loser's Db.init-equivalent does NOT reject — the duplicate DDL error is
|
|
221
231
|
// reconciled to success. (Pre-fix, this rejects with "Duplicate column name" — the red run.)
|
|
222
232
|
_a.sent();
|
|
233
|
+
toleratedWarn = loserWarnSpy.mock.calls.find(function (_a) {
|
|
234
|
+
var entry = _a[0];
|
|
235
|
+
return /\[schema reconcile\] tolerated concurrent ALREADY_EXISTS/.test(String(entry.message));
|
|
236
|
+
});
|
|
237
|
+
expect(toleratedWarn).toBeDefined();
|
|
238
|
+
expect(String(toleratedWarn[0].message)).toContain('db_test_reconcile');
|
|
223
239
|
return [4 /*yield*/, tableManager.schemaMetadata.getColumnMetadata(grownTable())];
|
|
224
240
|
case 4:
|
|
225
241
|
columnMetadata = _a.sent();
|
|
226
242
|
expect(columnMetadata['nickname']).toBeDefined();
|
|
227
243
|
expect(columnMetadata['nickname'].type).toBe('STRING(255)');
|
|
244
|
+
cleanWarnSpy = spyOnReconcileWarn(tableManager);
|
|
228
245
|
runUpdateSchemaSpy = jest.spyOn(spannerDriver, 'runUpdateSchema');
|
|
229
246
|
return [4 /*yield*/, tableManager.loadTable(grownTable())];
|
|
230
247
|
case 5:
|
|
231
248
|
_a.sent();
|
|
232
249
|
expect(runUpdateSchemaSpy).not.toHaveBeenCalled();
|
|
250
|
+
expect(cleanWarnSpy.mock.calls.filter(function (_a) {
|
|
251
|
+
var entry = _a[0];
|
|
252
|
+
return /\[schema reconcile\]/.test(String(entry.message));
|
|
253
|
+
})).toHaveLength(0);
|
|
233
254
|
return [2 /*return*/];
|
|
234
255
|
}
|
|
235
256
|
});
|
|
@@ -263,8 +284,59 @@ describe('Concurrent schema reconcile', function () {
|
|
|
263
284
|
}
|
|
264
285
|
});
|
|
265
286
|
}); }, 60000);
|
|
266
|
-
test('
|
|
287
|
+
test('PROPAGATION LAG: a briefly-stale re-read retries and succeeds instead of false-negative re-throwing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
267
288
|
var duplicateError, error_1;
|
|
289
|
+
return __generator(this, function (_a) {
|
|
290
|
+
switch (_a.label) {
|
|
291
|
+
case 0: return [4 /*yield*/, tableManager.loadTable(baseTable())];
|
|
292
|
+
case 1:
|
|
293
|
+
_a.sent();
|
|
294
|
+
_a.label = 2;
|
|
295
|
+
case 2:
|
|
296
|
+
_a.trys.push([2, 4, , 5]);
|
|
297
|
+
return [4 /*yield*/, spannerDriver.runUpdateSchema('ALTER TABLE `db_test_reconcile` ADD COLUMN `name` STRING(255)')];
|
|
298
|
+
case 3:
|
|
299
|
+
_a.sent();
|
|
300
|
+
return [3 /*break*/, 5];
|
|
301
|
+
case 4:
|
|
302
|
+
error_1 = _a.sent();
|
|
303
|
+
duplicateError = error_1;
|
|
304
|
+
return [3 /*break*/, 5];
|
|
305
|
+
case 5:
|
|
306
|
+
expect(duplicateError).toBeDefined();
|
|
307
|
+
expect(classifier(tableManager).isAlreadyExistsError(duplicateError)).toBe(true);
|
|
308
|
+
// Simulate brief INFORMATION_SCHEMA propagation lag on this connection: the FIRST re-read
|
|
309
|
+
// misses the table; the next sees the true schema. ALREADY_EXISTS guarantees the conflicting
|
|
310
|
+
// DDL committed — the object EXISTS — so the reconcile must ride out the stale read (bounded
|
|
311
|
+
// retry), not false-negative re-throw: a re-throw here IS the boot crash this fix closes.
|
|
312
|
+
jest.spyOn(tableManager.schemaMetadata, 'tableExists').mockImplementationOnce(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
313
|
+
return [2 /*return*/, false];
|
|
314
|
+
}); }); });
|
|
315
|
+
return [4 /*yield*/, expect(internals(tableManager).reconcileConcurrentSchemaChange([baseTable()], duplicateError)).resolves.toBeUndefined()];
|
|
316
|
+
case 6:
|
|
317
|
+
_a.sent();
|
|
318
|
+
return [2 /*return*/];
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}); }, 60000);
|
|
322
|
+
test('BOUNDED RETRY: a definition that never becomes visible exhausts the retries and re-throws', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
323
|
+
var ghostDuplicate;
|
|
324
|
+
return __generator(this, function (_a) {
|
|
325
|
+
switch (_a.label) {
|
|
326
|
+
case 0:
|
|
327
|
+
ghostDuplicate = {
|
|
328
|
+
code: 9,
|
|
329
|
+
message: '9 FAILED_PRECONDITION: Duplicate name in schema: db_test_reconcile.',
|
|
330
|
+
};
|
|
331
|
+
return [4 /*yield*/, expect(internals(tableManager).reconcileConcurrentSchemaChange([baseTable()], ghostDuplicate)).rejects.toBe(ghostDuplicate)];
|
|
332
|
+
case 1:
|
|
333
|
+
_a.sent();
|
|
334
|
+
return [2 /*return*/];
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}); }, 60000);
|
|
338
|
+
test('GENUINE CONFLICT: an already-exists error whose live definition differs from intent STILL throws', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
339
|
+
var duplicateError, error_2, warnSpy, conflictWarn;
|
|
268
340
|
return __generator(this, function (_a) {
|
|
269
341
|
switch (_a.label) {
|
|
270
342
|
case 0:
|
|
@@ -281,27 +353,28 @@ describe('Concurrent schema reconcile', function () {
|
|
|
281
353
|
_a.sent();
|
|
282
354
|
return [3 /*break*/, 5];
|
|
283
355
|
case 4:
|
|
284
|
-
|
|
285
|
-
duplicateError =
|
|
356
|
+
error_2 = _a.sent();
|
|
357
|
+
duplicateError = error_2;
|
|
286
358
|
return [3 /*break*/, 5];
|
|
287
359
|
case 5:
|
|
288
360
|
expect(duplicateError).toBeDefined();
|
|
289
361
|
expect(classifier(tableManager).isAlreadyExistsError(duplicateError)).toBe(true);
|
|
290
|
-
|
|
291
|
-
// so reconcile must RETHROW the original error rather than mask a genuine conflict.
|
|
292
|
-
// (Bite: turn reconcile into a blanket swallow and this rejection disappears.)
|
|
362
|
+
warnSpy = spyOnReconcileWarn(tableManager);
|
|
293
363
|
return [4 /*yield*/, expect(internals(tableManager).reconcileConcurrentSchemaChange([conflictIntegerTable()], duplicateError)).rejects.toBe(duplicateError)];
|
|
294
364
|
case 6:
|
|
295
|
-
// ...but the live schema (qty STRING(MAX)) does NOT match the intended definition (qty INT64),
|
|
296
|
-
// so reconcile must RETHROW the original error rather than mask a genuine conflict.
|
|
297
|
-
// (Bite: turn reconcile into a blanket swallow and this rejection disappears.)
|
|
298
365
|
_a.sent();
|
|
366
|
+
conflictWarn = warnSpy.mock.calls.find(function (_a) {
|
|
367
|
+
var entry = _a[0];
|
|
368
|
+
return /\[schema reconcile\].*genuine conflict/.test(String(entry.message));
|
|
369
|
+
});
|
|
370
|
+
expect(conflictWarn).toBeDefined();
|
|
371
|
+
expect(String(conflictWarn[0].message)).toContain('db_test_reconcile_conflict');
|
|
299
372
|
return [2 /*return*/];
|
|
300
373
|
}
|
|
301
374
|
});
|
|
302
375
|
}); }, 60000);
|
|
303
376
|
test('UNRELATED ERROR CLASS: a non-already-exists DDL error propagates unchanged', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
304
|
-
var unrelatedError,
|
|
377
|
+
var unrelatedError, error_3;
|
|
305
378
|
return __generator(this, function (_a) {
|
|
306
379
|
switch (_a.label) {
|
|
307
380
|
case 0:
|
|
@@ -318,8 +391,8 @@ describe('Concurrent schema reconcile', function () {
|
|
|
318
391
|
_a.sent();
|
|
319
392
|
return [3 /*break*/, 5];
|
|
320
393
|
case 4:
|
|
321
|
-
|
|
322
|
-
unrelatedError =
|
|
394
|
+
error_3 = _a.sent();
|
|
395
|
+
unrelatedError = error_3;
|
|
323
396
|
return [3 /*break*/, 5];
|
|
324
397
|
case 5:
|
|
325
398
|
expect(unrelatedError).toBeDefined();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConcurrentSchemaReconcile.test.js","sourceRoot":"","sources":["../../test/ConcurrentSchemaReconcile.test.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ConcurrentSchemaReconcile.test.js","sourceRoot":"","sources":["../../test/ConcurrentSchemaReconcile.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.20.
|
|
3
|
+
"version": "1.20.3",
|
|
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.32.
|
|
45
|
+
"@proteinjs/db": "^1.32.3",
|
|
46
46
|
"@proteinjs/db-query": "^1.7.0",
|
|
47
|
-
"@proteinjs/db-spanner-common": "^1.3.
|
|
48
|
-
"@proteinjs/db-transaction-context": "^0.5.
|
|
47
|
+
"@proteinjs/db-spanner-common": "^1.3.11",
|
|
48
|
+
"@proteinjs/db-transaction-context": "^0.5.11",
|
|
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": "
|
|
66
|
+
"gitHead": "3fa64176929e1c9183cf3d998d9d629dcbb669b0"
|
|
67
67
|
}
|
|
@@ -90,6 +90,15 @@ const internals = (tableManager: SpannerTableManager) => tableManager as unknown
|
|
|
90
90
|
const classifier = (tableManager: SpannerTableManager) =>
|
|
91
91
|
tableManager.schemaOperations as unknown as { isAlreadyExistsError(error: unknown): boolean };
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Spy on THIS TableManager instance's own logger.warn. TableManager (@proteinjs/db) and this test
|
|
95
|
+
* (@proteinjs/db-driver-spanner) resolve separate physical @proteinjs/logger copies, so a
|
|
96
|
+
* Logger.prototype spy here would never intercept the reconcile's warns — spying the instance's
|
|
97
|
+
* method does, regardless of module identity.
|
|
98
|
+
*/
|
|
99
|
+
const spyOnReconcileWarn = (tableManager: SpannerTableManager) =>
|
|
100
|
+
jest.spyOn((tableManager as unknown as { logger: { warn: (log: { message?: unknown }) => void } }).logger, 'warn');
|
|
101
|
+
|
|
93
102
|
describe('Concurrent schema reconcile', () => {
|
|
94
103
|
const dropTable = getDropTestTable(spannerDriver);
|
|
95
104
|
const tableManager = spannerDriver.getTableManager();
|
|
@@ -129,20 +138,34 @@ describe('Concurrent schema reconcile', () => {
|
|
|
129
138
|
delete columnMetadata['nickname'];
|
|
130
139
|
return columnMetadata;
|
|
131
140
|
});
|
|
141
|
+
const loserWarnSpy = spyOnReconcileWarn(loserTm);
|
|
132
142
|
|
|
133
143
|
// OUTCOME: the loser's Db.init-equivalent does NOT reject — the duplicate DDL error is
|
|
134
144
|
// reconciled to success. (Pre-fix, this rejects with "Duplicate column name" — the red run.)
|
|
135
145
|
await expect(loserTm.loadTable(grownTable())).resolves.toBeUndefined();
|
|
136
146
|
|
|
147
|
+
// OBSERVABILITY: the tolerance fired LOUDLY (WARN), naming the table so an unexpected
|
|
148
|
+
// activation is never silent in prod.
|
|
149
|
+
const toleratedWarn = loserWarnSpy.mock.calls.find(([entry]) =>
|
|
150
|
+
/\[schema reconcile\] tolerated concurrent ALREADY_EXISTS/.test(String(entry.message))
|
|
151
|
+
);
|
|
152
|
+
expect(toleratedWarn).toBeDefined();
|
|
153
|
+
expect(String(toleratedWarn![0].message)).toContain('db_test_reconcile');
|
|
154
|
+
|
|
137
155
|
// The column exists exactly once, with the intended type (StringColumn defaults to STRING(255)).
|
|
138
156
|
const columnMetadata = await tableManager.schemaMetadata.getColumnMetadata(grownTable());
|
|
139
157
|
expect(columnMetadata['nickname']).toBeDefined();
|
|
140
158
|
expect(columnMetadata['nickname'].type).toBe('STRING(255)');
|
|
141
159
|
|
|
142
|
-
//
|
|
160
|
+
// HAPPY PATH UNTOUCHED: a clean pass (DDL succeeds, nothing thrown) issues ZERO DDL and never
|
|
161
|
+
// reaches the reconcile path, so it emits no reconcile WARN.
|
|
162
|
+
const cleanWarnSpy = spyOnReconcileWarn(tableManager);
|
|
143
163
|
const runUpdateSchemaSpy = jest.spyOn(spannerDriver, 'runUpdateSchema');
|
|
144
164
|
await tableManager.loadTable(grownTable());
|
|
145
165
|
expect(runUpdateSchemaSpy).not.toHaveBeenCalled();
|
|
166
|
+
expect(
|
|
167
|
+
cleanWarnSpy.mock.calls.filter(([entry]) => /\[schema reconcile\]/.test(String(entry.message)))
|
|
168
|
+
).toHaveLength(0);
|
|
146
169
|
}, 60000);
|
|
147
170
|
|
|
148
171
|
test('two actors creating the SAME absent table both succeed; the table lands once', async () => {
|
|
@@ -161,6 +184,43 @@ describe('Concurrent schema reconcile', () => {
|
|
|
161
184
|
expect(await tableManager.tableExists(baseTable())).toBe(true);
|
|
162
185
|
}, 60000);
|
|
163
186
|
|
|
187
|
+
test('PROPAGATION LAG: a briefly-stale re-read retries and succeeds instead of false-negative re-throwing', async () => {
|
|
188
|
+
await tableManager.loadTable(baseTable());
|
|
189
|
+
|
|
190
|
+
// A REAL duplicate error — replaying an add of the already-present `name` column.
|
|
191
|
+
let duplicateError: unknown;
|
|
192
|
+
try {
|
|
193
|
+
await spannerDriver.runUpdateSchema('ALTER TABLE `db_test_reconcile` ADD COLUMN `name` STRING(255)');
|
|
194
|
+
} catch (error) {
|
|
195
|
+
duplicateError = error;
|
|
196
|
+
}
|
|
197
|
+
expect(duplicateError).toBeDefined();
|
|
198
|
+
expect(classifier(tableManager).isAlreadyExistsError(duplicateError)).toBe(true);
|
|
199
|
+
|
|
200
|
+
// Simulate brief INFORMATION_SCHEMA propagation lag on this connection: the FIRST re-read
|
|
201
|
+
// misses the table; the next sees the true schema. ALREADY_EXISTS guarantees the conflicting
|
|
202
|
+
// DDL committed — the object EXISTS — so the reconcile must ride out the stale read (bounded
|
|
203
|
+
// retry), not false-negative re-throw: a re-throw here IS the boot crash this fix closes.
|
|
204
|
+
jest.spyOn(tableManager.schemaMetadata, 'tableExists').mockImplementationOnce(async () => false);
|
|
205
|
+
|
|
206
|
+
await expect(
|
|
207
|
+
internals(tableManager).reconcileConcurrentSchemaChange([baseTable()], duplicateError)
|
|
208
|
+
).resolves.toBeUndefined();
|
|
209
|
+
}, 60000);
|
|
210
|
+
|
|
211
|
+
test('BOUNDED RETRY: a definition that never becomes visible exhausts the retries and re-throws', async () => {
|
|
212
|
+
// `db_test_reconcile` is dropped in beforeEach and never created here — the claimed duplicate
|
|
213
|
+
// can never verify. The reconcile must give up after its bounded re-reads (not hang, not
|
|
214
|
+
// swallow) and surface the original error. (Bite: an unbounded retry would time this test out.)
|
|
215
|
+
const ghostDuplicate = {
|
|
216
|
+
code: 9,
|
|
217
|
+
message: '9 FAILED_PRECONDITION: Duplicate name in schema: db_test_reconcile.',
|
|
218
|
+
};
|
|
219
|
+
await expect(internals(tableManager).reconcileConcurrentSchemaChange([baseTable()], ghostDuplicate)).rejects.toBe(
|
|
220
|
+
ghostDuplicate
|
|
221
|
+
);
|
|
222
|
+
}, 60000);
|
|
223
|
+
|
|
164
224
|
test('GENUINE CONFLICT: an already-exists error whose live definition differs from intent STILL throws', async () => {
|
|
165
225
|
// Winner landed `qty` as STRING(MAX).
|
|
166
226
|
await tableManager.loadTable(conflictStringTable());
|
|
@@ -179,9 +239,17 @@ describe('Concurrent schema reconcile', () => {
|
|
|
179
239
|
// ...but the live schema (qty STRING(MAX)) does NOT match the intended definition (qty INT64),
|
|
180
240
|
// so reconcile must RETHROW the original error rather than mask a genuine conflict.
|
|
181
241
|
// (Bite: turn reconcile into a blanket swallow and this rejection disappears.)
|
|
242
|
+
const warnSpy = spyOnReconcileWarn(tableManager);
|
|
182
243
|
await expect(
|
|
183
244
|
internals(tableManager).reconcileConcurrentSchemaChange([conflictIntegerTable()], duplicateError)
|
|
184
245
|
).rejects.toBe(duplicateError);
|
|
246
|
+
|
|
247
|
+
// OBSERVABILITY: a genuine conflict is ALSO loud (WARN) before it re-throws.
|
|
248
|
+
const conflictWarn = warnSpy.mock.calls.find(([entry]) =>
|
|
249
|
+
/\[schema reconcile\].*genuine conflict/.test(String(entry.message))
|
|
250
|
+
);
|
|
251
|
+
expect(conflictWarn).toBeDefined();
|
|
252
|
+
expect(String(conflictWarn![0].message)).toContain('db_test_reconcile_conflict');
|
|
185
253
|
}, 60000);
|
|
186
254
|
|
|
187
255
|
test('UNRELATED ERROR CLASS: a non-already-exists DDL error propagates unchanged', async () => {
|