@proteinjs/db-driver-spanner 1.18.0 → 1.20.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/generated/test/index.js +1 -1
  5. package/dist/generated/test/index.js.map +1 -1
  6. package/dist/src/SpannerDriver.d.ts +52 -3
  7. package/dist/src/SpannerDriver.d.ts.map +1 -1
  8. package/dist/src/SpannerDriver.js +141 -33
  9. package/dist/src/SpannerDriver.js.map +1 -1
  10. package/dist/src/SpannerSchemaOperations.d.ts +9 -1
  11. package/dist/src/SpannerSchemaOperations.d.ts.map +1 -1
  12. package/dist/src/SpannerSchemaOperations.js +88 -124
  13. package/dist/src/SpannerSchemaOperations.js.map +1 -1
  14. package/dist/test/BatchedDdl.test.d.ts +2 -0
  15. package/dist/test/BatchedDdl.test.d.ts.map +1 -0
  16. package/dist/test/BatchedDdl.test.js +496 -0
  17. package/dist/test/BatchedDdl.test.js.map +1 -0
  18. package/dist/test/DmlRetrySafety.test.d.ts +2 -0
  19. package/dist/test/DmlRetrySafety.test.d.ts.map +1 -0
  20. package/dist/test/DmlRetrySafety.test.js +472 -0
  21. package/dist/test/DmlRetrySafety.test.js.map +1 -0
  22. package/dist/test/RecordIterator.test.d.ts +2 -0
  23. package/dist/test/RecordIterator.test.d.ts.map +1 -0
  24. package/dist/test/RecordIterator.test.js +14 -0
  25. package/dist/test/RecordIterator.test.js.map +1 -0
  26. package/dist/test/RunPendingMigrations.test.d.ts +2 -0
  27. package/dist/test/RunPendingMigrations.test.d.ts.map +1 -0
  28. package/dist/test/RunPendingMigrations.test.js +354 -0
  29. package/dist/test/RunPendingMigrations.test.js.map +1 -0
  30. package/dist/test/SpannerOperationDeadline.test.js +1 -1
  31. package/dist/test/SpannerOperationDeadline.test.js.map +1 -1
  32. package/generated/index.ts +1 -1
  33. package/generated/test/index.ts +1 -1
  34. package/package.json +6 -6
  35. package/src/SpannerDriver.ts +110 -18
  36. package/src/SpannerSchemaOperations.ts +69 -55
  37. package/test/BatchedDdl.test.ts +293 -0
  38. package/test/DmlRetrySafety.test.ts +305 -0
  39. package/test/RecordIterator.test.ts +16 -0
  40. package/test/RunPendingMigrations.test.ts +201 -0
  41. package/test/SpannerOperationDeadline.test.ts +1 -1
@@ -0,0 +1,201 @@
1
+ import { SpannerDriver } from '@proteinjs/db-driver-spanner';
2
+ import { getDbAsSystem, Migration, MigrationRunner, MigrationTable, SourceRecordRepo, Table } from '@proteinjs/db';
3
+ import { registerTestUser, clearTestUser } from '@proteinjs/db/test';
4
+ import moment from 'moment';
5
+ import { getDropTestTable } from './util/getDropTestTable';
6
+ import { SpannerEmulatorProvisioner } from './util/SpannerEmulatorProvisioner';
7
+ import '../generated/test/index';
8
+
9
+ /**
10
+ * MigrationRunner.runPendingMigrations over the real stack (plans/POST_RELEASE_QUEUE.md 27f):
11
+ * the deploy-gated series the migration Job runs before a rollout advances.
12
+ *
13
+ * Outcome pins, on the ledger itself (rows written, run effects observed — not interactions):
14
+ * - SERIES ORDER: oldest-first by the ledger row's `created`, id tiebreak — regardless of
15
+ * insert order.
16
+ * - MANUAL EXCLUSION: `manual: true` migrations never run in the series, and the
17
+ * Migrations-page flow (`runMigration`) still runs them — the flag excludes, it does not
18
+ * disable.
19
+ * - FAILURE SURFACING: the first failure stops the series; the failed id + not-attempted tail
20
+ * are reported so the Job can fail the deploy; a later series retries the failed row and
21
+ * finishes the tail.
22
+ * - HISTORY HONESTY: ledger rows whose source loader is gone (the table keeps history) are
23
+ * skipped and reported as unresolved, never crash the series.
24
+ *
25
+ * All series runs execute SESSIONLESS (no test user registered) — the deploy Job has no user
26
+ * session, and UserAuth is fail-closed: these tests passing IS the pin that the series records
27
+ * through the system db.
28
+ */
29
+
30
+ const spannerDriver = new SpannerDriver({
31
+ projectId: 'proteinjs-test',
32
+ instanceName: 'proteinjs-test',
33
+ databaseName: 'test',
34
+ });
35
+
36
+ describe('MigrationRunner.runPendingMigrations (spanner)', () => {
37
+ const migrationTable = new MigrationTable() as Table<Migration>;
38
+ const dropTable = getDropTestTable(spannerDriver);
39
+ const sourceRecordRepo = new SourceRecordRepo();
40
+ const base = moment('2026-01-01T00:00:00Z');
41
+ let runLog: string[] = [];
42
+
43
+ /** A source-backed migration whose execution lands in runLog — order is observable. */
44
+ const plantMigration = (id: string, overrides: Partial<Migration> = {}): Migration => {
45
+ const migration = {
46
+ id,
47
+ description: `pending-series test migration ${id}`,
48
+ run: async () => {
49
+ runLog.push(id);
50
+ return `${id} output`;
51
+ },
52
+ ...overrides,
53
+ } as unknown as Migration;
54
+ sourceRecordRepo.loadSourceRecord(migrationTable.name, migration);
55
+ return migration;
56
+ };
57
+
58
+ /** Mirrors SourceRecordLoader's insert (system path) with an explicit ledger `created`. */
59
+ const insertLedgerRow = async (migration: Migration, createdOffsetMinutes: number) => {
60
+ await getDbAsSystem().insert(migrationTable, {
61
+ ...migration,
62
+ created: moment(base).add(createdOffsetMinutes, 'minutes'),
63
+ } as any);
64
+ };
65
+
66
+ beforeAll(async () => {
67
+ await SpannerEmulatorProvisioner.ensureProvisioned({
68
+ projectId: 'proteinjs-test',
69
+ instanceName: 'proteinjs-test',
70
+ databaseName: 'test',
71
+ });
72
+ }, 60000);
73
+
74
+ beforeEach(async () => {
75
+ // Fresh ledger per test: runPendingMigrations sweeps the WHOLE table, so each scenario owns
76
+ // its rows outright. Planted source records from earlier tests stay in the repo harmlessly —
77
+ // discovery is ledger-driven, a source record without a row is invisible.
78
+ runLog = [];
79
+ await dropTable(migrationTable);
80
+ await spannerDriver.getTableManager().loadTable(migrationTable);
81
+ }, 60000);
82
+
83
+ afterAll(async () => {
84
+ await dropTable(migrationTable);
85
+ SpannerEmulatorProvisioner.release();
86
+ }, 30000);
87
+
88
+ test('runs the series oldest-first by ledger created, id tiebreak — not insert order', async () => {
89
+ const newest = plantMigration('series-c-newest');
90
+ const oldest = plantMigration('series-a-oldest');
91
+ const middle = plantMigration('series-b-middle');
92
+ const tieB = plantMigration('tie-b');
93
+ const tieA = plantMigration('tie-a');
94
+ // Insert order deliberately scrambled vs ledger order; the tie pair shares one created.
95
+ await insertLedgerRow(newest, 2);
96
+ await insertLedgerRow(tieB, 3);
97
+ await insertLedgerRow(oldest, 0);
98
+ await insertLedgerRow(tieA, 3);
99
+ await insertLedgerRow(middle, 1);
100
+
101
+ const summary = await new MigrationRunner().runPendingMigrations();
102
+
103
+ expect(runLog).toEqual(['series-a-oldest', 'series-b-middle', 'series-c-newest', 'tie-a', 'tie-b']);
104
+ expect(summary.applied).toEqual(runLog);
105
+ expect(summary.failed).toBeUndefined();
106
+ expect(summary.notAttempted).toEqual([]);
107
+ for (const id of summary.applied) {
108
+ const row = await getDbAsSystem().get(migrationTable, { id });
109
+ expect(row.status).toBe('success');
110
+ expect(row.output).toBe(`${id} output`);
111
+ }
112
+ }, 60000);
113
+
114
+ test('a manual migration is excluded from the series but keeps the Migrations-page flow', async () => {
115
+ // Manual and OLDEST — exclusion must come from the flag, not from ordering luck.
116
+ const manual = plantMigration('manual-backfill', { manual: true } as Partial<Migration>);
117
+ const automated = plantMigration('automated-after-manual');
118
+ await insertLedgerRow(manual, 0);
119
+ await insertLedgerRow(automated, 1);
120
+
121
+ const summary = await new MigrationRunner().runPendingMigrations();
122
+
123
+ expect(runLog).toEqual(['automated-after-manual']);
124
+ expect(summary.skippedManual).toEqual(['manual-backfill']);
125
+ expect(summary.applied).toEqual(['automated-after-manual']);
126
+ const manualRow = await getDbAsSystem().get(migrationTable, { id: 'manual-backfill' });
127
+ expect(manualRow.status).toBe('proposed');
128
+
129
+ // The page flow still runs it: the flag excludes from the auto-series, it does not disable.
130
+ registerTestUser();
131
+ try {
132
+ await new MigrationRunner().runMigration('manual-backfill');
133
+ } finally {
134
+ clearTestUser();
135
+ }
136
+ expect(runLog).toEqual(['automated-after-manual', 'manual-backfill']);
137
+ const ranManualRow = await getDbAsSystem().get(migrationTable, { id: 'manual-backfill' });
138
+ expect(ranManualRow.status).toBe('success');
139
+ }, 60000);
140
+
141
+ test('the first failure stops the series; the next series retries it and finishes the tail', async () => {
142
+ let bFixed = false;
143
+ const a = plantMigration('fail-a-ok');
144
+ const b = plantMigration('fail-b-boom', {
145
+ run: async () => {
146
+ if (!bFixed) {
147
+ throw new Error('b blew up mid-run');
148
+ }
149
+ runLog.push('fail-b-boom');
150
+ return 'b fixed output';
151
+ },
152
+ } as Partial<Migration>);
153
+ const c = plantMigration('fail-c-after');
154
+ await insertLedgerRow(a, 0);
155
+ await insertLedgerRow(b, 1);
156
+ await insertLedgerRow(c, 2);
157
+
158
+ const firstSeries = await new MigrationRunner().runPendingMigrations();
159
+
160
+ // a ran, b failed, c never started — the deploy gate's failure surface.
161
+ expect(runLog).toEqual(['fail-a-ok']);
162
+ expect(firstSeries.applied).toEqual(['fail-a-ok']);
163
+ expect(firstSeries.failed).toEqual({
164
+ id: 'fail-b-boom',
165
+ description: 'pending-series test migration fail-b-boom',
166
+ failureMessage: 'b blew up mid-run',
167
+ });
168
+ expect(firstSeries.notAttempted).toEqual(['fail-c-after']);
169
+ expect((await getDbAsSystem().get(migrationTable, { id: 'fail-b-boom' })).status).toBe('failure');
170
+ expect((await getDbAsSystem().get(migrationTable, { id: 'fail-c-after' })).status).toBe('proposed');
171
+
172
+ // The "fixed migration ships, deploy re-runs" path: b retried, c finally runs, a skipped.
173
+ bFixed = true;
174
+ const secondSeries = await new MigrationRunner().runPendingMigrations();
175
+
176
+ expect(runLog).toEqual(['fail-a-ok', 'fail-b-boom', 'fail-c-after']);
177
+ expect(secondSeries.applied).toEqual(['fail-b-boom', 'fail-c-after']);
178
+ expect(secondSeries.alreadyApplied).toEqual(['fail-a-ok']);
179
+ expect(secondSeries.failed).toBeUndefined();
180
+ expect((await getDbAsSystem().get(migrationTable, { id: 'fail-c-after' })).status).toBe('success');
181
+ }, 60000);
182
+
183
+ test('ledger rows without a source record are history — skipped, reported, untouched', async () => {
184
+ const live = plantMigration('live-beside-history');
185
+ await insertLedgerRow(live, 1);
186
+ // A row whose loader was deleted after it ran in some past release
187
+ // (doNotDeleteSourceRecordsFromDb keeps it in the ledger forever).
188
+ await getDbAsSystem().insert(migrationTable, {
189
+ id: 'ghost-history',
190
+ description: 'loader deleted after an old release ran it',
191
+ created: moment(base),
192
+ } as any);
193
+
194
+ const summary = await new MigrationRunner().runPendingMigrations();
195
+
196
+ expect(summary.unresolved).toEqual(['ghost-history']);
197
+ expect(summary.applied).toEqual(['live-beside-history']);
198
+ expect(summary.failed).toBeUndefined();
199
+ expect((await getDbAsSystem().get(migrationTable, { id: 'ghost-history' })).status).toBe('proposed');
200
+ }, 60000);
201
+ });
@@ -87,7 +87,7 @@ describe('Spanner op deadlines', () => {
87
87
  // function must therefore be deadline-bounded, or the session leaks forever.
88
88
  const released: string[] = [];
89
89
  const transaction = {
90
- runUpdate: jest.fn(() => hang()),
90
+ batchUpdate: jest.fn(() => hang()),
91
91
  rollback: jest.fn(() => hang()),
92
92
  commit: jest.fn(() => hang()),
93
93
  };