@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,127 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type {
3
+ GetOntologyCommitByIdempotencyKeyInput,
4
+ GetOntologyCommitByIdInput,
5
+ GetOntologyCommitByOriginInput,
6
+ ListOntologyCommitsInput,
7
+ ListOntologyCommitsResult,
8
+ OntologyCommitRecord,
9
+ OntologyCommitStorage,
10
+ } from "@sixb/core/storage"
11
+ import {
12
+ assertNonblank,
13
+ assertNonnegativeInteger,
14
+ commitRecord,
15
+ originWhere,
16
+ type SqliteOntologyCommitRow,
17
+ type SqliteRootOperation,
18
+ } from "./shared"
19
+
20
+ export class SqliteOntologyCommitStorage implements OntologyCommitStorage {
21
+ constructor(
22
+ private readonly db: Database,
23
+ private readonly runRootOperation: SqliteRootOperation
24
+ ) {}
25
+
26
+ async getByIdempotencyKey(
27
+ input: GetOntologyCommitByIdempotencyKeyInput
28
+ ): Promise<OntologyCommitRecord | null> {
29
+ return this.runRootOperation(() => {
30
+ assertNonblank(input.projectId, "Ontology commit project id")
31
+ assertNonblank(input.idempotencyKey, "Ontology commit idempotency key")
32
+ return this.readOne(
33
+ "project_id = ? AND idempotency_key = ?",
34
+ input.projectId,
35
+ input.idempotencyKey
36
+ )
37
+ })
38
+ }
39
+
40
+ async getById(input: GetOntologyCommitByIdInput): Promise<OntologyCommitRecord | null> {
41
+ return this.runRootOperation(() => {
42
+ assertNonblank(input.projectId, "Ontology commit project id")
43
+ assertNonblank(input.id, "Ontology commit id")
44
+ return this.readOne("project_id = ? AND id = ?", input.projectId, input.id)
45
+ })
46
+ }
47
+
48
+ async getByOrigin(input: GetOntologyCommitByOriginInput): Promise<OntologyCommitRecord | null> {
49
+ return this.runRootOperation(() => {
50
+ assertNonblank(input.projectId, "Ontology commit project id")
51
+ const origin = originWhere(input.origin)
52
+ assertNonblank(origin.runId, "Ontology commit origin run id")
53
+ if (origin.batchOrdinal !== null) {
54
+ assertNonnegativeInteger(origin.batchOrdinal, "Ontology telemetry commit batch ordinal")
55
+ }
56
+ return origin.batchOrdinal === null
57
+ ? this.readOne(
58
+ "project_id = ? AND origin_kind = ? AND origin_run_id = ? AND origin_batch_ordinal IS NULL",
59
+ input.projectId,
60
+ origin.kind,
61
+ origin.runId
62
+ )
63
+ : this.readOne(
64
+ "project_id = ? AND origin_kind = ? AND origin_run_id = ? AND origin_batch_ordinal = ?",
65
+ input.projectId,
66
+ origin.kind,
67
+ origin.runId,
68
+ origin.batchOrdinal
69
+ )
70
+ })
71
+ }
72
+
73
+ async list(input: ListOntologyCommitsInput): Promise<ListOntologyCommitsResult> {
74
+ return this.runRootOperation(() => {
75
+ assertNonblank(input.projectId, "Ontology commit project id")
76
+ if (input.run) assertNonblank(input.run.id, "Ontology commit run id")
77
+ const offset = input.offset ?? 0
78
+ const limit = input.limit ?? -1
79
+ assertNonnegativeInteger(offset, "Ontology commit list offset")
80
+ if (limit !== -1) assertNonnegativeInteger(limit, "Ontology commit list limit")
81
+ const filters = ["project_id = ?"]
82
+ const values: (string | number)[] = [input.projectId]
83
+ if (input.run?.kind === "action") {
84
+ filters.push("origin_kind = 'action' AND origin_run_id = ?")
85
+ values.push(input.run.id)
86
+ } else if (input.run?.kind === "projection") {
87
+ filters.push("origin_run_id = ? AND origin_kind IN ('projection', 'telemetry')")
88
+ values.push(input.run.id)
89
+ }
90
+ const where = filters.join(" AND ")
91
+ const totalRow = this.db
92
+ .query(`SELECT COUNT(*) AS total FROM ontology_commits WHERE ${where}`)
93
+ .get(...values) as { readonly total: number }
94
+ const direction = input.order === "desc" ? "DESC" : "ASC"
95
+ const order =
96
+ input.run?.kind === "projection"
97
+ ? `CASE WHEN origin_kind = 'telemetry' THEN origin_batch_ordinal ELSE -1 END ${direction}, committed_at ${direction}, id ${direction}`
98
+ : `committed_at ${direction}, id ${direction}`
99
+ const rows = this.db
100
+ .query(
101
+ `
102
+ SELECT * FROM ontology_commits
103
+ WHERE ${where}
104
+ ORDER BY ${order}
105
+ LIMIT ? OFFSET ?
106
+ `
107
+ )
108
+ .all(...values, limit, offset) as SqliteOntologyCommitRow[]
109
+ const commits = rows.map(commitRecord)
110
+ return {
111
+ commits,
112
+ total: totalRow.total,
113
+ hasMore: offset + commits.length < totalRow.total,
114
+ }
115
+ })
116
+ }
117
+
118
+ private readOne(
119
+ where: string,
120
+ ...values: readonly (string | number)[]
121
+ ): OntologyCommitRecord | null {
122
+ const row = this.db
123
+ .query(`SELECT * FROM ontology_commits WHERE ${where}`)
124
+ .get(...values) as SqliteOntologyCommitRow | null
125
+ return row ? commitRecord(row) : null
126
+ }
127
+ }
@@ -0,0 +1,35 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { OntologyStorage } from "@sixb/core/storage"
3
+ import { SqliteOntologyCommitStorage } from "./commits"
4
+ import type { SqliteOntologyTransactionContext } from "./materialization-session"
5
+ import { SqliteOntologyMaterializationStorage } from "./materializations"
6
+ import { SqliteOntologyOutboxStorage } from "./outbox"
7
+ import type { SqliteRootOperation } from "./shared"
8
+ import { SqliteOntologySourceStorage } from "./sources"
9
+
10
+ export class SqliteOntologyStorage implements OntologyStorage {
11
+ readonly commits: SqliteOntologyCommitStorage
12
+ readonly sources: SqliteOntologySourceStorage
13
+ readonly materializations: SqliteOntologyMaterializationStorage
14
+ readonly outbox: SqliteOntologyOutboxStorage
15
+
16
+ constructor(input: {
17
+ readonly db: Database
18
+ readonly runRootOperation: SqliteRootOperation
19
+ readonly transactionContext: SqliteOntologyTransactionContext | null
20
+ }) {
21
+ this.commits = new SqliteOntologyCommitStorage(input.db, input.runRootOperation)
22
+ this.sources = new SqliteOntologySourceStorage(input.db, input.runRootOperation)
23
+ this.materializations = new SqliteOntologyMaterializationStorage(
24
+ input.db,
25
+ input.transactionContext
26
+ )
27
+ this.outbox = new SqliteOntologyOutboxStorage(input.db, input.runRootOperation)
28
+ }
29
+
30
+ deactivateSessions(): void {
31
+ this.materializations.deactivateSessions()
32
+ }
33
+ }
34
+
35
+ export type { SqliteOntologyTransactionContext } from "./materialization-session"
@@ -0,0 +1,393 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import {
3
+ linkRefKey,
4
+ MaterializationConflictError,
5
+ objectRefKey,
6
+ } from "@sixb/core/internal/materialization"
7
+ import {
8
+ correlateMaterializationChunk,
9
+ duplicateMaterializationWork as duplicateWork,
10
+ invalidCorrelation,
11
+ materializationPlanItems,
12
+ ProviderMaterializationSessionState,
13
+ type ProviderMaterializationTransactionLifecycle,
14
+ prepareMaterializationWork,
15
+ } from "@sixb/core/internal/ontology-storage-provider"
16
+ import type {
17
+ MaterializationEventWorkRecord,
18
+ MaterializationPlanChunk,
19
+ MaterializationPlanHeader,
20
+ MaterializationPlanWorkItem,
21
+ MaterializationSession,
22
+ MaterializationWorkPage,
23
+ MaterializationWorkRecord,
24
+ StageMaterializationWorkInput,
25
+ StreamMaterializationWorkInput,
26
+ } from "@sixb/core/storage"
27
+ import {
28
+ type ReplacementIdentity,
29
+ SQLITE_MATERIALIZATION_WORK_TABLE,
30
+ SQLITE_REPLACEMENT_WORK_TABLE,
31
+ } from "./materialization-state"
32
+ import { canonicalJson, isSqliteConstraintError, parseJson } from "./shared"
33
+
34
+ export interface SqliteOntologyTransactionContext {
35
+ readonly id: object
36
+ readonly materializations: ProviderMaterializationTransactionLifecycle
37
+ active: boolean
38
+ }
39
+
40
+ export class SqliteMaterializationSessionState extends ProviderMaterializationSessionState {}
41
+
42
+ export class SqliteMaterializationSessions {
43
+ private readonly sessions = new WeakMap<object, SqliteMaterializationSessionState>()
44
+ private readonly live = new Set<SqliteMaterializationSessionState>()
45
+
46
+ constructor(
47
+ private readonly db: Database,
48
+ private readonly context: SqliteOntologyTransactionContext | null
49
+ ) {}
50
+
51
+ create(header: MaterializationPlanHeader): SqliteMaterializationSessionState {
52
+ if (!this.context?.active) {
53
+ throw new MaterializationConflictError(
54
+ "effective-state",
55
+ "Materialization sessions require an active storage transaction."
56
+ )
57
+ }
58
+ this.ensureTables()
59
+ const session = new SqliteMaterializationSessionState(structuredClone(header), this.context.id)
60
+ this.sessions.set(session.providerToken, session)
61
+ this.live.add(session)
62
+ this.context.materializations.register(session.providerToken)
63
+ return session
64
+ }
65
+
66
+ require(session: MaterializationSession): SqliteMaterializationSessionState {
67
+ const value = this.sessions.get(session.providerToken)
68
+ if (
69
+ !value ||
70
+ !value.active ||
71
+ !this.context?.active ||
72
+ value.transactionId !== this.context.id
73
+ ) {
74
+ throw new MaterializationConflictError(
75
+ "effective-state",
76
+ "Materialization session is inactive."
77
+ )
78
+ }
79
+ return value
80
+ }
81
+
82
+ release(session: SqliteMaterializationSessionState): void {
83
+ if (!session.active) return
84
+ this.deleteWork(session.id)
85
+ session.active = false
86
+ session.replacement = null
87
+ this.live.delete(session)
88
+ this.context?.materializations.complete(session.providerToken)
89
+ }
90
+
91
+ deactivateAll(): void {
92
+ for (const session of this.live) this.release(session)
93
+ }
94
+
95
+ stage(input: StageMaterializationWorkInput): void {
96
+ const session = this.require(input.session)
97
+ const records = prepareMaterializationWork(session, input)
98
+ const insert = this.db.query(
99
+ `
100
+ INSERT INTO ${SQLITE_MATERIALIZATION_WORK_TABLE} (
101
+ session_id, record_key, unique_key, kind, lane,
102
+ major_order, minor_order, sort_one, sort_two, payload
103
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, json(?))
104
+ `
105
+ )
106
+ try {
107
+ for (const { record, uniqueKey, columns } of records) {
108
+ insert.run(
109
+ session.id,
110
+ record.recordKey,
111
+ uniqueKey,
112
+ record.kind,
113
+ columns.lane,
114
+ columns.majorOrder,
115
+ columns.minorOrder,
116
+ columns.sortOne,
117
+ columns.sortTwo,
118
+ canonicalJson(record)
119
+ )
120
+ }
121
+ } catch (error) {
122
+ if (isSqliteConstraintError(error)) {
123
+ throw duplicateWork(input.records[0]?.recordKey ?? "unknown")
124
+ }
125
+ throw error
126
+ }
127
+ }
128
+
129
+ async *stream(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
130
+ const session = this.require(input.session)
131
+ const stream = session.workStreams[input.order]
132
+ if (stream.started) {
133
+ throw new MaterializationConflictError(
134
+ "effective-state",
135
+ `Materialization ${input.order} work may only be streamed once per session.`
136
+ )
137
+ }
138
+ stream.started = true
139
+ session.workSealed = true
140
+ let offset = 0
141
+ while (true) {
142
+ this.require(input.session)
143
+ const rows = this.db
144
+ .query(
145
+ `
146
+ SELECT payload
147
+ FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
148
+ WHERE session_id = ? AND lane = ?
149
+ ORDER BY major_order, minor_order, sort_one, sort_two, record_key
150
+ LIMIT ? OFFSET ?
151
+ `
152
+ )
153
+ .all(session.id, input.order, input.pageRows, offset) as { readonly payload: string }[]
154
+ if (rows.length === 0) break
155
+ const records = rows.map((row) =>
156
+ parseJson<MaterializationWorkRecord>(row.payload)
157
+ ) as MaterializationWorkPage["records"]
158
+ offset += records.length
159
+ stream.emittedCount = offset
160
+ yield { records }
161
+ }
162
+ this.require(input.session)
163
+ stream.completed = true
164
+ }
165
+
166
+ readObjectExistence(
167
+ session: SqliteMaterializationSessionState,
168
+ refs: readonly { readonly objectTypeId: string; readonly primaryId: string }[]
169
+ ): readonly {
170
+ readonly ref: { readonly objectTypeId: string; readonly primaryId: string }
171
+ readonly exists: boolean
172
+ }[] {
173
+ const lookup = this.db.query(
174
+ `
175
+ SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
176
+ WHERE session_id = ? AND unique_key = ? AND kind = 'object-existence'
177
+ `
178
+ )
179
+ return refs.flatMap((ref) => {
180
+ const key = `object-existence:${JSON.stringify([ref.objectTypeId, ref.primaryId])}`
181
+ const row = lookup.get(session.id, key) as { readonly payload: string } | null
182
+ if (!row) return []
183
+ const record = parseJson<
184
+ Extract<MaterializationWorkRecord, { readonly kind: "object-existence" }>
185
+ >(row.payload)
186
+ return [{ ref: record.ref, exists: record.exists }]
187
+ })
188
+ }
189
+
190
+ assertChunkSequence(
191
+ session: SqliteMaterializationSessionState,
192
+ chunk: MaterializationPlanChunk
193
+ ): void {
194
+ const items = materializationPlanItems(chunk)
195
+ const progress = correlateMaterializationChunk(
196
+ session,
197
+ chunk,
198
+ items,
199
+ this.planItems(session, session.appliedPlanCount, items.length),
200
+ this.eventRecords(session, session.appliedOutboxCount, chunk.outbox.length)
201
+ )
202
+ session.appliedPlanCount = progress.appliedPlanCount
203
+ session.appliedOutboxCount = progress.appliedOutboxCount
204
+ }
205
+
206
+ recordReplacementIdentities(
207
+ session: SqliteMaterializationSessionState,
208
+ identities: readonly ReplacementIdentity[]
209
+ ): void {
210
+ const upsert = this.db.query(
211
+ `
212
+ INSERT INTO ${SQLITE_REPLACEMENT_WORK_TABLE} (
213
+ session_id, entity_kind, identity_key, diff_required
214
+ ) VALUES (?, ?, ?, ?)
215
+ ON CONFLICT(session_id, entity_kind, identity_key) DO UPDATE SET
216
+ diff_required = MAX(diff_required, excluded.diff_required)
217
+ `
218
+ )
219
+ for (const identity of identities) {
220
+ upsert.run(
221
+ session.id,
222
+ identity.kind,
223
+ identity.kind === "object" ? objectRefKey(identity.ref) : linkRefKey(identity.ref),
224
+ identity.diffRequired ? 1 : 0
225
+ )
226
+ }
227
+ }
228
+
229
+ count(session: SqliteMaterializationSessionState, kind: string): number {
230
+ const row = this.db
231
+ .query(
232
+ `SELECT COUNT(*) AS count FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
233
+ WHERE session_id = ? AND kind = ?`
234
+ )
235
+ .get(session.id, kind) as { readonly count: number }
236
+ return row.count
237
+ }
238
+
239
+ laneCounts(session: SqliteMaterializationSessionState): {
240
+ readonly apply: number
241
+ readonly cardinality: number
242
+ readonly event: number
243
+ } {
244
+ const rows = this.db
245
+ .query(
246
+ `SELECT lane, COUNT(*) AS count FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
247
+ WHERE session_id = ? AND lane <> 'none'
248
+ GROUP BY lane`
249
+ )
250
+ .all(session.id) as {
251
+ readonly lane: "apply" | "cardinality" | "event"
252
+ readonly count: number
253
+ }[]
254
+ const counts = { apply: 0, cardinality: 0, event: 0 }
255
+ for (const row of rows) counts[row.lane] = row.count
256
+ return counts
257
+ }
258
+
259
+ records(session: SqliteMaterializationSessionState, kind?: string): MaterializationWorkRecord[] {
260
+ const rows = this.db
261
+ .query(
262
+ `SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
263
+ WHERE session_id = ? ${kind ? "AND kind = ?" : ""}`
264
+ )
265
+ .all(...(kind ? [session.id, kind] : [session.id])) as { readonly payload: string }[]
266
+ return rows.map((row) => parseJson<MaterializationWorkRecord>(row.payload))
267
+ }
268
+
269
+ assertClassificationCoverage(session: SqliteMaterializationSessionState): void {
270
+ if (!session.replacement) return
271
+ const expectedObject = session.replacement.projectionKind === "object" ? 1 : 0
272
+ const mismatch = this.db
273
+ .query(
274
+ `
275
+ WITH expected AS (
276
+ SELECT entity_kind, identity_key
277
+ FROM ${SQLITE_REPLACEMENT_WORK_TABLE}
278
+ WHERE session_id = ? AND diff_required = 1
279
+ AND (entity_kind = 'link' OR ? = 1)
280
+ ), actual AS (
281
+ SELECT json_extract(payload, '$.entityKind') AS entity_kind,
282
+ json_extract(payload, '$.identityKey') AS identity_key
283
+ FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
284
+ WHERE session_id = ? AND kind = 'classification'
285
+ AND json_extract(payload, '$.entityKind') IN ('object', 'link')
286
+ ), differences AS (
287
+ SELECT * FROM expected EXCEPT SELECT * FROM actual
288
+ UNION ALL
289
+ SELECT * FROM actual EXCEPT SELECT * FROM expected
290
+ )
291
+ SELECT 1 FROM differences LIMIT 1
292
+ `
293
+ )
294
+ .get(session.id, expectedObject, session.id)
295
+ const pointClassifications = this.db
296
+ .query(
297
+ `
298
+ SELECT 1 FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
299
+ WHERE session_id = ? AND kind = 'classification'
300
+ AND json_extract(payload, '$.entityKind') = 'point'
301
+ LIMIT 1
302
+ `
303
+ )
304
+ .get(session.id)
305
+ if (mismatch || pointClassifications) {
306
+ invalidCorrelation(
307
+ "Projection replacement classification coverage does not match its streamed state."
308
+ )
309
+ }
310
+ }
311
+
312
+ private planItems(
313
+ session: SqliteMaterializationSessionState,
314
+ offset: number,
315
+ limit: number
316
+ ): MaterializationPlanWorkItem[] {
317
+ if (limit === 0) return []
318
+ const rows = this.db
319
+ .query(
320
+ `
321
+ SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
322
+ WHERE session_id = ? AND lane = 'apply'
323
+ ORDER BY major_order, minor_order, sort_one, sort_two, record_key
324
+ LIMIT ? OFFSET ?
325
+ `
326
+ )
327
+ .all(session.id, limit, offset) as { readonly payload: string }[]
328
+ return rows.map((row) => {
329
+ const record = parseJson<Extract<MaterializationWorkRecord, { readonly kind: "plan" }>>(
330
+ row.payload
331
+ )
332
+ return record.item
333
+ })
334
+ }
335
+
336
+ private eventRecords(
337
+ session: SqliteMaterializationSessionState,
338
+ offset: number,
339
+ limit: number
340
+ ): MaterializationEventWorkRecord[] {
341
+ if (limit === 0) return []
342
+ const rows = this.db
343
+ .query(
344
+ `
345
+ SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
346
+ WHERE session_id = ? AND lane = 'event'
347
+ ORDER BY major_order, minor_order, sort_one, sort_two, record_key
348
+ LIMIT ? OFFSET ?
349
+ `
350
+ )
351
+ .all(session.id, limit, offset) as { readonly payload: string }[]
352
+ return rows.map((row) => parseJson<MaterializationEventWorkRecord>(row.payload))
353
+ }
354
+
355
+ private ensureTables(): void {
356
+ this.db.run(`
357
+ CREATE TEMP TABLE IF NOT EXISTS ${SQLITE_MATERIALIZATION_WORK_TABLE} (
358
+ session_id TEXT NOT NULL,
359
+ record_key TEXT NOT NULL,
360
+ unique_key TEXT NOT NULL,
361
+ kind TEXT NOT NULL,
362
+ lane TEXT NOT NULL,
363
+ major_order INTEGER NOT NULL,
364
+ minor_order INTEGER NOT NULL,
365
+ sort_one TEXT NOT NULL,
366
+ sort_two TEXT NOT NULL,
367
+ payload TEXT NOT NULL CHECK (json_valid(payload)),
368
+ PRIMARY KEY (session_id, record_key),
369
+ UNIQUE (session_id, unique_key)
370
+ );
371
+ CREATE INDEX IF NOT EXISTS idx_ontology_materialization_work_lane
372
+ ON ${SQLITE_MATERIALIZATION_WORK_TABLE}(
373
+ session_id, lane, major_order, minor_order, sort_one, sort_two, record_key
374
+ );
375
+ CREATE TEMP TABLE IF NOT EXISTS ${SQLITE_REPLACEMENT_WORK_TABLE} (
376
+ session_id TEXT NOT NULL,
377
+ entity_kind TEXT NOT NULL,
378
+ identity_key TEXT NOT NULL,
379
+ diff_required INTEGER NOT NULL CHECK (diff_required IN (0, 1)),
380
+ PRIMARY KEY (session_id, entity_kind, identity_key)
381
+ );
382
+ `)
383
+ }
384
+
385
+ private deleteWork(sessionId: string): void {
386
+ this.db
387
+ .query(`DELETE FROM ${SQLITE_MATERIALIZATION_WORK_TABLE} WHERE session_id = ?`)
388
+ .run(sessionId)
389
+ this.db
390
+ .query(`DELETE FROM ${SQLITE_REPLACEMENT_WORK_TABLE} WHERE session_id = ?`)
391
+ .run(sessionId)
392
+ }
393
+ }