@sixb/sqlite 0.1.16 → 0.1.18

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 (70) hide show
  1. package/dist/ai-limit-storage.d.ts.map +1 -1
  2. package/dist/ai-usage-storage.d.ts.map +1 -1
  3. package/dist/execution-storage.d.ts +28 -0
  4. package/dist/execution-storage.d.ts.map +1 -1
  5. package/dist/file-download-grant-storage.d.ts +11 -0
  6. package/dist/file-download-grant-storage.d.ts.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1362 -579
  10. package/dist/index.js.map +27 -25
  11. package/dist/migrations.d.ts.map +1 -1
  12. package/dist/objects/query-compiler.d.ts.map +1 -1
  13. package/dist/objects/reader.d.ts.map +1 -1
  14. package/dist/objects/storage.d.ts.map +1 -1
  15. package/dist/ontology-storage/index.d.ts +2 -0
  16. package/dist/ontology-storage/index.d.ts.map +1 -1
  17. package/dist/ontology-storage/materialization-session.d.ts +17 -37
  18. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  19. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  20. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  21. package/dist/ontology-storage/materialization-writer.d.ts +1 -1
  22. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  23. package/dist/ontology-storage/materializations.d.ts +3 -6
  24. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  25. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  26. package/dist/ontology-storage/replacement-plans.d.ts +88 -0
  27. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  28. package/dist/ontology-storage/shared.d.ts +8 -5
  29. package/dist/ontology-storage/shared.d.ts.map +1 -1
  30. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  31. package/dist/ontology-storage/source-roots.d.ts +15 -2
  32. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  33. package/dist/ontology-storage/sources.d.ts +1 -2
  34. package/dist/ontology-storage/sources.d.ts.map +1 -1
  35. package/dist/projection-run-storage.d.ts.map +1 -1
  36. package/dist/rules-storage.d.ts.map +1 -1
  37. package/dist/testing.d.ts +4 -1
  38. package/dist/testing.d.ts.map +1 -1
  39. package/package.json +3 -3
  40. package/src/ai-cost-storage.ts +10 -0
  41. package/src/ai-limit-storage.ts +13 -2
  42. package/src/ai-usage-storage.ts +9 -1
  43. package/src/execution-storage.ts +7 -2
  44. package/src/file-download-grant-storage.ts +76 -0
  45. package/src/index.ts +10 -3
  46. package/src/migrations/053-replacement-plans.sql +73 -0
  47. package/src/migrations/054-slim-ontology-outbox.sql +21 -0
  48. package/src/migrations/055-commit-touches.sql +31 -0
  49. package/src/migrations/056-audio-transcription.sql +10 -0
  50. package/src/migrations/057-reranking-model-kind.sql +16 -0
  51. package/src/migrations/058-file-download-grants.sql +21 -0
  52. package/src/migrations/059-drained-source-versions.sql +11 -0
  53. package/src/migrations.ts +115 -0
  54. package/src/objects/query-compiler.ts +5 -0
  55. package/src/objects/reader.ts +1 -0
  56. package/src/objects/storage.ts +5 -11
  57. package/src/ontology-storage/index.ts +6 -1
  58. package/src/ontology-storage/materialization-session.ts +120 -211
  59. package/src/ontology-storage/materialization-state.ts +5 -261
  60. package/src/ontology-storage/materialization-writer.ts +11 -13
  61. package/src/ontology-storage/materializations.ts +133 -210
  62. package/src/ontology-storage/outbox.ts +50 -16
  63. package/src/ontology-storage/replacement-plans.ts +1064 -0
  64. package/src/ontology-storage/shared.ts +17 -12
  65. package/src/ontology-storage/source-cleanup.ts +24 -9
  66. package/src/ontology-storage/source-roots.ts +38 -14
  67. package/src/ontology-storage/sources.ts +0 -26
  68. package/src/projection-run-storage.ts +7 -1
  69. package/src/rules-storage.ts +9 -14
  70. package/src/testing.ts +6 -4
@@ -17,32 +17,33 @@ import {
17
17
  assertExpectedObjectRevision,
18
18
  assertMaterializationFinalizationCorrelation,
19
19
  assertMaterializationHeader,
20
- assertMaterializationLaneCompletion,
21
20
  assertPageRows,
22
- assertPlanChunkCorrelations,
23
21
  assertSourceActivationCorrelation,
22
+ beginMaterializationApply,
23
+ beginMaterializationVectorChanges,
24
+ createCardinalityValidator,
24
25
  invalidCorrelation,
26
+ materializationOutboxWrite,
27
+ materializationPlanChunk,
25
28
  sameNonnegativeCounts as sameCounts,
26
29
  uniqueSorted,
27
30
  } from "@sixb/core/internal/ontology-storage-provider"
28
31
  import type {
29
- ApplyMaterializationChunkInput,
32
+ AppliedMaterialization,
33
+ ApplyMaterializationInput,
30
34
  ApplyMaterializationResult,
31
35
  FinalizeMaterializationInput,
32
- MaterializationObjectExistence,
33
36
  MaterializationPlanHeader,
37
+ MaterializationPlanWorkRecord,
34
38
  MaterializationSession,
35
39
  MaterializationStatePage,
36
- MaterializationWorkPage,
40
+ MaterializationVectorChangePage,
37
41
  OntologyCommitRecord,
38
42
  OntologyMaterializationStorage,
39
- ReadMaterializationObjectExistenceInput,
40
43
  SourceActivationWrite,
41
- SourceReplacementStatePage,
42
44
  StageMaterializationWorkInput,
43
45
  StreamMaterializationStateInput,
44
- StreamMaterializationWorkInput,
45
- StreamSourceReplacementStateInput,
46
+ StreamMaterializationVectorChangesInput,
46
47
  } from "@sixb/core/storage"
47
48
  import { yieldSqliteEventLoop } from "../transactions"
48
49
  import {
@@ -50,11 +51,9 @@ import {
50
51
  SqliteMaterializationSessions,
51
52
  type SqliteOntologyTransactionContext,
52
53
  } from "./materialization-session"
53
- import {
54
- SQLITE_MATERIALIZATION_WORK_TABLE,
55
- SqliteMaterializationStateReader,
56
- } from "./materialization-state"
54
+ import { SqliteMaterializationStateReader } from "./materialization-state"
57
55
  import { SqliteMaterializationWriter } from "./materialization-writer"
56
+ import { boundReplacementPlan } from "./replacement-plans"
58
57
  import {
59
58
  assertProjectionExecution,
60
59
  canonicalJson,
@@ -67,6 +66,54 @@ import {
67
66
  } from "./shared"
68
67
  import { activateSourceRoots } from "./source-roots"
69
68
 
69
+ /** Rows the provider reads and writes at once while it applies a staged plan. */
70
+ const APPLY_PAGE_ROWS = 1_000
71
+
72
+ /**
73
+ * Records the entities whose plan inputs a commit's applied work changed, once each, as the
74
+ * in-memory `touchPlanItem` does: a replacement plan's refresh rechecks only those.
75
+ */
76
+ function touchAppliedWorkStatement(workTable: string): string {
77
+ return `
78
+ INSERT INTO ontology_commit_touches (project_id, commit_id, entity_kind, identity_key)
79
+ SELECT DISTINCT $projectId, $commitId, kind, CASE kind
80
+ WHEN 'object' THEN
81
+ json_array(json_extract(ref, '$.objectTypeId'), json_extract(ref, '$.primaryId'))
82
+ WHEN 'scope' THEN json_array(
83
+ json_extract(ref, '$.source.objectTypeId'), json_extract(ref, '$.source.primaryId'),
84
+ json_extract(ref, '$.linkId')
85
+ )
86
+ ELSE json_array(
87
+ json_extract(ref, '$.source.objectTypeId'), json_extract(ref, '$.source.primaryId'),
88
+ json_extract(ref, '$.linkId'), json_extract(ref, '$.target.objectTypeId'),
89
+ json_extract(ref, '$.target.primaryId')
90
+ )
91
+ END
92
+ FROM (
93
+ SELECT CASE json_extract(payload, '$.item.kind')
94
+ WHEN 'object-upsert' THEN 'object'
95
+ WHEN 'object-delete' THEN 'object'
96
+ WHEN 'object-override-upsert' THEN 'object'
97
+ WHEN 'object-override-delete' THEN 'object'
98
+ WHEN 'point-upsert' THEN 'object'
99
+ WHEN 'link-upsert' THEN 'link'
100
+ WHEN 'link-delete' THEN 'link'
101
+ WHEN 'link-override-upsert' THEN 'link'
102
+ WHEN 'link-override-delete' THEN 'link'
103
+ WHEN 'link-slot-override-upsert' THEN 'scope'
104
+ WHEN 'link-slot-override-delete' THEN 'scope'
105
+ END AS kind,
106
+ CASE json_extract(payload, '$.item.kind')
107
+ WHEN 'object-upsert' THEN json_extract(payload, '$.item.value.row.ref')
108
+ WHEN 'link-upsert' THEN json_extract(payload, '$.item.value.row.ref')
109
+ WHEN 'point-upsert' THEN json_extract(payload, '$.item.value.point.series.object')
110
+ ELSE json_extract(payload, '$.item.value.ref')
111
+ END AS ref
112
+ FROM ${workTable}
113
+ WHERE work_id = $workId AND lane = 'apply'
114
+ )`
115
+ }
116
+
70
117
  export class SqliteOntologyMaterializationStorage implements OntologyMaterializationStorage {
71
118
  private readonly sessions: SqliteMaterializationSessions
72
119
  private readonly writer: SqliteMaterializationWriter
@@ -93,7 +140,10 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
93
140
 
94
141
  async begin(input: MaterializationPlanHeader): Promise<MaterializationSession> {
95
142
  assertMaterializationHeader(input)
96
- const session = this.sessions.create(input)
143
+ const session = this.sessions.create(
144
+ input,
145
+ input.plan ? boundReplacementPlan(this.db, input) : null
146
+ )
97
147
  try {
98
148
  this.assertCommitAbsent(input)
99
149
  const reader = new SqliteMaterializationStateReader(this.db, input.commit.projectId)
@@ -178,134 +228,67 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
178
228
  }
179
229
  }
180
230
 
181
- async *streamSourceReplacementState(
182
- input: StreamSourceReplacementStateInput
183
- ): AsyncIterable<SourceReplacementStatePage> {
184
- const session = this.sessions.require(input.session)
185
- assertPageRows(input.pageRows)
186
- const replacement = this.requireReplacement(session, input)
187
- const reader = new SqliteMaterializationStateReader(this.db, session.header.commit.projectId)
188
- if (input.entityKind === "object") {
189
- if (replacement.projectionKind !== "object") {
190
- throw new MaterializationConflictError(
191
- "source-materialization",
192
- "Link projection replacement cannot stream object state."
193
- )
194
- }
195
- if (replacement.objectStreamStarted) {
196
- throw new MaterializationConflictError(
197
- "effective-state",
198
- "Replacement object state may only be streamed once per session."
199
- )
200
- }
201
- replacement.objectStreamStarted = true
202
- for (const identities of reader.replacementIdentities({
203
- sessionId: session.id,
204
- sourceId: replacement.sourceId,
205
- candidateMaterializationId: replacement.candidateMaterializationId,
206
- previousMaterializationId: replacement.previousMaterializationId,
207
- incremental: replacement.incremental,
208
- kind: "object",
209
- pageRows: input.pageRows,
210
- })) {
211
- this.sessions.require(input.session)
212
- const refs = identities.map((identity) => {
213
- if (identity.kind !== "object") {
214
- invalidCorrelation("Object replacement returned a link identity.")
215
- }
216
- return identity.ref
217
- })
218
- const objects = reader.replacementObjectStates(
219
- replacement.sourceId,
220
- replacement.candidateMaterializationId,
221
- refs
222
- )
223
- yield { objects, links: [] }
224
- }
225
- this.sessions.require(input.session)
226
- replacement.objectStreamCompleted = true
227
- return
228
- }
229
-
230
- if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
231
- throw new MaterializationConflictError(
232
- "effective-state",
233
- "Object projection replacement must fully stream object state before link state."
234
- )
235
- }
236
- if (replacement.linkStreamStarted) {
237
- throw new MaterializationConflictError(
238
- "effective-state",
239
- "Replacement link state may only be streamed once per session."
240
- )
241
- }
242
- replacement.linkStreamStarted = true
243
- const materializationIds = [
244
- replacement.candidateMaterializationId,
245
- ...(replacement.previousMaterializationId ? [replacement.previousMaterializationId] : []),
246
- ]
247
- for (const identities of reader.replacementIdentities({
248
- sessionId: session.id,
249
- sourceId: replacement.sourceId,
250
- candidateMaterializationId: replacement.candidateMaterializationId,
251
- previousMaterializationId: replacement.previousMaterializationId,
252
- incremental: replacement.incremental,
253
- kind: "link",
254
- pageRows: input.pageRows,
255
- })) {
256
- this.sessions.require(input.session)
257
- const linkIdentities = identities.map((identity) => {
258
- if (identity.kind !== "link") {
259
- invalidCorrelation("Link replacement returned an object identity.")
260
- }
261
- return identity
262
- })
263
- const links = reader.replacementLinkStates(
264
- replacement.sourceId,
265
- replacement.candidateMaterializationId,
266
- materializationIds,
267
- linkIdentities,
268
- replacement.incremental
269
- )
270
- yield { objects: [], links }
271
- }
272
- this.sessions.require(input.session)
273
- replacement.linkStreamCompleted = true
274
- }
275
-
276
231
  async stageWork(input: StageMaterializationWorkInput): Promise<void> {
277
232
  this.sessions.stage(input)
278
233
  await yieldSqliteEventLoop()
279
234
  }
280
235
 
281
- streamWork(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
282
- assertPageRows(input.pageRows)
283
- return this.sessions.stream(input)
284
- }
285
-
286
- async readObjectExistence(
287
- input: ReadMaterializationObjectExistenceInput
288
- ): Promise<readonly MaterializationObjectExistence[]> {
236
+ async *streamVectorChanges(
237
+ input: StreamMaterializationVectorChangesInput
238
+ ): AsyncIterable<MaterializationVectorChangePage> {
289
239
  const session = this.sessions.require(input.session)
290
- return this.sessions.readObjectExistence(session, input.refs)
240
+ assertPageRows(input.pageRows)
241
+ beginMaterializationVectorChanges(session)
242
+ for (const items of this.sessions.vectorChangePages(
243
+ session,
244
+ input.objectTypeIds,
245
+ input.pageRows
246
+ )) {
247
+ yield { items }
248
+ }
291
249
  }
292
250
 
293
- async applyChunk(input: ApplyMaterializationChunkInput): Promise<void> {
251
+ async apply(input: ApplyMaterializationInput): Promise<AppliedMaterialization> {
294
252
  const session = this.sessions.require(input.session)
253
+ beginMaterializationApply(session)
295
254
  const { commit } = session.header
296
- assertPlanChunkCorrelations(input.chunk, commit)
297
- const sequence = this.sessions.prepareChunkSequence(session, input.chunk)
298
- this.db.run("SAVEPOINT sixb_ontology_apply_chunk")
255
+ const cardinality = createCardinalityValidator()
256
+ for (const records of this.sessions.lanePages(session, "cardinality", APPLY_PAGE_ROWS)) {
257
+ for (const record of records) cardinality.accept(record)
258
+ }
259
+ let eventCount = 0
260
+ this.db.run("SAVEPOINT sixb_ontology_apply")
299
261
  try {
300
- this.writer.apply(commit.projectId, commit.id, input.chunk)
301
- this.db.run("RELEASE SAVEPOINT sixb_ontology_apply_chunk")
302
- this.sessions.commitChunkSequence(session, sequence)
262
+ for (const records of this.sessions.lanePages(session, "apply", APPLY_PAGE_ROWS)) {
263
+ // The writer orders tables its own way within a chunk; one phase per chunk keeps the
264
+ // canonical phase order.
265
+ for (const phase of splitByPhase(records)) {
266
+ this.writer.apply(
267
+ commit.projectId,
268
+ commit.id,
269
+ materializationPlanChunk(phase.map((record) => record.item))
270
+ )
271
+ }
272
+ await yieldSqliteEventLoop()
273
+ }
274
+ for (const records of this.sessions.lanePages(session, "event", APPLY_PAGE_ROWS)) {
275
+ const outbox = records.map((record) =>
276
+ materializationOutboxWrite(commit, record.draft, eventCount++)
277
+ )
278
+ this.writer.apply(commit.projectId, commit.id, materializationPlanChunk([], outbox))
279
+ await yieldSqliteEventLoop()
280
+ }
281
+ this.db
282
+ .query(touchAppliedWorkStatement(session.workTable))
283
+ .run({ $projectId: commit.projectId, $commitId: commit.id, $workId: session.workId })
284
+ this.db.run("RELEASE SAVEPOINT sixb_ontology_apply")
303
285
  } catch (error) {
304
- this.db.run("ROLLBACK TO SAVEPOINT sixb_ontology_apply_chunk")
305
- this.db.run("RELEASE SAVEPOINT sixb_ontology_apply_chunk")
286
+ this.db.run("ROLLBACK TO SAVEPOINT sixb_ontology_apply")
287
+ this.db.run("RELEASE SAVEPOINT sixb_ontology_apply")
306
288
  throw error
307
289
  }
308
- await yieldSqliteEventLoop()
290
+ session.appliedEventCount = eventCount
291
+ return { eventCount }
309
292
  }
310
293
 
311
294
  async finalize(input: FinalizeMaterializationInput): Promise<ApplyMaterializationResult> {
@@ -324,66 +307,6 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
324
307
  this.sessions.deactivateAll()
325
308
  }
326
309
 
327
- private requireReplacement(
328
- session: SqliteMaterializationSessionState,
329
- input: StreamSourceReplacementStateInput
330
- ): NonNullable<SqliteMaterializationSessionState["replacement"]> {
331
- if (session.replacement) {
332
- if (
333
- session.replacement.sourceId !== input.source.projectionId ||
334
- session.replacement.candidateMaterializationId !== input.candidateMaterializationId
335
- ) {
336
- throw new MaterializationConflictError(
337
- "source-materialization",
338
- "Materialization session already owns another replacement union."
339
- )
340
- }
341
- return session.replacement
342
- }
343
- const projectId = session.header.commit.projectId
344
- const candidate = this.getSource(
345
- projectId,
346
- input.source.projectionId,
347
- input.candidateMaterializationId
348
- )
349
- if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
350
- throw new MaterializationConflictError(
351
- "source-materialization",
352
- `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`
353
- )
354
- }
355
- assertProjectionExecution(this.db, {
356
- projectId,
357
- sourceId: input.source.projectionId,
358
- projectionRunId: candidate.projection_run_id,
359
- executionToken: candidate.execution_token,
360
- })
361
- const previous = this.getActiveSource(projectId, input.source.projectionId)
362
- if (
363
- candidate.protocol !== "replacement" ||
364
- (previous &&
365
- (previous.protocol !== candidate.protocol ||
366
- previous.projection_kind !== candidate.projection_kind))
367
- ) {
368
- throw new MaterializationConflictError(
369
- "source-materialization",
370
- "Source replacement kind or protocol does not match its active materialization."
371
- )
372
- }
373
- session.replacement = {
374
- sourceId: input.source.projectionId,
375
- candidateMaterializationId: input.candidateMaterializationId,
376
- previousMaterializationId: previous?.materialization_id ?? null,
377
- incremental: candidate.base_materialization_id !== null,
378
- projectionKind: candidate.projection_kind,
379
- objectStreamStarted: false,
380
- objectStreamCompleted: false,
381
- linkStreamStarted: false,
382
- linkStreamCompleted: false,
383
- }
384
- return session.replacement
385
- }
386
-
387
310
  private assertCommitAbsent(header: MaterializationPlanHeader): void {
388
311
  const origin = originColumns(header.commit.origin)
389
312
  const duplicate = this.db
@@ -454,11 +377,9 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
454
377
  const { commit } = session.header
455
378
  const { result } = input.finalization
456
379
  assertMaterializationFinalizationCorrelation(session, input)
457
- const laneCounts = this.sessions.laneCounts(session)
458
- assertMaterializationLaneCompletion(session, laneCounts)
459
380
  this.assertFinalCardinality(session)
460
381
  await yieldSqliteEventLoop()
461
- const eventCount = laneCounts.event
382
+ const eventCount = session.appliedEventCount
462
383
  const outbox = this.db
463
384
  .query(
464
385
  `
@@ -517,8 +438,8 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
517
438
  THEN cardinality_target_type_id END) AS expected_target_type_id,
518
439
  MAX(CASE WHEN cardinality_occupied = 1
519
440
  THEN cardinality_target_primary_id END) AS expected_target_id
520
- FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
521
- WHERE session_id = ? AND kind = 'cardinality'
441
+ FROM ${session.workTable}
442
+ WHERE work_id = ? AND kind = 'cardinality'
522
443
  AND cardinality_view = 'effective'
523
444
  GROUP BY sort_one, cardinality_source_type_id,
524
445
  cardinality_source_primary_id, cardinality_link_id
@@ -538,20 +459,14 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
538
459
  GROUP BY scopes.scope_sort_key, scopes.expected_count,
539
460
  scopes.expected_target_type_id, scopes.expected_target_id
540
461
  )
541
- SELECT CASE WHEN expected_count > 1 THEN 'duplicate' ELSE 'mismatch' END AS reason
462
+ SELECT 1 AS mismatch
542
463
  FROM validated
543
- WHERE expected_count > 1
544
- OR actual_count <> expected_count
545
- OR matching_count <> expected_count
464
+ WHERE actual_count <> expected_count OR matching_count <> expected_count
546
465
  LIMIT 1
547
466
  `
548
467
  )
549
- .get(session.id, session.header.commit.projectId) as {
550
- readonly reason: "duplicate" | "mismatch"
551
- } | null
552
- if (violation?.reason === "duplicate") {
553
- invalidCorrelation("Materialization cardinality work violates cardinality-one.")
554
- }
468
+ .get(session.workId, session.header.commit.projectId)
469
+ // Apply already rejected a scope with two staged occupants; this checks what it wrote.
555
470
  if (violation) {
556
471
  invalidCorrelation(
557
472
  "Materialization cardinality work does not match the final effective link scope."
@@ -599,7 +514,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
599
514
  invalidCorrelation("Source activation does not match its ready candidate identity.")
600
515
  }
601
516
  this.assertSource(activation.expected, commit.projectId)
602
- activateSourceRoots(this.db, commit.projectId, candidate, activation)
517
+ activateSourceRoots(this.db, commit.projectId, commit.id, candidate, activation)
603
518
  const previous = this.getActiveSource(commit.projectId, activation.source.projectionId)
604
519
  if (previous) {
605
520
  assertPinnedDatasetWatermark(
@@ -673,12 +588,10 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
673
588
  `
674
589
  INSERT INTO ontology_commits (
675
590
  project_id, id, idempotency_key, request_hash, execution_id,
676
- origin_kind, origin_run_id, origin_batch_ordinal, origin, requested_by, executor,
591
+ origin_kind, origin_run_id, origin_batch_ordinal, origin,
677
592
  ontology_revision, projection_revision, ownership_hash,
678
593
  intent, result, committed_at
679
- ) VALUES (
680
- ?, ?, ?, ?, ?, ?, ?, ?, json(?), json(?), json(?), ?, ?, ?, json(?), json(?), ?
681
- )
594
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, json(?), ?, ?, ?, json(?), json(?), ?)
682
595
  `
683
596
  )
684
597
  .run(
@@ -691,8 +604,6 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
691
604
  origin.runId,
692
605
  origin.batchOrdinal,
693
606
  canonicalJson(commit.origin),
694
- commit.requestedBy === undefined ? null : canonicalJson(commit.requestedBy),
695
- canonicalJson(commit.executor),
696
607
  commit.ontologyRevision,
697
608
  commit.projectionRevision ?? null,
698
609
  commit.ownershipHash ?? null,
@@ -737,3 +648,15 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
737
648
  .get(projectId, sourceId, materializationId) as SqliteOntologySourceRow | null
738
649
  }
739
650
  }
651
+
652
+ function splitByPhase(
653
+ records: readonly MaterializationPlanWorkRecord[]
654
+ ): MaterializationPlanWorkRecord[][] {
655
+ const phases: MaterializationPlanWorkRecord[][] = []
656
+ for (const record of records) {
657
+ const current = phases[phases.length - 1]
658
+ if (current && current[0]!.applyPhase === record.applyPhase) current.push(record)
659
+ else phases.push([record])
660
+ }
661
+ return phases
662
+ }
@@ -4,6 +4,10 @@ import {
4
4
  MaterializationConflictError,
5
5
  MaterializationValidationError,
6
6
  } from "@sixb/core/internal/materialization"
7
+ import {
8
+ claimedOutboxRows,
9
+ type OutboxEventContext,
10
+ } from "@sixb/core/internal/ontology-storage-provider"
7
11
  import type {
8
12
  ClaimedOntologyOutboxRow,
9
13
  ClaimOntologyOutboxInput,
@@ -15,15 +19,24 @@ import type {
15
19
  SummarizeOntologyOutboxInput,
16
20
  } from "@sixb/core/storage"
17
21
  import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage"
22
+ import { type SqliteExecutionRow, sqliteExecutionRecord } from "../execution-storage"
18
23
  import {
19
24
  assertNonblank,
20
25
  assertPositiveInteger,
21
26
  assertTimestamp,
22
- outboxRecord,
27
+ claimedOutboxDraft,
28
+ parseJson,
23
29
  type SqliteOntologyOutboxRow,
24
30
  type SqliteRootOperation,
25
31
  } from "./shared"
26
32
 
33
+ /** A commit's own fields, prefixed, beside every column of the execution it ran under. */
34
+ interface SqliteOutboxContextRow extends SqliteExecutionRow {
35
+ readonly commit_id: string
36
+ readonly commit_committed_at: string
37
+ readonly commit_origin: string
38
+ }
39
+
27
40
  export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
28
41
  constructor(
29
42
  private readonly db: Database,
@@ -74,8 +87,8 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
74
87
  UPDATE ontology_outbox
75
88
  SET attempts = attempts + 1, lease_id = ?, lease_expires_at = ?
76
89
  WHERE rowid IN (SELECT CAST(value AS INTEGER) FROM json_each(?))
77
- RETURNING envelope, available_at, attempts, lease_id, lease_expires_at,
78
- published_at, last_failure, created_at
90
+ RETURNING id, commit_id, commit_ordinal, event, available_at, attempts, lease_id,
91
+ lease_expires_at, published_at, last_failure, created_at
79
92
  `
80
93
  )
81
94
  .all(
@@ -83,22 +96,31 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
83
96
  input.leaseExpiresAt,
84
97
  JSON.stringify(selected.map((row) => row.rowId))
85
98
  ) as SqliteOntologyOutboxRow[]
86
- return rows
87
- .map((row) => outboxRecord(row))
99
+ // Each commit and its execution are read once, however many of its events the batch holds.
100
+ const contexts = this.db
101
+ .query(
102
+ `SELECT commits.id AS commit_id, commits.committed_at AS commit_committed_at,
103
+ commits.origin AS commit_origin, executions.*
104
+ FROM json_each(?) AS requested
105
+ CROSS JOIN ontology_commits AS commits
106
+ ON commits.project_id = ? AND commits.id = requested.value
107
+ CROSS JOIN executions
108
+ ON executions.project_id = commits.project_id AND executions.id = commits.execution_id`
109
+ )
110
+ .all(
111
+ JSON.stringify([...new Set(rows.map((row) => row.commit_id))]),
112
+ input.projectId
113
+ ) as SqliteOutboxContextRow[]
114
+ const contextOf = new Map(
115
+ contexts.map((row) => [row.commit_id, outboxEventContext(input.projectId, row)])
116
+ )
117
+ const drafts = rows
118
+ .map(claimedOutboxDraft)
88
119
  .sort(
89
120
  (left, right) =>
90
- left.createdAt.localeCompare(right.createdAt) ||
91
- left.envelope.id.localeCompare(right.envelope.id)
121
+ left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
92
122
  )
93
- .map((record) => {
94
- if (record.leaseId === null || record.leaseExpiresAt === null) {
95
- throw new MaterializationConflictError(
96
- "outbox-lease",
97
- "Ontology outbox claim returned an unpaired lease."
98
- )
99
- }
100
- return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt }
101
- })
123
+ return claimedOutboxRows(drafts, (commitId) => contextOf.get(commitId))
102
124
  })
103
125
  }
104
126
 
@@ -235,6 +257,18 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
235
257
  }
236
258
  }
237
259
 
260
+ function outboxEventContext(projectId: string, row: SqliteOutboxContextRow): OutboxEventContext {
261
+ return {
262
+ commit: {
263
+ projectId,
264
+ id: row.commit_id,
265
+ committedAt: row.commit_committed_at,
266
+ origin: parseJson<OutboxEventContext["commit"]["origin"]>(row.commit_origin),
267
+ },
268
+ execution: sqliteExecutionRecord(row),
269
+ }
270
+ }
271
+
238
272
  function validateLeaseIds(ids: readonly string[]): string[] {
239
273
  const seen = new Set<string>()
240
274
  for (const id of ids) {