@sixb/pg 0.1.15 → 0.1.17

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 (92) hide show
  1. package/README.md +125 -0
  2. package/dist/index.d.ts +15 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3202 -1474
  5. package/dist/index.js.map +32 -25
  6. package/dist/migrations.d.ts.map +1 -1
  7. package/dist/objects/aggregate-admission.d.ts +10 -0
  8. package/dist/objects/aggregate-admission.d.ts.map +1 -0
  9. package/dist/objects/preparation-lock.d.ts +6 -0
  10. package/dist/objects/preparation-lock.d.ts.map +1 -0
  11. package/dist/objects/prepare-queries.d.ts +5 -0
  12. package/dist/objects/prepare-queries.d.ts.map +1 -0
  13. package/dist/objects/query-compiler.d.ts +19 -0
  14. package/dist/objects/query-compiler.d.ts.map +1 -1
  15. package/dist/objects/query-indexes.d.ts +43 -0
  16. package/dist/objects/query-indexes.d.ts.map +1 -0
  17. package/dist/objects/reader.d.ts +7 -1
  18. package/dist/objects/reader.d.ts.map +1 -1
  19. package/dist/objects/storage.d.ts +1 -1
  20. package/dist/objects/storage.d.ts.map +1 -1
  21. package/dist/objects/text-count-index.d.ts +24 -0
  22. package/dist/objects/text-count-index.d.ts.map +1 -0
  23. package/dist/ontology-storage/index.d.ts +2 -0
  24. package/dist/ontology-storage/index.d.ts.map +1 -1
  25. package/dist/ontology-storage/materialization-session.d.ts +18 -38
  26. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  27. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  28. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  29. package/dist/ontology-storage/materialization-writer.d.ts +51 -9
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +5 -7
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/replacement-plans.d.ts +36 -0
  35. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  36. package/dist/ontology-storage/shared.d.ts +11 -5
  37. package/dist/ontology-storage/shared.d.ts.map +1 -1
  38. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  39. package/dist/ontology-storage/source-roots.d.ts +14 -1
  40. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  41. package/dist/ontology-storage/sources.d.ts +1 -2
  42. package/dist/ontology-storage/sources.d.ts.map +1 -1
  43. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  44. package/dist/pg-ai-limit-storage.d.ts.map +1 -1
  45. package/dist/pg-ai-usage-storage.d.ts +2 -0
  46. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  47. package/dist/pg-execution-storage.d.ts +28 -0
  48. package/dist/pg-execution-storage.d.ts.map +1 -1
  49. package/dist/pg-file-download-grant-storage.d.ts +11 -0
  50. package/dist/pg-file-download-grant-storage.d.ts.map +1 -0
  51. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  52. package/dist/pg-rules-storage.d.ts.map +1 -1
  53. package/dist/transactions.d.ts +6 -2
  54. package/dist/transactions.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/src/index.ts +53 -3
  57. package/src/migrations/053-replacement-plans.sql +72 -0
  58. package/src/migrations/054-slim-ontology-outbox.sql +108 -0
  59. package/src/migrations/055-object-query-preparation.sql +11 -0
  60. package/src/migrations/056-commit-touches.sql +41 -0
  61. package/src/migrations/057-audio-transcription.sql +10 -0
  62. package/src/migrations/058-reranking-model-kind.sql +11 -0
  63. package/src/migrations/059-file-download-grants.sql +16 -0
  64. package/src/migrations/060-drained-source-versions.sql +11 -0
  65. package/src/migrations.ts +20 -0
  66. package/src/objects/aggregate-admission.ts +21 -0
  67. package/src/objects/preparation-lock.ts +15 -0
  68. package/src/objects/prepare-queries.ts +181 -0
  69. package/src/objects/query-compiler.ts +405 -115
  70. package/src/objects/query-indexes.ts +348 -0
  71. package/src/objects/reader.ts +140 -51
  72. package/src/objects/storage.ts +15 -3
  73. package/src/objects/text-count-index.ts +455 -0
  74. package/src/ontology-storage/index.ts +3 -0
  75. package/src/ontology-storage/materialization-session.ts +116 -275
  76. package/src/ontology-storage/materialization-state.ts +8 -259
  77. package/src/ontology-storage/materialization-writer.ts +618 -508
  78. package/src/ontology-storage/materializations.ts +106 -289
  79. package/src/ontology-storage/outbox.ts +46 -15
  80. package/src/ontology-storage/replacement-plans.ts +1110 -0
  81. package/src/ontology-storage/shared.ts +27 -15
  82. package/src/ontology-storage/source-cleanup.ts +22 -10
  83. package/src/ontology-storage/source-roots.ts +35 -8
  84. package/src/ontology-storage/sources.ts +0 -30
  85. package/src/pg-ai-cost-storage.ts +12 -0
  86. package/src/pg-ai-limit-storage.ts +13 -2
  87. package/src/pg-ai-usage-storage.ts +8 -0
  88. package/src/pg-execution-storage.ts +8 -3
  89. package/src/pg-file-download-grant-storage.ts +76 -0
  90. package/src/pg-projection-run-storage.ts +6 -1
  91. package/src/pg-rules-storage.ts +7 -3
  92. package/src/transactions.ts +10 -3
@@ -1,4 +1,3 @@
1
- import type { EffectiveChangeCounts } from "@sixb/core/internal/materialization"
2
1
  import {
3
2
  assertPinnedDatasetWatermark,
4
3
  linkRefKey,
@@ -17,32 +16,30 @@ import {
17
16
  assertExpectedObjectRevision,
18
17
  assertMaterializationFinalizationCorrelation,
19
18
  assertMaterializationHeader,
20
- assertMaterializationLaneCompletion,
21
19
  assertPageRows,
22
- assertPlanChunkCorrelations,
23
20
  assertSourceActivationCorrelation,
21
+ beginMaterializationApply,
22
+ beginMaterializationVectorChanges,
23
+ cardinalityViolation,
24
24
  invalidCorrelation,
25
25
  sameNonnegativeCounts as sameCounts,
26
26
  uniqueSorted,
27
27
  } from "@sixb/core/internal/ontology-storage-provider"
28
28
  import type {
29
- ApplyMaterializationChunkInput,
29
+ AppliedMaterialization,
30
+ ApplyMaterializationInput,
30
31
  ApplyMaterializationResult,
31
32
  FinalizeMaterializationInput,
32
- MaterializationObjectExistence,
33
33
  MaterializationPlanHeader,
34
34
  MaterializationSession,
35
35
  MaterializationStatePage,
36
- MaterializationWorkPage,
36
+ MaterializationVectorChangePage,
37
37
  OntologyCommitRecord,
38
38
  OntologyMaterializationStorage,
39
- ReadMaterializationObjectExistenceInput,
40
39
  SourceActivationWrite,
41
- SourceReplacementStatePage,
42
40
  StageMaterializationWorkInput,
43
41
  StreamMaterializationStateInput,
44
- StreamMaterializationWorkInput,
45
- StreamSourceReplacementStateInput,
42
+ StreamMaterializationVectorChangesInput,
46
43
  } from "@sixb/core/storage"
47
44
  import type { SQLClient } from "../pg-client"
48
45
  import { isUniqueViolation } from "../storage-errors"
@@ -52,12 +49,9 @@ import {
52
49
  PgMaterializationSessions,
53
50
  type PgOntologyTransactionContext,
54
51
  } from "./materialization-session"
55
- import {
56
- linkSortExpression,
57
- PG_MATERIALIZATION_WORK_TABLE,
58
- PgMaterializationStateReader,
59
- } from "./materialization-state"
52
+ import { linkSortExpression, PgMaterializationStateReader } from "./materialization-state"
60
53
  import { PgMaterializationWriter } from "./materialization-writer"
54
+ import { boundReplacementPlan } from "./replacement-plans"
61
55
  import {
62
56
  assertProjectionExecution,
63
57
  commitRecord,
@@ -70,17 +64,6 @@ import {
70
64
  } from "./shared"
71
65
  import { activateSourceRoots } from "./source-roots"
72
66
 
73
- interface ProjectionCountsRow {
74
- readonly object_classifications: number | string
75
- readonly link_classifications: number | string
76
- readonly objects_created: number | string
77
- readonly objects_updated: number | string
78
- readonly objects_deleted: number | string
79
- readonly links_created: number | string
80
- readonly links_updated: number | string
81
- readonly links_deleted: number | string
82
- }
83
-
84
67
  interface TelemetrySummaryRow {
85
68
  readonly classified_points: number | string
86
69
  readonly points_created: number | string
@@ -114,7 +97,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
114
97
 
115
98
  async begin(input: MaterializationPlanHeader): Promise<MaterializationSession> {
116
99
  assertMaterializationHeader(input)
117
- const session = await this.sessions.create(input)
100
+ const session = await this.sessions.create(
101
+ input,
102
+ input.plan ? await boundReplacementPlan(this.sql, input) : null
103
+ )
118
104
  try {
119
105
  await lockAdvisoryKeys(this.sql, materializationLockKeys(input))
120
106
  await this.assertCommitAbsent(input)
@@ -220,128 +206,43 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
220
206
  }
221
207
  }
222
208
 
223
- async *streamSourceReplacementState(
224
- input: StreamSourceReplacementStateInput
225
- ): AsyncIterable<SourceReplacementStatePage> {
226
- const session = this.sessions.require(input.session)
227
- assertPageRows(input.pageRows)
228
- const replacement = await this.requireReplacement(session, input)
229
- const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId)
230
- if (input.entityKind === "object") {
231
- if (replacement.projectionKind !== "object") {
232
- throw new MaterializationConflictError(
233
- "source-materialization",
234
- "Link projection replacement cannot stream object state."
235
- )
236
- }
237
- if (replacement.objectStreamStarted) {
238
- throw new MaterializationConflictError(
239
- "effective-state",
240
- "Replacement object state may only be streamed once per session."
241
- )
242
- }
243
- replacement.objectStreamStarted = true
244
- for await (const identities of reader.replacementIdentities({
245
- sessionId: session.id,
246
- sourceId: replacement.sourceId,
247
- candidateMaterializationId: replacement.candidateMaterializationId,
248
- previousMaterializationId: replacement.previousMaterializationId,
249
- incremental: replacement.incremental,
250
- kind: "object",
251
- pageRows: input.pageRows,
252
- })) {
253
- this.sessions.require(input.session)
254
- const refs = identities.map((identity) => {
255
- if (identity.kind !== "object") {
256
- invalidCorrelation("Object replacement returned a link identity.")
257
- }
258
- return identity.ref
259
- })
260
- const objects = await reader.replacementObjectStates(
261
- replacement.sourceId,
262
- replacement.candidateMaterializationId,
263
- refs
264
- )
265
- yield { objects, links: [] }
266
- }
267
- this.sessions.require(input.session)
268
- replacement.objectStreamCompleted = true
269
- return
270
- }
271
-
272
- if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
273
- throw new MaterializationConflictError(
274
- "effective-state",
275
- "Object projection replacement must fully stream object state before link state."
276
- )
277
- }
278
- if (replacement.linkStreamStarted) {
279
- throw new MaterializationConflictError(
280
- "effective-state",
281
- "Replacement link state may only be streamed once per session."
282
- )
283
- }
284
- replacement.linkStreamStarted = true
285
- const materializationIds = [
286
- replacement.candidateMaterializationId,
287
- ...(replacement.previousMaterializationId ? [replacement.previousMaterializationId] : []),
288
- ]
289
- for await (const identities of reader.replacementIdentities({
290
- sessionId: session.id,
291
- sourceId: replacement.sourceId,
292
- candidateMaterializationId: replacement.candidateMaterializationId,
293
- previousMaterializationId: replacement.previousMaterializationId,
294
- incremental: replacement.incremental,
295
- kind: "link",
296
- pageRows: input.pageRows,
297
- })) {
298
- this.sessions.require(input.session)
299
- const linkIdentities = identities.map((identity) => {
300
- if (identity.kind !== "link") {
301
- invalidCorrelation("Link replacement returned an object identity.")
302
- }
303
- return identity
304
- })
305
- const links = await reader.replacementLinkStates(
306
- replacement.sourceId,
307
- replacement.candidateMaterializationId,
308
- materializationIds,
309
- linkIdentities,
310
- replacement.incremental
311
- )
312
- yield { objects: [], links }
313
- }
314
- this.sessions.require(input.session)
315
- replacement.linkStreamCompleted = true
316
- }
317
-
318
209
  async stageWork(input: StageMaterializationWorkInput): Promise<void> {
319
210
  await this.sessions.stage(input)
320
211
  }
321
212
 
322
- streamWork(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
323
- assertPageRows(input.pageRows)
324
- return this.sessions.stream(input)
325
- }
326
-
327
- async readObjectExistence(
328
- input: ReadMaterializationObjectExistenceInput
329
- ): Promise<readonly MaterializationObjectExistence[]> {
213
+ async *streamVectorChanges(
214
+ input: StreamMaterializationVectorChangesInput
215
+ ): AsyncIterable<MaterializationVectorChangePage> {
330
216
  const session = this.sessions.require(input.session)
331
- return this.sessions.readObjectExistence(session, input.refs)
217
+ assertPageRows(input.pageRows)
218
+ beginMaterializationVectorChanges(session)
219
+ await this.sessions.analyzeSealedWork(session)
220
+ for await (const items of this.sessions.vectorChangePages(
221
+ session,
222
+ input.objectTypeIds,
223
+ input.pageRows
224
+ )) {
225
+ yield { items }
226
+ }
332
227
  }
333
228
 
334
- async applyChunk(input: ApplyMaterializationChunkInput): Promise<void> {
229
+ async apply(input: ApplyMaterializationInput): Promise<AppliedMaterialization> {
335
230
  const session = this.sessions.require(input.session)
231
+ beginMaterializationApply(session)
232
+ await this.sessions.analyzeSealedWork(session)
233
+ await this.assertStagedCardinality(session)
336
234
  const { commit } = session.header
337
- assertPlanChunkCorrelations(input.chunk, commit)
338
- const progress = await this.sessions.prepareChunkSequence(session, input.chunk)
339
- await this.writer.apply(commit.projectId, commit.id, input.chunk)
340
- session.changedObjects +=
341
- input.chunk.effective.objectUpserts.length + input.chunk.effective.objectDeletes.length
342
- session.changedLinks +=
343
- input.chunk.effective.linkUpserts.length + input.chunk.effective.linkDeletes.length
344
- this.sessions.commitChunkSequence(session, progress)
235
+ const applied = await this.writer.applyStaged({
236
+ workTable: session.workTable,
237
+ workId: session.workId,
238
+ projectId: commit.projectId,
239
+ commitId: commit.id,
240
+ committedAt: commit.committedAt,
241
+ })
242
+ session.changedObjects = applied.objectWrites
243
+ session.changedLinks = applied.linkWrites
244
+ session.appliedEventCount = applied.eventCount
245
+ return { eventCount: applied.eventCount }
345
246
  }
346
247
 
347
248
  async finalize(input: FinalizeMaterializationInput): Promise<ApplyMaterializationResult> {
@@ -353,8 +254,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
353
254
  }
354
255
  // The next sparse projection must see the distribution of a newly published bulk load.
355
256
  // With empty/stale statistics, a link lookup can scan every edge sharing its target.
356
- if (session.changedObjects >= 1_000) await this.sql`ANALYZE objects`
357
- if (session.changedLinks >= 1_000) await this.sql`ANALYZE links`
257
+ await refreshMaterializedStatistics(this.sql, "objects", session.changedObjects)
258
+ await refreshMaterializedStatistics(this.sql, "links", session.changedLinks)
358
259
  const record = await this.insertCommit(session.header, input)
359
260
  await this.sessions.release(session)
360
261
  return { commit: record }
@@ -364,67 +265,6 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
364
265
  this.sessions.deactivateAll()
365
266
  }
366
267
 
367
- private async requireReplacement(
368
- session: PgMaterializationSessionState,
369
- input: StreamSourceReplacementStateInput
370
- ): Promise<NonNullable<PgMaterializationSessionState["replacement"]>> {
371
- if (session.replacement) {
372
- if (
373
- session.replacement.sourceId !== input.source.projectionId ||
374
- session.replacement.candidateMaterializationId !== input.candidateMaterializationId
375
- ) {
376
- throw new MaterializationConflictError(
377
- "source-materialization",
378
- "Materialization session already owns another replacement union."
379
- )
380
- }
381
- return session.replacement
382
- }
383
- const projectId = session.header.commit.projectId
384
- const candidate = await this.getSource(
385
- projectId,
386
- input.source.projectionId,
387
- input.candidateMaterializationId,
388
- true
389
- )
390
- if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
391
- throw new MaterializationConflictError(
392
- "source-materialization",
393
- `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`
394
- )
395
- }
396
- await assertProjectionExecution(this.sql, {
397
- projectId,
398
- sourceId: input.source.projectionId,
399
- projectionRunId: candidate.projection_run_id,
400
- executionToken: candidate.execution_token,
401
- })
402
- const previous = await this.getActiveSource(projectId, input.source.projectionId, true)
403
- if (
404
- candidate.protocol !== "replacement" ||
405
- (previous &&
406
- (previous.protocol !== candidate.protocol ||
407
- previous.projection_kind !== candidate.projection_kind))
408
- ) {
409
- throw new MaterializationConflictError(
410
- "source-materialization",
411
- "Source replacement kind or protocol does not match its active materialization."
412
- )
413
- }
414
- session.replacement = {
415
- sourceId: input.source.projectionId,
416
- candidateMaterializationId: input.candidateMaterializationId,
417
- previousMaterializationId: previous?.materialization_id ?? null,
418
- incremental: candidate.base_materialization_id !== null,
419
- projectionKind: candidate.projection_kind,
420
- objectStreamStarted: false,
421
- objectStreamCompleted: false,
422
- linkStreamStarted: false,
423
- linkStreamCompleted: false,
424
- }
425
- return session.replacement
426
- }
427
-
428
268
  private async assertCommitAbsent(header: MaterializationPlanHeader): Promise<void> {
429
269
  const [duplicateIdentity] = await this.sql<
430
270
  { readonly id: string; readonly idempotency_key: string }[]
@@ -518,10 +358,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
518
358
  const { commit } = session.header
519
359
  const { result } = input.finalization
520
360
  assertMaterializationFinalizationCorrelation(session, input)
521
- const laneCounts = await this.sessions.laneCounts(session)
522
- assertMaterializationLaneCompletion(session, laneCounts)
523
- if (laneCounts.cardinality > 0) await this.assertFinalCardinality(session)
524
- const eventCount = laneCounts.event
361
+ if (await this.sessions.hasCardinalityWork(session)) await this.assertFinalCardinality(session)
362
+ const eventCount = session.appliedEventCount
525
363
  const [outbox] = await this.sql<
526
364
  {
527
365
  readonly count: number | string
@@ -558,12 +396,45 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
558
396
  if (commit.intent.kind === "projection") {
559
397
  if (result.kind !== "projection") return
560
398
  await this.sessions.assertClassificationCoverage(session)
561
- if (!sameCounts(result.counts, await this.projectionCounts(session))) {
399
+ const counts = await this.sessions.projectionCounts(session)
400
+ if (!counts || !sameCounts(result.counts, counts)) {
562
401
  invalidCorrelation("Projection result counts do not correlate with finalized work.")
563
402
  }
564
403
  }
565
404
  }
566
405
 
406
+ /** Rejects a cardinality-one scope that two staged occupants claim, effective view first. */
407
+ private async assertStagedCardinality(session: PgMaterializationSessionState): Promise<void> {
408
+ // Cardinality records are unique per view, scope and link: two occupied in one scope is the
409
+ // violation, and only its scope needs reading back.
410
+ const [violation] = await this.sql<
411
+ { readonly major_order: number; readonly sort_one: string }[]
412
+ >`
413
+ SELECT major_order, sort_one
414
+ FROM ${this.sql(session.workTable)}
415
+ WHERE work_id = ${session.workId} AND lane = 'cardinality'
416
+ AND cardinality_occupied
417
+ GROUP BY major_order, sort_one
418
+ HAVING COUNT(*) > 1
419
+ ORDER BY major_order, sort_one
420
+ LIMIT 1
421
+ `
422
+ if (!violation) return
423
+ const [scope] = await this.sql<{ readonly source_type_id: string; readonly link_id: string }[]>`
424
+ SELECT payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
425
+ payload->'ref'->>'linkId' AS link_id
426
+ FROM ${this.sql(session.workTable)}
427
+ WHERE work_id = ${session.workId} AND lane = 'cardinality'
428
+ AND major_order = ${violation.major_order} AND sort_one = ${violation.sort_one}
429
+ LIMIT 1
430
+ `
431
+ throw cardinalityViolation(
432
+ violation.major_order === 0 ? "effective" : "candidate",
433
+ scope?.source_type_id ?? "",
434
+ scope?.link_id ?? ""
435
+ )
436
+ }
437
+
567
438
  private async assertFinalCardinality(session: PgMaterializationSessionState): Promise<void> {
568
439
  // A mixed work table can hide a few occupied scopes among millions of other records.
569
440
  // Even ANALYZE can miss that population. Isolate it before planning the final join so
@@ -574,21 +445,19 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
574
445
  await this.sql.unsafe(
575
446
  `CREATE TEMP TABLE ${table} ON COMMIT DROP AS
576
447
  SELECT sort_one AS scope_sort_key, sort_two AS link_sort_key,
577
- (payload->>'occupied')::boolean AS occupied,
448
+ cardinality_occupied AS occupied,
578
449
  payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
579
450
  payload->'ref'->'source'->>'primaryId' AS source_id,
580
451
  payload->'ref'->>'linkId' AS link_id
581
- FROM ${PG_MATERIALIZATION_WORK_TABLE}
582
- WHERE session_id = $1 AND lane = 'cardinality' AND major_order = 0`,
583
- [session.id]
452
+ FROM ${session.workTable}
453
+ WHERE work_id = $1 AND lane = 'cardinality' AND major_order = 0`,
454
+ [session.workId]
584
455
  )
585
456
  await this.sql.unsafe(`ANALYZE ${table}`)
586
- const [violation] = await this.sql.unsafe<{ readonly reason: "duplicate" | "mismatch" }[]>(
457
+ // Apply already rejected a scope with two staged occupants; this checks what it wrote.
458
+ const [violation] = await this.sql.unsafe<{ readonly mismatch: number }[]>(
587
459
  `WITH work AS (
588
460
  SELECT * FROM ${table}
589
- ), duplicate AS (
590
- SELECT scope_sort_key FROM work WHERE occupied
591
- GROUP BY scope_sort_key HAVING COUNT(*) > 1
592
461
  ), scopes AS (
593
462
  SELECT DISTINCT scope_sort_key, source_type_id, source_id, link_id FROM work
594
463
  ), expected AS (
@@ -604,16 +473,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
604
473
  UNION ALL
605
474
  (SELECT * FROM actual EXCEPT SELECT * FROM expected)
606
475
  )
607
- SELECT 'duplicate'::text AS reason FROM duplicate
608
- UNION ALL
609
- SELECT 'mismatch'::text AS reason FROM differences
610
- LIMIT 1`,
476
+ SELECT 1 AS mismatch FROM differences LIMIT 1`,
611
477
  [session.header.commit.projectId]
612
478
  )
613
479
  await this.sql.unsafe(`DROP TABLE ${table}`)
614
- if (violation?.reason === "duplicate") {
615
- invalidCorrelation("Materialization cardinality work violates cardinality-one.")
616
- }
617
480
  if (violation) {
618
481
  invalidCorrelation(
619
482
  "Materialization cardinality work does not match the final effective link scope."
@@ -621,65 +484,6 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
621
484
  }
622
485
  }
623
486
 
624
- private async projectionCounts(
625
- session: PgMaterializationSessionState
626
- ): Promise<EffectiveChangeCounts> {
627
- const [row] = await this.sql<ProjectionCountsRow[]>`
628
- SELECT
629
- COUNT(*) FILTER (
630
- WHERE kind = 'classification' AND payload->>'entityKind' = 'object'
631
- ) AS object_classifications,
632
- COUNT(*) FILTER (
633
- WHERE kind = 'classification' AND payload->>'entityKind' = 'link'
634
- ) AS link_classifications,
635
- COUNT(*) FILTER (
636
- WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
637
- AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
638
- ) AS objects_created,
639
- COUNT(*) FILTER (
640
- WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
641
- AND (payload->'item'->'value'->'expected'->>'exists')::boolean
642
- ) AS objects_updated,
643
- COUNT(*) FILTER (
644
- WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-delete'
645
- ) AS objects_deleted,
646
- COUNT(*) FILTER (
647
- WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
648
- AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
649
- ) AS links_created,
650
- COUNT(*) FILTER (
651
- WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
652
- AND (payload->'item'->'value'->'expected'->>'exists')::boolean
653
- ) AS links_updated,
654
- COUNT(*) FILTER (
655
- WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-delete'
656
- ) AS links_deleted
657
- FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
658
- WHERE session_id = ${session.id}
659
- `
660
- const objectsCreated = databaseCount(row?.objects_created)
661
- const objectsUpdated = databaseCount(row?.objects_updated)
662
- const objectsDeleted = databaseCount(row?.objects_deleted)
663
- const linksCreated = databaseCount(row?.links_created)
664
- const linksUpdated = databaseCount(row?.links_updated)
665
- const linksDeleted = databaseCount(row?.links_deleted)
666
- return {
667
- objectsCreated,
668
- objectsUpdated,
669
- objectsDeleted,
670
- objectsUnchanged:
671
- databaseCount(row?.object_classifications) -
672
- objectsCreated -
673
- objectsUpdated -
674
- objectsDeleted,
675
- linksCreated,
676
- linksUpdated,
677
- linksDeleted,
678
- linksUnchanged:
679
- databaseCount(row?.link_classifications) - linksCreated - linksUpdated - linksDeleted,
680
- }
681
- }
682
-
683
487
  private async telemetrySummary(session: PgMaterializationSessionState, pointCount: number) {
684
488
  const [row] = await this.sql<TelemetrySummaryRow[]>`
685
489
  SELECT
@@ -697,8 +501,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
697
501
  COUNT(*) FILTER (
698
502
  WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
699
503
  ) AS latest_objects_changed
700
- FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
701
- WHERE session_id = ${session.id}
504
+ FROM ${this.sql(session.workTable)}
505
+ WHERE work_id = ${session.workId}
702
506
  `
703
507
  const pointsCreated = databaseCount(row?.points_created)
704
508
  const pointsUpdated = databaseCount(row?.points_updated)
@@ -828,20 +632,17 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
828
632
  ): Promise<OntologyCommitRecord> {
829
633
  const { commit } = header
830
634
  const origin = originColumns(commit.origin)
831
- const requestedBy =
832
- commit.requestedBy === undefined ? null : jsonParameter(this.sql, commit.requestedBy)
833
635
  const rows = await this.sql<PgOntologyCommitRow[]>`
834
636
  INSERT INTO ontology_commits (
835
637
  project_id, id, idempotency_key, request_hash, execution_id,
836
- origin_kind, origin_run_id, origin_batch_ordinal, origin, requested_by, executor,
638
+ origin_kind, origin_run_id, origin_batch_ordinal, origin,
837
639
  ontology_revision, projection_revision, ownership_hash,
838
640
  intent, result, committed_at
839
641
  ) VALUES (
840
642
  ${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
841
643
  ${commit.executionId},
842
644
  ${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
843
- ${jsonParameter(this.sql, commit.origin)}, ${requestedBy},
844
- ${jsonParameter(this.sql, commit.executor)}, ${commit.ontologyRevision},
645
+ ${jsonParameter(this.sql, commit.origin)}, ${commit.ontologyRevision},
845
646
  ${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
846
647
  ${jsonParameter(this.sql, commit.intent)},
847
648
  ${jsonParameter(this.sql, input.finalization.result)}, ${commit.committedAt}
@@ -935,3 +736,19 @@ function materializationLockKeys(header: MaterializationPlanHeader): string[] {
935
736
  function databaseCount(value: number | string | undefined): number {
936
737
  return Number(value ?? 0)
937
738
  }
739
+
740
+ /** Keep the first bulk publication queryable without analyzing every small subsequent delta.
741
+ * Expression indexes make full ANALYZE expensive. Only a substantial current publication
742
+ * forces synchronous work. Autovacuum accumulates smaller writes separately; its modification
743
+ * count can still include a previous transaction's manually analyzed writes at this point. */
744
+ async function refreshMaterializedStatistics(
745
+ sql: SQLClient,
746
+ table: "objects" | "links",
747
+ changed: number
748
+ ): Promise<void> {
749
+ if (changed < 1_000) return
750
+ const [state] = await sql<{ rows: number }[]>`
751
+ SELECT reltuples AS rows FROM pg_class WHERE oid=${table}::regclass`
752
+ if (!state || state.rows <= 0 || changed >= Math.max(10_000, state.rows * 0.1))
753
+ await sql.unsafe(`ANALYZE ${table}`)
754
+ }
@@ -2,7 +2,12 @@ import { serializeSixbFailure } from "@sixb/core/internal/errors"
2
2
  import {
3
3
  MaterializationConflictError,
4
4
  MaterializationValidationError,
5
+ type OntologyMaterializationEventCommit,
5
6
  } from "@sixb/core/internal/materialization"
7
+ import {
8
+ claimedOutboxRows,
9
+ type OutboxEventContext,
10
+ } from "@sixb/core/internal/ontology-storage-provider"
6
11
  import type {
7
12
  ClaimedOntologyOutboxRow,
8
13
  ClaimOntologyOutboxInput,
@@ -14,15 +19,24 @@ import type {
14
19
  SummarizeOntologyOutboxInput,
15
20
  } from "@sixb/core/storage"
16
21
  import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage"
22
+ import { type PgExecutionRow, pgExecutionRecord } from "../pg-execution-storage"
17
23
  import {
18
24
  assertNonblank,
19
25
  assertPositiveInteger,
20
26
  assertTimestamp,
21
- outboxRecord,
27
+ claimedOutboxDraft,
22
28
  type PgOntologyOutboxRow,
23
29
  type PgRootOperation,
30
+ toIsoString,
24
31
  } from "./shared"
25
32
 
33
+ /** A commit's own fields, prefixed, beside every column of the execution it ran under. */
34
+ interface PgOutboxContextRow extends PgExecutionRow {
35
+ readonly commit_id: string
36
+ readonly commit_committed_at: Date | string
37
+ readonly commit_origin: OntologyMaterializationEventCommit["origin"]
38
+ }
39
+
26
40
  export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
27
41
  constructor(private readonly runRootOperation: PgRootOperation) {}
28
42
 
@@ -57,23 +71,28 @@ export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
57
71
  lease_expires_at = ${input.leaseExpiresAt}
58
72
  FROM candidates
59
73
  WHERE outbox.project_id = candidates.project_id AND outbox.id = candidates.id
60
- RETURNING outbox.id AS row_id, outbox.envelope, outbox.available_at,
61
- outbox.attempts, outbox.lease_id, outbox.lease_expires_at,
62
- outbox.published_at, outbox.last_failure, outbox.created_at,
63
- outbox.commit_id, outbox.commit_ordinal
74
+ RETURNING outbox.id, outbox.commit_id, outbox.commit_ordinal, outbox.event,
75
+ outbox.available_at, outbox.attempts, outbox.lease_id, outbox.lease_expires_at,
76
+ outbox.published_at, outbox.last_failure, outbox.created_at
64
77
  )
65
78
  SELECT * FROM claimed ORDER BY created_at, commit_id, commit_ordinal
66
79
  `
67
- return rows.map((row) => {
68
- const record = outboxRecord(row)
69
- if (record.leaseId === null || record.leaseExpiresAt === null) {
70
- throw new MaterializationConflictError(
71
- "outbox-lease",
72
- "Ontology outbox claim returned an unpaired lease."
73
- )
74
- }
75
- return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt }
76
- })
80
+ if (rows.length === 0) return []
81
+ // Each commit and its execution are read once, however many of its events the batch holds.
82
+ const commitIds = [...new Set(rows.map((row) => row.commit_id))]
83
+ const contexts = await sql<PgOutboxContextRow[]>`
84
+ SELECT commits.id AS commit_id, commits.committed_at AS commit_committed_at,
85
+ commits.origin AS commit_origin, executions.*
86
+ FROM ontology_commits AS commits
87
+ JOIN executions
88
+ ON executions.project_id = commits.project_id AND executions.id = commits.execution_id
89
+ WHERE commits.project_id = ${input.projectId}
90
+ AND commits.id = ANY(${sql.array(commitIds)}::text[])
91
+ `
92
+ const contextOf = new Map(
93
+ contexts.map((row) => [row.commit_id, outboxEventContext(input.projectId, row)])
94
+ )
95
+ return claimedOutboxRows(rows.map(claimedOutboxDraft), (commitId) => contextOf.get(commitId))
77
96
  })
78
97
  }
79
98
 
@@ -192,6 +211,18 @@ export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
192
211
  }
193
212
  }
194
213
 
214
+ function outboxEventContext(projectId: string, row: PgOutboxContextRow): OutboxEventContext {
215
+ return {
216
+ commit: {
217
+ projectId,
218
+ id: row.commit_id,
219
+ committedAt: toIsoString(row.commit_committed_at),
220
+ origin: row.commit_origin,
221
+ },
222
+ execution: pgExecutionRecord(row),
223
+ }
224
+ }
225
+
195
226
  function toOptionalIsoString(value: Date | string | null | undefined): string | null {
196
227
  if (value === null || value === undefined) return null
197
228
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString()