@sixb/sqlite 0.1.9 → 0.1.11
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/dist/index.js +449 -195
- package/dist/index.js.map +13 -11
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +2 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- 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 +2 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts +4 -0
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
- package/dist/ontology-storage/source-roots.d.ts +27 -0
- package/dist/ontology-storage/source-roots.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/projection-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/connector-connection-storage.ts +13 -3
- package/src/migrations/038-projection-source-roots.sql +44 -0
- package/src/migrations/039-connector-optional-pkce.sql +52 -0
- package/src/migrations/040-outbox-publication-order.sql +8 -0
- package/src/migrations.ts +13 -0
- package/src/ontology-storage/materialization-state.ts +56 -91
- package/src/ontology-storage/materializations.ts +7 -1
- package/src/ontology-storage/outbox.ts +33 -16
- package/src/ontology-storage/shared.ts +10 -0
- package/src/ontology-storage/source-cleanup.ts +59 -0
- package/src/ontology-storage/source-roots.ts +230 -0
- package/src/ontology-storage/sources.ts +25 -78
- package/src/projection-run-storage.ts +11 -3
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
type SqliteOntologyCommitRow,
|
|
66
66
|
type SqliteOntologySourceRow,
|
|
67
67
|
} from "./shared"
|
|
68
|
+
import { activateSourceRoots } from "./source-roots"
|
|
68
69
|
|
|
69
70
|
export class SqliteOntologyMaterializationStorage implements OntologyMaterializationStorage {
|
|
70
71
|
private readonly sessions: SqliteMaterializationSessions
|
|
@@ -192,6 +193,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
192
193
|
sourceId: replacement.sourceId,
|
|
193
194
|
candidateMaterializationId: replacement.candidateMaterializationId,
|
|
194
195
|
previousMaterializationId: replacement.previousMaterializationId,
|
|
196
|
+
incremental: replacement.incremental,
|
|
195
197
|
kind: "object",
|
|
196
198
|
pageRows: input.pageRows,
|
|
197
199
|
})) {
|
|
@@ -236,6 +238,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
236
238
|
sourceId: replacement.sourceId,
|
|
237
239
|
candidateMaterializationId: replacement.candidateMaterializationId,
|
|
238
240
|
previousMaterializationId: replacement.previousMaterializationId,
|
|
241
|
+
incremental: replacement.incremental,
|
|
239
242
|
kind: "link",
|
|
240
243
|
pageRows: input.pageRows,
|
|
241
244
|
})) {
|
|
@@ -250,7 +253,8 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
250
253
|
replacement.sourceId,
|
|
251
254
|
replacement.candidateMaterializationId,
|
|
252
255
|
materializationIds,
|
|
253
|
-
linkIdentities
|
|
256
|
+
linkIdentities,
|
|
257
|
+
replacement.incremental
|
|
254
258
|
)
|
|
255
259
|
yield { objects: [], links }
|
|
256
260
|
}
|
|
@@ -359,6 +363,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
359
363
|
sourceId: input.source.projectionId,
|
|
360
364
|
candidateMaterializationId: input.candidateMaterializationId,
|
|
361
365
|
previousMaterializationId: previous?.materialization_id ?? null,
|
|
366
|
+
incremental: candidate.base_materialization_id !== null,
|
|
362
367
|
projectionKind: candidate.projection_kind,
|
|
363
368
|
objectStreamStarted: false,
|
|
364
369
|
objectStreamCompleted: false,
|
|
@@ -627,6 +632,7 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
627
632
|
invalidCorrelation("Source activation does not match its ready candidate identity.")
|
|
628
633
|
}
|
|
629
634
|
this.assertSource(activation.expected, commit.projectId)
|
|
635
|
+
activateSourceRoots(this.db, commit.projectId, candidate, activation)
|
|
630
636
|
const previous = this.getActiveSource(commit.projectId, activation.source.projectionId)
|
|
631
637
|
if (previous) {
|
|
632
638
|
assertPinnedDatasetWatermark(
|
|
@@ -43,19 +43,37 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
|
43
43
|
)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Probe at most one page plus a sentinel through the eligibility index. Sparse retries
|
|
47
|
+
// avoid scanning delayed rows; overflowing backlogs use publication order without a sort.
|
|
48
|
+
const eligible = this.db
|
|
49
|
+
.query(
|
|
50
|
+
`SELECT CAST(rowid AS TEXT) AS rowId
|
|
51
|
+
FROM ontology_outbox INDEXED BY idx_ontology_outbox_claim
|
|
52
|
+
WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
|
|
53
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
|
|
54
|
+
LIMIT ?`
|
|
55
|
+
)
|
|
56
|
+
.all(input.projectId, input.now, input.now, input.limit + 1) as { rowId: string }[]
|
|
57
|
+
if (eligible.length === 0) return []
|
|
58
|
+
const selected =
|
|
59
|
+
eligible.length <= input.limit
|
|
60
|
+
? eligible
|
|
61
|
+
: (this.db
|
|
62
|
+
.query(
|
|
63
|
+
`SELECT CAST(rowid AS TEXT) AS rowId
|
|
64
|
+
FROM ontology_outbox INDEXED BY idx_ontology_outbox_publication_order
|
|
65
|
+
WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
|
|
66
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
|
|
67
|
+
ORDER BY created_at, commit_id, commit_ordinal
|
|
68
|
+
LIMIT ?`
|
|
69
|
+
)
|
|
70
|
+
.all(input.projectId, input.now, input.now, input.limit) as { rowId: string }[])
|
|
46
71
|
const rows = this.db
|
|
47
72
|
.query(
|
|
48
73
|
`
|
|
49
74
|
UPDATE ontology_outbox
|
|
50
75
|
SET attempts = attempts + 1, lease_id = ?, lease_expires_at = ?
|
|
51
|
-
WHERE rowid IN (
|
|
52
|
-
SELECT rowid
|
|
53
|
-
FROM ontology_outbox
|
|
54
|
-
WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
|
|
55
|
-
AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
|
|
56
|
-
ORDER BY created_at, commit_id, commit_ordinal
|
|
57
|
-
LIMIT ?
|
|
58
|
-
)
|
|
76
|
+
WHERE rowid IN (SELECT CAST(value AS INTEGER) FROM json_each(?))
|
|
59
77
|
RETURNING envelope, available_at, attempts, lease_id, lease_expires_at,
|
|
60
78
|
published_at, last_failure, created_at
|
|
61
79
|
`
|
|
@@ -63,10 +81,7 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
|
63
81
|
.all(
|
|
64
82
|
input.leaseId,
|
|
65
83
|
input.leaseExpiresAt,
|
|
66
|
-
|
|
67
|
-
input.now,
|
|
68
|
-
input.now,
|
|
69
|
-
input.limit
|
|
84
|
+
JSON.stringify(selected.map((row) => row.rowId))
|
|
70
85
|
) as SqliteOntologyOutboxRow[]
|
|
71
86
|
return rows
|
|
72
87
|
.map((row) => outboxRecord(row))
|
|
@@ -98,8 +113,9 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
|
98
113
|
WITH requested(id) AS MATERIALIZED (
|
|
99
114
|
SELECT CAST(value AS TEXT) FROM json_each(?)
|
|
100
115
|
), leased(id) AS MATERIALIZED (
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
-- Drive validation from the bounded ID list even before ANALYZE.
|
|
117
|
+
SELECT outbox.id FROM requested
|
|
118
|
+
CROSS JOIN ontology_outbox AS outbox USING (id)
|
|
103
119
|
WHERE outbox.project_id = ? AND outbox.lease_id = ?
|
|
104
120
|
AND outbox.lease_expires_at IS NOT NULL
|
|
105
121
|
), eligible(id) AS (
|
|
@@ -139,8 +155,9 @@ export class SqliteOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
|
139
155
|
WITH requested(id) AS MATERIALIZED (
|
|
140
156
|
SELECT CAST(value AS TEXT) FROM json_each(?)
|
|
141
157
|
), leased(id) AS MATERIALIZED (
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
-- Drive validation from the bounded ID list even before ANALYZE.
|
|
159
|
+
SELECT outbox.id FROM requested
|
|
160
|
+
CROSS JOIN ontology_outbox AS outbox USING (id)
|
|
144
161
|
WHERE outbox.project_id = ? AND outbox.lease_id = ?
|
|
145
162
|
AND outbox.lease_expires_at IS NOT NULL
|
|
146
163
|
), eligible(id) AS (
|
|
@@ -62,6 +62,8 @@ export interface SqliteOntologySourceRow {
|
|
|
62
62
|
readonly ownership_hash: string
|
|
63
63
|
readonly ontology_revision: string
|
|
64
64
|
readonly root_count: number | null
|
|
65
|
+
readonly base_materialization_id: string | null
|
|
66
|
+
readonly base_commit_id: string | null
|
|
65
67
|
readonly assertion_count: number | null
|
|
66
68
|
readonly created_at: string
|
|
67
69
|
readonly ready_at: string | null
|
|
@@ -125,6 +127,14 @@ export function sourceRecord(row: SqliteOntologySourceRow): OntologySourceRecord
|
|
|
125
127
|
projectionRevision: row.projection_revision,
|
|
126
128
|
ownershipHash: row.ownership_hash,
|
|
127
129
|
ontologyRevision: row.ontology_revision,
|
|
130
|
+
...(row.base_materialization_id === null
|
|
131
|
+
? {}
|
|
132
|
+
: {
|
|
133
|
+
base: {
|
|
134
|
+
materializationId: row.base_materialization_id,
|
|
135
|
+
lastCommitId: row.base_commit_id!,
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
128
138
|
rootCount: row.root_count,
|
|
129
139
|
assertionCount: row.assertion_count,
|
|
130
140
|
createdAt: row.created_at,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
CleanupTerminalSourceMaterializationsInput,
|
|
4
|
+
CleanupTerminalSourceMaterializationsResult,
|
|
5
|
+
} from "@sixb/core/storage"
|
|
6
|
+
|
|
7
|
+
export function cleanupSourceVersions(
|
|
8
|
+
db: Database,
|
|
9
|
+
input: CleanupTerminalSourceMaterializationsInput
|
|
10
|
+
): CleanupTerminalSourceMaterializationsResult {
|
|
11
|
+
let remaining = input.limit
|
|
12
|
+
let rowsDeleted = db
|
|
13
|
+
.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
|
|
14
|
+
SELECT rows.rowid FROM ontology_source_roots AS roots
|
|
15
|
+
JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
|
|
16
|
+
CROSS JOIN ontology_source_rows AS rows
|
|
17
|
+
ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
18
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
19
|
+
WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
|
|
20
|
+
AND sources.status IN ('superseded', 'abandoned')
|
|
21
|
+
ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
|
|
22
|
+
LIMIT ?
|
|
23
|
+
)`)
|
|
24
|
+
.run(input.projectId, input.terminalBefore, remaining).changes
|
|
25
|
+
remaining -= rowsDeleted
|
|
26
|
+
if (remaining > 0) {
|
|
27
|
+
const removed = db
|
|
28
|
+
.query(`DELETE FROM ontology_source_roots WHERE rowid IN (
|
|
29
|
+
SELECT roots.rowid FROM ontology_source_roots AS roots
|
|
30
|
+
JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
|
|
31
|
+
WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
|
|
32
|
+
AND sources.status IN ('superseded', 'abandoned')
|
|
33
|
+
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
|
|
34
|
+
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
35
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
|
|
36
|
+
ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key LIMIT ?
|
|
37
|
+
)`)
|
|
38
|
+
.run(input.projectId, input.terminalBefore, remaining).changes
|
|
39
|
+
rowsDeleted += removed
|
|
40
|
+
remaining -= removed
|
|
41
|
+
}
|
|
42
|
+
const materializationsDeleted =
|
|
43
|
+
remaining === 0
|
|
44
|
+
? 0
|
|
45
|
+
: db
|
|
46
|
+
.query(`DELETE FROM ontology_sources WHERE rowid IN (
|
|
47
|
+
SELECT sources.rowid FROM ontology_sources AS sources
|
|
48
|
+
WHERE project_id = ? AND status IN ('superseded', 'abandoned') AND terminal_at < ?
|
|
49
|
+
AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
|
|
50
|
+
WHERE roots.project_id = sources.project_id AND roots.source_id = sources.source_id
|
|
51
|
+
AND roots.materialization_id = sources.materialization_id)
|
|
52
|
+
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
|
|
53
|
+
WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
|
|
54
|
+
AND rows.materialization_id = sources.materialization_id)
|
|
55
|
+
ORDER BY terminal_at, source_id, materialization_id LIMIT ?
|
|
56
|
+
)`)
|
|
57
|
+
.run(input.projectId, input.terminalBefore, remaining).changes
|
|
58
|
+
return { rowsDeleted, materializationsDeleted }
|
|
59
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
|
|
3
|
+
import {
|
|
4
|
+
MaterializationConflictError,
|
|
5
|
+
MaterializationValidationError,
|
|
6
|
+
projectionEntityKey,
|
|
7
|
+
} from "@sixb/core/internal/materialization"
|
|
8
|
+
import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider"
|
|
9
|
+
import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
|
|
10
|
+
import type { SqliteOntologySourceAssertionRow } from "./shared"
|
|
11
|
+
import { canonicalJson, type SqliteOntologySourceRow } from "./shared"
|
|
12
|
+
|
|
13
|
+
export function replacementSourceRows(input: {
|
|
14
|
+
readonly projectId: string
|
|
15
|
+
readonly sourceId: string
|
|
16
|
+
readonly materializationId: string
|
|
17
|
+
readonly incremental: boolean
|
|
18
|
+
}): { readonly sql: string; readonly values: readonly string[] } {
|
|
19
|
+
const roots = input.incremental
|
|
20
|
+
? `ontology_source_roots AS changed
|
|
21
|
+
CROSS JOIN ontology_source_roots AS roots ON roots.project_id = changed.project_id
|
|
22
|
+
AND roots.source_id = changed.source_id AND roots.root_sort_key = changed.root_sort_key AND roots.active = 1`
|
|
23
|
+
: "ontology_source_roots AS roots"
|
|
24
|
+
const selected = input.incremental ? "changed" : "roots"
|
|
25
|
+
return {
|
|
26
|
+
sql: `SELECT * FROM ontology_source_rows WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
27
|
+
UNION ALL
|
|
28
|
+
SELECT rows.* FROM ${roots}
|
|
29
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id
|
|
30
|
+
AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
|
|
31
|
+
AND rows.root_sort_key = roots.root_sort_key
|
|
32
|
+
WHERE ${selected}.project_id = ? AND ${selected}.source_id = ?
|
|
33
|
+
AND ${input.incremental ? "changed.materialization_id = ?" : "roots.active = 1"}`,
|
|
34
|
+
values: [
|
|
35
|
+
input.projectId,
|
|
36
|
+
input.sourceId,
|
|
37
|
+
input.materializationId,
|
|
38
|
+
input.projectId,
|
|
39
|
+
input.sourceId,
|
|
40
|
+
...(input.incremental ? [input.materializationId] : []),
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stageSourceRoots(
|
|
46
|
+
db: Database,
|
|
47
|
+
manifest: SqliteOntologySourceRow,
|
|
48
|
+
input: StageSourceRowsInput
|
|
49
|
+
): void {
|
|
50
|
+
const roots = sourceStageRoots(manifest.projection_kind, input)
|
|
51
|
+
if (!manifest.base_materialization_id && roots.some((root) => root.deleted)) {
|
|
52
|
+
throw new MaterializationValidationError("Root deletions require a source delta base.")
|
|
53
|
+
}
|
|
54
|
+
const existing = db.query(`SELECT staging_ordinal, deleted FROM ontology_source_roots
|
|
55
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND root_sort_key = ?`)
|
|
56
|
+
const insert = db.query(`INSERT INTO ontology_source_roots (
|
|
57
|
+
project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
|
|
58
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING root_sort_key`)
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
const inserted = insert.get(
|
|
61
|
+
input.projectId,
|
|
62
|
+
input.source.projectionId,
|
|
63
|
+
input.materializationId,
|
|
64
|
+
root.sortKey,
|
|
65
|
+
root.root.kind,
|
|
66
|
+
root.rootKey,
|
|
67
|
+
canonicalJson(root.root),
|
|
68
|
+
root.stagingOrdinal,
|
|
69
|
+
Number(root.deleted)
|
|
70
|
+
)
|
|
71
|
+
if (inserted) continue
|
|
72
|
+
const previous = existing.get(
|
|
73
|
+
input.projectId,
|
|
74
|
+
input.source.projectionId,
|
|
75
|
+
input.materializationId,
|
|
76
|
+
root.sortKey
|
|
77
|
+
) as { staging_ordinal: number; deleted: number } | null
|
|
78
|
+
if (
|
|
79
|
+
!previous ||
|
|
80
|
+
previous.staging_ordinal !== root.stagingOrdinal ||
|
|
81
|
+
Boolean(previous.deleted) !== root.deleted
|
|
82
|
+
) {
|
|
83
|
+
throw new MaterializationValidationError(
|
|
84
|
+
"Source materialization repeats root or stream ordinal with different content."
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function activateSourceRoots(
|
|
91
|
+
db: Database,
|
|
92
|
+
projectId: string,
|
|
93
|
+
candidate: SqliteOntologySourceRow,
|
|
94
|
+
activation: SourceActivationWrite
|
|
95
|
+
): void {
|
|
96
|
+
if (
|
|
97
|
+
candidate.base_materialization_id !== null &&
|
|
98
|
+
(candidate.base_materialization_id !== activation.expected.activeMaterializationId ||
|
|
99
|
+
candidate.base_commit_id !== activation.expected.lastCommitId)
|
|
100
|
+
)
|
|
101
|
+
throw new MaterializationConflictError(
|
|
102
|
+
"projection-fence",
|
|
103
|
+
"Source delta base changed before activation."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if (candidate.base_materialization_id !== null && candidate.root_count === 0) return
|
|
107
|
+
const scope = [projectId, activation.source.projectionId, activation.materializationId] as const
|
|
108
|
+
db.query(`UPDATE ontology_source_roots AS roots SET active = 0, retired_at = ?
|
|
109
|
+
WHERE project_id = ? AND source_id = ? AND active = 1
|
|
110
|
+
${
|
|
111
|
+
candidate.base_materialization_id === null
|
|
112
|
+
? ""
|
|
113
|
+
: `AND root_sort_key IN (
|
|
114
|
+
SELECT root_sort_key FROM ontology_source_roots
|
|
115
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?)`
|
|
116
|
+
}`).run(
|
|
117
|
+
activation.updatedAt,
|
|
118
|
+
projectId,
|
|
119
|
+
activation.source.projectionId,
|
|
120
|
+
...(candidate.base_materialization_id === null ? [] : [...scope])
|
|
121
|
+
)
|
|
122
|
+
db.query(`UPDATE ontology_source_roots SET active = CASE WHEN deleted = 1 THEN 0 ELSE 1 END,
|
|
123
|
+
retired_at = CASE WHEN deleted = 1 THEN ? ELSE NULL END
|
|
124
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?`).run(
|
|
125
|
+
activation.updatedAt,
|
|
126
|
+
...scope
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function assertSourceRootCoverage(db: Database, manifest: SqliteOntologySourceRow): void {
|
|
131
|
+
const invalid = db
|
|
132
|
+
.query(`SELECT 1 FROM ontology_source_roots AS roots
|
|
133
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
134
|
+
AND roots.deleted = EXISTS (
|
|
135
|
+
SELECT 1 FROM ontology_source_rows AS rows
|
|
136
|
+
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
137
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
138
|
+
) LIMIT 1`)
|
|
139
|
+
.get(manifest.project_id, manifest.source_id, manifest.materialization_id)
|
|
140
|
+
if (invalid)
|
|
141
|
+
throw new MaterializationValidationError(
|
|
142
|
+
"Source roots must have complete assertions or an explicit deletion, never both."
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function activeSourceRows(
|
|
147
|
+
db: Database,
|
|
148
|
+
projectId: string,
|
|
149
|
+
refs: readonly ProjectionEntityRef[]
|
|
150
|
+
): SqliteOntologySourceAssertionRow[] {
|
|
151
|
+
const keys = refs.flatMap((entity) => {
|
|
152
|
+
const entitySortKey = utf8SortKey(projectionEntityKey(entity))
|
|
153
|
+
const roots =
|
|
154
|
+
entity.kind === "object"
|
|
155
|
+
? [entitySortKey]
|
|
156
|
+
: [
|
|
157
|
+
entitySortKey,
|
|
158
|
+
utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source })),
|
|
159
|
+
]
|
|
160
|
+
return roots.map((rootSortKey) => ({ rootSortKey, entitySortKey }))
|
|
161
|
+
})
|
|
162
|
+
return db
|
|
163
|
+
.query(`WITH requested AS (
|
|
164
|
+
SELECT DISTINCT json_extract(value, '$.rootSortKey') AS root_sort_key,
|
|
165
|
+
json_extract(value, '$.entitySortKey') AS entity_sort_key FROM json_each(?)
|
|
166
|
+
) SELECT rows.* FROM requested
|
|
167
|
+
CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
|
|
168
|
+
AND roots.active = 1 AND roots.root_sort_key = requested.root_sort_key
|
|
169
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
170
|
+
AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
|
|
171
|
+
AND rows.root_sort_key = roots.root_sort_key`)
|
|
172
|
+
.all(canonicalJson(keys), projectId) as SqliteOntologySourceAssertionRow[]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function replacementAssertionRows(
|
|
176
|
+
db: Database,
|
|
177
|
+
input: {
|
|
178
|
+
projectId: string
|
|
179
|
+
sourceId: string
|
|
180
|
+
materializationId: string
|
|
181
|
+
incremental: boolean
|
|
182
|
+
includePrevious: boolean
|
|
183
|
+
refs: readonly ProjectionEntityRef[]
|
|
184
|
+
}
|
|
185
|
+
): SqliteOntologySourceAssertionRow[] {
|
|
186
|
+
const keys = input.refs.map((ref) => ({
|
|
187
|
+
entityKind: ref.kind,
|
|
188
|
+
entityKey: projectionEntityKey(ref),
|
|
189
|
+
rootKeys:
|
|
190
|
+
ref.kind === "object"
|
|
191
|
+
? [utf8SortKey(projectionEntityKey(ref))]
|
|
192
|
+
: [
|
|
193
|
+
utf8SortKey(projectionEntityKey(ref)),
|
|
194
|
+
utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source })),
|
|
195
|
+
],
|
|
196
|
+
}))
|
|
197
|
+
const previous = !input.includePrevious
|
|
198
|
+
? ""
|
|
199
|
+
: `UNION ALL
|
|
200
|
+
SELECT rows.* FROM requested_entities AS requested
|
|
201
|
+
CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ? AND roots.source_id = ?
|
|
202
|
+
AND roots.active = 1 AND roots.root_sort_key IN (SELECT value FROM json_each(requested.root_keys))
|
|
203
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
204
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
205
|
+
AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
206
|
+
${
|
|
207
|
+
!input.incremental
|
|
208
|
+
? ""
|
|
209
|
+
: `WHERE EXISTS (SELECT 1 FROM ontology_source_roots AS changed
|
|
210
|
+
WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
|
|
211
|
+
AND changed.materialization_id = ? AND changed.root_sort_key = roots.root_sort_key)`
|
|
212
|
+
}`
|
|
213
|
+
return db
|
|
214
|
+
.query(`WITH requested_entities AS (
|
|
215
|
+
SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
|
|
216
|
+
json_extract(value, '$.entityKey') AS entity_key, json_extract(value, '$.rootKeys') AS root_keys FROM json_each(?)
|
|
217
|
+
) SELECT rows.* FROM requested_entities AS requested
|
|
218
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.project_id = ? AND rows.source_id = ?
|
|
219
|
+
AND rows.materialization_id = ? AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
220
|
+
${previous}`)
|
|
221
|
+
.all(
|
|
222
|
+
canonicalJson(keys),
|
|
223
|
+
input.projectId,
|
|
224
|
+
input.sourceId,
|
|
225
|
+
input.materializationId,
|
|
226
|
+
...(input.includePrevious
|
|
227
|
+
? [input.projectId, input.sourceId, ...(input.incremental ? [input.materializationId] : [])]
|
|
228
|
+
: [])
|
|
229
|
+
) as SqliteOntologySourceAssertionRow[]
|
|
230
|
+
}
|
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
sourceAssertion,
|
|
48
48
|
sourceRecord,
|
|
49
49
|
} from "./shared"
|
|
50
|
+
import { cleanupSourceVersions } from "./source-cleanup"
|
|
51
|
+
import { assertSourceRootCoverage, stageSourceRoots } from "./source-roots"
|
|
50
52
|
|
|
51
53
|
export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
52
54
|
constructor(
|
|
@@ -98,9 +100,9 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
98
100
|
dataset_id, dataset_version_id, dataset_version_created_at,
|
|
99
101
|
projection_revision, ownership_hash, ontology_revision,
|
|
100
102
|
root_count, assertion_count, created_at, ready_at, activated_at,
|
|
101
|
-
terminal_at, last_commit_id, updated_at
|
|
103
|
+
terminal_at, last_commit_id, updated_at, base_materialization_id, base_commit_id
|
|
102
104
|
) VALUES (?, ?, ?, ?, ?, 'replacement', 'staging', ?, ?, ?, ?, ?, ?, ?,
|
|
103
|
-
NULL, NULL, ?, NULL, NULL, NULL, NULL, ?)
|
|
105
|
+
NULL, NULL, ?, NULL, NULL, NULL, NULL, ?, ?, ?)
|
|
104
106
|
`
|
|
105
107
|
)
|
|
106
108
|
.run(
|
|
@@ -117,7 +119,9 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
117
119
|
input.ownershipHash,
|
|
118
120
|
input.ontologyRevision,
|
|
119
121
|
input.createdAt,
|
|
120
|
-
input.createdAt
|
|
122
|
+
input.createdAt,
|
|
123
|
+
input.base?.materializationId ?? null,
|
|
124
|
+
input.base?.lastCommitId ?? null
|
|
121
125
|
)
|
|
122
126
|
return sourceRecord(
|
|
123
127
|
this.requireManifest(input.projectId, input.source.projectionId, input.materializationId)
|
|
@@ -144,6 +148,7 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
144
148
|
const rows = sourceStageRows(manifest.projection_kind, input.rows)
|
|
145
149
|
const existing = this.findStageRows(input, rows)
|
|
146
150
|
const { pending, unchanged } = reconcileSourceStageRows(rows, existing)
|
|
151
|
+
stageSourceRoots(this.db, manifest, input)
|
|
147
152
|
this.insertStageRows(input, pending)
|
|
148
153
|
return { inserted: pending.length, unchanged }
|
|
149
154
|
})
|
|
@@ -249,78 +254,7 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
249
254
|
assertNonblank(input.projectId, "Terminal source cleanup project id")
|
|
250
255
|
assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true)
|
|
251
256
|
assertPositiveInteger(input.limit, "Terminal source cleanup limit")
|
|
252
|
-
|
|
253
|
-
.query(
|
|
254
|
-
`
|
|
255
|
-
SELECT * FROM ontology_sources
|
|
256
|
-
WHERE project_id = ? AND status IN ('superseded', 'abandoned')
|
|
257
|
-
AND terminal_at < ?
|
|
258
|
-
ORDER BY terminal_at, source_id, materialization_id
|
|
259
|
-
LIMIT ?
|
|
260
|
-
`
|
|
261
|
-
)
|
|
262
|
-
.all(input.projectId, input.terminalBefore, input.limit) as SqliteOntologySourceRow[]
|
|
263
|
-
let remaining = input.limit
|
|
264
|
-
let rowsDeleted = 0
|
|
265
|
-
let materializationsDeleted = 0
|
|
266
|
-
for (const manifest of manifests) {
|
|
267
|
-
if (remaining === 0) break
|
|
268
|
-
const deleted = this.db
|
|
269
|
-
.query(
|
|
270
|
-
`
|
|
271
|
-
DELETE FROM ontology_source_rows
|
|
272
|
-
WHERE rowid IN (
|
|
273
|
-
SELECT rows.rowid
|
|
274
|
-
FROM ontology_source_rows AS rows
|
|
275
|
-
JOIN ontology_sources AS sources
|
|
276
|
-
ON sources.project_id = rows.project_id
|
|
277
|
-
AND sources.source_id = rows.source_id
|
|
278
|
-
AND sources.materialization_id = rows.materialization_id
|
|
279
|
-
WHERE rows.project_id = ? AND rows.source_id = ? AND rows.materialization_id = ?
|
|
280
|
-
AND sources.status IN ('superseded', 'abandoned')
|
|
281
|
-
AND sources.terminal_at = ? AND sources.terminal_at < ?
|
|
282
|
-
ORDER BY rows.entity_sort_key
|
|
283
|
-
LIMIT ?
|
|
284
|
-
)
|
|
285
|
-
`
|
|
286
|
-
)
|
|
287
|
-
.run(
|
|
288
|
-
manifest.project_id,
|
|
289
|
-
manifest.source_id,
|
|
290
|
-
manifest.materialization_id,
|
|
291
|
-
manifest.terminal_at,
|
|
292
|
-
input.terminalBefore,
|
|
293
|
-
remaining
|
|
294
|
-
).changes
|
|
295
|
-
rowsDeleted += deleted
|
|
296
|
-
remaining -= deleted
|
|
297
|
-
if (remaining === 0) break
|
|
298
|
-
const removed = this.db
|
|
299
|
-
.query(
|
|
300
|
-
`
|
|
301
|
-
DELETE FROM ontology_sources
|
|
302
|
-
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
303
|
-
AND status IN ('superseded', 'abandoned')
|
|
304
|
-
AND terminal_at = ? AND terminal_at < ?
|
|
305
|
-
AND NOT EXISTS (
|
|
306
|
-
SELECT 1 FROM ontology_source_rows AS rows
|
|
307
|
-
WHERE rows.project_id = ontology_sources.project_id
|
|
308
|
-
AND rows.source_id = ontology_sources.source_id
|
|
309
|
-
AND rows.materialization_id = ontology_sources.materialization_id
|
|
310
|
-
)
|
|
311
|
-
`
|
|
312
|
-
)
|
|
313
|
-
.run(
|
|
314
|
-
manifest.project_id,
|
|
315
|
-
manifest.source_id,
|
|
316
|
-
manifest.materialization_id,
|
|
317
|
-
manifest.terminal_at,
|
|
318
|
-
input.terminalBefore
|
|
319
|
-
).changes
|
|
320
|
-
materializationsDeleted += removed
|
|
321
|
-
remaining -= removed
|
|
322
|
-
}
|
|
323
|
-
return { rowsDeleted, materializationsDeleted }
|
|
257
|
+
return cleanupSourceVersions(this.db, input)
|
|
324
258
|
})
|
|
325
259
|
}
|
|
326
260
|
|
|
@@ -433,6 +367,10 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
433
367
|
`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`
|
|
434
368
|
)
|
|
435
369
|
}
|
|
370
|
+
this.db
|
|
371
|
+
.query(`UPDATE ontology_source_roots SET retired_at = ?
|
|
372
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND active = 0`)
|
|
373
|
+
.run(abandonedAt, manifest.project_id, manifest.source_id, manifest.materialization_id)
|
|
436
374
|
return sourceRecord(
|
|
437
375
|
this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id)
|
|
438
376
|
)
|
|
@@ -446,14 +384,22 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
446
384
|
const counts = this.db
|
|
447
385
|
.query(
|
|
448
386
|
`
|
|
449
|
-
SELECT
|
|
387
|
+
SELECT (SELECT COUNT(*) FROM ontology_source_rows
|
|
388
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?) AS assertions, COUNT(*) AS roots,
|
|
450
389
|
COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
451
390
|
MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
|
|
452
|
-
FROM
|
|
391
|
+
FROM ontology_source_roots
|
|
453
392
|
WHERE project_id = ? AND source_id = ? AND materialization_id = ?
|
|
454
393
|
`
|
|
455
394
|
)
|
|
456
|
-
.get(
|
|
395
|
+
.get(
|
|
396
|
+
manifest.project_id,
|
|
397
|
+
manifest.source_id,
|
|
398
|
+
manifest.materialization_id,
|
|
399
|
+
manifest.project_id,
|
|
400
|
+
manifest.source_id,
|
|
401
|
+
manifest.materialization_id
|
|
402
|
+
) as {
|
|
457
403
|
assertions: number
|
|
458
404
|
roots: number
|
|
459
405
|
ordinals: number
|
|
@@ -470,6 +416,7 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
470
416
|
"Source ready counts do not match the staged roots and assertions."
|
|
471
417
|
)
|
|
472
418
|
}
|
|
419
|
+
assertSourceRootCoverage(this.db, manifest)
|
|
473
420
|
const invalid = this.db
|
|
474
421
|
.query(
|
|
475
422
|
manifest.projection_kind === "link"
|
|
@@ -114,7 +114,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
|
|
|
114
114
|
input_exhausted = ?, missing_target_object_type_id = NULL,
|
|
115
115
|
missing_target_object_id = NULL, missing_target_batch_ordinal = NULL,
|
|
116
116
|
missing_target_first_seen_at = NULL, source_rows_read = 0,
|
|
117
|
-
source_rows_skipped = 0, error = NULL
|
|
117
|
+
source_rows_skipped = 0, source_changes_read = NULL, error = NULL
|
|
118
118
|
WHERE project_id = ? AND id = ? AND status = 'failed'
|
|
119
119
|
AND json_extract(error, '$.code') = 'queue.enqueue_failed'
|
|
120
120
|
`
|
|
@@ -257,6 +257,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
|
|
|
257
257
|
execution_token = NULL,
|
|
258
258
|
source_rows_read = ?,
|
|
259
259
|
source_rows_skipped = ?,
|
|
260
|
+
source_changes_read = ?,
|
|
260
261
|
input_exhausted = ?,
|
|
261
262
|
error = ?
|
|
262
263
|
WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
|
|
@@ -267,6 +268,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
|
|
|
267
268
|
plan.finishedAt.toISOString(),
|
|
268
269
|
plan.progress.sourceRowsRead,
|
|
269
270
|
plan.progress.sourceRowsSkipped,
|
|
271
|
+
plan.progress.sourceChangesRead ?? null,
|
|
270
272
|
plan.inputExhausted ? 1 : existingRow.input_exhausted,
|
|
271
273
|
plan.error === undefined
|
|
272
274
|
? null
|
|
@@ -531,9 +533,10 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
|
|
|
531
533
|
progress: ProjectionRunProgress,
|
|
532
534
|
executionToken: string
|
|
533
535
|
): number {
|
|
534
|
-
const args: (string | number)[] = [
|
|
536
|
+
const args: (string | number | null)[] = [
|
|
535
537
|
progress.sourceRowsRead,
|
|
536
538
|
progress.sourceRowsSkipped,
|
|
539
|
+
progress.sourceChangesRead ?? null,
|
|
537
540
|
existing.project_id,
|
|
538
541
|
existing.id,
|
|
539
542
|
executionToken,
|
|
@@ -544,7 +547,8 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
|
|
|
544
547
|
UPDATE projection_runs
|
|
545
548
|
SET
|
|
546
549
|
source_rows_read = ?,
|
|
547
|
-
source_rows_skipped =
|
|
550
|
+
source_rows_skipped = ?,
|
|
551
|
+
source_changes_read = ?
|
|
548
552
|
WHERE project_id = ? AND id = ? AND status = 'running'
|
|
549
553
|
AND execution_token = ?
|
|
550
554
|
`
|
|
@@ -631,6 +635,9 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
631
635
|
progress: {
|
|
632
636
|
sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
|
|
633
637
|
sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
|
|
638
|
+
...(row.source_changes_read == null
|
|
639
|
+
? {}
|
|
640
|
+
: { sourceChangesRead: databaseSafeInteger(row.source_changes_read, "sourceChangesRead") }),
|
|
634
641
|
},
|
|
635
642
|
error: row.error ?? undefined,
|
|
636
643
|
}
|
|
@@ -694,6 +701,7 @@ interface DatabaseRow {
|
|
|
694
701
|
missing_target_first_seen_at: string | null
|
|
695
702
|
source_rows_read: number
|
|
696
703
|
source_rows_skipped: number
|
|
704
|
+
source_changes_read: number | null
|
|
697
705
|
error: string | null
|
|
698
706
|
}
|
|
699
707
|
|