@sixb/sqlite 0.1.14 → 0.1.16
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 +739 -261
- package/dist/index.js.map +14 -14
- package/dist/latest-run-query.d.ts +2 -0
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/query-compiler.d.ts +3 -0
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +2 -0
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +1 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/rows.d.ts +1 -0
- package/dist/objects/rows.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +11 -8
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts +7 -1
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
- package/dist/ontology-storage/source-roots.d.ts +14 -2
- package/dist/ontology-storage/source-roots.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +8 -3
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/projection-run-storage.d.ts +3 -1
- package/dist/projection-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/latest-run-query.ts +8 -2
- package/src/migrations/051-projection-run-supersession.sql +126 -0
- package/src/migrations/052-compact-source-storage.sql +181 -0
- package/src/migrations.ts +38 -0
- package/src/objects/README.md +19 -0
- package/src/objects/query-compiler.ts +44 -7
- package/src/objects/read-scope.ts +10 -8
- package/src/objects/reader.ts +43 -8
- package/src/objects/rows.ts +1 -0
- package/src/ontology-storage/materialization-state.ts +34 -44
- package/src/ontology-storage/shared.ts +20 -11
- package/src/ontology-storage/source-cleanup.ts +61 -24
- package/src/ontology-storage/source-roots.ts +166 -110
- package/src/ontology-storage/sources.ts +169 -118
- package/src/projection-run-storage.ts +59 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import { parseSixbFailure } from "@sixb/core/internal/errors"
|
|
3
|
-
import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
|
|
4
3
|
import { MaterializationConflictError } from "@sixb/core/internal/materialization"
|
|
4
|
+
import {
|
|
5
|
+
sourceAssertionFromColumns,
|
|
6
|
+
sourceEntityFromKey,
|
|
7
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
5
8
|
import type {
|
|
6
9
|
AssertSourceMaterializationExecutionInput,
|
|
7
10
|
OntologyCommitRecord,
|
|
@@ -48,6 +51,7 @@ export interface SqliteOntologyCommitRow {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface SqliteOntologySourceRow {
|
|
54
|
+
readonly version_id: number
|
|
51
55
|
readonly project_id: string
|
|
52
56
|
readonly source_id: string
|
|
53
57
|
readonly materialization_id: string
|
|
@@ -74,19 +78,21 @@ export interface SqliteOntologySourceRow {
|
|
|
74
78
|
readonly updated_at: string
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
/** An assertion with its root and version, as `SOURCE_ASSERTION_COLUMNS` selects it. */
|
|
77
82
|
export interface SqliteOntologySourceAssertionRow {
|
|
78
|
-
readonly project_id: string
|
|
79
83
|
readonly source_id: string
|
|
80
84
|
readonly materialization_id: string
|
|
81
|
-
readonly entity_kind: "object" | "link"
|
|
82
|
-
readonly entity_key: string
|
|
83
|
-
readonly entity_sort_key: string
|
|
84
|
-
readonly root_kind: "object" | "link"
|
|
85
85
|
readonly root_key: string
|
|
86
|
-
readonly root_sort_key: string
|
|
87
86
|
readonly staging_ordinal: number
|
|
88
|
-
readonly
|
|
89
|
-
readonly
|
|
87
|
+
readonly entity_kind: "object" | "link"
|
|
88
|
+
readonly object_type_id: string | null
|
|
89
|
+
readonly primary_id: string | null
|
|
90
|
+
readonly source_type_id: string | null
|
|
91
|
+
readonly source_primary_id: string | null
|
|
92
|
+
readonly link_id: string | null
|
|
93
|
+
readonly target_type_id: string | null
|
|
94
|
+
readonly target_primary_id: string | null
|
|
95
|
+
readonly payload: string | null
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
export interface SqliteStoredOverrideRow {
|
|
@@ -149,8 +155,11 @@ export function sourceRecord(row: SqliteOntologySourceRow): OntologySourceRecord
|
|
|
149
155
|
|
|
150
156
|
export function sourceAssertion(row: SqliteOntologySourceAssertionRow): StageSourceAssertion {
|
|
151
157
|
return {
|
|
152
|
-
root:
|
|
153
|
-
assertion:
|
|
158
|
+
root: sourceEntityFromKey(row.root_key),
|
|
159
|
+
assertion: sourceAssertionFromColumns(
|
|
160
|
+
row,
|
|
161
|
+
row.payload === null ? null : parseJson<unknown>(row.payload)
|
|
162
|
+
),
|
|
154
163
|
stagingOrdinal: row.staging_ordinal,
|
|
155
164
|
}
|
|
156
165
|
}
|
|
@@ -2,38 +2,79 @@ import type { Database } from "bun:sqlite"
|
|
|
2
2
|
import type {
|
|
3
3
|
CleanupTerminalSourceMaterializationsInput,
|
|
4
4
|
CleanupTerminalSourceMaterializationsResult,
|
|
5
|
+
PurgeAbandonedSourceMaterializationsInput,
|
|
5
6
|
} from "@sixb/core/storage"
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Superseded versions can still hold live roots (a delta keeps unchanged roots where they were),
|
|
10
|
+
* so they are cleaned root by root once retired. An abandoned candidate was never published:
|
|
11
|
+
* everything it holds goes, one whole version at a time, each delete on its own version prefix.
|
|
12
|
+
*/
|
|
13
|
+
export function purgeAbandonedSourceVersions(
|
|
14
|
+
db: Database,
|
|
15
|
+
input: PurgeAbandonedSourceMaterializationsInput
|
|
16
|
+
): CleanupTerminalSourceMaterializationsResult {
|
|
17
|
+
let remaining = input.limit
|
|
18
|
+
let rowsDeleted = 0
|
|
19
|
+
let materializationsDeleted = 0
|
|
20
|
+
const nextCandidate = db.query(`
|
|
21
|
+
SELECT version_id FROM ontology_sources
|
|
22
|
+
WHERE project_id = ? AND status = 'abandoned'
|
|
23
|
+
ORDER BY terminal_at, source_id, materialization_id LIMIT 1
|
|
24
|
+
`)
|
|
25
|
+
const deleteRows = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
|
|
26
|
+
SELECT rows.rowid FROM ontology_source_roots AS roots
|
|
27
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
28
|
+
WHERE roots.version_id = ? LIMIT ?)`)
|
|
29
|
+
const deleteRoots = db.query(`DELETE FROM ontology_source_roots WHERE id IN (
|
|
30
|
+
SELECT id FROM ontology_source_roots WHERE version_id = ? LIMIT ?)`)
|
|
31
|
+
const deleteManifest = db.query(
|
|
32
|
+
"DELETE FROM ontology_sources WHERE version_id = ? AND status = 'abandoned'"
|
|
33
|
+
)
|
|
34
|
+
while (remaining > 0) {
|
|
35
|
+
const candidate = nextCandidate.get(input.projectId) as { readonly version_id: number } | null
|
|
36
|
+
if (!candidate) break
|
|
37
|
+
const rows = deleteRows.run(candidate.version_id, remaining).changes
|
|
38
|
+
rowsDeleted += rows
|
|
39
|
+
remaining -= rows
|
|
40
|
+
if (remaining === 0) break
|
|
41
|
+
const roots = deleteRoots.run(candidate.version_id, remaining).changes
|
|
42
|
+
rowsDeleted += roots
|
|
43
|
+
remaining -= roots
|
|
44
|
+
if (remaining === 0) break
|
|
45
|
+
const manifest = deleteManifest.run(candidate.version_id).changes
|
|
46
|
+
if (manifest === 0) break
|
|
47
|
+
materializationsDeleted += manifest
|
|
48
|
+
remaining -= manifest
|
|
49
|
+
}
|
|
50
|
+
return { rowsDeleted, materializationsDeleted }
|
|
51
|
+
}
|
|
52
|
+
|
|
7
53
|
export function cleanupSourceVersions(
|
|
8
54
|
db: Database,
|
|
9
55
|
input: CleanupTerminalSourceMaterializationsInput
|
|
10
56
|
): CleanupTerminalSourceMaterializationsResult {
|
|
11
57
|
let remaining = input.limit
|
|
58
|
+
const retired = `roots.project_id = ? AND roots.retired_at < ? AND versions.status = 'superseded'`
|
|
12
59
|
let rowsDeleted = db
|
|
13
60
|
.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
|
|
14
61
|
SELECT rows.rowid FROM ontology_source_roots AS roots
|
|
15
|
-
JOIN ontology_sources AS
|
|
16
|
-
CROSS JOIN ontology_source_rows AS rows
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
62
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
63
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
64
|
+
WHERE ${retired}
|
|
65
|
+
ORDER BY roots.retired_at, roots.id
|
|
22
66
|
LIMIT ?
|
|
23
67
|
)`)
|
|
24
68
|
.run(input.projectId, input.terminalBefore, remaining).changes
|
|
25
69
|
remaining -= rowsDeleted
|
|
26
70
|
if (remaining > 0) {
|
|
27
71
|
const removed = db
|
|
28
|
-
.query(`DELETE FROM ontology_source_roots WHERE
|
|
29
|
-
SELECT roots.
|
|
30
|
-
JOIN ontology_sources AS
|
|
31
|
-
WHERE
|
|
32
|
-
AND
|
|
33
|
-
|
|
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 ?
|
|
72
|
+
.query(`DELETE FROM ontology_source_roots WHERE id IN (
|
|
73
|
+
SELECT roots.id FROM ontology_source_roots AS roots
|
|
74
|
+
JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
75
|
+
WHERE ${retired}
|
|
76
|
+
AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
|
|
77
|
+
ORDER BY roots.retired_at, roots.id LIMIT ?
|
|
37
78
|
)`)
|
|
38
79
|
.run(input.projectId, input.terminalBefore, remaining).changes
|
|
39
80
|
rowsDeleted += removed
|
|
@@ -43,15 +84,11 @@ export function cleanupSourceVersions(
|
|
|
43
84
|
remaining === 0
|
|
44
85
|
? 0
|
|
45
86
|
: db
|
|
46
|
-
.query(`DELETE FROM ontology_sources WHERE
|
|
47
|
-
SELECT
|
|
48
|
-
WHERE project_id = ? AND status
|
|
87
|
+
.query(`DELETE FROM ontology_sources WHERE version_id IN (
|
|
88
|
+
SELECT versions.version_id FROM ontology_sources AS versions
|
|
89
|
+
WHERE project_id = ? AND status = 'superseded' AND terminal_at < ?
|
|
49
90
|
AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
|
|
50
|
-
WHERE roots.
|
|
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)
|
|
91
|
+
WHERE roots.version_id = versions.version_id)
|
|
55
92
|
ORDER BY terminal_at, source_id, materialization_id LIMIT ?
|
|
56
93
|
)`)
|
|
57
94
|
.run(input.projectId, input.terminalBefore, remaining).changes
|
|
@@ -5,40 +5,121 @@ import {
|
|
|
5
5
|
MaterializationValidationError,
|
|
6
6
|
projectionEntityKey,
|
|
7
7
|
} from "@sixb/core/internal/materialization"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
sourceEntityColumns,
|
|
10
|
+
sourceStageRoots,
|
|
11
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
9
12
|
import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
|
|
10
13
|
import type { SqliteOntologySourceAssertionRow } from "./shared"
|
|
11
14
|
import { canonicalJson, type SqliteOntologySourceRow } from "./shared"
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Source storage in three levels: a version (`ontology_sources`, one per run), its roots (one per
|
|
18
|
+
* logical dataset row, keyed by the canonical entity key) and their rows (what each root asserts).
|
|
19
|
+
*
|
|
20
|
+
* A root is live while its version is published (active or superseded), it is not retired and it
|
|
21
|
+
* is not a deletion. Activation retires the live roots its candidate replaces and flips the
|
|
22
|
+
* version; the candidate's own roots are never rewritten. One live root per source and key holds
|
|
23
|
+
* because activation, under the source fence, retires every root the candidate replaces.
|
|
24
|
+
*/
|
|
25
|
+
const PUBLISHED = "('active', 'superseded')"
|
|
26
|
+
|
|
27
|
+
/** The columns `sourceAssertion` reads, over `versions`, `roots` and `rows`. */
|
|
28
|
+
export const SOURCE_ASSERTION_COLUMNS = `versions.source_id, versions.materialization_id,
|
|
29
|
+
roots.root_key, roots.staging_ordinal, rows.entity_kind, rows.object_type_id, rows.primary_id,
|
|
30
|
+
rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
|
|
31
|
+
rows.target_primary_id, rows.payload`
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Keeps `rows` to those of live roots, joined as `roots` and `versions`; binds the project id.
|
|
35
|
+
* CROSS JOIN keeps it a filter on the rows already selected: as a plain JOIN, SQLite may start
|
|
36
|
+
* from every live root of the project instead.
|
|
37
|
+
*/
|
|
38
|
+
export const LIVE_SOURCE_ROWS_JOIN = `
|
|
39
|
+
CROSS JOIN ontology_source_roots AS roots ON roots.id = rows.root_id
|
|
40
|
+
AND roots.project_id = ? AND roots.retired_at IS NULL AND roots.deleted = 0
|
|
41
|
+
CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
42
|
+
AND versions.status IN ${PUBLISHED}`
|
|
43
|
+
|
|
44
|
+
/** The typed identity of `rows` equals the one requested (`IS` matches NULLs). */
|
|
45
|
+
const ENTITY_MATCH = `rows.entity_kind = requested.entity_kind
|
|
46
|
+
AND rows.object_type_id IS requested.object_type_id
|
|
47
|
+
AND rows.primary_id IS requested.primary_id
|
|
48
|
+
AND rows.source_type_id IS requested.source_type_id
|
|
49
|
+
AND rows.source_primary_id IS requested.source_primary_id
|
|
50
|
+
AND rows.link_id IS requested.link_id
|
|
51
|
+
AND rows.target_type_id IS requested.target_type_id
|
|
52
|
+
AND rows.target_primary_id IS requested.target_primary_id`
|
|
53
|
+
|
|
54
|
+
const REQUESTED_ENTITY = `json_extract(value, '$.entityKind') AS entity_kind,
|
|
55
|
+
json_extract(value, '$.objectTypeId') AS object_type_id,
|
|
56
|
+
json_extract(value, '$.primaryId') AS primary_id,
|
|
57
|
+
json_extract(value, '$.sourceTypeId') AS source_type_id,
|
|
58
|
+
json_extract(value, '$.sourcePrimaryId') AS source_primary_id,
|
|
59
|
+
json_extract(value, '$.linkId') AS link_id,
|
|
60
|
+
json_extract(value, '$.targetTypeId') AS target_type_id,
|
|
61
|
+
json_extract(value, '$.targetPrimaryId') AS target_primary_id`
|
|
62
|
+
|
|
63
|
+
/** An entity's own key, and for a link the key of the object root that may assert it. */
|
|
64
|
+
function requestedEntity(ref: ProjectionEntityRef) {
|
|
65
|
+
const key = projectionEntityKey(ref)
|
|
66
|
+
return {
|
|
67
|
+
entityKind: ref.kind,
|
|
68
|
+
...sourceEntityColumns(ref),
|
|
69
|
+
rootKeys:
|
|
70
|
+
ref.kind === "object"
|
|
71
|
+
? [key]
|
|
72
|
+
: [key, projectionEntityKey({ kind: "object", ref: ref.ref.source })],
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sourceVersionId(
|
|
77
|
+
db: Database,
|
|
78
|
+
input: {
|
|
79
|
+
readonly projectId: string
|
|
80
|
+
readonly sourceId: string
|
|
81
|
+
readonly materializationId: string
|
|
82
|
+
}
|
|
83
|
+
): number | null {
|
|
84
|
+
const version = db
|
|
85
|
+
.query(`SELECT version_id FROM ontology_sources
|
|
86
|
+
WHERE project_id = ? AND source_id = ? AND materialization_id = ?`)
|
|
87
|
+
.get(input.projectId, input.sourceId, input.materializationId) as {
|
|
88
|
+
readonly version_id: number
|
|
89
|
+
} | null
|
|
90
|
+
return version?.version_id ?? null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The candidate's rows, plus the rows of the live roots it may replace. */
|
|
94
|
+
export function replacementSourceRows(
|
|
95
|
+
db: Database,
|
|
96
|
+
input: {
|
|
97
|
+
readonly projectId: string
|
|
98
|
+
readonly sourceId: string
|
|
99
|
+
readonly materializationId: string
|
|
100
|
+
readonly incremental: boolean
|
|
101
|
+
}
|
|
102
|
+
): { readonly sql: string; readonly values: readonly (string | number | null)[] } {
|
|
103
|
+
const version = sourceVersionId(db, input)
|
|
104
|
+
const live = `live.retired_at IS NULL AND live.deleted = 0
|
|
105
|
+
AND versions.source_id = ? AND versions.status IN ${PUBLISHED}`
|
|
106
|
+
const replaced = input.incremental
|
|
107
|
+
? `SELECT live.id FROM ontology_source_roots AS changed
|
|
108
|
+
CROSS JOIN ontology_source_roots AS live
|
|
109
|
+
ON live.project_id = ? AND live.root_key = changed.root_key
|
|
110
|
+
CROSS JOIN ontology_sources AS versions ON versions.version_id = live.version_id
|
|
111
|
+
WHERE changed.version_id = ? AND ${live}`
|
|
112
|
+
: `SELECT live.id FROM ontology_sources AS versions
|
|
113
|
+
CROSS JOIN ontology_source_roots AS live ON live.version_id = versions.version_id
|
|
114
|
+
WHERE versions.project_id = ? AND ${live}`
|
|
25
115
|
return {
|
|
26
|
-
sql: `SELECT
|
|
116
|
+
sql: `SELECT rows.* FROM ontology_source_roots AS roots
|
|
117
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
|
|
118
|
+
WHERE roots.version_id = ?
|
|
27
119
|
UNION ALL
|
|
28
|
-
SELECT rows.* FROM ${roots
|
|
29
|
-
CROSS JOIN ontology_source_rows AS rows ON rows.
|
|
30
|
-
|
|
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
|
-
],
|
|
120
|
+
SELECT rows.* FROM (${replaced}) AS roots
|
|
121
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id`,
|
|
122
|
+
values: [version, input.projectId, ...(input.incremental ? [version] : []), input.sourceId],
|
|
42
123
|
}
|
|
43
124
|
}
|
|
44
125
|
|
|
@@ -52,29 +133,23 @@ export function stageSourceRoots(
|
|
|
52
133
|
throw new MaterializationValidationError("Root deletions require a source delta base.")
|
|
53
134
|
}
|
|
54
135
|
const existing = db.query(`SELECT staging_ordinal, deleted FROM ontology_source_roots
|
|
55
|
-
WHERE
|
|
136
|
+
WHERE version_id = ? AND root_key = ?`)
|
|
56
137
|
const insert = db.query(`INSERT INTO ontology_source_roots (
|
|
57
|
-
|
|
58
|
-
) VALUES (?, ?, ?, ?,
|
|
138
|
+
version_id, project_id, root_key, staging_ordinal, deleted
|
|
139
|
+
) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING id`)
|
|
59
140
|
for (const root of roots) {
|
|
60
141
|
const inserted = insert.get(
|
|
142
|
+
manifest.version_id,
|
|
61
143
|
input.projectId,
|
|
62
|
-
input.source.projectionId,
|
|
63
|
-
input.materializationId,
|
|
64
|
-
root.sortKey,
|
|
65
|
-
root.root.kind,
|
|
66
144
|
root.rootKey,
|
|
67
|
-
canonicalJson(root.root),
|
|
68
145
|
root.stagingOrdinal,
|
|
69
146
|
Number(root.deleted)
|
|
70
147
|
)
|
|
71
148
|
if (inserted) continue
|
|
72
|
-
const previous = existing.get(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
root.sortKey
|
|
77
|
-
) as { staging_ordinal: number; deleted: number } | null
|
|
149
|
+
const previous = existing.get(manifest.version_id, root.rootKey) as {
|
|
150
|
+
staging_ordinal: number
|
|
151
|
+
deleted: number
|
|
152
|
+
} | null
|
|
78
153
|
if (
|
|
79
154
|
!previous ||
|
|
80
155
|
previous.staging_ordinal !== root.stagingOrdinal ||
|
|
@@ -87,6 +162,7 @@ export function stageSourceRoots(
|
|
|
87
162
|
}
|
|
88
163
|
}
|
|
89
164
|
|
|
165
|
+
/** Retires what the candidate replaces; its own roots go live when its version turns active. */
|
|
90
166
|
export function activateSourceRoots(
|
|
91
167
|
db: Database,
|
|
92
168
|
projectId: string,
|
|
@@ -102,76 +178,66 @@ export function activateSourceRoots(
|
|
|
102
178
|
"projection-fence",
|
|
103
179
|
"Source delta base changed before activation."
|
|
104
180
|
)
|
|
105
|
-
|
|
106
181
|
if (candidate.base_materialization_id !== null && candidate.root_count === 0) return
|
|
107
|
-
const
|
|
108
|
-
db.query(`UPDATE ontology_source_roots
|
|
109
|
-
WHERE
|
|
182
|
+
const delta = candidate.base_materialization_id !== null
|
|
183
|
+
db.query(`UPDATE ontology_source_roots SET retired_at = ?
|
|
184
|
+
WHERE retired_at IS NULL AND deleted = 0
|
|
185
|
+
AND version_id IN (SELECT version_id FROM ontology_sources
|
|
186
|
+
WHERE project_id = ? AND source_id = ? AND status IN ${PUBLISHED})
|
|
110
187
|
${
|
|
111
|
-
|
|
112
|
-
?
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
WHERE project_id = ? AND source_id = ? AND materialization_id = ?)`
|
|
188
|
+
delta
|
|
189
|
+
? `AND project_id = ? AND root_key IN (
|
|
190
|
+
SELECT root_key FROM ontology_source_roots WHERE version_id = ?)`
|
|
191
|
+
: ""
|
|
116
192
|
}`).run(
|
|
117
193
|
activation.updatedAt,
|
|
118
194
|
projectId,
|
|
119
195
|
activation.source.projectionId,
|
|
120
|
-
...(
|
|
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
|
|
196
|
+
...(delta ? [projectId, candidate.version_id] : [])
|
|
127
197
|
)
|
|
198
|
+
// Deletions are never live; retiring them lets cleanup remove them with their version.
|
|
199
|
+
db.query(`UPDATE ontology_source_roots SET retired_at = ?
|
|
200
|
+
WHERE version_id = ? AND deleted = 1`).run(activation.updatedAt, candidate.version_id)
|
|
128
201
|
}
|
|
129
202
|
|
|
130
203
|
export function assertSourceRootCoverage(db: Database, manifest: SqliteOntologySourceRow): void {
|
|
131
204
|
const invalid = db
|
|
132
205
|
.query(`SELECT 1 FROM ontology_source_roots AS roots
|
|
133
|
-
WHERE
|
|
206
|
+
WHERE roots.version_id = ?
|
|
134
207
|
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
|
|
208
|
+
SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id
|
|
138
209
|
) LIMIT 1`)
|
|
139
|
-
.get(manifest.
|
|
210
|
+
.get(manifest.version_id)
|
|
140
211
|
if (invalid)
|
|
141
212
|
throw new MaterializationValidationError(
|
|
142
213
|
"Source roots must have complete assertions or an explicit deletion, never both."
|
|
143
214
|
)
|
|
144
215
|
}
|
|
145
216
|
|
|
217
|
+
/** The live assertions of `refs`, whichever source asserts them. */
|
|
146
218
|
export function activeSourceRows(
|
|
147
219
|
db: Database,
|
|
148
220
|
projectId: string,
|
|
149
221
|
refs: readonly ProjectionEntityRef[]
|
|
150
222
|
): SqliteOntologySourceAssertionRow[] {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
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 }))
|
|
223
|
+
const requested = refs.flatMap((ref) => {
|
|
224
|
+
const { rootKeys, ...entity } = requestedEntity(ref)
|
|
225
|
+
return rootKeys.map((rootKey) => ({ ...entity, rootKey }))
|
|
161
226
|
})
|
|
162
227
|
return db
|
|
163
228
|
.query(`WITH requested AS (
|
|
164
|
-
SELECT DISTINCT json_extract(value, '$.
|
|
165
|
-
|
|
166
|
-
) SELECT
|
|
229
|
+
SELECT DISTINCT json_extract(value, '$.rootKey') AS root_key, ${REQUESTED_ENTITY}
|
|
230
|
+
FROM json_each(?)
|
|
231
|
+
) SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
|
|
167
232
|
CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
|
|
168
|
-
AND roots.
|
|
169
|
-
CROSS JOIN
|
|
170
|
-
AND
|
|
171
|
-
|
|
172
|
-
.all(canonicalJson(
|
|
233
|
+
AND roots.root_key = requested.root_key AND roots.retired_at IS NULL AND roots.deleted = 0
|
|
234
|
+
CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
235
|
+
AND versions.status IN ${PUBLISHED}
|
|
236
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}`)
|
|
237
|
+
.all(canonicalJson(requested), projectId) as SqliteOntologySourceAssertionRow[]
|
|
173
238
|
}
|
|
174
239
|
|
|
240
|
+
/** The candidate's assertions of `refs`, and optionally the live ones they would replace. */
|
|
175
241
|
export function replacementAssertionRows(
|
|
176
242
|
db: Database,
|
|
177
243
|
input: {
|
|
@@ -183,48 +249,38 @@ export function replacementAssertionRows(
|
|
|
183
249
|
refs: readonly ProjectionEntityRef[]
|
|
184
250
|
}
|
|
185
251
|
): SqliteOntologySourceAssertionRow[] {
|
|
186
|
-
const
|
|
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
|
-
}))
|
|
252
|
+
const version = sourceVersionId(db, input)
|
|
197
253
|
const previous = !input.includePrevious
|
|
198
254
|
? ""
|
|
199
255
|
: `UNION ALL
|
|
200
|
-
SELECT
|
|
201
|
-
CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
|
|
202
|
-
AND roots.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
AND
|
|
256
|
+
SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested_entities AS requested
|
|
257
|
+
CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
|
|
258
|
+
AND roots.root_key IN (SELECT value FROM json_each(requested.root_keys))
|
|
259
|
+
AND roots.retired_at IS NULL AND roots.deleted = 0
|
|
260
|
+
CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
261
|
+
AND versions.source_id = ? AND versions.status IN ${PUBLISHED}
|
|
262
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}
|
|
206
263
|
${
|
|
207
264
|
!input.incremental
|
|
208
265
|
? ""
|
|
209
266
|
: `WHERE EXISTS (SELECT 1 FROM ontology_source_roots AS changed
|
|
210
|
-
WHERE changed.
|
|
211
|
-
AND changed.materialization_id = ? AND changed.root_sort_key = roots.root_sort_key)`
|
|
267
|
+
WHERE changed.version_id = ? AND changed.root_key = roots.root_key)`
|
|
212
268
|
}`
|
|
213
269
|
return db
|
|
214
270
|
.query(`WITH requested_entities AS (
|
|
215
|
-
SELECT DISTINCT json_extract(value, '$.
|
|
216
|
-
|
|
217
|
-
) SELECT
|
|
218
|
-
CROSS JOIN
|
|
219
|
-
AND
|
|
271
|
+
SELECT DISTINCT ${REQUESTED_ENTITY}, json_extract(value, '$.rootKeys') AS root_keys
|
|
272
|
+
FROM json_each(?)
|
|
273
|
+
) SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested_entities AS requested
|
|
274
|
+
CROSS JOIN ontology_source_roots AS roots ON roots.version_id = ?
|
|
275
|
+
AND roots.root_key IN (SELECT value FROM json_each(requested.root_keys))
|
|
276
|
+
CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
|
|
277
|
+
CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}
|
|
220
278
|
${previous}`)
|
|
221
279
|
.all(
|
|
222
|
-
canonicalJson(
|
|
223
|
-
|
|
224
|
-
input.sourceId,
|
|
225
|
-
input.materializationId,
|
|
280
|
+
canonicalJson(input.refs.map(requestedEntity)),
|
|
281
|
+
version,
|
|
226
282
|
...(input.includePrevious
|
|
227
|
-
? [input.projectId, input.sourceId, ...(input.incremental ? [
|
|
283
|
+
? [input.projectId, input.sourceId, ...(input.incremental ? [version] : [])]
|
|
228
284
|
: [])
|
|
229
285
|
) as SqliteOntologySourceAssertionRow[]
|
|
230
286
|
}
|