@sixb/sqlite 0.1.0 → 0.1.2
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.
- package/README.md +18 -13
- package/dist/action-run-storage.d.ts +5 -2
- package/dist/action-run-storage.d.ts.map +1 -1
- package/dist/agents/agent-storage.d.ts +4 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +3 -10
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts +22 -0
- package/dist/ai-usage-storage.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -10
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/connector-connection-storage.d.ts +9 -0
- package/dist/connector-connection-storage.d.ts.map +1 -0
- package/dist/execution-storage.d.ts +14 -0
- package/dist/execution-storage.d.ts.map +1 -0
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4114 -1149
- package/dist/index.js.map +30 -27
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts +0 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +50 -8
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +4 -5
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +0 -2
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/outbox.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +3 -3
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pipeline-run-storage.d.ts +7 -2
- package/dist/pipeline-run-storage.d.ts.map +1 -1
- package/dist/projection-run-storage.d.ts +9 -2
- package/dist/projection-run-storage.d.ts.map +1 -1
- package/dist/run-list-query.d.ts +1 -1
- package/dist/run-list-query.d.ts.map +1 -1
- package/dist/sync-run-storage.d.ts +7 -2
- package/dist/sync-run-storage.d.ts.map +1 -1
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/transactions.d.ts +3 -0
- package/dist/transactions.d.ts.map +1 -1
- package/dist/webhook-run-storage.d.ts +9 -2
- package/dist/webhook-run-storage.d.ts.map +1 -1
- package/dist/workflow-run-storage.d.ts +7 -3
- package/dist/workflow-run-storage.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/action-run-storage.ts +51 -74
- package/src/agents/agent-storage.ts +5 -3
- package/src/agents/rows.ts +8 -40
- package/src/agents/runs.ts +27 -21
- package/src/ai-usage-storage.ts +324 -0
- package/src/auth-storage/service-accounts.ts +14 -21
- package/src/auth-storage/shared.ts +0 -22
- package/src/connector-connection-storage.ts +784 -0
- package/src/execution-storage.ts +158 -0
- package/src/index.ts +63 -22
- package/src/latest-run-query.ts +7 -1
- package/src/migrations/002-workflow-run-output.sql +27 -0
- package/src/migrations/003-merge-sync-runs.sql +71 -0
- package/src/migrations/004-executions.sql +142 -0
- package/src/migrations/005-workflow-executions.sql +7 -0
- package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
- package/src/migrations/007-split-overrides.sql +154 -0
- package/src/migrations/008-action-executions.sql +4 -0
- package/src/migrations/009-agent-executions.sql +115 -0
- package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
- package/src/migrations/011-sync-failure-record.sql +22 -0
- package/src/migrations/012-pipeline-failure-record.sql +45 -0
- package/src/migrations/013-workflow-failure-record.sql +45 -0
- package/src/migrations/014-agent-failure-record.sql +59 -0
- package/src/migrations/015-projection-failure-record.sql +20 -0
- package/src/migrations/016-webhook-run-failure-record.sql +19 -0
- package/src/migrations/017-action-failure-record.sql +65 -0
- package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
- package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
- package/src/migrations/020-drop-run-usage-projections.sql +7 -0
- package/src/migrations/021-sync-pipeline-executions.sql +101 -0
- package/src/migrations/022-projection-executions.sql +244 -0
- package/src/migrations/023-webhook-executions.sql +51 -0
- package/src/migrations/024-ontology-commit-executions.sql +58 -0
- package/src/migrations/025-connector-connections.sql +257 -0
- package/src/migrations.ts +126 -5
- package/src/ontology-storage/materialization-session.ts +317 -143
- package/src/ontology-storage/materialization-state.ts +398 -150
- package/src/ontology-storage/materialization-writer.ts +142 -67
- package/src/ontology-storage/materializations.ts +67 -141
- package/src/ontology-storage/outbox.ts +10 -3
- package/src/ontology-storage/shared.ts +10 -4
- package/src/ontology-storage/sources.ts +4 -1
- package/src/pipeline-run-storage.ts +128 -43
- package/src/projection-run-storage.ts +209 -97
- package/src/run-list-query.ts +9 -4
- package/src/sync-run-storage.ts +120 -34
- package/src/testing.ts +20 -0
- package/src/transactions.ts +10 -3
- package/src/webhook-run-storage.ts +118 -74
- package/src/workflow-run-storage.ts +124 -133
- package/dist/validation.d.ts +0 -5
- package/dist/validation.d.ts.map +0 -1
- package/dist/webhook-delivery-storage.d.ts +0 -27
- package/dist/webhook-delivery-storage.d.ts.map +0 -1
- package/src/validation.ts +0 -30
- package/src/webhook-delivery-storage.ts +0 -272
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
type EffectiveChangeCounts,
|
|
4
4
|
MaterializationConflictError,
|
|
5
|
-
objectRefKey,
|
|
6
5
|
} from "@sixb/core/internal/materialization"
|
|
7
6
|
import {
|
|
8
7
|
correlateMaterializationChunk,
|
|
@@ -17,7 +16,6 @@ import type {
|
|
|
17
16
|
MaterializationEventWorkRecord,
|
|
18
17
|
MaterializationPlanChunk,
|
|
19
18
|
MaterializationPlanHeader,
|
|
20
|
-
MaterializationPlanWorkItem,
|
|
21
19
|
MaterializationSession,
|
|
22
20
|
MaterializationWorkPage,
|
|
23
21
|
MaterializationWorkRecord,
|
|
@@ -25,7 +23,6 @@ import type {
|
|
|
25
23
|
StreamMaterializationWorkInput,
|
|
26
24
|
} from "@sixb/core/storage"
|
|
27
25
|
import {
|
|
28
|
-
type ReplacementIdentity,
|
|
29
26
|
SQLITE_MATERIALIZATION_WORK_TABLE,
|
|
30
27
|
SQLITE_REPLACEMENT_WORK_TABLE,
|
|
31
28
|
} from "./materialization-state"
|
|
@@ -37,7 +34,53 @@ export interface SqliteOntologyTransactionContext {
|
|
|
37
34
|
active: boolean
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
interface WorkCursor {
|
|
38
|
+
readonly majorOrder: number
|
|
39
|
+
readonly minorOrder: number
|
|
40
|
+
readonly sortOne: string
|
|
41
|
+
readonly sortTwo: string
|
|
42
|
+
readonly recordKey: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface WorkDatabaseRow {
|
|
46
|
+
readonly record_key: string
|
|
47
|
+
readonly major_order: number
|
|
48
|
+
readonly minor_order: number
|
|
49
|
+
readonly sort_one: string
|
|
50
|
+
readonly sort_two: string
|
|
51
|
+
readonly payload: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface WorkSummary {
|
|
55
|
+
apply: number
|
|
56
|
+
cardinality: number
|
|
57
|
+
event: number
|
|
58
|
+
objectClassifications: number
|
|
59
|
+
linkClassifications: number
|
|
60
|
+
pointClassifications: number
|
|
61
|
+
objectsCreated: number
|
|
62
|
+
objectsUpdated: number
|
|
63
|
+
objectsDeleted: number
|
|
64
|
+
linksCreated: number
|
|
65
|
+
linksUpdated: number
|
|
66
|
+
linksDeleted: number
|
|
67
|
+
pointsCreated: number
|
|
68
|
+
pointsUpdated: number
|
|
69
|
+
latestObjectsChanged: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SqliteChunkSequenceProgress {
|
|
73
|
+
readonly appliedPlanCount: number
|
|
74
|
+
readonly appliedOutboxCount: number
|
|
75
|
+
readonly appliedPlanCursor: WorkCursor | null
|
|
76
|
+
readonly appliedEventCursor: WorkCursor | null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class SqliteMaterializationSessionState extends ProviderMaterializationSessionState {
|
|
80
|
+
appliedPlanCursor: WorkCursor | null = null
|
|
81
|
+
appliedEventCursor: WorkCursor | null = null
|
|
82
|
+
readonly summary: WorkSummary = emptyWorkSummary()
|
|
83
|
+
}
|
|
41
84
|
|
|
42
85
|
export class SqliteMaterializationSessions {
|
|
43
86
|
private readonly sessions = new WeakMap<object, SqliteMaterializationSessionState>()
|
|
@@ -81,10 +124,14 @@ export class SqliteMaterializationSessions {
|
|
|
81
124
|
|
|
82
125
|
release(session: SqliteMaterializationSessionState): void {
|
|
83
126
|
if (!session.active) return
|
|
84
|
-
this.deleteWork(session.id)
|
|
85
127
|
session.active = false
|
|
86
128
|
session.replacement = null
|
|
87
129
|
this.live.delete(session)
|
|
130
|
+
// The normal transaction has one session. Dropping its transaction-local spool avoids an
|
|
131
|
+
// indexed DELETE over hundreds of thousands of rows and releases temp pages immediately.
|
|
132
|
+
// Preserve per-session deletion only when another live session still shares the tables.
|
|
133
|
+
if (this.live.size === 0) this.dropWorkTables()
|
|
134
|
+
else this.deleteWork(session.id)
|
|
88
135
|
this.context?.materializations.complete(session.providerToken)
|
|
89
136
|
}
|
|
90
137
|
|
|
@@ -95,16 +142,26 @@ export class SqliteMaterializationSessions {
|
|
|
95
142
|
stage(input: StageMaterializationWorkInput): void {
|
|
96
143
|
const session = this.require(input.session)
|
|
97
144
|
const records = prepareMaterializationWork(session, input)
|
|
145
|
+
if (records.length === 0) return
|
|
98
146
|
const insert = this.db.query(
|
|
99
147
|
`
|
|
100
148
|
INSERT INTO ${SQLITE_MATERIALIZATION_WORK_TABLE} (
|
|
101
149
|
session_id, record_key, unique_key, kind, lane,
|
|
102
|
-
major_order, minor_order, sort_one, sort_two,
|
|
103
|
-
|
|
150
|
+
major_order, minor_order, sort_one, sort_two,
|
|
151
|
+
classification_entity_kind, classification_identity_key,
|
|
152
|
+
cardinality_view, cardinality_occupied, cardinality_source_type_id,
|
|
153
|
+
cardinality_source_primary_id, cardinality_link_id,
|
|
154
|
+
cardinality_target_type_id, cardinality_target_primary_id, payload
|
|
155
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, json(?))
|
|
104
156
|
`
|
|
105
157
|
)
|
|
158
|
+
this.db.run("SAVEPOINT sixb_ontology_stage_work")
|
|
159
|
+
let currentRecordKey: string | undefined
|
|
106
160
|
try {
|
|
107
161
|
for (const { record, uniqueKey, columns } of records) {
|
|
162
|
+
currentRecordKey = record.recordKey
|
|
163
|
+
const classification = record.kind === "classification" ? record : null
|
|
164
|
+
const cardinality = record.kind === "cardinality" ? record : null
|
|
108
165
|
insert.run(
|
|
109
166
|
session.id,
|
|
110
167
|
record.recordKey,
|
|
@@ -115,15 +172,29 @@ export class SqliteMaterializationSessions {
|
|
|
115
172
|
columns.minorOrder,
|
|
116
173
|
columns.sortOne,
|
|
117
174
|
columns.sortTwo,
|
|
175
|
+
classification?.entityKind ?? null,
|
|
176
|
+
classification?.identityKey ?? null,
|
|
177
|
+
cardinality?.view ?? null,
|
|
178
|
+
cardinality ? Number(cardinality.occupied) : null,
|
|
179
|
+
cardinality?.ref.source.objectTypeId ?? null,
|
|
180
|
+
cardinality?.ref.source.primaryId ?? null,
|
|
181
|
+
cardinality?.ref.linkId ?? null,
|
|
182
|
+
cardinality?.ref.target.objectTypeId ?? null,
|
|
183
|
+
cardinality?.ref.target.primaryId ?? null,
|
|
118
184
|
canonicalJson(record)
|
|
119
185
|
)
|
|
120
186
|
}
|
|
187
|
+
this.db.run("RELEASE SAVEPOINT sixb_ontology_stage_work")
|
|
121
188
|
} catch (error) {
|
|
189
|
+
this.db.run("ROLLBACK TO SAVEPOINT sixb_ontology_stage_work")
|
|
190
|
+
this.db.run("RELEASE SAVEPOINT sixb_ontology_stage_work")
|
|
122
191
|
if (isSqliteConstraintError(error)) {
|
|
123
|
-
throw duplicateWork(
|
|
192
|
+
throw duplicateWork(currentRecordKey)
|
|
124
193
|
}
|
|
125
194
|
throw error
|
|
126
195
|
}
|
|
196
|
+
for (const prepared of records)
|
|
197
|
+
recordSummary(session.summary, prepared.record, prepared.columns)
|
|
127
198
|
}
|
|
128
199
|
|
|
129
200
|
async *stream(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
|
|
@@ -137,26 +208,16 @@ export class SqliteMaterializationSessions {
|
|
|
137
208
|
}
|
|
138
209
|
stream.started = true
|
|
139
210
|
session.workSealed = true
|
|
140
|
-
let
|
|
211
|
+
let cursor: WorkCursor | null = null
|
|
141
212
|
while (true) {
|
|
142
213
|
this.require(input.session)
|
|
143
|
-
const rows = this.
|
|
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 }[]
|
|
214
|
+
const rows = this.readLane(input.order, session.id, cursor, input.pageRows)
|
|
154
215
|
if (rows.length === 0) break
|
|
155
216
|
const records = rows.map((row) =>
|
|
156
217
|
parseJson<MaterializationWorkRecord>(row.payload)
|
|
157
218
|
) as MaterializationWorkPage["records"]
|
|
158
|
-
|
|
159
|
-
stream.emittedCount
|
|
219
|
+
cursor = workCursor(rows[rows.length - 1]!)
|
|
220
|
+
stream.emittedCount += records.length
|
|
160
221
|
yield { records }
|
|
161
222
|
}
|
|
162
223
|
this.require(input.session)
|
|
@@ -187,53 +248,55 @@ export class SqliteMaterializationSessions {
|
|
|
187
248
|
})
|
|
188
249
|
}
|
|
189
250
|
|
|
190
|
-
|
|
251
|
+
prepareChunkSequence(
|
|
191
252
|
session: SqliteMaterializationSessionState,
|
|
192
253
|
chunk: MaterializationPlanChunk
|
|
193
|
-
):
|
|
254
|
+
): SqliteChunkSequenceProgress {
|
|
194
255
|
const items = materializationPlanItems(chunk)
|
|
256
|
+
const planRows = this.readLane("apply", session.id, session.appliedPlanCursor, items.length)
|
|
257
|
+
const expectedItems = planRows.map((row) => {
|
|
258
|
+
const record = parseJson<Extract<MaterializationWorkRecord, { readonly kind: "plan" }>>(
|
|
259
|
+
row.payload
|
|
260
|
+
)
|
|
261
|
+
return record.item
|
|
262
|
+
})
|
|
263
|
+
const eventRows = this.readLane(
|
|
264
|
+
"event",
|
|
265
|
+
session.id,
|
|
266
|
+
session.appliedEventCursor,
|
|
267
|
+
chunk.outbox.length
|
|
268
|
+
)
|
|
269
|
+
const expectedEvents = eventRows.map((row) =>
|
|
270
|
+
parseJson<MaterializationEventWorkRecord>(row.payload)
|
|
271
|
+
)
|
|
195
272
|
const progress = correlateMaterializationChunk(
|
|
196
273
|
session,
|
|
197
274
|
chunk,
|
|
198
275
|
items,
|
|
199
|
-
|
|
200
|
-
|
|
276
|
+
expectedItems,
|
|
277
|
+
expectedEvents
|
|
201
278
|
)
|
|
202
|
-
|
|
203
|
-
|
|
279
|
+
return {
|
|
280
|
+
...progress,
|
|
281
|
+
appliedPlanCursor:
|
|
282
|
+
planRows.length === 0
|
|
283
|
+
? session.appliedPlanCursor
|
|
284
|
+
: workCursor(planRows[planRows.length - 1]!),
|
|
285
|
+
appliedEventCursor:
|
|
286
|
+
eventRows.length === 0
|
|
287
|
+
? session.appliedEventCursor
|
|
288
|
+
: workCursor(eventRows[eventRows.length - 1]!),
|
|
289
|
+
}
|
|
204
290
|
}
|
|
205
291
|
|
|
206
|
-
|
|
292
|
+
commitChunkSequence(
|
|
207
293
|
session: SqliteMaterializationSessionState,
|
|
208
|
-
|
|
294
|
+
progress: SqliteChunkSequenceProgress
|
|
209
295
|
): void {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
296
|
+
session.appliedPlanCount = progress.appliedPlanCount
|
|
297
|
+
session.appliedOutboxCount = progress.appliedOutboxCount
|
|
298
|
+
session.appliedPlanCursor = progress.appliedPlanCursor
|
|
299
|
+
session.appliedEventCursor = progress.appliedEventCursor
|
|
237
300
|
}
|
|
238
301
|
|
|
239
302
|
laneCounts(session: SqliteMaterializationSessionState): {
|
|
@@ -241,29 +304,46 @@ export class SqliteMaterializationSessions {
|
|
|
241
304
|
readonly cardinality: number
|
|
242
305
|
readonly event: number
|
|
243
306
|
} {
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
307
|
+
return {
|
|
308
|
+
apply: session.summary.apply,
|
|
309
|
+
cardinality: session.summary.cardinality,
|
|
310
|
+
event: session.summary.event,
|
|
311
|
+
}
|
|
257
312
|
}
|
|
258
313
|
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
projectionCounts(session: SqliteMaterializationSessionState): EffectiveChangeCounts {
|
|
315
|
+
const summary = session.summary
|
|
316
|
+
return {
|
|
317
|
+
objectsCreated: summary.objectsCreated,
|
|
318
|
+
objectsUpdated: summary.objectsUpdated,
|
|
319
|
+
objectsDeleted: summary.objectsDeleted,
|
|
320
|
+
objectsUnchanged:
|
|
321
|
+
summary.objectClassifications -
|
|
322
|
+
summary.objectsCreated -
|
|
323
|
+
summary.objectsUpdated -
|
|
324
|
+
summary.objectsDeleted,
|
|
325
|
+
linksCreated: summary.linksCreated,
|
|
326
|
+
linksUpdated: summary.linksUpdated,
|
|
327
|
+
linksDeleted: summary.linksDeleted,
|
|
328
|
+
linksUnchanged:
|
|
329
|
+
summary.linkClassifications -
|
|
330
|
+
summary.linksCreated -
|
|
331
|
+
summary.linksUpdated -
|
|
332
|
+
summary.linksDeleted,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
telemetrySummary(session: SqliteMaterializationSessionState, pointCount: number) {
|
|
337
|
+
const summary = session.summary
|
|
338
|
+
return {
|
|
339
|
+
classifiedPoints: summary.pointClassifications,
|
|
340
|
+
counts: {
|
|
341
|
+
pointsCreated: summary.pointsCreated,
|
|
342
|
+
pointsUpdated: summary.pointsUpdated,
|
|
343
|
+
pointsUnchanged: pointCount - summary.pointsCreated - summary.pointsUpdated,
|
|
344
|
+
latestObjectsChanged: summary.latestObjectsChanged,
|
|
345
|
+
},
|
|
346
|
+
}
|
|
267
347
|
}
|
|
268
348
|
|
|
269
349
|
assertClassificationCoverage(session: SqliteMaterializationSessionState): void {
|
|
@@ -272,84 +352,84 @@ export class SqliteMaterializationSessions {
|
|
|
272
352
|
const mismatch = this.db
|
|
273
353
|
.query(
|
|
274
354
|
`
|
|
275
|
-
WITH
|
|
276
|
-
SELECT
|
|
277
|
-
FROM ${SQLITE_REPLACEMENT_WORK_TABLE}
|
|
278
|
-
WHERE session_id = ? AND diff_required = 1
|
|
279
|
-
AND (entity_kind = 'link' OR ? = 1)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
), differences AS (
|
|
287
|
-
SELECT * FROM expected EXCEPT SELECT * FROM actual
|
|
355
|
+
WITH invalid AS (
|
|
356
|
+
SELECT 1
|
|
357
|
+
FROM ${SQLITE_REPLACEMENT_WORK_TABLE} AS expected
|
|
358
|
+
WHERE expected.session_id = ? AND expected.diff_required = 1
|
|
359
|
+
AND (expected.entity_kind = 'link' OR ? = 1)
|
|
360
|
+
AND NOT EXISTS (
|
|
361
|
+
SELECT 1 FROM ${SQLITE_MATERIALIZATION_WORK_TABLE} AS actual
|
|
362
|
+
WHERE actual.session_id = expected.session_id
|
|
363
|
+
AND actual.unique_key = 'classification:' || expected.entity_kind || ':'
|
|
364
|
+
|| expected.identity_key
|
|
365
|
+
)
|
|
288
366
|
UNION ALL
|
|
289
|
-
SELECT
|
|
367
|
+
SELECT 1
|
|
368
|
+
FROM ${SQLITE_MATERIALIZATION_WORK_TABLE} AS actual
|
|
369
|
+
WHERE actual.session_id = ?
|
|
370
|
+
-- A bounded range uses the existing (session_id, unique_key) unique index and scans
|
|
371
|
+
-- classification entries only, instead of sorting the complete JSON work spool.
|
|
372
|
+
AND actual.unique_key >= 'classification:'
|
|
373
|
+
AND actual.unique_key < 'classification;'
|
|
374
|
+
AND (
|
|
375
|
+
actual.classification_entity_kind = 'point'
|
|
376
|
+
OR NOT EXISTS (
|
|
377
|
+
SELECT 1 FROM ${SQLITE_REPLACEMENT_WORK_TABLE} AS expected
|
|
378
|
+
WHERE expected.session_id = actual.session_id
|
|
379
|
+
AND expected.entity_kind = actual.classification_entity_kind
|
|
380
|
+
AND expected.identity_key = actual.classification_identity_key
|
|
381
|
+
AND expected.diff_required = 1
|
|
382
|
+
AND (expected.entity_kind = 'link' OR ? = 1)
|
|
383
|
+
)
|
|
384
|
+
)
|
|
290
385
|
)
|
|
291
|
-
SELECT 1 FROM
|
|
386
|
+
SELECT 1 FROM invalid LIMIT 1
|
|
292
387
|
`
|
|
293
388
|
)
|
|
294
|
-
.get(session.id, expectedObject, session.id)
|
|
295
|
-
|
|
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) {
|
|
389
|
+
.get(session.id, expectedObject, session.id, expectedObject)
|
|
390
|
+
if (mismatch) {
|
|
306
391
|
invalidCorrelation(
|
|
307
392
|
"Projection replacement classification coverage does not match its streamed state."
|
|
308
393
|
)
|
|
309
394
|
}
|
|
310
395
|
}
|
|
311
396
|
|
|
312
|
-
private
|
|
313
|
-
|
|
314
|
-
|
|
397
|
+
private readLane(
|
|
398
|
+
lane: StreamMaterializationWorkInput["order"],
|
|
399
|
+
sessionId: string,
|
|
400
|
+
cursor: WorkCursor | null,
|
|
315
401
|
limit: number
|
|
316
|
-
):
|
|
402
|
+
): WorkDatabaseRow[] {
|
|
317
403
|
if (limit === 0) return []
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
404
|
+
const selected = `record_key, major_order, minor_order, sort_one, sort_two, payload`
|
|
405
|
+
if (!cursor) {
|
|
406
|
+
return this.db
|
|
407
|
+
.query(
|
|
408
|
+
`SELECT ${selected} FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
409
|
+
WHERE session_id = ? AND lane = ?
|
|
410
|
+
ORDER BY major_order, minor_order, sort_one, sort_two, record_key
|
|
411
|
+
LIMIT ?`
|
|
412
|
+
)
|
|
413
|
+
.all(sessionId, lane, limit) as WorkDatabaseRow[]
|
|
414
|
+
}
|
|
415
|
+
return this.db
|
|
343
416
|
.query(
|
|
344
|
-
`
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
`
|
|
417
|
+
`SELECT ${selected} FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
418
|
+
WHERE session_id = ? AND lane = ?
|
|
419
|
+
AND (major_order, minor_order, sort_one, sort_two, record_key) > (?, ?, ?, ?, ?)
|
|
420
|
+
ORDER BY major_order, minor_order, sort_one, sort_two, record_key
|
|
421
|
+
LIMIT ?`
|
|
350
422
|
)
|
|
351
|
-
.all(
|
|
352
|
-
|
|
423
|
+
.all(
|
|
424
|
+
sessionId,
|
|
425
|
+
lane,
|
|
426
|
+
cursor.majorOrder,
|
|
427
|
+
cursor.minorOrder,
|
|
428
|
+
cursor.sortOne,
|
|
429
|
+
cursor.sortTwo,
|
|
430
|
+
cursor.recordKey,
|
|
431
|
+
limit
|
|
432
|
+
) as WorkDatabaseRow[]
|
|
353
433
|
}
|
|
354
434
|
|
|
355
435
|
private ensureTables(): void {
|
|
@@ -364,6 +444,15 @@ export class SqliteMaterializationSessions {
|
|
|
364
444
|
minor_order INTEGER NOT NULL,
|
|
365
445
|
sort_one TEXT NOT NULL,
|
|
366
446
|
sort_two TEXT NOT NULL,
|
|
447
|
+
classification_entity_kind TEXT,
|
|
448
|
+
classification_identity_key TEXT,
|
|
449
|
+
cardinality_view TEXT,
|
|
450
|
+
cardinality_occupied INTEGER,
|
|
451
|
+
cardinality_source_type_id TEXT,
|
|
452
|
+
cardinality_source_primary_id TEXT,
|
|
453
|
+
cardinality_link_id TEXT,
|
|
454
|
+
cardinality_target_type_id TEXT,
|
|
455
|
+
cardinality_target_primary_id TEXT,
|
|
367
456
|
payload TEXT NOT NULL CHECK (json_valid(payload)),
|
|
368
457
|
PRIMARY KEY (session_id, record_key),
|
|
369
458
|
UNIQUE (session_id, unique_key)
|
|
@@ -376,9 +465,14 @@ export class SqliteMaterializationSessions {
|
|
|
376
465
|
session_id TEXT NOT NULL,
|
|
377
466
|
entity_kind TEXT NOT NULL,
|
|
378
467
|
identity_key TEXT NOT NULL,
|
|
468
|
+
sort_key TEXT NOT NULL,
|
|
379
469
|
diff_required INTEGER NOT NULL CHECK (diff_required IN (0, 1)),
|
|
380
470
|
PRIMARY KEY (session_id, entity_kind, identity_key)
|
|
381
471
|
);
|
|
472
|
+
CREATE INDEX IF NOT EXISTS idx_ontology_replacement_work_order
|
|
473
|
+
ON ${SQLITE_REPLACEMENT_WORK_TABLE}(
|
|
474
|
+
session_id, entity_kind, sort_key, identity_key
|
|
475
|
+
);
|
|
382
476
|
`)
|
|
383
477
|
}
|
|
384
478
|
|
|
@@ -390,4 +484,84 @@ export class SqliteMaterializationSessions {
|
|
|
390
484
|
.query(`DELETE FROM ${SQLITE_REPLACEMENT_WORK_TABLE} WHERE session_id = ?`)
|
|
391
485
|
.run(sessionId)
|
|
392
486
|
}
|
|
487
|
+
|
|
488
|
+
private dropWorkTables(): void {
|
|
489
|
+
this.db.run(`
|
|
490
|
+
DROP TABLE IF EXISTS ${SQLITE_MATERIALIZATION_WORK_TABLE};
|
|
491
|
+
DROP TABLE IF EXISTS ${SQLITE_REPLACEMENT_WORK_TABLE};
|
|
492
|
+
`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function workCursor(row: WorkDatabaseRow): WorkCursor {
|
|
497
|
+
return {
|
|
498
|
+
majorOrder: row.major_order,
|
|
499
|
+
minorOrder: row.minor_order,
|
|
500
|
+
sortOne: row.sort_one,
|
|
501
|
+
sortTwo: row.sort_two,
|
|
502
|
+
recordKey: row.record_key,
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function emptyWorkSummary(): WorkSummary {
|
|
507
|
+
return {
|
|
508
|
+
apply: 0,
|
|
509
|
+
cardinality: 0,
|
|
510
|
+
event: 0,
|
|
511
|
+
objectClassifications: 0,
|
|
512
|
+
linkClassifications: 0,
|
|
513
|
+
pointClassifications: 0,
|
|
514
|
+
objectsCreated: 0,
|
|
515
|
+
objectsUpdated: 0,
|
|
516
|
+
objectsDeleted: 0,
|
|
517
|
+
linksCreated: 0,
|
|
518
|
+
linksUpdated: 0,
|
|
519
|
+
linksDeleted: 0,
|
|
520
|
+
pointsCreated: 0,
|
|
521
|
+
pointsUpdated: 0,
|
|
522
|
+
latestObjectsChanged: 0,
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function recordSummary(
|
|
527
|
+
summary: WorkSummary,
|
|
528
|
+
record: MaterializationWorkRecord,
|
|
529
|
+
columns: { readonly lane: "none" | "apply" | "cardinality" | "event" }
|
|
530
|
+
): void {
|
|
531
|
+
if (columns.lane !== "none") summary[columns.lane] += 1
|
|
532
|
+
|
|
533
|
+
if (record.kind === "classification") {
|
|
534
|
+
if (record.entityKind === "object") summary.objectClassifications += 1
|
|
535
|
+
if (record.entityKind === "link") summary.linkClassifications += 1
|
|
536
|
+
if (record.entityKind === "point") summary.pointClassifications += 1
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
if (record.kind !== "plan") return
|
|
540
|
+
|
|
541
|
+
switch (record.item.kind) {
|
|
542
|
+
case "object-upsert":
|
|
543
|
+
summary.latestObjectsChanged += 1
|
|
544
|
+
if (record.item.value.expected.exists) summary.objectsUpdated += 1
|
|
545
|
+
else summary.objectsCreated += 1
|
|
546
|
+
return
|
|
547
|
+
case "object-delete":
|
|
548
|
+
summary.objectsDeleted += 1
|
|
549
|
+
return
|
|
550
|
+
case "link-upsert":
|
|
551
|
+
if (record.item.value.expected.exists) summary.linksUpdated += 1
|
|
552
|
+
else summary.linksCreated += 1
|
|
553
|
+
return
|
|
554
|
+
case "link-delete":
|
|
555
|
+
summary.linksDeleted += 1
|
|
556
|
+
return
|
|
557
|
+
case "point-upsert":
|
|
558
|
+
if (record.item.value.expected.lastCommitId === null) summary.pointsCreated += 1
|
|
559
|
+
else summary.pointsUpdated += 1
|
|
560
|
+
return
|
|
561
|
+
case "object-override-upsert":
|
|
562
|
+
case "object-override-delete":
|
|
563
|
+
case "link-override-upsert":
|
|
564
|
+
case "link-override-delete":
|
|
565
|
+
return
|
|
566
|
+
}
|
|
393
567
|
}
|