@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.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/action-run-storage.d.ts +29 -0
- package/dist/action-run-storage.d.ts.map +1 -0
- package/dist/agents/agent-storage.d.ts +25 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +14 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +26 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +14 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +46 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +28 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +134 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +131 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10204 -0
- package/dist/index.js.map +54 -0
- package/dist/latest-run-query.d.ts +22 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +18 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/object-query-compiler.d.ts +44 -0
- package/dist/object-query-compiler.d.ts.map +1 -0
- package/dist/object-storage.d.ts +93 -0
- package/dist/object-storage.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +14 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +49 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +63 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +32 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +14 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +91 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +26 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts +26 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -0
- package/dist/projection-run-storage.d.ts +32 -0
- package/dist/projection-run-storage.d.ts.map +1 -0
- package/dist/rules-storage.d.ts +33 -0
- package/dist/rules-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +27 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/sync-run-storage.d.ts +23 -0
- package/dist/sync-run-storage.d.ts.map +1 -0
- package/dist/timeseries-storage.d.ts +41 -0
- package/dist/timeseries-storage.d.ts.map +1 -0
- package/dist/transactions.d.ts +15 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/webhook-delivery-storage.d.ts +27 -0
- package/dist/webhook-delivery-storage.d.ts.map +1 -0
- package/dist/webhook-run-storage.d.ts +22 -0
- package/dist/webhook-run-storage.d.ts.map +1 -0
- package/dist/workflow-intervention-storage.d.ts +26 -0
- package/dist/workflow-intervention-storage.d.ts.map +1 -0
- package/dist/workflow-run-storage.d.ts +63 -0
- package/dist/workflow-run-storage.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/action-run-storage.ts +761 -0
- package/src/agents/agent-storage.ts +46 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +162 -0
- package/src/agents/rows.ts +205 -0
- package/src/agents/runs.ts +389 -0
- package/src/agents/threads.ts +133 -0
- package/src/auth-storage/access-tokens.ts +289 -0
- package/src/auth-storage/auth-storage.ts +784 -0
- package/src/auth-storage/group-memberships.ts +52 -0
- package/src/auth-storage/identities.ts +98 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +258 -0
- package/src/auth-storage/magic-links.ts +149 -0
- package/src/auth-storage/oidc-attempts.ts +109 -0
- package/src/auth-storage/rows.ts +338 -0
- package/src/auth-storage/service-account-group-memberships.ts +161 -0
- package/src/auth-storage/service-accounts.ts +168 -0
- package/src/auth-storage/sessions.ts +230 -0
- package/src/auth-storage/shared.ts +707 -0
- package/src/auth-storage/users.ts +226 -0
- package/src/index.ts +292 -0
- package/src/latest-run-query.ts +49 -0
- package/src/migrations/001-initial-schema.sql +1068 -0
- package/src/migrations.ts +245 -0
- package/src/object-query-compiler.ts +1198 -0
- package/src/object-storage.ts +605 -0
- package/src/ontology-storage/commits.ts +127 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +393 -0
- package/src/ontology-storage/materialization-state.ts +901 -0
- package/src/ontology-storage/materialization-writer.ts +394 -0
- package/src/ontology-storage/materializations.ts +838 -0
- package/src/ontology-storage/outbox.ts +237 -0
- package/src/ontology-storage/shared.ts +269 -0
- package/src/ontology-storage/sources.ts +653 -0
- package/src/pipeline-run-storage.ts +506 -0
- package/src/projection-run-storage.ts +592 -0
- package/src/rules-storage.ts +375 -0
- package/src/run-list-query.ts +98 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +3 -0
- package/src/sync-run-storage.ts +299 -0
- package/src/timeseries-storage.ts +212 -0
- package/src/transactions.ts +96 -0
- package/src/validation.ts +30 -0
- package/src/webhook-delivery-storage.ts +272 -0
- package/src/webhook-run-storage.ts +240 -0
- package/src/workflow-intervention-storage.ts +398 -0
- package/src/workflow-run-storage.ts +1148 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { MaterializationValidationError } from "@sixb/core/internal/materialization"
|
|
3
|
+
import {
|
|
4
|
+
assertSourceBeginInput as assertBeginInput,
|
|
5
|
+
assertSourceExecutionIdentity as assertExecutionIdentity,
|
|
6
|
+
assertSourceProject as assertProjectAndSource,
|
|
7
|
+
assertSourceCandidateOwner,
|
|
8
|
+
assertSourceWriteIdentity as assertWriteIdentity,
|
|
9
|
+
sourceEntityColumns as entityColumns,
|
|
10
|
+
isExactStagingManifest,
|
|
11
|
+
reconcileSourceStageRows,
|
|
12
|
+
type SourceStageRow,
|
|
13
|
+
sourceConflict,
|
|
14
|
+
sourceMaterializationIdentity,
|
|
15
|
+
sourceStageRow,
|
|
16
|
+
sourceStageRows,
|
|
17
|
+
utf8SortKey,
|
|
18
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
19
|
+
import type {
|
|
20
|
+
AbandonSourceMaterializationCandidateInput,
|
|
21
|
+
AbandonSourceMaterializationInput,
|
|
22
|
+
AssertSourceMaterializationExecutionInput,
|
|
23
|
+
BeginSourceMaterializationInput,
|
|
24
|
+
CleanupTerminalSourceMaterializationsInput,
|
|
25
|
+
CleanupTerminalSourceMaterializationsResult,
|
|
26
|
+
GetActiveOntologySourceInput,
|
|
27
|
+
MarkSourceMaterializationReadyInput,
|
|
28
|
+
OntologySourceRecord,
|
|
29
|
+
OntologySourceStorage,
|
|
30
|
+
ReclaimSourceMaterializationInput,
|
|
31
|
+
StageSourceRowsInput,
|
|
32
|
+
StageSourceRowsResult,
|
|
33
|
+
SummarizeTerminalSourceMaterializationsInput,
|
|
34
|
+
TerminalSourceMaterializationSummary,
|
|
35
|
+
} from "@sixb/core/storage"
|
|
36
|
+
import {
|
|
37
|
+
assertNonblank,
|
|
38
|
+
assertNonnegativeInteger,
|
|
39
|
+
assertPositiveInteger,
|
|
40
|
+
assertProjectionExecution,
|
|
41
|
+
assertTimestamp,
|
|
42
|
+
canonicalJson,
|
|
43
|
+
type SqliteOntologySourceAssertionRow,
|
|
44
|
+
type SqliteOntologySourceRow,
|
|
45
|
+
type SqliteRootOperation,
|
|
46
|
+
sourceAssertion,
|
|
47
|
+
sourceRecord,
|
|
48
|
+
} from "./shared"
|
|
49
|
+
|
|
50
|
+
export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly db: Database,
|
|
53
|
+
private readonly runRootOperation: SqliteRootOperation
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
async beginMaterialization(
|
|
57
|
+
input: BeginSourceMaterializationInput
|
|
58
|
+
): Promise<OntologySourceRecord> {
|
|
59
|
+
return this.runRootOperation(() => {
|
|
60
|
+
assertBeginInput(input)
|
|
61
|
+
this.assertExecution(input, sourceMaterializationIdentity(input))
|
|
62
|
+
const existing = this.getManifest(
|
|
63
|
+
input.projectId,
|
|
64
|
+
input.source.projectionId,
|
|
65
|
+
input.materializationId
|
|
66
|
+
)
|
|
67
|
+
if (existing) {
|
|
68
|
+
if (isExactStagingManifest(sourceRecord(existing), input)) return sourceRecord(existing)
|
|
69
|
+
throw sourceConflict(
|
|
70
|
+
`Source materialization '${input.materializationId}' already exists with different identity or state.`
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const candidate = this.db
|
|
75
|
+
.query(
|
|
76
|
+
`
|
|
77
|
+
SELECT 1
|
|
78
|
+
FROM ontology_sources
|
|
79
|
+
WHERE project_id = ? AND projection_run_id = ?
|
|
80
|
+
AND status IN ('staging', 'ready')
|
|
81
|
+
LIMIT 1
|
|
82
|
+
`
|
|
83
|
+
)
|
|
84
|
+
.get(input.projectId, input.execution.projectionRunId)
|
|
85
|
+
if (candidate) {
|
|
86
|
+
throw sourceConflict(
|
|
87
|
+
`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.db
|
|
92
|
+
.query(
|
|
93
|
+
`
|
|
94
|
+
INSERT INTO ontology_sources (
|
|
95
|
+
project_id, source_id, materialization_id, projection_run_id,
|
|
96
|
+
projection_kind, protocol, status, execution_token,
|
|
97
|
+
dataset_id, dataset_version_id, dataset_version_created_at,
|
|
98
|
+
projection_revision, ownership_hash, ontology_revision,
|
|
99
|
+
root_count, assertion_count, created_at, ready_at, activated_at,
|
|
100
|
+
terminal_at, last_commit_id, updated_at
|
|
101
|
+
) VALUES (?, ?, ?, ?, ?, 'replacement', 'staging', ?, ?, ?, ?, ?, ?, ?,
|
|
102
|
+
NULL, NULL, ?, NULL, NULL, NULL, NULL, ?)
|
|
103
|
+
`
|
|
104
|
+
)
|
|
105
|
+
.run(
|
|
106
|
+
input.projectId,
|
|
107
|
+
input.source.projectionId,
|
|
108
|
+
input.materializationId,
|
|
109
|
+
input.execution.projectionRunId,
|
|
110
|
+
input.projectionKind,
|
|
111
|
+
input.execution.executionToken,
|
|
112
|
+
input.datasetVersion.datasetId,
|
|
113
|
+
input.datasetVersion.versionId,
|
|
114
|
+
input.datasetVersion.createdAt,
|
|
115
|
+
input.projectionRevision,
|
|
116
|
+
input.ownershipHash,
|
|
117
|
+
input.ontologyRevision,
|
|
118
|
+
input.createdAt,
|
|
119
|
+
input.createdAt
|
|
120
|
+
)
|
|
121
|
+
return sourceRecord(
|
|
122
|
+
this.requireManifest(input.projectId, input.source.projectionId, input.materializationId)
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async stageRows(input: StageSourceRowsInput): Promise<StageSourceRowsResult> {
|
|
128
|
+
return this.runRootOperation(() => {
|
|
129
|
+
assertWriteIdentity(input)
|
|
130
|
+
this.assertExecution(input)
|
|
131
|
+
const manifest = this.requireManifest(
|
|
132
|
+
input.projectId,
|
|
133
|
+
input.source.projectionId,
|
|
134
|
+
input.materializationId
|
|
135
|
+
)
|
|
136
|
+
assertSourceCandidateOwner(sourceRecord(manifest), input.execution)
|
|
137
|
+
if (manifest.status !== "staging") {
|
|
138
|
+
throw sourceConflict(
|
|
139
|
+
`Source materialization '${input.materializationId}' is '${manifest.status}' and cannot accept rows.`
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rows = sourceStageRows(manifest.projection_kind, input.rows)
|
|
144
|
+
const existing = this.findStageRows(input, rows)
|
|
145
|
+
const { pending, unchanged } = reconcileSourceStageRows(rows, existing)
|
|
146
|
+
this.insertStageRows(input, pending)
|
|
147
|
+
return { inserted: pending.length, unchanged }
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async markReady(input: MarkSourceMaterializationReadyInput): Promise<OntologySourceRecord> {
|
|
152
|
+
return this.runRootOperation(() => {
|
|
153
|
+
assertWriteIdentity(input)
|
|
154
|
+
assertTimestamp(input.readyAt, "Source readyAt", true)
|
|
155
|
+
assertNonnegativeInteger(input.rootCount, "Source root count")
|
|
156
|
+
assertNonnegativeInteger(input.assertionCount, "Source assertion count")
|
|
157
|
+
this.assertExecution(input)
|
|
158
|
+
const manifest = this.requireManifest(
|
|
159
|
+
input.projectId,
|
|
160
|
+
input.source.projectionId,
|
|
161
|
+
input.materializationId
|
|
162
|
+
)
|
|
163
|
+
assertSourceCandidateOwner(sourceRecord(manifest), input.execution)
|
|
164
|
+
if (manifest.status === "ready") {
|
|
165
|
+
if (
|
|
166
|
+
manifest.root_count === input.rootCount &&
|
|
167
|
+
manifest.assertion_count === input.assertionCount &&
|
|
168
|
+
manifest.ready_at === input.readyAt
|
|
169
|
+
) {
|
|
170
|
+
return sourceRecord(manifest)
|
|
171
|
+
}
|
|
172
|
+
throw sourceConflict(
|
|
173
|
+
`Source materialization '${input.materializationId}' was marked ready with different counts or time.`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
if (manifest.status !== "staging") {
|
|
177
|
+
throw sourceConflict(
|
|
178
|
+
`Source materialization '${input.materializationId}' cannot transition from '${manifest.status}' to 'ready'.`
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
if (input.readyAt < manifest.created_at) {
|
|
182
|
+
throw new MaterializationValidationError("Source readyAt cannot precede source createdAt.")
|
|
183
|
+
}
|
|
184
|
+
this.assertReadyState(manifest, input.rootCount, input.assertionCount)
|
|
185
|
+
const result = this.db
|
|
186
|
+
.query(
|
|
187
|
+
`
|
|
188
|
+
UPDATE ontology_sources
|
|
189
|
+
SET status = 'ready', root_count = ?, assertion_count = ?, ready_at = ?, updated_at = ?
|
|
190
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
191
|
+
AND status = 'staging' AND execution_token = ?
|
|
192
|
+
`
|
|
193
|
+
)
|
|
194
|
+
.run(
|
|
195
|
+
input.rootCount,
|
|
196
|
+
input.assertionCount,
|
|
197
|
+
input.readyAt,
|
|
198
|
+
input.readyAt,
|
|
199
|
+
input.projectId,
|
|
200
|
+
input.source.projectionId,
|
|
201
|
+
input.materializationId,
|
|
202
|
+
input.execution.executionToken
|
|
203
|
+
)
|
|
204
|
+
if (result.changes !== 1) {
|
|
205
|
+
throw sourceConflict(`Source materialization '${input.materializationId}' changed.`)
|
|
206
|
+
}
|
|
207
|
+
return sourceRecord(
|
|
208
|
+
this.requireManifest(input.projectId, input.source.projectionId, input.materializationId)
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async getActive(input: GetActiveOntologySourceInput): Promise<OntologySourceRecord | null> {
|
|
214
|
+
return this.runRootOperation(() => {
|
|
215
|
+
assertProjectAndSource(input)
|
|
216
|
+
const row = this.db
|
|
217
|
+
.query(
|
|
218
|
+
`
|
|
219
|
+
SELECT * FROM ontology_sources
|
|
220
|
+
WHERE project_id = ? AND source_id = ? AND status = 'active'
|
|
221
|
+
`
|
|
222
|
+
)
|
|
223
|
+
.get(input.projectId, input.source.projectionId) as SqliteOntologySourceRow | null
|
|
224
|
+
return row ? sourceRecord(row) : null
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async abandon(input: AbandonSourceMaterializationCandidateInput): Promise<OntologySourceRecord>
|
|
229
|
+
async abandon(input: ReclaimSourceMaterializationInput): Promise<OntologySourceRecord | null>
|
|
230
|
+
async abandon(input: AbandonSourceMaterializationInput): Promise<OntologySourceRecord | null> {
|
|
231
|
+
return this.runRootOperation(() => {
|
|
232
|
+
assertProjectAndSource(input)
|
|
233
|
+
assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken)
|
|
234
|
+
assertTimestamp(input.abandonedAt, "Source abandonedAt", true)
|
|
235
|
+
this.assertExecution(input)
|
|
236
|
+
return input.kind === "candidate"
|
|
237
|
+
? this.abandonCandidate(input)
|
|
238
|
+
: this.abandonForReclaim(input)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async cleanupTerminal(
|
|
243
|
+
input: CleanupTerminalSourceMaterializationsInput
|
|
244
|
+
): Promise<CleanupTerminalSourceMaterializationsResult> {
|
|
245
|
+
return this.runRootOperation(() => {
|
|
246
|
+
assertNonblank(input.projectId, "Terminal source cleanup project id")
|
|
247
|
+
assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true)
|
|
248
|
+
assertPositiveInteger(input.limit, "Terminal source cleanup limit")
|
|
249
|
+
const manifests = this.db
|
|
250
|
+
.query(
|
|
251
|
+
`
|
|
252
|
+
SELECT * FROM ontology_sources
|
|
253
|
+
WHERE project_id = ? AND status IN ('superseded', 'abandoned')
|
|
254
|
+
AND terminal_at < ?
|
|
255
|
+
ORDER BY terminal_at, source_id, materialization_id
|
|
256
|
+
LIMIT ?
|
|
257
|
+
`
|
|
258
|
+
)
|
|
259
|
+
.all(input.projectId, input.terminalBefore, input.limit) as SqliteOntologySourceRow[]
|
|
260
|
+
let remaining = input.limit
|
|
261
|
+
let rowsDeleted = 0
|
|
262
|
+
let materializationsDeleted = 0
|
|
263
|
+
for (const manifest of manifests) {
|
|
264
|
+
if (remaining === 0) break
|
|
265
|
+
const deleted = this.db
|
|
266
|
+
.query(
|
|
267
|
+
`
|
|
268
|
+
DELETE FROM ontology_source_rows
|
|
269
|
+
WHERE rowid IN (
|
|
270
|
+
SELECT rows.rowid
|
|
271
|
+
FROM ontology_source_rows AS rows
|
|
272
|
+
JOIN ontology_sources AS sources
|
|
273
|
+
ON sources.project_id = rows.project_id
|
|
274
|
+
AND sources.source_id = rows.source_id
|
|
275
|
+
AND sources.materialization_id = rows.materialization_id
|
|
276
|
+
WHERE rows.project_id = ? AND rows.source_id = ? AND rows.materialization_id = ?
|
|
277
|
+
AND sources.status IN ('superseded', 'abandoned')
|
|
278
|
+
AND sources.terminal_at = ? AND sources.terminal_at < ?
|
|
279
|
+
ORDER BY rows.entity_sort_key
|
|
280
|
+
LIMIT ?
|
|
281
|
+
)
|
|
282
|
+
`
|
|
283
|
+
)
|
|
284
|
+
.run(
|
|
285
|
+
manifest.project_id,
|
|
286
|
+
manifest.source_id,
|
|
287
|
+
manifest.materialization_id,
|
|
288
|
+
manifest.terminal_at,
|
|
289
|
+
input.terminalBefore,
|
|
290
|
+
remaining
|
|
291
|
+
).changes
|
|
292
|
+
rowsDeleted += deleted
|
|
293
|
+
remaining -= deleted
|
|
294
|
+
if (remaining === 0) break
|
|
295
|
+
const removed = this.db
|
|
296
|
+
.query(
|
|
297
|
+
`
|
|
298
|
+
DELETE FROM ontology_sources
|
|
299
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
300
|
+
AND status IN ('superseded', 'abandoned')
|
|
301
|
+
AND terminal_at = ? AND terminal_at < ?
|
|
302
|
+
AND NOT EXISTS (
|
|
303
|
+
SELECT 1 FROM ontology_source_rows AS rows
|
|
304
|
+
WHERE rows.project_id = ontology_sources.project_id
|
|
305
|
+
AND rows.source_id = ontology_sources.source_id
|
|
306
|
+
AND rows.materialization_id = ontology_sources.materialization_id
|
|
307
|
+
)
|
|
308
|
+
`
|
|
309
|
+
)
|
|
310
|
+
.run(
|
|
311
|
+
manifest.project_id,
|
|
312
|
+
manifest.source_id,
|
|
313
|
+
manifest.materialization_id,
|
|
314
|
+
manifest.terminal_at,
|
|
315
|
+
input.terminalBefore
|
|
316
|
+
).changes
|
|
317
|
+
materializationsDeleted += removed
|
|
318
|
+
remaining -= removed
|
|
319
|
+
}
|
|
320
|
+
return { rowsDeleted, materializationsDeleted }
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async summarizeTerminal(
|
|
325
|
+
input: SummarizeTerminalSourceMaterializationsInput
|
|
326
|
+
): Promise<TerminalSourceMaterializationSummary> {
|
|
327
|
+
return this.runRootOperation(() => {
|
|
328
|
+
assertNonblank(input.projectId, "Terminal source summary project id")
|
|
329
|
+
const summary = this.db
|
|
330
|
+
.query(
|
|
331
|
+
`
|
|
332
|
+
SELECT COUNT(*) AS terminal_count, MIN(terminal_at) AS oldest_terminal_at
|
|
333
|
+
FROM ontology_sources
|
|
334
|
+
WHERE project_id = ? AND status IN ('superseded', 'abandoned')
|
|
335
|
+
`
|
|
336
|
+
)
|
|
337
|
+
.get(input.projectId) as {
|
|
338
|
+
readonly terminal_count: number
|
|
339
|
+
readonly oldest_terminal_at: string | null
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
count: summary.terminal_count,
|
|
343
|
+
oldestTerminalAt: summary.oldest_terminal_at,
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private abandonCandidate(
|
|
349
|
+
input: AbandonSourceMaterializationCandidateInput
|
|
350
|
+
): OntologySourceRecord {
|
|
351
|
+
const manifest = this.requireManifest(
|
|
352
|
+
input.projectId,
|
|
353
|
+
input.source.projectionId,
|
|
354
|
+
input.materializationId
|
|
355
|
+
)
|
|
356
|
+
if (manifest.status === "abandoned") {
|
|
357
|
+
if (
|
|
358
|
+
manifest.projection_run_id === input.execution.projectionRunId &&
|
|
359
|
+
manifest.terminal_at === input.abandonedAt
|
|
360
|
+
) {
|
|
361
|
+
return sourceRecord(manifest)
|
|
362
|
+
}
|
|
363
|
+
throw sourceConflict(
|
|
364
|
+
`Source materialization '${input.materializationId}' was abandoned by another execution or at another time.`
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
assertSourceCandidateOwner(sourceRecord(manifest), input.execution)
|
|
368
|
+
return this.transitionToAbandoned(manifest, input.abandonedAt)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private abandonForReclaim(input: ReclaimSourceMaterializationInput): OntologySourceRecord | null {
|
|
372
|
+
const candidates = this.db
|
|
373
|
+
.query(
|
|
374
|
+
`
|
|
375
|
+
SELECT * FROM ontology_sources
|
|
376
|
+
WHERE project_id = ? AND source_id = ? AND projection_run_id = ?
|
|
377
|
+
AND status IN ('staging', 'ready')
|
|
378
|
+
`
|
|
379
|
+
)
|
|
380
|
+
.all(
|
|
381
|
+
input.projectId,
|
|
382
|
+
input.source.projectionId,
|
|
383
|
+
input.execution.projectionRunId
|
|
384
|
+
) as SqliteOntologySourceRow[]
|
|
385
|
+
if (
|
|
386
|
+
candidates.some((candidate) => candidate.execution_token === input.execution.executionToken)
|
|
387
|
+
) {
|
|
388
|
+
throw sourceConflict(
|
|
389
|
+
`Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
if (candidates.length > 1) {
|
|
393
|
+
throw sourceConflict(
|
|
394
|
+
`Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
return candidates[0] ? this.transitionToAbandoned(candidates[0], input.abandonedAt) : null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private transitionToAbandoned(
|
|
401
|
+
manifest: SqliteOntologySourceRow,
|
|
402
|
+
abandonedAt: string
|
|
403
|
+
): OntologySourceRecord {
|
|
404
|
+
if (
|
|
405
|
+
abandonedAt < manifest.created_at ||
|
|
406
|
+
(manifest.ready_at !== null && abandonedAt < manifest.ready_at)
|
|
407
|
+
) {
|
|
408
|
+
throw new MaterializationValidationError(
|
|
409
|
+
"Source abandonedAt cannot precede source creation or readiness."
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
const changed = this.db
|
|
413
|
+
.query(
|
|
414
|
+
`
|
|
415
|
+
UPDATE ontology_sources
|
|
416
|
+
SET status = 'abandoned', execution_token = NULL, terminal_at = ?, updated_at = ?
|
|
417
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
418
|
+
AND status IN ('staging', 'ready')
|
|
419
|
+
`
|
|
420
|
+
)
|
|
421
|
+
.run(
|
|
422
|
+
abandonedAt,
|
|
423
|
+
abandonedAt,
|
|
424
|
+
manifest.project_id,
|
|
425
|
+
manifest.source_id,
|
|
426
|
+
manifest.materialization_id
|
|
427
|
+
).changes
|
|
428
|
+
if (changed !== 1) {
|
|
429
|
+
throw sourceConflict(
|
|
430
|
+
`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
return sourceRecord(
|
|
434
|
+
this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id)
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private assertReadyState(
|
|
439
|
+
manifest: SqliteOntologySourceRow,
|
|
440
|
+
rootCount: number,
|
|
441
|
+
assertionCount: number
|
|
442
|
+
): void {
|
|
443
|
+
const counts = this.db
|
|
444
|
+
.query(
|
|
445
|
+
`
|
|
446
|
+
SELECT COUNT(*) AS assertions, COUNT(DISTINCT root_key) AS roots,
|
|
447
|
+
COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
448
|
+
MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
|
|
449
|
+
FROM ontology_source_rows
|
|
450
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
451
|
+
`
|
|
452
|
+
)
|
|
453
|
+
.get(manifest.project_id, manifest.source_id, manifest.materialization_id) as {
|
|
454
|
+
assertions: number
|
|
455
|
+
roots: number
|
|
456
|
+
ordinals: number
|
|
457
|
+
min_ordinal: number | null
|
|
458
|
+
max_ordinal: number | null
|
|
459
|
+
}
|
|
460
|
+
if (
|
|
461
|
+
counts.assertions !== assertionCount ||
|
|
462
|
+
counts.roots !== rootCount ||
|
|
463
|
+
counts.ordinals !== rootCount ||
|
|
464
|
+
(rootCount > 0 && (counts.min_ordinal !== 0 || counts.max_ordinal !== rootCount - 1))
|
|
465
|
+
) {
|
|
466
|
+
throw new MaterializationValidationError(
|
|
467
|
+
"Source ready counts do not match the staged roots and assertions."
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
const invalid = this.db
|
|
471
|
+
.query(
|
|
472
|
+
manifest.projection_kind === "link"
|
|
473
|
+
? `
|
|
474
|
+
SELECT root_key
|
|
475
|
+
FROM ontology_source_rows
|
|
476
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
477
|
+
GROUP BY root_key
|
|
478
|
+
HAVING COUNT(*) <> 1 OR MIN(root_kind) <> 'link' OR MIN(entity_kind) <> 'link'
|
|
479
|
+
OR MIN(root_key) <> MIN(entity_key)
|
|
480
|
+
LIMIT 1
|
|
481
|
+
`
|
|
482
|
+
: `
|
|
483
|
+
SELECT root_key
|
|
484
|
+
FROM ontology_source_rows
|
|
485
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
486
|
+
GROUP BY root_key
|
|
487
|
+
HAVING MIN(root_kind) <> 'object'
|
|
488
|
+
OR SUM(CASE WHEN entity_kind = 'object' AND entity_key = root_key THEN 1 ELSE 0 END) <> 1
|
|
489
|
+
OR SUM(CASE
|
|
490
|
+
WHEN entity_kind = 'object' AND entity_key <> root_key THEN 1
|
|
491
|
+
WHEN entity_kind = 'link' AND (
|
|
492
|
+
source_type_id <> root_object_type_id OR source_primary_id <> root_primary_id
|
|
493
|
+
) THEN 1
|
|
494
|
+
ELSE 0
|
|
495
|
+
END) <> 0
|
|
496
|
+
LIMIT 1
|
|
497
|
+
`
|
|
498
|
+
)
|
|
499
|
+
.get(manifest.project_id, manifest.source_id, manifest.materialization_id)
|
|
500
|
+
if (invalid) {
|
|
501
|
+
throw new MaterializationValidationError(
|
|
502
|
+
manifest.projection_kind === "link"
|
|
503
|
+
? "Link projection roots must contain exactly their matching link assertion."
|
|
504
|
+
: "Object projection roots must contain exactly their matching object assertion plus links sourced from that root."
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private findStageRows(
|
|
510
|
+
input: StageSourceRowsInput,
|
|
511
|
+
rows: readonly SourceStageRow[]
|
|
512
|
+
): readonly SourceStageRow[] {
|
|
513
|
+
if (rows.length === 0) return []
|
|
514
|
+
const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey(row.rootKey)))]
|
|
515
|
+
const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))]
|
|
516
|
+
const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey(row.entityKey)))]
|
|
517
|
+
const existing = this.db
|
|
518
|
+
.query(
|
|
519
|
+
`
|
|
520
|
+
SELECT * FROM ontology_source_rows
|
|
521
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
522
|
+
AND (
|
|
523
|
+
root_sort_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))
|
|
524
|
+
OR staging_ordinal IN (SELECT CAST(value AS INTEGER) FROM json_each(?))
|
|
525
|
+
OR entity_sort_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))
|
|
526
|
+
)
|
|
527
|
+
`
|
|
528
|
+
)
|
|
529
|
+
.all(
|
|
530
|
+
input.projectId,
|
|
531
|
+
input.source.projectionId,
|
|
532
|
+
input.materializationId,
|
|
533
|
+
JSON.stringify(rootSortKeys),
|
|
534
|
+
JSON.stringify(ordinals),
|
|
535
|
+
JSON.stringify(entitySortKeys)
|
|
536
|
+
) as SqliteOntologySourceAssertionRow[]
|
|
537
|
+
return existing.map((row) => sourceStageRow(sourceAssertion(row)))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private insertStageRows(input: StageSourceRowsInput, rows: readonly SourceStageRow[]): void {
|
|
541
|
+
if (rows.length === 0) return
|
|
542
|
+
const payload = rows.map(({ entityKey, rootKey, row }) => {
|
|
543
|
+
const entity = entityColumns(row.assertion)
|
|
544
|
+
const root = entityColumns(row.root)
|
|
545
|
+
return {
|
|
546
|
+
entityKind: row.assertion.kind,
|
|
547
|
+
entityKey,
|
|
548
|
+
entitySortKey: utf8SortKey(entityKey),
|
|
549
|
+
rootKind: row.root.kind,
|
|
550
|
+
rootKey,
|
|
551
|
+
rootSortKey: utf8SortKey(rootKey),
|
|
552
|
+
stagingOrdinal: row.stagingOrdinal,
|
|
553
|
+
root: row.root,
|
|
554
|
+
assertion: row.assertion,
|
|
555
|
+
objectTypeId: entity.objectTypeId,
|
|
556
|
+
primaryId: entity.primaryId,
|
|
557
|
+
sourceTypeId: entity.sourceTypeId,
|
|
558
|
+
sourcePrimaryId: entity.sourcePrimaryId,
|
|
559
|
+
linkId: entity.linkId,
|
|
560
|
+
targetTypeId: entity.targetTypeId,
|
|
561
|
+
targetPrimaryId: entity.targetPrimaryId,
|
|
562
|
+
rootObjectTypeId: root.objectTypeId,
|
|
563
|
+
rootPrimaryId: root.primaryId,
|
|
564
|
+
rootSourceTypeId: root.sourceTypeId,
|
|
565
|
+
rootSourcePrimaryId: root.sourcePrimaryId,
|
|
566
|
+
rootLinkId: root.linkId,
|
|
567
|
+
rootTargetTypeId: root.targetTypeId,
|
|
568
|
+
rootTargetPrimaryId: root.targetPrimaryId,
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
this.db
|
|
572
|
+
.query(
|
|
573
|
+
`
|
|
574
|
+
WITH staged(value) AS (SELECT value FROM json_each(?))
|
|
575
|
+
INSERT INTO ontology_source_rows (
|
|
576
|
+
project_id, source_id, materialization_id,
|
|
577
|
+
entity_kind, entity_key, entity_sort_key,
|
|
578
|
+
root_kind, root_key, root_sort_key, staging_ordinal,
|
|
579
|
+
root, assertion,
|
|
580
|
+
object_type_id, primary_id,
|
|
581
|
+
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id,
|
|
582
|
+
root_object_type_id, root_primary_id,
|
|
583
|
+
root_source_type_id, root_source_primary_id, root_link_id,
|
|
584
|
+
root_target_type_id, root_target_primary_id
|
|
585
|
+
)
|
|
586
|
+
SELECT
|
|
587
|
+
?, ?, ?,
|
|
588
|
+
json_extract(value, '$.entityKind'), json_extract(value, '$.entityKey'),
|
|
589
|
+
json_extract(value, '$.entitySortKey'),
|
|
590
|
+
json_extract(value, '$.rootKind'), json_extract(value, '$.rootKey'),
|
|
591
|
+
json_extract(value, '$.rootSortKey'), json_extract(value, '$.stagingOrdinal'),
|
|
592
|
+
json(json_extract(value, '$.root')), json(json_extract(value, '$.assertion')),
|
|
593
|
+
json_extract(value, '$.objectTypeId'), json_extract(value, '$.primaryId'),
|
|
594
|
+
json_extract(value, '$.sourceTypeId'), json_extract(value, '$.sourcePrimaryId'),
|
|
595
|
+
json_extract(value, '$.linkId'), json_extract(value, '$.targetTypeId'),
|
|
596
|
+
json_extract(value, '$.targetPrimaryId'),
|
|
597
|
+
json_extract(value, '$.rootObjectTypeId'), json_extract(value, '$.rootPrimaryId'),
|
|
598
|
+
json_extract(value, '$.rootSourceTypeId'),
|
|
599
|
+
json_extract(value, '$.rootSourcePrimaryId'), json_extract(value, '$.rootLinkId'),
|
|
600
|
+
json_extract(value, '$.rootTargetTypeId'), json_extract(value, '$.rootTargetPrimaryId')
|
|
601
|
+
FROM staged
|
|
602
|
+
`
|
|
603
|
+
)
|
|
604
|
+
.run(
|
|
605
|
+
canonicalJson(payload),
|
|
606
|
+
input.projectId,
|
|
607
|
+
input.source.projectionId,
|
|
608
|
+
input.materializationId
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private assertExecution(
|
|
613
|
+
input: {
|
|
614
|
+
readonly projectId: string
|
|
615
|
+
readonly source: { readonly projectionId: string }
|
|
616
|
+
readonly execution: { readonly projectionRunId: string; readonly executionToken: string }
|
|
617
|
+
},
|
|
618
|
+
identity?: AssertSourceMaterializationExecutionInput["identity"]
|
|
619
|
+
): void {
|
|
620
|
+
assertProjectionExecution(this.db, {
|
|
621
|
+
projectId: input.projectId,
|
|
622
|
+
sourceId: input.source.projectionId,
|
|
623
|
+
projectionRunId: input.execution.projectionRunId,
|
|
624
|
+
executionToken: input.execution.executionToken,
|
|
625
|
+
...(identity ? { identity } : {}),
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private getManifest(
|
|
630
|
+
projectId: string,
|
|
631
|
+
sourceId: string,
|
|
632
|
+
materializationId: string
|
|
633
|
+
): SqliteOntologySourceRow | null {
|
|
634
|
+
return this.db
|
|
635
|
+
.query(
|
|
636
|
+
`
|
|
637
|
+
SELECT * FROM ontology_sources
|
|
638
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
639
|
+
`
|
|
640
|
+
)
|
|
641
|
+
.get(projectId, sourceId, materializationId) as SqliteOntologySourceRow | null
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private requireManifest(
|
|
645
|
+
projectId: string,
|
|
646
|
+
sourceId: string,
|
|
647
|
+
materializationId: string
|
|
648
|
+
): SqliteOntologySourceRow {
|
|
649
|
+
const row = this.getManifest(projectId, sourceId, materializationId)
|
|
650
|
+
if (!row) throw sourceConflict(`Source materialization '${materializationId}' does not exist.`)
|
|
651
|
+
return row
|
|
652
|
+
}
|
|
653
|
+
}
|