@proteinjs/db 1.34.3 → 1.34.4

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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/generated/test/index.d.ts.map +1 -1
  5. package/dist/generated/test/index.js +3 -1
  6. package/dist/generated/test/index.js.map +1 -1
  7. package/dist/src/source/SourceRecord.d.ts +35 -2
  8. package/dist/src/source/SourceRecord.d.ts.map +1 -1
  9. package/dist/src/source/SourceRecord.js +8 -1
  10. package/dist/src/source/SourceRecord.js.map +1 -1
  11. package/dist/src/source/SourceRecordLoader.d.ts +98 -7
  12. package/dist/src/source/SourceRecordLoader.d.ts.map +1 -1
  13. package/dist/src/source/SourceRecordLoader.js +348 -93
  14. package/dist/src/source/SourceRecordLoader.js.map +1 -1
  15. package/dist/src/tables/MigrationTable.d.ts +2 -0
  16. package/dist/src/tables/MigrationTable.d.ts.map +1 -1
  17. package/dist/test/reusable/SourceRecordSyncTests.d.ts.map +1 -1
  18. package/dist/test/reusable/SourceRecordSyncTests.js +803 -27
  19. package/dist/test/reusable/SourceRecordSyncTests.js.map +1 -1
  20. package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts +16 -0
  21. package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts.map +1 -1
  22. package/dist/test/util/tables/sourceRecordSyncTestTables.js +23 -1
  23. package/dist/test/util/tables/sourceRecordSyncTestTables.js.map +1 -1
  24. package/generated/index.ts +1 -1
  25. package/generated/test/index.ts +3 -1
  26. package/package.json +4 -4
  27. package/src/source/SourceRecord.ts +42 -4
  28. package/src/source/SourceRecordLoader.ts +333 -48
  29. package/test/reusable/SourceRecordSyncTests.ts +462 -11
  30. package/test/util/tables/sourceRecordSyncTestTables.ts +20 -0
@@ -9,6 +9,7 @@ import {
9
9
  Table,
10
10
  TableWatcher,
11
11
  getDbAsSystem,
12
+ isSourceRecordTable,
12
13
  withSourceRecordColumns,
13
14
  } from '@proteinjs/db';
14
15
  import type { DefaultTransactionContextFactory } from '@proteinjs/db';
@@ -24,7 +25,11 @@ import {
24
25
  sourceRecordSyncTestTables,
25
26
  } from '../util/tables/sourceRecordSyncTestTables';
26
27
 
27
- type SourceRepositoryInternals = { objectCache: { [qualifiedName: string]: unknown[] } };
28
+ type SourceRepositoryInternals = {
29
+ objectCache: { [qualifiedName: string]: unknown[] };
30
+ namedObjectCache: { [qualifiedName: string]: { qualifiedName: string; packageName: string; object: unknown }[] };
31
+ };
32
+ type LoaderInternals = { resolveSourceVersion: (source: string) => string | undefined };
28
33
  type DbStatics = { defaultDbDriver?: DbDriver };
29
34
  type TableWatcherRunnerStatics = { tableWatcherMap?: unknown };
30
35
 
@@ -62,14 +67,45 @@ export const sourceRecordSyncTests = (
62
67
  const machineTable = sourceRecordSyncTestTables.SyncMachineAccount;
63
68
  const defaultPolicyTable = sourceRecordSyncTestTables.SyncDefaultPolicy;
64
69
  const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
70
+ const namedObjectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).namedObjectCache;
65
71
  let originalWatchers: unknown[] | undefined;
66
72
 
67
- /** Seed the boot declarations and run the sync one boot of `Db.init`'s loader leg. */
68
- const boot = async (declarations: { table: Table<any>; record: any }[]) => {
69
- objectCache()['@proteinjs/db/SourceRecordLoader'] = declarations;
70
- await new SourceRecordLoader().load();
73
+ /** The single source most of the suite boots as (multi-source tests name their own). */
74
+ const DEFAULT_TEST_SOURCE = '@test/source-a';
75
+
76
+ /**
77
+ * One boot of the loader leg as a BUILD: each declaration owned by its declaring package
78
+ * (`source`), seeded through the named-object cache the loader resolves declarations (and
79
+ * their owning packages) from. `versions` pins each package's version for the boot (the
80
+ * loader's version resolution is overridden — a package absent from the map is versionless,
81
+ * matching a build whose package.json could not be resolved).
82
+ */
83
+ const bootBuild = async (
84
+ declarations: { source: string; table: Table<any>; record: any }[],
85
+ versions: { [source: string]: string | undefined } = {}
86
+ ) => {
87
+ namedObjectCache()['@proteinjs/db/SourceRecordLoader'] = declarations.map((declaration, i) => ({
88
+ qualifiedName: `${declaration.source}/SeededLoader${i}`,
89
+ packageName: declaration.source,
90
+ object: { table: declaration.table, record: declaration.record },
91
+ }));
92
+ const loader = new SourceRecordLoader();
93
+ (loader as unknown as LoaderInternals).resolveSourceVersion = (source) =>
94
+ Object.prototype.hasOwnProperty.call(versions, source) ? versions[source] : undefined;
95
+ return await loader.load();
71
96
  };
72
97
 
98
+ /** One boot of the loader leg as a build declaring everything from a single package. */
99
+ const bootAsSource = async (source: string, declarations: { table: Table<any>; record: any }[], version?: string) =>
100
+ await bootBuild(
101
+ declarations.map((declaration) => ({ source, ...declaration })),
102
+ version === undefined ? {} : { [source]: version }
103
+ );
104
+
105
+ /** Seed the boot declarations and run the sync — one boot of `Db.init`'s loader leg. */
106
+ const boot = async (declarations: { table: Table<any>; record: any }[]) =>
107
+ await bootAsSource(DEFAULT_TEST_SOURCE, declarations);
108
+
73
109
  const machineDeclaration = (record: Partial<SyncMachineAccount> & { id: string; email: string }) => ({
74
110
  table: machineTable,
75
111
  record: { status: 'active', ...record },
@@ -95,6 +131,7 @@ export const sourceRecordSyncTests = (
95
131
 
96
132
  afterAll(async () => {
97
133
  await testEnv.afterAll();
134
+ delete namedObjectCache()['@proteinjs/db/SourceRecordLoader'];
98
135
  delete objectCache()['@proteinjs/db/SourceRecordLoader'];
99
136
  delete objectCache()['@proteinjs/db/DefaultDbDriverFactory'];
100
137
  delete objectCache()['@proteinjs/db/DefaultTransactionContextFactory'];
@@ -111,6 +148,7 @@ export const sourceRecordSyncTests = (
111
148
  const db = getDbAsSystem();
112
149
  await db.delete(machineTable, {});
113
150
  await db.delete(defaultPolicyTable, {});
151
+ await db.delete(sourceRecordSyncTestTables.InheritedStamp, {});
114
152
  RecordingMachineAccountWatcher.updates = [];
115
153
  });
116
154
 
@@ -200,10 +238,13 @@ export const sourceRecordSyncTests = (
200
238
  test('onSourceRemoved update: removed rows are flagged through Db.update (watchers fire), never deleted; re-declaring reverts', async () => {
201
239
  const db = getDbAsSystem();
202
240
  const human = await db.insert(machineTable, { email: 'human@test.local', displayName: 'A human' });
203
- await boot([machineDeclaration({ id: 'machine-1', email: 'machine@test.local' })]);
241
+ const keeper = machineDeclaration({ id: 'machine-keep', email: 'keeper@test.local' });
242
+ await boot([machineDeclaration({ id: 'machine-1', email: 'machine@test.local' }), keeper]);
204
243
 
244
+ // The source's next build drops machine-1 (while still declaring from this table —
245
+ // ownership is per source, and a source only reconciles what it still speaks for).
205
246
  RecordingMachineAccountWatcher.updates = [];
206
- await boot([]);
247
+ await boot([keeper]);
207
248
 
208
249
  const removed = await db.get(machineTable, { id: 'machine-1' });
209
250
  expect(removed).toBeDefined();
@@ -217,25 +258,408 @@ export const sourceRecordSyncTests = (
217
258
 
218
259
  // Idempotent: an already-flagged row is not re-written on the next boot.
219
260
  RecordingMachineAccountWatcher.updates = [];
220
- await boot([]);
261
+ await boot([keeper]);
221
262
  expect(RecordingMachineAccountWatcher.updates).toHaveLength(0);
222
263
 
223
264
  // Removal is reversible in source: re-declaring reverts the patch via drift reversion.
224
- await boot([machineDeclaration({ id: 'machine-1', email: 'machine@test.local' })]);
265
+ await boot([machineDeclaration({ id: 'machine-1', email: 'machine@test.local' }), keeper]);
225
266
  expect((await db.get(machineTable, { id: 'machine-1' })).status).toBe('active');
226
267
  });
227
268
 
228
269
  test(`onSourceRemoved default: removed source rows are deleted; human rows survive`, async () => {
229
270
  const db = getDbAsSystem();
230
271
  const human = await db.insert(defaultPolicyTable, { email: 'human@test.local' });
231
- await boot([{ table: defaultPolicyTable, record: { id: 'default-1', email: 'temp@test.local' } }]);
272
+ const keeper = { table: defaultPolicyTable, record: { id: 'default-keep', email: 'keeper@test.local' } };
273
+ await boot([{ table: defaultPolicyTable, record: { id: 'default-1', email: 'temp@test.local' } }, keeper]);
232
274
  expect(await db.get(defaultPolicyTable, { id: 'default-1' })).toBeDefined();
233
275
 
234
- await boot([]);
276
+ await boot([keeper]);
235
277
  expect(await db.get(defaultPolicyTable, { id: 'default-1' })).toBeUndefined();
278
+ expect(await db.get(defaultPolicyTable, { id: 'default-keep' })).toBeDefined();
236
279
  expect(await db.get(defaultPolicyTable, { id: human.id })).toBeDefined();
237
280
  });
238
281
 
282
+ /**
283
+ * Source-scoped pruning: multiple servers running DIFFERENT builds against ONE shared
284
+ * database (the thought_type dev topology — two dev servers on one Spanner database).
285
+ * Ownership grain is the declaring package: each declaration's source is the package that
286
+ * compiled it into the build. The removed-reconcile must stay authoritative WITHIN a source
287
+ * (a source still deletes declarations it itself removed) and be a strict no-op ACROSS
288
+ * sources (a boot never deletes rows owned by a package absent from its build).
289
+ */
290
+ describe('source-scoped pruning (shared DB, per-package ownership)', () => {
291
+ const decl = (id: string, email: string) => ({ table: defaultPolicyTable, record: { id, email } });
292
+
293
+ test("one source's sync never deletes rows owned by a different source", async () => {
294
+ const db = getDbAsSystem();
295
+
296
+ // Build A (e.g. this checkout's flow types) declares {a-1, a-2}; build B (the other
297
+ // checkout's media types) declares a disjoint set {b-1}.
298
+ await bootAsSource('@test/pkg-a', [decl('a-1', 'a1@test.local'), decl('a-2', 'a2@test.local')]);
299
+ await bootAsSource('@test/pkg-b', [decl('b-1', 'b1@test.local')]);
300
+
301
+ // B's boot must NOT have pruned A's rows — this is the cross-server nuke.
302
+ expect(await db.get(defaultPolicyTable, { id: 'a-1' })).toBeDefined();
303
+ expect(await db.get(defaultPolicyTable, { id: 'a-2' })).toBeDefined();
304
+ expect(await db.get(defaultPolicyTable, { id: 'b-1' })).toBeDefined();
305
+
306
+ // Each row is stamped with its owning package — the scoping the prune runs on.
307
+ expect((await db.get(defaultPolicyTable, { id: 'a-1' })).sourcePackage).toBe('@test/pkg-a');
308
+ expect((await db.get(defaultPolicyTable, { id: 'b-1' })).sourcePackage).toBe('@test/pkg-b');
309
+
310
+ // And symmetrically: A re-booting must not prune B's row.
311
+ await bootAsSource('@test/pkg-a', [decl('a-1', 'a1@test.local'), decl('a-2', 'a2@test.local')]);
312
+ expect(await db.get(defaultPolicyTable, { id: 'b-1' })).toBeDefined();
313
+ expect(await db.get(defaultPolicyTable, { id: 'a-1' })).toBeDefined();
314
+ expect(await db.get(defaultPolicyTable, { id: 'a-2' })).toBeDefined();
315
+ });
316
+
317
+ test('a source still prunes its OWN removed declarations without touching other sources', async () => {
318
+ const db = getDbAsSystem();
319
+
320
+ await bootAsSource('@test/pkg-a', [decl('a-1', 'a1@test.local'), decl('a-2', 'a2@test.local')]);
321
+ await bootAsSource('@test/pkg-b', [decl('b-1', 'b1@test.local')]);
322
+
323
+ // A's next build drops a-2: A's own removed declaration is pruned...
324
+ await bootAsSource('@test/pkg-a', [decl('a-1', 'a1@test.local')]);
325
+ expect(await db.get(defaultPolicyTable, { id: 'a-2' })).toBeUndefined();
326
+ // ...while A's still-declared row and B's row both survive.
327
+ expect(await db.get(defaultPolicyTable, { id: 'a-1' })).toBeDefined();
328
+ expect(await db.get(defaultPolicyTable, { id: 'b-1' })).toBeDefined();
329
+ });
330
+
331
+ test('a build declaring nothing for a table prunes nothing', async () => {
332
+ const db = getDbAsSystem();
333
+
334
+ await bootAsSource('@test/pkg-a', [decl('a-1', 'a1@test.local')]);
335
+ // A build whose declaring packages are all absent (or declare nothing for this table)
336
+ // makes no claim about it — pre-fix this boot deleted EVERY source-loaded row.
337
+ await bootAsSource('@test/pkg-b', []);
338
+ expect(await db.get(defaultPolicyTable, { id: 'a-1' })).toBeDefined();
339
+ });
340
+
341
+ test("onSourceRemoved update is source-scoped too: a foreign boot does not flag another source's rows", async () => {
342
+ const db = getDbAsSystem();
343
+
344
+ await bootAsSource('@test/pkg-a', [
345
+ { table: machineTable, record: { id: 'm-a', email: 'ma@test.local', status: 'active' } },
346
+ ]);
347
+ await bootAsSource('@test/pkg-b', [
348
+ { table: machineTable, record: { id: 'm-b', email: 'mb@test.local', status: 'active' } },
349
+ ]);
350
+
351
+ // B's boot must not have treated A's machine account as removed.
352
+ expect((await db.get(machineTable, { id: 'm-a' })).status).toBe('active');
353
+ expect((await db.get(machineTable, { id: 'm-b' })).status).toBe('active');
354
+
355
+ // A dropping its own declaration still deactivates it — and leaves B's untouched.
356
+ await bootAsSource('@test/pkg-a', [
357
+ { table: machineTable, record: { id: 'm-a2', email: 'ma2@test.local', status: 'active' } },
358
+ ]);
359
+ expect((await db.get(machineTable, { id: 'm-a' })).status).toBe('deactivated');
360
+ expect((await db.get(machineTable, { id: 'm-b' })).status).toBe('active');
361
+ });
362
+
363
+ test('a declaration migrating to a new source is re-owned, not pruned by its old source', async () => {
364
+ const db = getDbAsSystem();
365
+
366
+ // k-1 starts life declared by pkg-a, then moves to pkg-b (package rename/move).
367
+ await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local')]);
368
+ await bootAsSource('@test/pkg-b', [decl('k-1', 'k1@test.local')]);
369
+ // The later declaration re-owns the row in place (adoption keeps the id).
370
+ expect((await db.get(defaultPolicyTable, { id: 'k-1' })).sourcePackage).toBe('@test/pkg-b');
371
+
372
+ // pkg-a's next build no longer carries k-1 — but the row now belongs to pkg-b.
373
+ await bootAsSource('@test/pkg-a', [decl('a-x', 'ax@test.local')]);
374
+ expect(await db.get(defaultPolicyTable, { id: 'k-1' })).toBeDefined();
375
+ });
376
+
377
+ test('legacy rows (source_package NULL) are never pruned; still-declared ones are adopted and stamped', async () => {
378
+ const db = getDbAsSystem();
379
+ // Rows written before source_package existed: loaded from source, no ownership stamp.
380
+ const legacyDeclared = await db.insert(defaultPolicyTable, {
381
+ email: 'l1@test.local',
382
+ isLoadedFromSource: true,
383
+ });
384
+ const legacyForeign = await db.insert(defaultPolicyTable, {
385
+ email: 'l2@test.local',
386
+ isLoadedFromSource: true,
387
+ });
388
+
389
+ await bootAsSource('@test/pkg-a', [decl(legacyDeclared.id, 'l1@test.local')]);
390
+
391
+ // The declared legacy row converges: adopted by its declaring package and stamped.
392
+ expect((await db.get(defaultPolicyTable, { id: legacyDeclared.id })).sourcePackage).toBe('@test/pkg-a');
393
+ // The undeclared legacy row is out of prune scope — it may belong to a build that has
394
+ // not rebooted onto the stamped world yet; its owner's next boot will claim it.
395
+ expect(await db.get(defaultPolicyTable, { id: legacyForeign.id })).toBeDefined();
396
+ });
397
+
398
+ test('a declaration moving between packages WITHIN one build is re-owned in place — never deleted and re-inserted', async () => {
399
+ const db = getDbAsSystem();
400
+
401
+ // k-1 and a-other both start under pkg-a.
402
+ await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local'), decl('a-other', 'aother@test.local')]);
403
+ const before = await db.get(defaultPolicyTable, { id: 'k-1' });
404
+
405
+ // The next build moves k-1 to pkg-b while pkg-a still declares a-other — ONE boot.
406
+ // pkg-a's prune scope must not treat k-1 as removed (it is declared elsewhere in the
407
+ // build): the row survives in place, then the stamp leg re-owns it.
408
+ await bootBuild([
409
+ { source: '@test/pkg-a', ...decl('a-other', 'aother@test.local') },
410
+ { source: '@test/pkg-b', ...decl('k-1', 'k1@test.local') },
411
+ ]);
412
+
413
+ const after = await db.get(defaultPolicyTable, { id: 'k-1' });
414
+ expect(after).toBeDefined();
415
+ // Row continuity is the outcome: created unchanged means no delete+re-insert cycle
416
+ // (a re-insert restamps created), so watchers saw no delete and references held.
417
+ expect(after.created.valueOf()).toBe(before.created.valueOf());
418
+ expect(after.sourcePackage).toBe('@test/pkg-b');
419
+ expect(await db.get(defaultPolicyTable, { id: 'a-other' })).toBeDefined();
420
+ });
421
+
422
+ test('a within-build package move on an update-policy table never passes through the removed patch', async () => {
423
+ const db = getDbAsSystem();
424
+
425
+ await bootAsSource('@test/pkg-a', [
426
+ { table: machineTable, record: { id: 'm-move', email: 'mmove@test.local', status: 'active' } },
427
+ { table: machineTable, record: { id: 'm-stay', email: 'mstay@test.local', status: 'active' } },
428
+ ]);
429
+ // Runtime-owned state that must survive the move untouched (the credential stand-in).
430
+ await db.update(machineTable, { id: 'm-move', runtimeNote: 'provisioned' });
431
+
432
+ RecordingMachineAccountWatcher.updates = [];
433
+ await bootBuild([
434
+ {
435
+ source: '@test/pkg-a',
436
+ table: machineTable,
437
+ record: { id: 'm-stay', email: 'mstay@test.local', status: 'active' },
438
+ },
439
+ {
440
+ source: '@test/pkg-b',
441
+ table: machineTable,
442
+ record: { id: 'm-move', email: 'mmove@test.local', status: 'active' },
443
+ },
444
+ ]);
445
+
446
+ const moved = await db.get(machineTable, { id: 'm-move' });
447
+ expect(moved).toMatchObject({ status: 'active', runtimeNote: 'provisioned', sourcePackage: '@test/pkg-b' });
448
+ // The row never transited 'deactivated': no watcher observed the removed patch.
449
+ const deactivations = RecordingMachineAccountWatcher.updates.filter((u) => u.status === 'deactivated');
450
+ expect(deactivations).toHaveLength(0);
451
+ });
452
+
453
+ test('cross-source boots leave a natural-key-adopted row in place — adopted id preserved', async () => {
454
+ const db = getDbAsSystem();
455
+ // Hand-made row with an environment-random id, adopted by pkg-b via natural key.
456
+ const handMade = await db.insert(machineTable, { email: 'bridge2@test.local', runtimeNote: 'cred' });
457
+ await bootAsSource('@test/pkg-b', [
458
+ { table: machineTable, record: { id: 'declared-bridge2', email: 'bridge2@test.local', status: 'active' } },
459
+ ]);
460
+ expect((await db.get(machineTable, { email: 'bridge2@test.local' })).id).toBe(handMade.id);
461
+
462
+ // A DIFFERENT source booting (with its own machine declaration) must not prune, flag,
463
+ // or disturb the adopted row — same id, same runtime state, still active.
464
+ RecordingMachineAccountWatcher.updates = [];
465
+ await bootAsSource('@test/pkg-a', [
466
+ { table: machineTable, record: { id: 'm-a3', email: 'ma3@test.local', status: 'active' } },
467
+ ]);
468
+ const adopted = await db.get(machineTable, { email: 'bridge2@test.local' });
469
+ expect(adopted).toMatchObject({ id: handMade.id, status: 'active', runtimeNote: 'cred' });
470
+ const foreignTouches = RecordingMachineAccountWatcher.updates.filter((u) => u.id === handMade.id);
471
+ expect(foreignTouches).toHaveLength(0);
472
+ });
473
+
474
+ test('legacy rows (source_package NULL) are excluded from the update-policy leg too', async () => {
475
+ const db = getDbAsSystem();
476
+ // A pre-source_package row on the update-policy table: loaded from source, unstamped.
477
+ const legacy = await db.insert(machineTable, {
478
+ email: 'legacy@test.local',
479
+ status: 'active',
480
+ isLoadedFromSource: true,
481
+ });
482
+
483
+ RecordingMachineAccountWatcher.updates = [];
484
+ await bootAsSource('@test/pkg-a', [
485
+ { table: machineTable, record: { id: 'm-a4', email: 'ma4@test.local', status: 'active' } },
486
+ ]);
487
+
488
+ // NULL-stamped rows are out of BOTH reconcile legs — not deleted (covered above) and
489
+ // not flagged: the legacy machine account stays active, unobserved by watchers.
490
+ expect((await db.get(machineTable, { id: legacy.id })).status).toBe('active');
491
+ const legacyTouches = RecordingMachineAccountWatcher.updates.filter((u) => u.id === legacy.id);
492
+ expect(legacyTouches).toHaveLength(0);
493
+ });
494
+
495
+ test('a package in the build that no longer declares anything for a table prunes its rows there — dropping the last declaration is a removal', async () => {
496
+ const db = getDbAsSystem();
497
+
498
+ // pkg-a declares a-1 on the default-policy table and m-1 on the machine table; pkg-b owns b-1.
499
+ await bootBuild([
500
+ { source: '@test/pkg-a', ...decl('a-1', 'a1@test.local') },
501
+ {
502
+ source: '@test/pkg-a',
503
+ table: machineTable,
504
+ record: { id: 'm-1', email: 'm1@test.local', status: 'active' },
505
+ },
506
+ { source: '@test/pkg-b', ...decl('b-1', 'b1@test.local') },
507
+ ]);
508
+
509
+ // pkg-a's next build keeps m-1 but drops its LAST default-policy declaration. The package is
510
+ // still in the build, so it is authoritative for its own rows on every table: a-1 is removed.
511
+ await bootBuild([
512
+ {
513
+ source: '@test/pkg-a',
514
+ table: machineTable,
515
+ record: { id: 'm-1', email: 'm1@test.local', status: 'active' },
516
+ },
517
+ ]);
518
+ expect(await db.get(defaultPolicyTable, { id: 'a-1' })).toBeUndefined();
519
+ // pkg-b's row on that table is untouched, and pkg-a's still-declared machine row survives.
520
+ expect(await db.get(defaultPolicyTable, { id: 'b-1' })).toBeDefined();
521
+ expect((await db.get(machineTable, { id: 'm-1' })).status).toBe('active');
522
+ });
523
+
524
+ test('unowned legacy rows no declaration in the build claims are counted, never pruned', async () => {
525
+ const db = getDbAsSystem();
526
+ const unowned = await db.insert(defaultPolicyTable, { email: 'u@test.local', isLoadedFromSource: true });
527
+
528
+ // The build declares something else entirely: the legacy row is reported as unowned and left alone.
529
+ const summary = await bootAsSource('@test/pkg-a', [decl('a-1', 'a1@test.local')]);
530
+ expect(summary[defaultPolicyTable.name].unowned).toBe(1);
531
+ expect(await db.get(defaultPolicyTable, { id: unowned.id })).toBeDefined();
532
+
533
+ // Once a declaration claims it, it is owned (stamped) and no longer reported.
534
+ const claimed = await bootAsSource('@test/pkg-a', [
535
+ decl('a-1', 'a1@test.local'),
536
+ decl(unowned.id, 'u@test.local'),
537
+ ]);
538
+ expect(claimed[defaultPolicyTable.name].unowned).toBe(0);
539
+ expect((await db.get(defaultPolicyTable, { id: unowned.id })).sourcePackage).toBe('@test/pkg-a');
540
+ });
541
+ });
542
+
543
+ /**
544
+ * Version-scoped pruning WITHIN one package: two servers running different VERSIONS of the
545
+ * same package against one shared database (the actual brent-dev-2 incident — the main app
546
+ * at thought-common 3.29.0 and a feature worktree at 3.32.0, whose added thought types the
547
+ * older build deleted on every boot). Every row carries the declaring package's version;
548
+ * a boot never prunes (or flags) a row stamped by a NEWER version of the same package.
549
+ * Within the same or an older stamped version, the package stays authoritative:
550
+ * removals still land, and equal-version skew is last-writer-wins by design.
551
+ */
552
+ describe('version-scoped pruning (same package, different builds)', () => {
553
+ const decl = (id: string, email: string) => ({ table: defaultPolicyTable, record: { id, email } });
554
+
555
+ test('an older build of a package never prunes rows a newer build of the same package declared (the incident)', async () => {
556
+ const db = getDbAsSystem();
557
+
558
+ // The newer build (e.g. thought-common 3.32.0) declares an added type k-2.
559
+ await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local'), decl('k-2', 'k2@test.local')], '3.32.0');
560
+ // The older build (3.29.0) boots without k-2 — pre-fix this deleted k-2 every boot.
561
+ await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local')], '3.29.0');
562
+
563
+ expect(await db.get(defaultPolicyTable, { id: 'k-2' })).toBeDefined();
564
+ expect(await db.get(defaultPolicyTable, { id: 'k-1' })).toBeDefined();
565
+
566
+ // The NEWER build remains authoritative for genuine removals: it drops k-2 for real.
567
+ await bootAsSource('@test/pkg-a', [decl('k-1', 'k1@test.local')], '3.32.0');
568
+ expect(await db.get(defaultPolicyTable, { id: 'k-2' })).toBeUndefined();
569
+ expect(await db.get(defaultPolicyTable, { id: 'k-1' })).toBeDefined();
570
+ });
571
+
572
+ test('an older build never writes over a row a newer build of the same package stamped — content and stamp survive', async () => {
573
+ const db = getDbAsSystem();
574
+ const k1 = (displayName: string) => ({
575
+ table: defaultPolicyTable,
576
+ record: { id: 'k-1', email: 'k1@test.local', displayName },
577
+ });
578
+
579
+ // The newer build (3.32.0) declares k-1 with its current definition.
580
+ await bootAsSource('@test/pkg-a', [k1('newer')], '3.32.0');
581
+ // The older build (3.29.0) still declares k-1, with its older definition. Pre-fix this boot
582
+ // rewrote the row (and downgraded its stamp) on every restart — content churn on a shared DB.
583
+ const summary = await bootAsSource('@test/pkg-a', [k1('older')], '3.29.0');
584
+
585
+ const row = await db.get(defaultPolicyTable, { id: 'k-1' });
586
+ expect(row.displayName).toBe('newer');
587
+ expect(row.sourcePackageVersion).toBe('3.32.0');
588
+ expect(summary[defaultPolicyTable.name].skippedNewer).toBe(1);
589
+ expect(summary[defaultPolicyTable.name].updates).toBe(0);
590
+
591
+ // The newer build remains authoritative over its own rows: its next definition lands.
592
+ await bootAsSource('@test/pkg-a', [k1('newest')], '3.32.0');
593
+ expect((await db.get(defaultPolicyTable, { id: 'k-1' })).displayName).toBe('newest');
594
+ });
595
+
596
+ test('version skew on an update-policy table: an older build does not flag newer rows', async () => {
597
+ const db = getDbAsSystem();
598
+
599
+ await bootAsSource(
600
+ '@test/pkg-a',
601
+ [
602
+ { table: machineTable, record: { id: 'mv-1', email: 'mv1@test.local', status: 'active' } },
603
+ { table: machineTable, record: { id: 'mv-2', email: 'mv2@test.local', status: 'active' } },
604
+ ],
605
+ '2.0.0'
606
+ );
607
+
608
+ RecordingMachineAccountWatcher.updates = [];
609
+ await bootAsSource(
610
+ '@test/pkg-a',
611
+ [{ table: machineTable, record: { id: 'mv-1', email: 'mv1@test.local', status: 'active' } }],
612
+ '1.0.0'
613
+ );
614
+
615
+ // The newer build's machine account is not deactivated by the older build's boot.
616
+ expect((await db.get(machineTable, { id: 'mv-2' })).status).toBe('active');
617
+ const deactivations = RecordingMachineAccountWatcher.updates.filter((u) => u.status === 'deactivated');
618
+ expect(deactivations).toHaveLength(0);
619
+ });
620
+
621
+ test('equal versions with differing sets remain last-writer-wins — the deliberate residual', async () => {
622
+ const db = getDbAsSystem();
623
+
624
+ // Two builds at the SAME version (uncommitted local skew) cannot be ordered; the
625
+ // package stays authoritative within its own version: the later boot's set wins.
626
+ await bootAsSource('@test/pkg-a', [decl('e-1', 'e1@test.local'), decl('e-2', 'e2@test.local')], '1.0.0');
627
+ await bootAsSource('@test/pkg-a', [decl('e-1', 'e1@test.local')], '1.0.0');
628
+ expect(await db.get(defaultPolicyTable, { id: 'e-2' })).toBeUndefined();
629
+ expect(await db.get(defaultPolicyTable, { id: 'e-1' })).toBeDefined();
630
+ });
631
+
632
+ test('a build without a resolvable version never prunes version-stamped rows; unversioned stamps stay last-writer-wins', async () => {
633
+ const db = getDbAsSystem();
634
+
635
+ // Unversioned stamps carry no ordering: prunable by any build of the package —
636
+ // versionless-to-versionless keeps the pre-version last-writer-wins behavior.
637
+ await bootAsSource('@test/pkg-a', [decl('n-1', 'n1@test.local'), decl('n-2', 'n2@test.local')]);
638
+ await bootAsSource('@test/pkg-a', [decl('n-1', 'n1@test.local')]);
639
+ expect(await db.get(defaultPolicyTable, { id: 'n-2' })).toBeUndefined();
640
+
641
+ // Versioned rows survive an unorderable (versionless) build's boot: it cannot place
642
+ // itself before or after the stamp, so it must not delete what might be newer.
643
+ await bootAsSource('@test/pkg-a', [decl('u-1', 'u1@test.local'), decl('u-2', 'u2@test.local')], '1.0.0');
644
+ await bootAsSource('@test/pkg-a', [decl('u-1', 'u1@test.local')]);
645
+ expect(await db.get(defaultPolicyTable, { id: 'u-2' })).toBeDefined();
646
+
647
+ // A versioned build prunes older-or-equal stamps as usual (1.0.0 <= 1.0.1).
648
+ await bootAsSource('@test/pkg-a', [decl('u-1', 'u1@test.local')], '1.0.1');
649
+ expect(await db.get(defaultPolicyTable, { id: 'u-2' })).toBeUndefined();
650
+ expect(await db.get(defaultPolicyTable, { id: 'u-1' })).toBeDefined();
651
+ });
652
+
653
+ test("the version resolver reads the declaring package's real package.json", async () => {
654
+ const internals = new SourceRecordLoader() as unknown as LoaderInternals;
655
+ // A real dependency of this build resolves to its actual version (entry resolution +
656
+ // walk-up works for exports-mapped packages, where `<pkg>/package.json` is not requireable).
657
+ expect(internals.resolveSourceVersion('@proteinjs/db-query')).toMatch(/^\d+\.\d+\.\d+/);
658
+ // Resolution failure degrades to versionless, never throws.
659
+ expect(internals.resolveSourceVersion('@test/no-such-package')).toBeUndefined();
660
+ });
661
+ });
662
+
239
663
  test('a natural key without a unique guarantee fails boot loudly', async () => {
240
664
  // Local (unregistered, never created) on purpose: a registered table with an invalid
241
665
  // naturalKey would poison every loader run in the process — validation fires before any
@@ -284,5 +708,32 @@ export const sourceRecordSyncTests = (
284
708
  await dropTable(generationOne);
285
709
  }
286
710
  });
711
+
712
+ test('a new source-record table inherits the ownership stamp columns from SourceRecord — never declared per table', async () => {
713
+ // The fixture ({@link InheritedStampTable}) declares ONLY its own column. The ownership
714
+ // stamps (source_package, source_package_version) must arrive from withSourceRecordColumns —
715
+ // the single owner of the SourceRecord column set. If tables had to hand-declare the
716
+ // stamps, any table whose author forgot them would silently lose the shared-DB prune
717
+ // scoping this suite exists to guarantee.
718
+ const table = sourceRecordSyncTestTables.InheritedStamp;
719
+
720
+ // Type layer: the minted table carries the stamp columns, at their physical names,
721
+ // without declaring them.
722
+ expect(table.columns.sourcePackage?.name).toBe('source_package');
723
+ expect(table.columns.sourcePackageVersion?.name).toBe('source_package_version');
724
+ expect(isSourceRecordTable(table)).toBe(true);
725
+
726
+ // Schema layer: the harness minted the physical table through TableManager, which derives
727
+ // DDL from the type — so the stamp columns must have been emitted without the fixture
728
+ // declaring them. One boot of the sync stamps a row through them; the write itself fails
729
+ // if the physical columns were not emitted.
730
+ await bootAsSource('@test/pkg-a', [{ table, record: { id: 's-1', email: 's1@test.local' } }], '1.0.0');
731
+ const row = await getDbAsSystem().get(table, { id: 's-1' });
732
+ expect(row).toMatchObject({
733
+ sourcePackage: '@test/pkg-a',
734
+ sourcePackageVersion: '1.0.0',
735
+ isLoadedFromSource: true,
736
+ });
737
+ });
287
738
  };
288
739
  };
@@ -78,7 +78,27 @@ export class DupePreflightUniqueEmailTable extends Table<DupePreflightRecord> {
78
78
  });
79
79
  }
80
80
 
81
+ export interface InheritedStampRecord extends SourceRecord {
82
+ email: string;
83
+ }
84
+
85
+ /**
86
+ * The one-owner guard fixture: declares ONLY its own column. The ownership stamps
87
+ * (source_package, source_package_version) must arrive from withSourceRecordColumns — the single
88
+ * owner of the SourceRecord column set — never by per-table declaration. Its physical table is
89
+ * minted by the harness through the normal schema-sync path (TableManager deriving DDL from the
90
+ * type), so the inheritance guard test proves a NEW table gets the stamp columns without
91
+ * declaring them.
92
+ */
93
+ export class InheritedStampTable extends Table<InheritedStampRecord> {
94
+ name = 'db_test_sync_inherited_stamp';
95
+ columns: Table<InheritedStampRecord>['columns'] = withSourceRecordColumns<InheritedStampRecord>({
96
+ email: new StringColumn('email'),
97
+ });
98
+ }
99
+
81
100
  export const sourceRecordSyncTestTables = {
82
101
  SyncMachineAccount: new SyncMachineAccountTable() as Table<SyncMachineAccount>,
83
102
  SyncDefaultPolicy: new SyncDefaultPolicyTable() as Table<SyncDefaultPolicyRecord>,
103
+ InheritedStamp: new InheritedStampTable() as Table<InheritedStampRecord>,
84
104
  };