@sixb/sqlite 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +18 -13
  2. package/dist/action-run-storage.d.ts +5 -2
  3. package/dist/action-run-storage.d.ts.map +1 -1
  4. package/dist/agents/agent-storage.d.ts +9 -5
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/checkpoints.d.ts +14 -0
  7. package/dist/agents/checkpoints.d.ts.map +1 -0
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/messages.d.ts +2 -1
  11. package/dist/agents/messages.d.ts.map +1 -1
  12. package/dist/agents/rows.d.ts +21 -11
  13. package/dist/agents/rows.d.ts.map +1 -1
  14. package/dist/agents/runs.d.ts +3 -2
  15. package/dist/agents/runs.d.ts.map +1 -1
  16. package/dist/ai-cost-storage.d.ts +15 -0
  17. package/dist/ai-cost-storage.d.ts.map +1 -0
  18. package/dist/ai-usage-storage.d.ts +22 -0
  19. package/dist/ai-usage-storage.d.ts.map +1 -0
  20. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  21. package/dist/auth-storage/shared.d.ts +1 -10
  22. package/dist/auth-storage/shared.d.ts.map +1 -1
  23. package/dist/connector-connection-storage.d.ts +9 -0
  24. package/dist/connector-connection-storage.d.ts.map +1 -0
  25. package/dist/execution-storage.d.ts +14 -0
  26. package/dist/execution-storage.d.ts.map +1 -0
  27. package/dist/index.d.ts +14 -9
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4633 -1066
  30. package/dist/index.js.map +33 -28
  31. package/dist/latest-run-query.d.ts.map +1 -1
  32. package/dist/migrations.d.ts +0 -1
  33. package/dist/migrations.d.ts.map +1 -1
  34. package/dist/ontology-storage/materialization-session.d.ts +50 -8
  35. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  36. package/dist/ontology-storage/materialization-state.d.ts +4 -5
  37. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  38. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  39. package/dist/ontology-storage/materializations.d.ts +0 -2
  40. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  41. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  42. package/dist/ontology-storage/shared.d.ts +3 -3
  43. package/dist/ontology-storage/shared.d.ts.map +1 -1
  44. package/dist/ontology-storage/sources.d.ts.map +1 -1
  45. package/dist/pipeline-run-storage.d.ts +7 -2
  46. package/dist/pipeline-run-storage.d.ts.map +1 -1
  47. package/dist/projection-run-storage.d.ts +9 -2
  48. package/dist/projection-run-storage.d.ts.map +1 -1
  49. package/dist/run-list-query.d.ts +1 -1
  50. package/dist/run-list-query.d.ts.map +1 -1
  51. package/dist/sync-run-storage.d.ts +7 -2
  52. package/dist/sync-run-storage.d.ts.map +1 -1
  53. package/dist/testing.d.ts +6 -0
  54. package/dist/testing.d.ts.map +1 -0
  55. package/dist/transactions.d.ts +3 -0
  56. package/dist/transactions.d.ts.map +1 -1
  57. package/dist/webhook-run-storage.d.ts +9 -2
  58. package/dist/webhook-run-storage.d.ts.map +1 -1
  59. package/dist/workflow-run-storage.d.ts +7 -3
  60. package/dist/workflow-run-storage.d.ts.map +1 -1
  61. package/package.json +3 -3
  62. package/src/action-run-storage.ts +51 -74
  63. package/src/agents/agent-storage.ts +11 -6
  64. package/src/agents/checkpoints.ts +175 -0
  65. package/src/agents/index.ts +1 -0
  66. package/src/agents/messages.ts +59 -0
  67. package/src/agents/rows.ts +52 -40
  68. package/src/agents/runs.ts +27 -21
  69. package/src/ai-cost-storage.ts +744 -0
  70. package/src/ai-usage-storage.ts +324 -0
  71. package/src/auth-storage/service-accounts.ts +14 -21
  72. package/src/auth-storage/shared.ts +0 -22
  73. package/src/connector-connection-storage.ts +784 -0
  74. package/src/execution-storage.ts +158 -0
  75. package/src/index.ts +69 -22
  76. package/src/latest-run-query.ts +7 -1
  77. package/src/migrations/004-executions.sql +142 -0
  78. package/src/migrations/005-workflow-executions.sql +7 -0
  79. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  80. package/src/migrations/007-split-overrides.sql +154 -0
  81. package/src/migrations/008-action-executions.sql +4 -0
  82. package/src/migrations/009-agent-executions.sql +115 -0
  83. package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
  84. package/src/migrations/011-sync-failure-record.sql +22 -0
  85. package/src/migrations/012-pipeline-failure-record.sql +45 -0
  86. package/src/migrations/013-workflow-failure-record.sql +45 -0
  87. package/src/migrations/014-agent-failure-record.sql +59 -0
  88. package/src/migrations/015-projection-failure-record.sql +20 -0
  89. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  90. package/src/migrations/017-action-failure-record.sql +65 -0
  91. package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
  92. package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
  93. package/src/migrations/020-drop-run-usage-projections.sql +7 -0
  94. package/src/migrations/021-sync-pipeline-executions.sql +101 -0
  95. package/src/migrations/022-projection-executions.sql +244 -0
  96. package/src/migrations/023-webhook-executions.sql +51 -0
  97. package/src/migrations/024-ontology-commit-executions.sql +58 -0
  98. package/src/migrations/025-connector-connections.sql +257 -0
  99. package/src/migrations/026-ai-cost-accounting.sql +57 -0
  100. package/src/migrations/027-agent-context-checkpoints.sql +28 -0
  101. package/src/migrations/028-object-override-edit-times.sql +3 -0
  102. package/src/migrations.ts +129 -4
  103. package/src/ontology-storage/materialization-session.ts +317 -143
  104. package/src/ontology-storage/materialization-state.ts +323 -112
  105. package/src/ontology-storage/materialization-writer.ts +145 -67
  106. package/src/ontology-storage/materializations.ts +67 -141
  107. package/src/ontology-storage/outbox.ts +10 -3
  108. package/src/ontology-storage/shared.ts +10 -4
  109. package/src/ontology-storage/sources.ts +4 -1
  110. package/src/pipeline-run-storage.ts +128 -43
  111. package/src/projection-run-storage.ts +209 -97
  112. package/src/run-list-query.ts +9 -4
  113. package/src/sync-run-storage.ts +118 -32
  114. package/src/testing.ts +20 -0
  115. package/src/transactions.ts +10 -3
  116. package/src/webhook-run-storage.ts +118 -74
  117. package/src/workflow-run-storage.ts +117 -131
  118. package/dist/validation.d.ts +0 -5
  119. package/dist/validation.d.ts.map +0 -1
  120. package/dist/webhook-delivery-storage.d.ts +0 -27
  121. package/dist/webhook-delivery-storage.d.ts.map +0 -1
  122. package/src/validation.ts +0 -30
  123. package/src/webhook-delivery-storage.ts +0 -272
@@ -5,11 +5,7 @@ import {
5
5
  objectRefKey,
6
6
  telemetryPointKey,
7
7
  } from "@sixb/core/internal/materialization"
8
- import {
9
- effectiveConflict,
10
- type OverrideEntity,
11
- overrideEntityColumns as overrideColumns,
12
- } from "@sixb/core/internal/ontology-storage-provider"
8
+ import { effectiveConflict } from "@sixb/core/internal/ontology-storage-provider"
13
9
  import type {
14
10
  ExactEffectiveLinkWrite,
15
11
  ExactEffectiveObjectWrite,
@@ -17,6 +13,16 @@ import type {
17
13
  } from "@sixb/core/storage"
18
14
  import { assertTimestamp, canonicalJson, isSqliteConstraintError, requireChanges } from "./shared"
19
15
 
16
+ function linkOverrideConflictMessage(identityKind: "edge" | "slot"): string {
17
+ return identityKind === "edge"
18
+ ? "Expected link edge override changed."
19
+ : "Expected link slot override changed."
20
+ }
21
+
22
+ function linkOverrideConflict(identityKind: "edge" | "slot"): MaterializationConflictError {
23
+ return effectiveConflict(linkOverrideConflictMessage(identityKind))
24
+ }
25
+
20
26
  /** Applies one already-validated exact plan chunk to SQLite authority tables. */
21
27
  export class SqliteMaterializationWriter {
22
28
  constructor(private readonly db: Database) {}
@@ -29,103 +35,175 @@ export class SqliteMaterializationWriter {
29
35
  }
30
36
 
31
37
  private applyOverrides(projectId: string, chunk: MaterializationPlanChunk): void {
32
- const upsert = (
33
- entity: OverrideEntity,
34
- item:
35
- | (typeof chunk.overrides.objectUpserts)[number]
36
- | (typeof chunk.overrides.linkUpserts)[number]
37
- ): void => {
38
- const kind = entity.kind
39
- const key = kind === "object" ? objectRefKey(entity.ref) : linkRefKey(entity.ref)
40
- const columns = overrideColumns(entity)
38
+ for (const item of chunk.overrides.objects.upserts) {
41
39
  if (item.expectedLastCommitId === null) {
42
40
  try {
43
41
  this.db
44
42
  .query(
45
- `
46
- INSERT INTO ontology_overrides (
47
- project_id, entity_kind, entity_key, entity_sort_key,
48
- object_type_id, primary_id, source_type_id, source_primary_id,
49
- link_id, target_type_id, target_primary_id,
50
- value, last_commit_id, updated_at
51
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, json(?), ?, ?)
52
- `
43
+ `INSERT INTO ontology_object_overrides (
44
+ project_id, object_type_id, primary_id, value, edited_at,
45
+ last_commit_id, updated_at
46
+ ) VALUES (?, ?, ?, json(?), json(?), ?, ?)`
53
47
  )
54
48
  .run(
55
49
  projectId,
56
- kind,
57
- key,
58
- columns.sortKey,
59
- columns.objectTypeId,
60
- columns.primaryId,
61
- columns.sourceTypeId,
62
- columns.sourcePrimaryId,
63
- columns.linkId,
64
- columns.targetTypeId,
65
- columns.targetPrimaryId,
50
+ item.ref.objectTypeId,
51
+ item.ref.primaryId,
66
52
  canonicalJson(item.value),
53
+ canonicalJson(item.editedAt),
67
54
  item.lastCommitId,
68
55
  item.updatedAt
69
56
  )
70
57
  } catch (error) {
71
58
  if (isSqliteConstraintError(error)) {
72
- throw effectiveConflict(`Expected ${kind} override changed.`)
59
+ throw effectiveConflict("Expected object override changed.")
73
60
  }
74
61
  throw error
75
62
  }
76
- } else {
77
- requireChanges(
63
+ continue
64
+ }
65
+ requireChanges(
66
+ this.db
67
+ .query(
68
+ `UPDATE ontology_object_overrides
69
+ SET value = json(?), edited_at = json(?), last_commit_id = ?, updated_at = ?
70
+ WHERE project_id = ? AND object_type_id = ? AND primary_id = ?
71
+ AND last_commit_id = ?`
72
+ )
73
+ .run(
74
+ canonicalJson(item.value),
75
+ canonicalJson(item.editedAt),
76
+ item.lastCommitId,
77
+ item.updatedAt,
78
+ projectId,
79
+ item.ref.objectTypeId,
80
+ item.ref.primaryId,
81
+ item.expectedLastCommitId
82
+ ).changes,
83
+ "effective-state",
84
+ "Expected object override changed."
85
+ )
86
+ }
87
+ for (const item of chunk.overrides.objects.deletes) {
88
+ requireChanges(
89
+ this.db
90
+ .query(
91
+ `DELETE FROM ontology_object_overrides
92
+ WHERE project_id = ? AND object_type_id = ? AND primary_id = ?
93
+ AND last_commit_id = ?`
94
+ )
95
+ .run(projectId, item.ref.objectTypeId, item.ref.primaryId, item.expectedLastCommitId)
96
+ .changes,
97
+ "effective-state",
98
+ "Expected object override changed."
99
+ )
100
+ }
101
+
102
+ const upsertLink = (
103
+ identityKind: "edge" | "slot",
104
+ identityKey: string,
105
+ ref: {
106
+ readonly source: { readonly objectTypeId: string; readonly primaryId: string }
107
+ readonly linkId: string
108
+ readonly target: { readonly objectTypeId: string; readonly primaryId: string }
109
+ },
110
+ item:
111
+ | (typeof chunk.overrides.links.edges.upserts)[number]
112
+ | (typeof chunk.overrides.links.slots.upserts)[number]
113
+ ): void => {
114
+ if (item.expectedLastCommitId === null) {
115
+ try {
78
116
  this.db
79
117
  .query(
80
- `
81
- UPDATE ontology_overrides
82
- SET value = json(?), last_commit_id = ?, updated_at = ?
83
- WHERE project_id = ? AND entity_kind = ? AND entity_key = ?
84
- AND last_commit_id = ?
85
- `
118
+ `INSERT INTO ontology_link_overrides (
119
+ project_id, identity_kind, identity_key,
120
+ source_type_id, source_primary_id, link_id,
121
+ target_type_id, target_primary_id, value, last_commit_id, updated_at
122
+ ) VALUES (?, ?, json(?), ?, ?, ?, ?, ?, json(?), ?, ?)`
86
123
  )
87
124
  .run(
125
+ projectId,
126
+ identityKind,
127
+ identityKey,
128
+ ref.source.objectTypeId,
129
+ ref.source.primaryId,
130
+ ref.linkId,
131
+ ref.target.objectTypeId,
132
+ ref.target.primaryId,
88
133
  canonicalJson(item.value),
89
134
  item.lastCommitId,
90
- item.updatedAt,
91
- projectId,
92
- kind,
93
- key,
94
- item.expectedLastCommitId
95
- ).changes,
96
- "effective-state",
97
- `Expected ${kind} override changed.`
98
- )
135
+ item.updatedAt
136
+ )
137
+ } catch (error) {
138
+ if (isSqliteConstraintError(error)) {
139
+ throw linkOverrideConflict(identityKind)
140
+ }
141
+ throw error
142
+ }
143
+ return
99
144
  }
100
- }
101
- for (const item of chunk.overrides.objectUpserts) {
102
- upsert({ kind: "object", ref: item.ref }, item)
103
- }
104
- for (const item of chunk.overrides.objectDeletes) {
105
145
  requireChanges(
106
146
  this.db
107
147
  .query(
108
- `DELETE FROM ontology_overrides
109
- WHERE project_id = ? AND entity_kind = 'object' AND entity_key = ? AND last_commit_id = ?`
148
+ `UPDATE ontology_link_overrides
149
+ SET target_type_id = ?, target_primary_id = ?, value = json(?),
150
+ last_commit_id = ?, updated_at = ?
151
+ WHERE project_id = ? AND identity_kind = ? AND identity_key = json(?)
152
+ AND last_commit_id = ?`
110
153
  )
111
- .run(projectId, objectRefKey(item.ref), item.expectedLastCommitId).changes,
154
+ .run(
155
+ ref.target.objectTypeId,
156
+ ref.target.primaryId,
157
+ canonicalJson(item.value),
158
+ item.lastCommitId,
159
+ item.updatedAt,
160
+ projectId,
161
+ identityKind,
162
+ identityKey,
163
+ item.expectedLastCommitId
164
+ ).changes,
112
165
  "effective-state",
113
- "Expected object override changed."
166
+ linkOverrideConflictMessage(identityKind)
114
167
  )
115
168
  }
116
- for (const item of chunk.overrides.linkUpserts) {
117
- upsert({ kind: "link", ref: item.ref }, item)
169
+
170
+ for (const item of chunk.overrides.links.edges.upserts) {
171
+ upsertLink("edge", linkRefKey(item.ref), item.ref, item)
172
+ }
173
+ for (const item of chunk.overrides.links.slots.upserts) {
174
+ upsertLink(
175
+ "slot",
176
+ JSON.stringify([item.ref.source.objectTypeId, item.ref.source.primaryId, item.ref.linkId]),
177
+ { source: item.ref.source, linkId: item.ref.linkId, target: item.value.target },
178
+ item
179
+ )
118
180
  }
119
- for (const item of chunk.overrides.linkDeletes) {
181
+
182
+ const deleteLink = (
183
+ identityKind: "edge" | "slot",
184
+ identityKey: string,
185
+ expectedLastCommitId: string
186
+ ): void => {
120
187
  requireChanges(
121
188
  this.db
122
189
  .query(
123
- `DELETE FROM ontology_overrides
124
- WHERE project_id = ? AND entity_kind = 'link' AND entity_key = ? AND last_commit_id = ?`
190
+ `DELETE FROM ontology_link_overrides
191
+ WHERE project_id = ? AND identity_kind = ? AND identity_key = json(?)
192
+ AND last_commit_id = ?`
125
193
  )
126
- .run(projectId, linkRefKey(item.ref), item.expectedLastCommitId).changes,
194
+ .run(projectId, identityKind, identityKey, expectedLastCommitId).changes,
127
195
  "effective-state",
128
- "Expected link override changed."
196
+ linkOverrideConflictMessage(identityKind)
197
+ )
198
+ }
199
+ for (const item of chunk.overrides.links.edges.deletes) {
200
+ deleteLink("edge", linkRefKey(item.ref), item.expectedLastCommitId)
201
+ }
202
+ for (const item of chunk.overrides.links.slots.deletes) {
203
+ deleteLink(
204
+ "slot",
205
+ JSON.stringify([item.ref.source.objectTypeId, item.ref.source.primaryId, item.ref.linkId]),
206
+ item.expectedLastCommitId
129
207
  )
130
208
  }
131
209
  }
@@ -366,7 +444,7 @@ export class SqliteMaterializationWriter {
366
444
  INSERT INTO ontology_outbox (
367
445
  project_id, id, commit_id, commit_ordinal, envelope,
368
446
  available_at, attempts, lease_id, lease_expires_at,
369
- published_at, last_error, created_at
447
+ published_at, last_failure, created_at
370
448
  ) VALUES (?, ?, ?, ?, json(?), ?, 0, NULL, NULL, NULL, NULL, ?)
371
449
  `
372
450
  )
@@ -1,6 +1,5 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import type {
3
- EffectiveChangeCounts,
4
3
  ExpectedLinkRevision,
5
4
  ExpectedObjectRevision,
6
5
  } from "@sixb/core/internal/materialization"
@@ -30,11 +29,8 @@ import type {
30
29
  ApplyMaterializationChunkInput,
31
30
  ApplyMaterializationResult,
32
31
  FinalizeMaterializationInput,
33
- MaterializationCardinalityOccupantWorkRecord,
34
- MaterializationClassificationWorkRecord,
35
32
  MaterializationObjectExistence,
36
33
  MaterializationPlanHeader,
37
- MaterializationPlanWorkRecord,
38
34
  MaterializationSession,
39
35
  MaterializationStatePage,
40
36
  MaterializationWorkPage,
@@ -48,14 +44,13 @@ import type {
48
44
  StreamMaterializationWorkInput,
49
45
  StreamSourceReplacementStateInput,
50
46
  } from "@sixb/core/storage"
51
- import { stableJsonStringify } from "@sixb/core/storage"
47
+ import { yieldSqliteEventLoop } from "../transactions"
52
48
  import {
53
49
  type SqliteMaterializationSessionState,
54
50
  SqliteMaterializationSessions,
55
51
  type SqliteOntologyTransactionContext,
56
52
  } from "./materialization-session"
57
53
  import {
58
- linkSortExpression,
59
54
  SQLITE_MATERIALIZATION_WORK_TABLE,
60
55
  SqliteMaterializationStateReader,
61
56
  } from "./materialization-state"
@@ -202,7 +197,6 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
202
197
  pageRows: input.pageRows,
203
198
  })) {
204
199
  this.sessions.require(input.session)
205
- this.sessions.recordReplacementIdentities(session, identities)
206
200
  const refs = identities.map((identity) => {
207
201
  if (identity.kind !== "object") {
208
202
  invalidCorrelation("Object replacement returned a link identity.")
@@ -247,7 +241,6 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
247
241
  pageRows: input.pageRows,
248
242
  })) {
249
243
  this.sessions.require(input.session)
250
- this.sessions.recordReplacementIdentities(session, identities)
251
244
  const linkIdentities = identities.map((identity) => {
252
245
  if (identity.kind !== "link") {
253
246
  invalidCorrelation("Link replacement returned an object identity.")
@@ -268,6 +261,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
268
261
 
269
262
  async stageWork(input: StageMaterializationWorkInput): Promise<void> {
270
263
  this.sessions.stage(input)
264
+ await yieldSqliteEventLoop()
271
265
  }
272
266
 
273
267
  streamWork(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
@@ -286,26 +280,24 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
286
280
  const session = this.sessions.require(input.session)
287
281
  const { commit } = session.header
288
282
  assertPlanChunkCorrelations(input.chunk, commit)
289
- const planCount = session.appliedPlanCount
290
- const outboxCount = session.appliedOutboxCount
291
- this.sessions.assertChunkSequence(session, input.chunk)
283
+ const sequence = this.sessions.prepareChunkSequence(session, input.chunk)
292
284
  this.db.run("SAVEPOINT sixb_ontology_apply_chunk")
293
285
  try {
294
286
  this.writer.apply(commit.projectId, commit.id, input.chunk)
295
287
  this.db.run("RELEASE SAVEPOINT sixb_ontology_apply_chunk")
288
+ this.sessions.commitChunkSequence(session, sequence)
296
289
  } catch (error) {
297
290
  this.db.run("ROLLBACK TO SAVEPOINT sixb_ontology_apply_chunk")
298
291
  this.db.run("RELEASE SAVEPOINT sixb_ontology_apply_chunk")
299
- session.appliedPlanCount = planCount
300
- session.appliedOutboxCount = outboxCount
301
292
  throw error
302
293
  }
294
+ await yieldSqliteEventLoop()
303
295
  }
304
296
 
305
297
  async finalize(input: FinalizeMaterializationInput): Promise<ApplyMaterializationResult> {
306
298
  const session = this.sessions.require(input.session)
307
299
  this.assertCommitAbsent(session.header)
308
- this.assertFinalization(session, input)
300
+ await this.assertFinalization(session, input)
309
301
  for (const activation of input.finalization.sourceActivations) {
310
302
  this.activateSource(session, activation)
311
303
  }
@@ -484,16 +476,17 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
484
476
  }
485
477
  }
486
478
 
487
- private assertFinalization(
479
+ private async assertFinalization(
488
480
  session: SqliteMaterializationSessionState,
489
481
  input: FinalizeMaterializationInput
490
- ): void {
482
+ ): Promise<void> {
491
483
  const { commit } = session.header
492
484
  const { result } = input.finalization
493
485
  assertMaterializationFinalizationCorrelation(session, input)
494
486
  const laneCounts = this.sessions.laneCounts(session)
495
487
  assertMaterializationLaneCompletion(session, laneCounts)
496
488
  this.assertFinalCardinality(session)
489
+ await yieldSqliteEventLoop()
497
490
  const eventCount = laneCounts.event
498
491
  const outbox = this.db
499
492
  .query(
@@ -513,149 +506,85 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
513
506
  ) {
514
507
  invalidCorrelation("Outbox event ordinals must be contiguous from zero.")
515
508
  }
509
+ await yieldSqliteEventLoop()
516
510
 
517
511
  if (commit.intent.kind === "telemetry") {
518
- const classifiedPoints = this.db
519
- .query(
520
- `
521
- SELECT COUNT(*) AS count FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
522
- WHERE session_id = ? AND kind = 'classification'
523
- AND json_extract(payload, '$.entityKind') = 'point'
524
- `
525
- )
526
- .get(session.id) as { readonly count: number }
527
- if (classifiedPoints.count !== commit.intent.pointCount) {
512
+ const summary = this.sessions.telemetrySummary(session, commit.intent.pointCount)
513
+ await yieldSqliteEventLoop()
514
+ if (summary.classifiedPoints !== commit.intent.pointCount) {
528
515
  invalidCorrelation(
529
516
  "Telemetry point classification coverage does not match the commit intent."
530
517
  )
531
518
  }
532
- if (
533
- result.kind !== "telemetry" ||
534
- !sameCounts(result, this.telemetryCounts(session, commit.intent.pointCount))
535
- ) {
519
+ if (result.kind !== "telemetry" || !sameCounts(result, summary.counts)) {
536
520
  invalidCorrelation("Telemetry result counts do not correlate with finalized work.")
537
521
  }
538
522
  }
539
523
  if (commit.intent.kind === "projection") {
540
524
  if (result.kind !== "projection") return
541
525
  this.sessions.assertClassificationCoverage(session)
542
- if (!sameCounts(result.counts, this.projectionCounts(session))) {
526
+ await yieldSqliteEventLoop()
527
+ const counts = this.sessions.projectionCounts(session)
528
+ await yieldSqliteEventLoop()
529
+ if (!sameCounts(result.counts, counts)) {
543
530
  invalidCorrelation("Projection result counts do not correlate with finalized work.")
544
531
  }
545
532
  }
546
533
  }
547
534
 
548
535
  private assertFinalCardinality(session: SqliteMaterializationSessionState): void {
549
- const duplicate = this.db
536
+ const violation = this.db
550
537
  .query(
551
538
  `
552
- SELECT json_extract(payload, '$.scopeSortKey') AS scope_key
553
- FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
554
- WHERE session_id = ? AND kind = 'cardinality'
555
- AND json_extract(payload, '$.occupied') = 1
556
- GROUP BY scope_key HAVING COUNT(*) > 1 LIMIT 1
539
+ WITH scopes AS (
540
+ SELECT sort_one AS scope_sort_key,
541
+ cardinality_source_type_id AS source_type_id,
542
+ cardinality_source_primary_id AS source_id,
543
+ cardinality_link_id AS link_id,
544
+ SUM(cardinality_occupied) AS expected_count,
545
+ MAX(CASE WHEN cardinality_occupied = 1
546
+ THEN cardinality_target_type_id END) AS expected_target_type_id,
547
+ MAX(CASE WHEN cardinality_occupied = 1
548
+ THEN cardinality_target_primary_id END) AS expected_target_id
549
+ FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
550
+ WHERE session_id = ? AND kind = 'cardinality'
551
+ AND cardinality_view = 'effective'
552
+ GROUP BY sort_one, cardinality_source_type_id,
553
+ cardinality_source_primary_id, cardinality_link_id
554
+ ), validated AS (
555
+ SELECT scopes.scope_sort_key, scopes.expected_count,
556
+ COUNT(links.source_id) AS actual_count,
557
+ COUNT(links.source_id) FILTER (
558
+ WHERE links.target_type_id = scopes.expected_target_type_id
559
+ AND links.target_id = scopes.expected_target_id
560
+ ) AS matching_count
561
+ FROM scopes
562
+ LEFT JOIN links
563
+ ON links.project_id = ?
564
+ AND links.source_type_id = scopes.source_type_id
565
+ AND links.source_id = scopes.source_id
566
+ AND links.link_id = scopes.link_id
567
+ GROUP BY scopes.scope_sort_key, scopes.expected_count,
568
+ scopes.expected_target_type_id, scopes.expected_target_id
569
+ )
570
+ SELECT CASE WHEN expected_count > 1 THEN 'duplicate' ELSE 'mismatch' END AS reason
571
+ FROM validated
572
+ WHERE expected_count > 1
573
+ OR actual_count <> expected_count
574
+ OR matching_count <> expected_count
575
+ LIMIT 1
557
576
  `
558
577
  )
559
- .get(session.id)
560
- if (duplicate) {
578
+ .get(session.id, session.header.commit.projectId) as {
579
+ readonly reason: "duplicate" | "mismatch"
580
+ } | null
581
+ if (violation?.reason === "duplicate") {
561
582
  invalidCorrelation("Materialization cardinality work violates cardinality-one.")
562
583
  }
563
- const records = this.sessions.records(
564
- session,
565
- "cardinality"
566
- ) as MaterializationCardinalityOccupantWorkRecord[]
567
- const scopes = new Map<string, MaterializationCardinalityOccupantWorkRecord>()
568
- for (const record of records) scopes.set(record.scopeSortKey, record)
569
- for (const record of scopes.values()) {
570
- const effective = this.db
571
- .query(
572
- `
573
- SELECT ${linkSortExpression()} AS sort_key FROM links
574
- WHERE project_id = ? AND source_type_id = ? AND source_id = ? AND link_id = ?
575
- ORDER BY sort_key
576
- `
577
- )
578
- .all(
579
- session.header.commit.projectId,
580
- record.ref.source.objectTypeId,
581
- record.ref.source.primaryId,
582
- record.ref.linkId
583
- ) as { readonly sort_key: string }[]
584
- const occupied = records
585
- .filter((candidate) => candidate.scopeSortKey === record.scopeSortKey && candidate.occupied)
586
- .map((candidate) => candidate.linkSortKey)
587
- .sort()
588
- if (
589
- stableJsonStringify(effective.map((row) => row.sort_key)) !== stableJsonStringify(occupied)
590
- ) {
591
- invalidCorrelation(
592
- "Materialization cardinality work does not match the final effective link scope."
593
- )
594
- }
595
- }
596
- }
597
-
598
- private projectionCounts(session: SqliteMaterializationSessionState): EffectiveChangeCounts {
599
- const classifications = this.sessions.records(
600
- session,
601
- "classification"
602
- ) as MaterializationClassificationWorkRecord[]
603
- const plans = this.sessions.records(session, "plan") as MaterializationPlanWorkRecord[]
604
- const counts: MutableEffectiveChangeCounts = {
605
- objectsCreated: 0,
606
- objectsUpdated: 0,
607
- objectsDeleted: 0,
608
- objectsUnchanged: classifications.filter((record) => record.entityKind === "object").length,
609
- linksCreated: 0,
610
- linksUpdated: 0,
611
- linksDeleted: 0,
612
- linksUnchanged: classifications.filter((record) => record.entityKind === "link").length,
613
- }
614
- for (const record of plans) {
615
- switch (record.item.kind) {
616
- case "object-upsert":
617
- if (record.item.value.expected.exists) counts.objectsUpdated += 1
618
- else counts.objectsCreated += 1
619
- counts.objectsUnchanged -= 1
620
- break
621
- case "object-delete":
622
- counts.objectsDeleted += 1
623
- counts.objectsUnchanged -= 1
624
- break
625
- case "link-upsert":
626
- if (record.item.value.expected.exists) counts.linksUpdated += 1
627
- else counts.linksCreated += 1
628
- counts.linksUnchanged -= 1
629
- break
630
- case "link-delete":
631
- counts.linksDeleted += 1
632
- counts.linksUnchanged -= 1
633
- break
634
- }
635
- }
636
- return counts
637
- }
638
-
639
- private telemetryCounts(session: SqliteMaterializationSessionState, pointCount: number) {
640
- let pointsCreated = 0
641
- let pointsUpdated = 0
642
- let latestObjectsChanged = 0
643
- for (const record of this.sessions.records(
644
- session,
645
- "plan"
646
- ) as MaterializationPlanWorkRecord[]) {
647
- if (record.item.kind === "point-upsert") {
648
- if (record.item.value.expected.lastCommitId === null) pointsCreated += 1
649
- else pointsUpdated += 1
650
- } else if (record.item.kind === "object-upsert") {
651
- latestObjectsChanged += 1
652
- }
653
- }
654
- return {
655
- pointsCreated,
656
- pointsUpdated,
657
- pointsUnchanged: pointCount - pointsCreated - pointsUpdated,
658
- latestObjectsChanged,
584
+ if (violation) {
585
+ invalidCorrelation(
586
+ "Materialization cardinality work does not match the final effective link scope."
587
+ )
659
588
  }
660
589
  }
661
590
 
@@ -771,11 +700,11 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
771
700
  .query(
772
701
  `
773
702
  INSERT INTO ontology_commits (
774
- project_id, id, idempotency_key, request_hash,
703
+ project_id, id, idempotency_key, request_hash, execution_id,
775
704
  origin_kind, origin_run_id, origin_batch_ordinal, origin, actor,
776
705
  ontology_revision, projection_revision, ownership_hash,
777
706
  intent, result, committed_at
778
- ) VALUES (?, ?, ?, ?, ?, ?, ?, json(?), json(?), ?, ?, ?, json(?), json(?), ?)
707
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, json(?), json(?), ?, ?, ?, json(?), json(?), ?)
779
708
  `
780
709
  )
781
710
  .run(
@@ -783,6 +712,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
783
712
  commit.id,
784
713
  commit.idempotencyKey,
785
714
  commit.requestHash,
715
+ commit.executionId,
786
716
  origin.kind,
787
717
  origin.runId,
788
718
  origin.batchOrdinal,
@@ -832,7 +762,3 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
832
762
  .get(projectId, sourceId, materializationId) as SqliteOntologySourceRow | null
833
763
  }
834
764
  }
835
-
836
- type MutableEffectiveChangeCounts = {
837
- -readonly [TKey in keyof EffectiveChangeCounts]: EffectiveChangeCounts[TKey]
838
- }
@@ -1,4 +1,5 @@
1
1
  import type { Database } from "bun:sqlite"
2
+ import { serializeSixbFailure } from "@sixb/core/internal/errors"
2
3
  import {
3
4
  MaterializationConflictError,
4
5
  MaterializationValidationError,
@@ -13,6 +14,7 @@ import type {
13
14
  RescheduleOntologyOutboxLeaseInput,
14
15
  SummarizeOntologyOutboxInput,
15
16
  } from "@sixb/core/storage"
17
+ import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage"
16
18
  import {
17
19
  assertNonblank,
18
20
  assertPositiveInteger,
@@ -55,7 +57,7 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
55
57
  LIMIT ?
56
58
  )
57
59
  RETURNING envelope, available_at, attempts, lease_id, lease_expires_at,
58
- published_at, last_error, created_at
60
+ published_at, last_failure, created_at
59
61
  `
60
62
  )
61
63
  .all(
@@ -127,6 +129,10 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
127
129
  assertLeaseInput(input)
128
130
  assertTimestamp(input.availableAt, "Ontology outbox availableAt")
129
131
  const ids = validateLeaseIds(input.ids)
132
+ const failure =
133
+ input.failure === undefined
134
+ ? null
135
+ : serializeSixbFailure(input.failure, ONTOLOGY_OUTBOX_FAILURE_CODES)
130
136
  const changed = this.db
131
137
  .query(
132
138
  `
@@ -141,7 +147,8 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
141
147
  SELECT id FROM leased WHERE (SELECT COUNT(*) FROM leased) = ?
142
148
  )
143
149
  UPDATE ontology_outbox
144
- SET available_at = ?, last_error = ?, lease_id = NULL, lease_expires_at = NULL
150
+ SET available_at = ?, last_failure = COALESCE(?, last_failure),
151
+ lease_id = NULL, lease_expires_at = NULL
145
152
  WHERE project_id = ? AND id IN (SELECT id FROM eligible)
146
153
  AND lease_id = ? AND lease_expires_at IS NOT NULL
147
154
  `
@@ -152,7 +159,7 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
152
159
  input.leaseId,
153
160
  ids.length,
154
161
  input.availableAt,
155
- input.error,
162
+ failure,
156
163
  input.projectId,
157
164
  input.leaseId
158
165
  ).changes