@sixb/sqlite 0.1.13 → 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.
Files changed (34) hide show
  1. package/dist/auth-storage/auth-storage.d.ts +2 -1
  2. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  3. package/dist/auth-storage/rows.d.ts +1 -1
  4. package/dist/auth-storage/rows.d.ts.map +1 -1
  5. package/dist/index.js +726 -286
  6. package/dist/index.js.map +12 -12
  7. package/dist/latest-run-query.d.ts +2 -0
  8. package/dist/latest-run-query.d.ts.map +1 -1
  9. package/dist/migrations.d.ts.map +1 -1
  10. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  11. package/dist/ontology-storage/shared.d.ts +11 -8
  12. package/dist/ontology-storage/shared.d.ts.map +1 -1
  13. package/dist/ontology-storage/source-cleanup.d.ts +7 -1
  14. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  15. package/dist/ontology-storage/source-roots.d.ts +14 -2
  16. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  17. package/dist/ontology-storage/sources.d.ts +8 -3
  18. package/dist/ontology-storage/sources.d.ts.map +1 -1
  19. package/dist/projection-run-storage.d.ts +3 -1
  20. package/dist/projection-run-storage.d.ts.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth-storage/auth-storage.ts +44 -79
  23. package/src/auth-storage/rows.ts +1 -1
  24. package/src/latest-run-query.ts +8 -2
  25. package/src/migrations/050-directory-group-memberships.sql +21 -0
  26. package/src/migrations/051-projection-run-supersession.sql +126 -0
  27. package/src/migrations/052-compact-source-storage.sql +181 -0
  28. package/src/migrations.ts +42 -0
  29. package/src/ontology-storage/materialization-state.ts +34 -44
  30. package/src/ontology-storage/shared.ts +20 -11
  31. package/src/ontology-storage/source-cleanup.ts +61 -24
  32. package/src/ontology-storage/source-roots.ts +166 -110
  33. package/src/ontology-storage/sources.ts +169 -118
  34. package/src/projection-run-storage.ts +59 -1
@@ -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 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
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 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 ?
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 rowid IN (
47
- SELECT sources.rowid FROM ontology_sources AS sources
48
- WHERE project_id = ? AND status IN ('superseded', 'abandoned') AND terminal_at < ?
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.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)
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 { 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
  }