@sixb/sqlite 0.1.14 → 0.1.15

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.
@@ -5,40 +5,121 @@ import {
5
5
  MaterializationValidationError,
6
6
  projectionEntityKey,
7
7
  } from "@sixb/core/internal/materialization"
8
- import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider"
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
- 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"
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 * FROM ontology_source_rows WHERE project_id = ? AND source_id = ? AND materialization_id = ?
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.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
- ],
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 project_id = ? AND source_id = ? AND materialization_id = ? AND root_sort_key = ?`)
136
+ WHERE version_id = ? AND root_key = ?`)
56
137
  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`)
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
- input.projectId,
74
- input.source.projectionId,
75
- input.materializationId,
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 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
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
- 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 = ?)`
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
- ...(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
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 project_id = ? AND source_id = ? AND materialization_id = ?
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.project_id, manifest.source_id, manifest.materialization_id)
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 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 }))
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, '$.rootSortKey') AS root_sort_key,
165
- json_extract(value, '$.entitySortKey') AS entity_sort_key FROM json_each(?)
166
- ) SELECT rows.* FROM requested
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.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[]
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 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
- }))
252
+ const version = sourceVersionId(db, input)
197
253
  const previous = !input.includePrevious
198
254
  ? ""
199
255
  : `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
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.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)`
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, '$.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
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(keys),
223
- input.projectId,
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 ? [input.materializationId] : [])]
283
+ ? [input.projectId, input.sourceId, ...(input.incremental ? [version] : [])]
228
284
  : [])
229
285
  ) as SqliteOntologySourceAssertionRow[]
230
286
  }