@sixb/pg 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 (46) 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.d.ts +8 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +779 -357
  8. package/dist/index.js.map +16 -16
  9. package/dist/latest-run-query.d.ts +2 -0
  10. package/dist/latest-run-query.d.ts.map +1 -1
  11. package/dist/migrations.d.ts.map +1 -1
  12. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  13. package/dist/ontology-storage/shared.d.ts +12 -9
  14. package/dist/ontology-storage/shared.d.ts.map +1 -1
  15. package/dist/ontology-storage/source-cleanup.d.ts +7 -1
  16. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  17. package/dist/ontology-storage/source-roots.d.ts +8 -0
  18. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  19. package/dist/ontology-storage/sources.d.ts +8 -3
  20. package/dist/ontology-storage/sources.d.ts.map +1 -1
  21. package/dist/pg-client.d.ts +2 -0
  22. package/dist/pg-client.d.ts.map +1 -1
  23. package/dist/pg-projection-run-storage.d.ts +3 -1
  24. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  25. package/dist/storage-errors.d.ts +13 -3
  26. package/dist/storage-errors.d.ts.map +1 -1
  27. package/dist/transactions.d.ts +23 -1
  28. package/dist/transactions.d.ts.map +1 -1
  29. package/package.json +3 -3
  30. package/src/auth-storage/auth-storage.ts +43 -75
  31. package/src/auth-storage/rows.ts +1 -1
  32. package/src/index.ts +8 -1
  33. package/src/latest-run-query.ts +10 -2
  34. package/src/migrations/050-directory-group-memberships.sql +5 -0
  35. package/src/migrations/051-projection-run-supersession.sql +43 -0
  36. package/src/migrations/052-compact-source-storage.sql +118 -0
  37. package/src/migrations.ts +30 -23
  38. package/src/ontology-storage/materialization-state.ts +20 -46
  39. package/src/ontology-storage/shared.ts +18 -12
  40. package/src/ontology-storage/source-cleanup.ts +66 -22
  41. package/src/ontology-storage/source-roots.ts +164 -102
  42. package/src/ontology-storage/sources.ts +150 -120
  43. package/src/pg-client.ts +132 -2
  44. package/src/pg-projection-run-storage.ts +56 -2
  45. package/src/storage-errors.ts +101 -15
  46. package/src/transactions.ts +113 -20
@@ -4,12 +4,91 @@ import {
4
4
  MaterializationValidationError,
5
5
  projectionEntityKey,
6
6
  } from "@sixb/core/internal/materialization"
7
- import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider"
7
+ import {
8
+ sourceEntityColumns,
9
+ sourceStageRoots,
10
+ } from "@sixb/core/internal/ontology-storage-provider"
8
11
  import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
9
12
  import type { SQLClient } from "../pg-client"
10
13
  import type { PgOntologySourceAssertionRow } from "./shared"
11
14
  import { jsonParameter, type PgOntologySourceRow } from "./shared"
12
15
 
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 function sourceAssertionColumns(sql: SQLClient) {
29
+ return sql.unsafe(`versions.source_id, versions.materialization_id, roots.root_key,
30
+ roots.staging_ordinal, rows.entity_kind, rows.object_type_id, rows.primary_id,
31
+ rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
32
+ rows.target_primary_id, rows.payload`)
33
+ }
34
+
35
+ /** Keeps `rows` to those of live roots in `projectId`, joined as `roots` and `versions`. */
36
+ export function liveSourceRowsJoin(sql: SQLClient, projectId: string) {
37
+ return sql`
38
+ JOIN ontology_source_roots AS roots ON roots.id = rows.root_id
39
+ AND roots.project_id = ${projectId} AND roots.retired_at IS NULL AND NOT roots.deleted
40
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
41
+ AND versions.status IN ${sql.unsafe(PUBLISHED)}
42
+ `
43
+ }
44
+
45
+ function candidateVersion(
46
+ sql: SQLClient,
47
+ input: {
48
+ readonly projectId: string
49
+ readonly sourceId: string
50
+ readonly materializationId: string
51
+ }
52
+ ) {
53
+ return sql`(SELECT version_id FROM ontology_sources WHERE project_id = ${input.projectId}
54
+ AND source_id = ${input.sourceId} AND materialization_id = ${input.materializationId})`
55
+ }
56
+
57
+ /** The typed identity of `rows` equals the one requested. */
58
+ const ENTITY_MATCH = `rows.entity_kind = requested.entity_kind
59
+ AND rows.object_type_id IS NOT DISTINCT FROM requested.object_type_id
60
+ AND rows.primary_id IS NOT DISTINCT FROM requested.primary_id
61
+ AND rows.source_type_id IS NOT DISTINCT FROM requested.source_type_id
62
+ AND rows.source_primary_id IS NOT DISTINCT FROM requested.source_primary_id
63
+ AND rows.link_id IS NOT DISTINCT FROM requested.link_id
64
+ AND rows.target_type_id IS NOT DISTINCT FROM requested.target_type_id
65
+ AND rows.target_primary_id IS NOT DISTINCT FROM requested.target_primary_id`
66
+
67
+ const REQUESTED_ENTITY = `entity_kind TEXT, object_type_id TEXT, primary_id TEXT,
68
+ source_type_id TEXT, source_primary_id TEXT, link_id TEXT, target_type_id TEXT,
69
+ target_primary_id TEXT`
70
+
71
+ /** An entity's own key, and for a link the key of the object root that may assert it. */
72
+ function requestedEntity(ref: ProjectionEntityRef) {
73
+ const columns = sourceEntityColumns(ref)
74
+ const key = projectionEntityKey(ref)
75
+ return {
76
+ entity_kind: ref.kind,
77
+ object_type_id: columns.objectTypeId,
78
+ primary_id: columns.primaryId,
79
+ source_type_id: columns.sourceTypeId,
80
+ source_primary_id: columns.sourcePrimaryId,
81
+ link_id: columns.linkId,
82
+ target_type_id: columns.targetTypeId,
83
+ target_primary_id: columns.targetPrimaryId,
84
+ root_keys:
85
+ ref.kind === "object"
86
+ ? [key]
87
+ : [key, projectionEntityKey({ kind: "object", ref: ref.ref.source })],
88
+ }
89
+ }
90
+
91
+ /** The candidate's rows, plus the rows of the live roots it may replace. */
13
92
  export function replacementSourceRows(
14
93
  sql: SQLClient,
15
94
  input: {
@@ -19,27 +98,32 @@ export function replacementSourceRows(
19
98
  readonly incremental: boolean
20
99
  }
21
100
  ) {
101
+ const live = sql`roots.retired_at IS NULL AND NOT roots.deleted
102
+ AND versions.source_id = ${input.sourceId} AND versions.status IN ${sql.unsafe(PUBLISHED)}`
22
103
  const roots = input.incremental
23
104
  ? sql`
24
- SELECT roots.* FROM ontology_source_roots AS changed
105
+ SELECT live.id FROM ontology_source_roots AS changed
25
106
  CROSS JOIN LATERAL (
26
- SELECT roots.* FROM ontology_source_roots AS roots
27
- WHERE roots.project_id = changed.project_id AND roots.source_id = changed.source_id
28
- AND roots.root_sort_key = changed.root_sort_key AND roots.active OFFSET 0
29
- ) AS roots
30
- WHERE changed.project_id = ${input.projectId} AND changed.source_id = ${input.sourceId}
31
- AND changed.materialization_id = ${input.materializationId}
107
+ SELECT roots.id FROM ontology_source_roots AS roots
108
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
109
+ WHERE roots.project_id = ${input.projectId} AND roots.root_key = changed.root_key AND ${live}
110
+ OFFSET 0
111
+ ) AS live
112
+ WHERE changed.version_id = ${candidateVersion(sql, input)}
113
+ `
114
+ : sql`
115
+ SELECT roots.id FROM ontology_sources AS versions
116
+ JOIN ontology_source_roots AS roots ON roots.version_id = versions.version_id
117
+ WHERE versions.project_id = ${input.projectId} AND ${live}
32
118
  `
33
- : sql`SELECT * FROM ontology_source_roots WHERE project_id = ${input.projectId} AND source_id = ${input.sourceId} AND active`
34
119
  return sql`
35
- SELECT * FROM ontology_source_rows WHERE project_id = ${input.projectId}
36
- AND source_id = ${input.sourceId} AND materialization_id = ${input.materializationId}
120
+ SELECT rows.* FROM ontology_source_roots AS roots
121
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
122
+ WHERE roots.version_id = ${candidateVersion(sql, input)}
37
123
  UNION ALL
38
124
  SELECT rows.* FROM (${roots}) AS roots
39
125
  CROSS JOIN LATERAL (
40
- SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.project_id = roots.project_id
41
- AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
42
- AND rows.root_sort_key = roots.root_sort_key OFFSET 0
126
+ SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id OFFSET 0
43
127
  ) AS rows
44
128
  `
45
129
  }
@@ -57,38 +141,31 @@ export async function stageSourceRoots(
57
141
  const values = jsonParameter(
58
142
  sql,
59
143
  roots.map((root) => ({
60
- root_sort_key: root.sortKey,
61
- root_kind: root.root.kind,
62
- root_key: JSON.parse(root.rootKey),
63
- root: root.root,
144
+ root_key: root.rootKey,
64
145
  staging_ordinal: root.stagingOrdinal,
65
146
  deleted: root.deleted,
66
147
  }))
67
148
  )
68
- const inserted = await sql<{ root_sort_key: string }[]>`
69
- INSERT INTO ontology_source_roots (
70
- project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
71
- ) SELECT ${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
72
- root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
73
- FROM jsonb_to_recordset(${values}) AS input(
74
- root_sort_key TEXT, root_kind TEXT, root_key JSONB, root JSONB, staging_ordinal BIGINT, deleted BOOLEAN)
75
- ON CONFLICT DO NOTHING RETURNING root_sort_key
149
+ const inserted = await sql<{ root_key: string }[]>`
150
+ INSERT INTO ontology_source_roots (version_id, project_id, root_key, staging_ordinal, deleted)
151
+ SELECT ${manifest.version_id}, ${input.projectId}, root_key, staging_ordinal, deleted
152
+ FROM jsonb_to_recordset(${values}) AS input(root_key TEXT, staging_ordinal BIGINT, deleted BOOLEAN)
153
+ ON CONFLICT DO NOTHING RETURNING root_key
76
154
  `
77
155
  if (inserted.length === roots.length) return
78
- const insertedKeys = new Set(inserted.map((row) => row.root_sort_key))
79
- const skipped = roots.filter((root) => !insertedKeys.has(root.sortKey))
156
+ const insertedKeys = new Set(inserted.map((row) => row.root_key))
157
+ const skipped = roots.filter((root) => !insertedKeys.has(root.rootKey))
80
158
  const stored = await sql<
81
- { root_sort_key: string; staging_ordinal: string | number; deleted: boolean }[]
159
+ { root_key: string; staging_ordinal: string | number; deleted: boolean }[]
82
160
  >`
83
- SELECT root_sort_key, staging_ordinal, deleted FROM ontology_source_roots
84
- WHERE project_id = ${input.projectId} AND source_id = ${input.source.projectionId}
85
- AND materialization_id = ${input.materializationId}
86
- AND root_sort_key = ANY(${sql.array(skipped.map((root) => root.sortKey))}::text[])
161
+ SELECT root_key, staging_ordinal, deleted FROM ontology_source_roots
162
+ WHERE version_id = ${manifest.version_id}
163
+ AND root_key = ANY(${sql.array(skipped.map((root) => root.rootKey))}::text[])
87
164
  `
88
- const existing = new Map(stored.map((root) => [root.root_sort_key, root]))
165
+ const existing = new Map(stored.map((root) => [root.root_key, root]))
89
166
  if (
90
167
  skipped.some((root) => {
91
- const previous = existing.get(root.sortKey)
168
+ const previous = existing.get(root.rootKey)
92
169
  return (
93
170
  !previous ||
94
171
  Number(previous.staging_ordinal) !== root.stagingOrdinal ||
@@ -101,6 +178,7 @@ export async function stageSourceRoots(
101
178
  )
102
179
  }
103
180
 
181
+ /** Retires what the candidate replaces; its own roots go live when its version turns active. */
104
182
  export async function activateSourceRoots(
105
183
  sql: SQLClient,
106
184
  projectId: string,
@@ -117,24 +195,27 @@ export async function activateSourceRoots(
117
195
  "Source delta base changed before activation."
118
196
  )
119
197
  if (candidate.base_materialization_id !== null && Number(candidate.root_count) === 0) return
120
- const affected =
121
- candidate.base_materialization_id === null
122
- ? sql``
123
- : sql`AND root_sort_key IN (
124
- SELECT root_sort_key FROM ontology_source_roots
125
- WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
126
- AND materialization_id = ${activation.materializationId}
127
- )`
128
- await sql`
129
- UPDATE ontology_source_roots AS roots SET active = FALSE, retired_at = ${activation.updatedAt}
130
- WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId} AND active
131
- ${affected}
132
- `
198
+ const live = sql`roots.retired_at IS NULL AND NOT roots.deleted
199
+ AND versions.version_id = roots.version_id AND versions.project_id = ${projectId}
200
+ AND versions.source_id = ${activation.source.projectionId}
201
+ AND versions.status IN ${sql.unsafe(PUBLISHED)}`
202
+ if (candidate.base_materialization_id === null) {
203
+ await sql`
204
+ UPDATE ontology_source_roots AS roots SET retired_at = ${activation.updatedAt}
205
+ FROM ontology_sources AS versions WHERE ${live}
206
+ `
207
+ } else {
208
+ await sql`
209
+ UPDATE ontology_source_roots AS roots SET retired_at = ${activation.updatedAt}
210
+ FROM ontology_source_roots AS changed, ontology_sources AS versions
211
+ WHERE changed.version_id = ${candidate.version_id} AND roots.project_id = ${projectId}
212
+ AND roots.root_key = changed.root_key AND ${live}
213
+ `
214
+ }
215
+ // Deletions are never live; retiring them lets cleanup remove them with their version.
133
216
  await sql`
134
- UPDATE ontology_source_roots SET active = NOT deleted,
135
- retired_at = CASE WHEN deleted THEN ${activation.updatedAt}::timestamptz ELSE NULL END
136
- WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
137
- AND materialization_id = ${activation.materializationId}
217
+ UPDATE ontology_source_roots SET retired_at = ${activation.updatedAt}
218
+ WHERE version_id = ${candidate.version_id} AND deleted
138
219
  `
139
220
  }
140
221
 
@@ -144,13 +225,11 @@ export async function assertSourceRootCoverage(
144
225
  ): Promise<void> {
145
226
  const [invalid] = await sql`
146
227
  SELECT 1 FROM ontology_source_roots AS roots
147
- WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
148
- AND materialization_id = ${manifest.materialization_id}
228
+ WHERE roots.version_id = ${manifest.version_id}
149
229
  AND roots.deleted = EXISTS (
150
- SELECT 1 FROM ontology_source_rows AS rows
151
- WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
152
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
153
- ) LIMIT 1
230
+ SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id
231
+ )
232
+ LIMIT 1
154
233
  `
155
234
  if (invalid)
156
235
  throw new MaterializationValidationError(
@@ -158,35 +237,29 @@ export async function assertSourceRootCoverage(
158
237
  )
159
238
  }
160
239
 
240
+ /** The live assertions of `refs`, whichever source asserts them. */
161
241
  export function activeSourceRows(
162
242
  sql: SQLClient,
163
243
  projectId: string,
164
244
  refs: readonly ProjectionEntityRef[]
165
245
  ) {
166
- const keys = refs.flatMap((entity) => {
167
- const entitySortKey = utf8SortKey(projectionEntityKey(entity))
168
- const roots =
169
- entity.kind === "object"
170
- ? [entitySortKey]
171
- : [
172
- entitySortKey,
173
- utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source })),
174
- ]
175
- return roots.map((rootSortKey) => ({
176
- root_sort_key: rootSortKey,
177
- entity_sort_key: entitySortKey,
178
- }))
246
+ const requested = refs.flatMap((ref) => {
247
+ const { root_keys, ...entity } = requestedEntity(ref)
248
+ return root_keys.map((root_key) => ({ ...entity, root_key }))
179
249
  })
180
250
  return sql<PgOntologySourceAssertionRow[]>`
181
- SELECT rows.* FROM jsonb_to_recordset(${jsonParameter(sql, keys)}) AS requested(root_sort_key TEXT, entity_sort_key TEXT)
251
+ SELECT ${sourceAssertionColumns(sql)}
252
+ FROM jsonb_to_recordset(${jsonParameter(sql, requested)})
253
+ AS requested(root_key TEXT, ${sql.unsafe(REQUESTED_ENTITY)})
182
254
  JOIN ontology_source_roots AS roots ON roots.project_id = ${projectId}
183
- AND roots.active AND roots.root_sort_key = requested.root_sort_key
184
- JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
185
- AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
186
- AND rows.root_sort_key = roots.root_sort_key
255
+ AND roots.root_key = requested.root_key AND roots.retired_at IS NULL AND NOT roots.deleted
256
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
257
+ AND versions.status IN ${sql.unsafe(PUBLISHED)}
258
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${sql.unsafe(ENTITY_MATCH)}
187
259
  `
188
260
  }
189
261
 
262
+ /** The candidate's assertions of `refs`, and optionally the live ones they would replace. */
190
263
  export function replacementAssertionRows(
191
264
  sql: SQLClient,
192
265
  input: {
@@ -198,52 +271,41 @@ export function replacementAssertionRows(
198
271
  refs: readonly ProjectionEntityRef[]
199
272
  }
200
273
  ) {
201
- const keys = input.refs.map((ref) => ({
202
- entity_kind: ref.kind,
203
- entity_key: JSON.parse(projectionEntityKey(ref)),
204
- root_keys:
205
- ref.kind === "object"
206
- ? [utf8SortKey(projectionEntityKey(ref))]
207
- : [
208
- utf8SortKey(projectionEntityKey(ref)),
209
- utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source })),
210
- ],
211
- }))
274
+ const version = candidateVersion(sql, input)
212
275
  const previous = !input.includePrevious
213
276
  ? sql``
214
277
  : sql`UNION ALL
215
- SELECT rows.* FROM (
278
+ SELECT ${sourceAssertionColumns(sql)} FROM (
216
279
  SELECT roots.* FROM ontology_source_roots AS roots
217
- WHERE roots.project_id = ${input.projectId} AND roots.source_id = ${input.sourceId}
218
- AND roots.active AND roots.root_sort_key = ANY(requested.root_keys)
280
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
281
+ WHERE roots.project_id = ${input.projectId} AND roots.root_key = ANY(requested.root_keys)
282
+ AND roots.retired_at IS NULL AND NOT roots.deleted
283
+ AND versions.source_id = ${input.sourceId} AND versions.status IN ${sql.unsafe(PUBLISHED)}
219
284
  ${
220
285
  !input.incremental
221
286
  ? sql``
222
287
  : sql`AND EXISTS (SELECT 1 FROM ontology_source_roots AS changed
223
- WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
224
- AND changed.materialization_id = ${input.materializationId} AND changed.root_sort_key = roots.root_sort_key)`
288
+ WHERE changed.version_id = ${version} AND changed.root_key = roots.root_key)`
225
289
  }
226
-
227
290
  OFFSET 0
228
291
  ) AS roots
292
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
229
293
  CROSS JOIN LATERAL (
230
294
  SELECT rows.* FROM ontology_source_rows AS rows
231
- WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
232
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
233
- AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
295
+ WHERE rows.root_id = roots.id AND ${sql.unsafe(ENTITY_MATCH)}
234
296
  OFFSET 0
235
297
  ) AS rows
236
298
  `
237
299
  // Keep each JSON request as an indexed identity lookup even with fresh/stale statistics.
238
300
  // Flattening this LATERAL caused a cold 10k-root run to spend seconds on each state page.
239
301
  return sql<PgOntologySourceAssertionRow[]>`
240
- SELECT selected.* FROM jsonb_to_recordset(${jsonParameter(sql, keys)})
241
- AS requested(entity_kind TEXT, entity_key JSONB, root_keys TEXT[])
302
+ SELECT selected.* FROM jsonb_to_recordset(${jsonParameter(sql, input.refs.map(requestedEntity))})
303
+ AS requested(${sql.unsafe(REQUESTED_ENTITY)}, root_keys TEXT[])
242
304
  CROSS JOIN LATERAL (
243
- SELECT rows.* FROM ontology_source_rows AS rows
244
- WHERE rows.project_id = ${input.projectId} AND rows.source_id = ${input.sourceId}
245
- AND rows.materialization_id = ${input.materializationId}
246
- AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
305
+ SELECT ${sourceAssertionColumns(sql)} FROM ontology_source_roots AS roots
306
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
307
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${sql.unsafe(ENTITY_MATCH)}
308
+ WHERE roots.version_id = ${version} AND roots.root_key = ANY(requested.root_keys)
247
309
  ${previous}
248
310
  OFFSET 0
249
311
  ) AS selected