@sixb/sqlite 0.0.1

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. package/src/workflow-run-storage.ts +1148 -0
@@ -0,0 +1,237 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import {
3
+ MaterializationConflictError,
4
+ MaterializationValidationError,
5
+ } from "@sixb/core/internal/materialization"
6
+ import type {
7
+ ClaimedOntologyOutboxRow,
8
+ ClaimOntologyOutboxInput,
9
+ CompleteOntologyOutboxLeaseInput,
10
+ OntologyOutboxStorage,
11
+ OntologyOutboxSummary,
12
+ PurgePublishedOntologyOutboxInput,
13
+ RescheduleOntologyOutboxLeaseInput,
14
+ SummarizeOntologyOutboxInput,
15
+ } from "@sixb/core/storage"
16
+ import {
17
+ assertNonblank,
18
+ assertPositiveInteger,
19
+ assertTimestamp,
20
+ outboxRecord,
21
+ type SqliteOntologyOutboxRow,
22
+ type SqliteRootOperation,
23
+ } from "./shared"
24
+
25
+ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
26
+ constructor(
27
+ private readonly db: Database,
28
+ private readonly runRootOperation: SqliteRootOperation
29
+ ) {}
30
+
31
+ async claim(input: ClaimOntologyOutboxInput): Promise<readonly ClaimedOntologyOutboxRow[]> {
32
+ return this.runRootOperation(() => {
33
+ assertNonblank(input.projectId, "Ontology outbox project id")
34
+ assertPositiveInteger(input.limit, "Ontology outbox limit")
35
+ const now = assertTimestamp(input.now, "Ontology outbox claim time")
36
+ const leaseExpiry = assertTimestamp(input.leaseExpiresAt, "Ontology outbox lease expiry")
37
+ assertNonblank(input.leaseId, "Ontology outbox lease id")
38
+ if (leaseExpiry <= now) {
39
+ throw new MaterializationValidationError(
40
+ "Ontology outbox lease expiry must be later than the claim time."
41
+ )
42
+ }
43
+
44
+ const rows = this.db
45
+ .query(
46
+ `
47
+ UPDATE ontology_outbox
48
+ SET attempts = attempts + 1, lease_id = ?, lease_expires_at = ?
49
+ WHERE rowid IN (
50
+ SELECT rowid
51
+ FROM ontology_outbox
52
+ WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
53
+ AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
54
+ ORDER BY created_at, commit_id, commit_ordinal
55
+ LIMIT ?
56
+ )
57
+ RETURNING envelope, available_at, attempts, lease_id, lease_expires_at,
58
+ published_at, last_error, created_at
59
+ `
60
+ )
61
+ .all(
62
+ input.leaseId,
63
+ input.leaseExpiresAt,
64
+ input.projectId,
65
+ input.now,
66
+ input.now,
67
+ input.limit
68
+ ) as SqliteOntologyOutboxRow[]
69
+ return rows
70
+ .map((row) => outboxRecord(row))
71
+ .sort(
72
+ (left, right) =>
73
+ left.createdAt.localeCompare(right.createdAt) ||
74
+ left.envelope.id.localeCompare(right.envelope.id)
75
+ )
76
+ .map((record) => {
77
+ if (record.leaseId === null || record.leaseExpiresAt === null) {
78
+ throw new MaterializationConflictError(
79
+ "outbox-lease",
80
+ "Ontology outbox claim returned an unpaired lease."
81
+ )
82
+ }
83
+ return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt }
84
+ })
85
+ })
86
+ }
87
+
88
+ async markPublished(input: CompleteOntologyOutboxLeaseInput): Promise<void> {
89
+ await this.runRootOperation(() => {
90
+ assertLeaseInput(input)
91
+ assertTimestamp(input.publishedAt, "Ontology outbox publishedAt")
92
+ const ids = validateLeaseIds(input.ids)
93
+ const changed = this.db
94
+ .query(
95
+ `
96
+ WITH requested(id) AS MATERIALIZED (
97
+ SELECT CAST(value AS TEXT) FROM json_each(?)
98
+ ), leased(id) AS MATERIALIZED (
99
+ SELECT outbox.id FROM ontology_outbox AS outbox
100
+ JOIN requested USING (id)
101
+ WHERE outbox.project_id = ? AND outbox.lease_id = ?
102
+ AND outbox.lease_expires_at IS NOT NULL
103
+ ), eligible(id) AS (
104
+ SELECT id FROM leased WHERE (SELECT COUNT(*) FROM leased) = ?
105
+ )
106
+ UPDATE ontology_outbox
107
+ SET published_at = ?, lease_id = NULL, lease_expires_at = NULL
108
+ WHERE project_id = ? AND id IN (SELECT id FROM eligible)
109
+ AND lease_id = ? AND lease_expires_at IS NOT NULL
110
+ `
111
+ )
112
+ .run(
113
+ JSON.stringify(ids),
114
+ input.projectId,
115
+ input.leaseId,
116
+ ids.length,
117
+ input.publishedAt,
118
+ input.projectId,
119
+ input.leaseId
120
+ ).changes
121
+ if (changed !== ids.length) throw leaseConflict()
122
+ })
123
+ }
124
+
125
+ async reschedule(input: RescheduleOntologyOutboxLeaseInput): Promise<void> {
126
+ await this.runRootOperation(() => {
127
+ assertLeaseInput(input)
128
+ assertTimestamp(input.availableAt, "Ontology outbox availableAt")
129
+ const ids = validateLeaseIds(input.ids)
130
+ const changed = this.db
131
+ .query(
132
+ `
133
+ WITH requested(id) AS MATERIALIZED (
134
+ SELECT CAST(value AS TEXT) FROM json_each(?)
135
+ ), leased(id) AS MATERIALIZED (
136
+ SELECT outbox.id FROM ontology_outbox AS outbox
137
+ JOIN requested USING (id)
138
+ WHERE outbox.project_id = ? AND outbox.lease_id = ?
139
+ AND outbox.lease_expires_at IS NOT NULL
140
+ ), eligible(id) AS (
141
+ SELECT id FROM leased WHERE (SELECT COUNT(*) FROM leased) = ?
142
+ )
143
+ UPDATE ontology_outbox
144
+ SET available_at = ?, last_error = ?, lease_id = NULL, lease_expires_at = NULL
145
+ WHERE project_id = ? AND id IN (SELECT id FROM eligible)
146
+ AND lease_id = ? AND lease_expires_at IS NOT NULL
147
+ `
148
+ )
149
+ .run(
150
+ JSON.stringify(ids),
151
+ input.projectId,
152
+ input.leaseId,
153
+ ids.length,
154
+ input.availableAt,
155
+ input.error,
156
+ input.projectId,
157
+ input.leaseId
158
+ ).changes
159
+ if (changed !== ids.length) throw leaseConflict()
160
+ })
161
+ }
162
+
163
+ async purgePublished(input: PurgePublishedOntologyOutboxInput): Promise<number> {
164
+ return this.runRootOperation(() => {
165
+ assertNonblank(input.projectId, "Ontology outbox project id")
166
+ assertPositiveInteger(input.limit, "Ontology outbox limit")
167
+ assertTimestamp(input.publishedBefore, "Ontology outbox purge cutoff")
168
+ return this.db
169
+ .query(
170
+ `
171
+ DELETE FROM ontology_outbox
172
+ WHERE rowid IN (
173
+ SELECT rowid FROM ontology_outbox
174
+ WHERE project_id = ? AND published_at IS NOT NULL AND published_at < ?
175
+ ORDER BY published_at, id
176
+ LIMIT ?
177
+ )
178
+ `
179
+ )
180
+ .run(input.projectId, input.publishedBefore, input.limit).changes
181
+ })
182
+ }
183
+
184
+ async summarize(input: SummarizeOntologyOutboxInput): Promise<OntologyOutboxSummary> {
185
+ return this.runRootOperation(() => {
186
+ assertNonblank(input.projectId, "Ontology outbox project id")
187
+ const summary = this.db
188
+ .query(
189
+ `
190
+ SELECT COUNT(*) AS pending_count,
191
+ MIN(created_at) AS oldest_pending_at,
192
+ SUM(CASE WHEN attempts > 0 THEN 1 ELSE 0 END) AS retrying_count,
193
+ COALESCE(MAX(attempts), 0) AS max_attempts
194
+ FROM ontology_outbox
195
+ WHERE project_id = ? AND published_at IS NULL
196
+ `
197
+ )
198
+ .get(input.projectId) as {
199
+ readonly pending_count: number
200
+ readonly oldest_pending_at: string | null
201
+ readonly retrying_count: number | null
202
+ readonly max_attempts: number
203
+ }
204
+ return {
205
+ pendingCount: summary.pending_count,
206
+ oldestPendingAt: summary.oldest_pending_at,
207
+ retryingCount: summary.retrying_count ?? 0,
208
+ maxAttempts: summary.max_attempts,
209
+ }
210
+ })
211
+ }
212
+ }
213
+
214
+ function validateLeaseIds(ids: readonly string[]): string[] {
215
+ const seen = new Set<string>()
216
+ for (const id of ids) {
217
+ assertNonblank(id, "Ontology outbox event id")
218
+ if (seen.has(id)) {
219
+ throw new MaterializationValidationError(`Ontology outbox lease batch repeats event '${id}'.`)
220
+ }
221
+ seen.add(id)
222
+ }
223
+ return [...seen]
224
+ }
225
+
226
+ function assertLeaseInput(input: {
227
+ readonly projectId: string
228
+ readonly ids: readonly string[]
229
+ readonly leaseId: string
230
+ }): void {
231
+ assertNonblank(input.projectId, "Ontology outbox project id")
232
+ assertNonblank(input.leaseId, "Ontology outbox lease id")
233
+ }
234
+
235
+ function leaseConflict(): MaterializationConflictError {
236
+ return new MaterializationConflictError("outbox-lease", "Ontology outbox lease does not match.")
237
+ }
@@ -0,0 +1,269 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
3
+ import { MaterializationConflictError } from "@sixb/core/internal/materialization"
4
+ import type {
5
+ AssertSourceMaterializationExecutionInput,
6
+ OntologyCommitRecord,
7
+ OntologyCommitWrite,
8
+ OntologyOutboxRecord,
9
+ OntologySourceRecord,
10
+ StageSourceAssertion,
11
+ } from "@sixb/core/storage"
12
+
13
+ export {
14
+ assertNonblank,
15
+ assertNonnegativeInteger,
16
+ assertPositiveInteger,
17
+ assertTimestamp,
18
+ canonicalJson,
19
+ linkRefFromColumns,
20
+ objectRefFromColumns,
21
+ originColumns,
22
+ originWhere,
23
+ sourceEntityKey,
24
+ } from "@sixb/core/internal/ontology-storage-provider"
25
+
26
+ export type SqliteRootOperation = <T>(run: () => Promise<T> | T) => Promise<T>
27
+
28
+ export interface SqliteOntologyCommitRow {
29
+ readonly project_id: string
30
+ readonly id: string
31
+ readonly idempotency_key: string
32
+ readonly request_hash: string
33
+ readonly origin_kind: string
34
+ readonly origin_run_id: string | null
35
+ readonly origin_batch_ordinal: number | null
36
+ readonly origin: string
37
+ readonly actor: string | null
38
+ readonly ontology_revision: string
39
+ readonly projection_revision: string | null
40
+ readonly ownership_hash: string | null
41
+ readonly intent: string
42
+ readonly result: string
43
+ readonly committed_at: string
44
+ }
45
+
46
+ export interface SqliteOntologySourceRow {
47
+ readonly project_id: string
48
+ readonly source_id: string
49
+ readonly materialization_id: string
50
+ readonly projection_run_id: string
51
+ readonly projection_kind: "object" | "link"
52
+ readonly protocol: "replacement"
53
+ readonly status: OntologySourceRecord["status"]
54
+ readonly execution_token: string | null
55
+ readonly dataset_id: string
56
+ readonly dataset_version_id: string
57
+ readonly dataset_version_created_at: string
58
+ readonly projection_revision: string
59
+ readonly ownership_hash: string
60
+ readonly ontology_revision: string
61
+ readonly root_count: number | null
62
+ readonly assertion_count: number | null
63
+ readonly created_at: string
64
+ readonly ready_at: string | null
65
+ readonly activated_at: string | null
66
+ readonly terminal_at: string | null
67
+ readonly last_commit_id: string | null
68
+ readonly updated_at: string
69
+ }
70
+
71
+ export interface SqliteOntologySourceAssertionRow {
72
+ readonly project_id: string
73
+ readonly source_id: string
74
+ readonly materialization_id: string
75
+ readonly entity_kind: "object" | "link"
76
+ readonly entity_key: string
77
+ readonly entity_sort_key: string
78
+ readonly root_kind: "object" | "link"
79
+ readonly root_key: string
80
+ readonly root_sort_key: string
81
+ readonly staging_ordinal: number
82
+ readonly root: string
83
+ readonly assertion: string
84
+ }
85
+
86
+ export interface SqliteOntologyOverrideRow {
87
+ readonly entity_kind: "object" | "link"
88
+ readonly value: string
89
+ readonly last_commit_id: string
90
+ readonly updated_at: string
91
+ }
92
+
93
+ export interface SqliteOntologyOutboxRow {
94
+ readonly envelope: string
95
+ readonly available_at: string
96
+ readonly attempts: number
97
+ readonly lease_id: string | null
98
+ readonly lease_expires_at: string | null
99
+ readonly published_at: string | null
100
+ readonly last_error: string | null
101
+ readonly created_at: string
102
+ }
103
+
104
+ export function parseJson<T>(value: string): T {
105
+ return JSON.parse(value) as T
106
+ }
107
+
108
+ export function sourceRecord(row: SqliteOntologySourceRow): OntologySourceRecord {
109
+ return {
110
+ projectId: row.project_id,
111
+ source: { projectionId: row.source_id },
112
+ materializationId: row.materialization_id,
113
+ projectionRunId: row.projection_run_id,
114
+ projectionKind: row.projection_kind,
115
+ protocol: row.protocol,
116
+ status: row.status,
117
+ executionToken: row.execution_token,
118
+ datasetVersion: {
119
+ datasetId: row.dataset_id,
120
+ versionId: row.dataset_version_id,
121
+ createdAt: row.dataset_version_created_at,
122
+ },
123
+ projectionRevision: row.projection_revision,
124
+ ownershipHash: row.ownership_hash,
125
+ ontologyRevision: row.ontology_revision,
126
+ rootCount: row.root_count,
127
+ assertionCount: row.assertion_count,
128
+ createdAt: row.created_at,
129
+ readyAt: row.ready_at,
130
+ activatedAt: row.activated_at,
131
+ terminalAt: row.terminal_at,
132
+ lastCommitId: row.last_commit_id,
133
+ updatedAt: row.updated_at,
134
+ }
135
+ }
136
+
137
+ export function sourceAssertion(row: SqliteOntologySourceAssertionRow): StageSourceAssertion {
138
+ return {
139
+ root: parseJson<ProjectionEntityRef>(row.root),
140
+ assertion: parseJson<StageSourceAssertion["assertion"]>(row.assertion),
141
+ stagingOrdinal: row.staging_ordinal,
142
+ }
143
+ }
144
+
145
+ export function commitRecord(row: SqliteOntologyCommitRow): OntologyCommitRecord {
146
+ const base = {
147
+ projectId: row.project_id,
148
+ id: row.id,
149
+ idempotencyKey: row.idempotency_key,
150
+ requestHash: row.request_hash,
151
+ origin: parseJson<OntologyCommitWrite["origin"]>(row.origin),
152
+ ...(row.actor === null
153
+ ? {}
154
+ : { actor: parseJson<NonNullable<OntologyCommitWrite["actor"]>>(row.actor) }),
155
+ ontologyRevision: row.ontology_revision,
156
+ ...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
157
+ ...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
158
+ committedAt: row.committed_at,
159
+ }
160
+ const intent = parseJson<OntologyCommitWrite["intent"]>(row.intent)
161
+ const result = parseJson<OntologyCommitRecord["result"]>(row.result)
162
+ return { ...base, intent, result } as OntologyCommitRecord
163
+ }
164
+
165
+ export function outboxRecord(row: SqliteOntologyOutboxRow): OntologyOutboxRecord {
166
+ return {
167
+ envelope: parseJson<OntologyOutboxRecord["envelope"]>(row.envelope),
168
+ availableAt: row.available_at,
169
+ attempts: row.attempts,
170
+ leaseId: row.lease_id,
171
+ leaseExpiresAt: row.lease_expires_at,
172
+ publishedAt: row.published_at,
173
+ lastError: row.last_error,
174
+ createdAt: row.created_at,
175
+ }
176
+ }
177
+
178
+ export function assertProjectionExecution(
179
+ db: Database,
180
+ input: {
181
+ readonly projectId: string
182
+ readonly sourceId: string
183
+ readonly projectionRunId: string
184
+ readonly executionToken: string
185
+ readonly identity?: AssertSourceMaterializationExecutionInput["identity"]
186
+ }
187
+ ): void {
188
+ const run = db
189
+ .query(
190
+ `
191
+ SELECT projection_id, projection_kind, status, materialization_protocol, execution_token,
192
+ dataset_id, dataset_version_id, dataset_version_created_at,
193
+ ontology_revision, projection_revision, ownership_hash
194
+ FROM projection_runs
195
+ WHERE project_id = ? AND id = ?
196
+ `
197
+ )
198
+ .get(input.projectId, input.projectionRunId) as {
199
+ projection_id: string
200
+ projection_kind: string
201
+ status: string
202
+ materialization_protocol: string | null
203
+ execution_token: string | null
204
+ dataset_id: string
205
+ dataset_version_id: string
206
+ dataset_version_created_at: string | null
207
+ ontology_revision: string | null
208
+ projection_revision: string | null
209
+ ownership_hash: string | null
210
+ } | null
211
+ if (!run || run.status !== "running") {
212
+ throw new MaterializationConflictError(
213
+ "run-correlation",
214
+ `Projection run '${input.projectionRunId}' is missing or is not running.`
215
+ )
216
+ }
217
+ if (run.projection_id !== input.sourceId || run.materialization_protocol !== "replacement") {
218
+ throw new MaterializationConflictError(
219
+ "run-correlation",
220
+ `Projection run '${input.projectionRunId}' does not own replacement source '${input.sourceId}'.`
221
+ )
222
+ }
223
+ if (run.execution_token !== input.executionToken) {
224
+ throw new MaterializationConflictError(
225
+ "execution-lost",
226
+ `Projection run '${input.projectionRunId}' execution token is stale.`
227
+ )
228
+ }
229
+ const identity = input.identity
230
+ if (
231
+ identity &&
232
+ (run.projection_kind !== identity.projectionKind ||
233
+ run.materialization_protocol !== identity.protocol ||
234
+ run.dataset_id !== identity.datasetVersion.datasetId ||
235
+ run.dataset_version_id !== identity.datasetVersion.versionId ||
236
+ run.dataset_version_created_at !== identity.datasetVersion.createdAt ||
237
+ run.ontology_revision !== identity.ontologyRevision ||
238
+ run.projection_revision !== identity.projectionRevision ||
239
+ run.ownership_hash !== identity.ownershipHash)
240
+ ) {
241
+ throw new MaterializationConflictError(
242
+ "run-correlation",
243
+ `Projection run '${input.projectionRunId}' immutable source identity does not match.`
244
+ )
245
+ }
246
+ }
247
+
248
+ export function requireChanges(
249
+ changes: number,
250
+ kind: ConstructorParameters<typeof MaterializationConflictError>[0],
251
+ message: string
252
+ ): void {
253
+ if (changes !== 1) throw new MaterializationConflictError(kind, message)
254
+ }
255
+
256
+ export function isSqliteConstraintError(error: unknown): boolean {
257
+ if (!(error instanceof Error)) return false
258
+ const details = error as Error & { readonly code?: unknown; readonly errno?: unknown }
259
+ if (typeof details.code === "string") {
260
+ return [
261
+ "SQLITE_CONSTRAINT_CHECK",
262
+ "SQLITE_CONSTRAINT_FOREIGNKEY",
263
+ "SQLITE_CONSTRAINT_NOTNULL",
264
+ "SQLITE_CONSTRAINT_PRIMARYKEY",
265
+ "SQLITE_CONSTRAINT_UNIQUE",
266
+ ].includes(details.code)
267
+ }
268
+ return details.errno === 19
269
+ }