@sixb/pg 0.1.15 → 0.1.17

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 (92) hide show
  1. package/README.md +125 -0
  2. package/dist/index.d.ts +15 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3202 -1474
  5. package/dist/index.js.map +32 -25
  6. package/dist/migrations.d.ts.map +1 -1
  7. package/dist/objects/aggregate-admission.d.ts +10 -0
  8. package/dist/objects/aggregate-admission.d.ts.map +1 -0
  9. package/dist/objects/preparation-lock.d.ts +6 -0
  10. package/dist/objects/preparation-lock.d.ts.map +1 -0
  11. package/dist/objects/prepare-queries.d.ts +5 -0
  12. package/dist/objects/prepare-queries.d.ts.map +1 -0
  13. package/dist/objects/query-compiler.d.ts +19 -0
  14. package/dist/objects/query-compiler.d.ts.map +1 -1
  15. package/dist/objects/query-indexes.d.ts +43 -0
  16. package/dist/objects/query-indexes.d.ts.map +1 -0
  17. package/dist/objects/reader.d.ts +7 -1
  18. package/dist/objects/reader.d.ts.map +1 -1
  19. package/dist/objects/storage.d.ts +1 -1
  20. package/dist/objects/storage.d.ts.map +1 -1
  21. package/dist/objects/text-count-index.d.ts +24 -0
  22. package/dist/objects/text-count-index.d.ts.map +1 -0
  23. package/dist/ontology-storage/index.d.ts +2 -0
  24. package/dist/ontology-storage/index.d.ts.map +1 -1
  25. package/dist/ontology-storage/materialization-session.d.ts +18 -38
  26. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  27. package/dist/ontology-storage/materialization-state.d.ts +3 -26
  28. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  29. package/dist/ontology-storage/materialization-writer.d.ts +51 -9
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +5 -7
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/replacement-plans.d.ts +36 -0
  35. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
  36. package/dist/ontology-storage/shared.d.ts +11 -5
  37. package/dist/ontology-storage/shared.d.ts.map +1 -1
  38. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  39. package/dist/ontology-storage/source-roots.d.ts +14 -1
  40. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  41. package/dist/ontology-storage/sources.d.ts +1 -2
  42. package/dist/ontology-storage/sources.d.ts.map +1 -1
  43. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  44. package/dist/pg-ai-limit-storage.d.ts.map +1 -1
  45. package/dist/pg-ai-usage-storage.d.ts +2 -0
  46. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  47. package/dist/pg-execution-storage.d.ts +28 -0
  48. package/dist/pg-execution-storage.d.ts.map +1 -1
  49. package/dist/pg-file-download-grant-storage.d.ts +11 -0
  50. package/dist/pg-file-download-grant-storage.d.ts.map +1 -0
  51. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  52. package/dist/pg-rules-storage.d.ts.map +1 -1
  53. package/dist/transactions.d.ts +6 -2
  54. package/dist/transactions.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/src/index.ts +53 -3
  57. package/src/migrations/053-replacement-plans.sql +72 -0
  58. package/src/migrations/054-slim-ontology-outbox.sql +108 -0
  59. package/src/migrations/055-object-query-preparation.sql +11 -0
  60. package/src/migrations/056-commit-touches.sql +41 -0
  61. package/src/migrations/057-audio-transcription.sql +10 -0
  62. package/src/migrations/058-reranking-model-kind.sql +11 -0
  63. package/src/migrations/059-file-download-grants.sql +16 -0
  64. package/src/migrations/060-drained-source-versions.sql +11 -0
  65. package/src/migrations.ts +20 -0
  66. package/src/objects/aggregate-admission.ts +21 -0
  67. package/src/objects/preparation-lock.ts +15 -0
  68. package/src/objects/prepare-queries.ts +181 -0
  69. package/src/objects/query-compiler.ts +405 -115
  70. package/src/objects/query-indexes.ts +348 -0
  71. package/src/objects/reader.ts +140 -51
  72. package/src/objects/storage.ts +15 -3
  73. package/src/objects/text-count-index.ts +455 -0
  74. package/src/ontology-storage/index.ts +3 -0
  75. package/src/ontology-storage/materialization-session.ts +116 -275
  76. package/src/ontology-storage/materialization-state.ts +8 -259
  77. package/src/ontology-storage/materialization-writer.ts +618 -508
  78. package/src/ontology-storage/materializations.ts +106 -289
  79. package/src/ontology-storage/outbox.ts +46 -15
  80. package/src/ontology-storage/replacement-plans.ts +1110 -0
  81. package/src/ontology-storage/shared.ts +27 -15
  82. package/src/ontology-storage/source-cleanup.ts +22 -10
  83. package/src/ontology-storage/source-roots.ts +35 -8
  84. package/src/ontology-storage/sources.ts +0 -30
  85. package/src/pg-ai-cost-storage.ts +12 -0
  86. package/src/pg-ai-limit-storage.ts +13 -2
  87. package/src/pg-ai-usage-storage.ts +8 -0
  88. package/src/pg-execution-storage.ts +8 -3
  89. package/src/pg-file-download-grant-storage.ts +76 -0
  90. package/src/pg-projection-run-storage.ts +6 -1
  91. package/src/pg-rules-storage.ts +7 -3
  92. package/src/transactions.ts +10 -3
@@ -1,26 +1,28 @@
1
1
  import { MaterializationConflictError } from "@sixb/core/internal/materialization"
2
2
  import {
3
- correlateMaterializationChunk,
4
3
  duplicateMaterializationWork as duplicateWork,
5
- invalidCorrelation,
6
- materializationPlanItems,
4
+ materializationApplyPhase,
5
+ materializationPlanKindRank,
7
6
  ProviderMaterializationSessionState,
8
7
  type ProviderMaterializationTransactionLifecycle,
9
8
  prepareMaterializationWork,
10
9
  } from "@sixb/core/internal/ontology-storage-provider"
11
10
  import type {
12
- MaterializationEventWorkRecord,
13
- MaterializationPlanChunk,
14
11
  MaterializationPlanHeader,
12
+ MaterializationPlanWorkRecord,
15
13
  MaterializationSession,
16
- MaterializationWorkPage,
17
- MaterializationWorkRecord,
14
+ MaterializationVectorChange,
18
15
  StageMaterializationWorkInput,
19
- StreamMaterializationWorkInput,
20
16
  } from "@sixb/core/storage"
21
17
  import type { SQLClient } from "../pg-client"
22
18
  import { isUniqueViolation } from "../storage-errors"
23
- import { PG_MATERIALIZATION_WORK_TABLE, PG_REPLACEMENT_WORK_TABLE } from "./materialization-state"
19
+ import { PG_MATERIALIZATION_WORK_TABLE } from "./materialization-state"
20
+ import {
21
+ assertReplacementPlanCoverage,
22
+ PG_PLAN_WORK_TABLE,
23
+ type PgReplacementPlan,
24
+ replacementPlanCounts,
25
+ } from "./replacement-plans"
24
26
  import { jsonParameter } from "./shared"
25
27
 
26
28
  export interface PgOntologyTransactionContext {
@@ -29,36 +31,24 @@ export interface PgOntologyTransactionContext {
29
31
  active: boolean
30
32
  }
31
33
 
32
- interface WorkCursor {
33
- readonly majorOrder: number
34
- readonly minorOrder: number
35
- readonly sortOne: string
36
- readonly sortTwo: string
37
- readonly recordKey: string
38
- }
39
-
40
- export interface PgChunkSequenceProgress {
41
- readonly appliedPlanCount: number
42
- readonly appliedOutboxCount: number
43
- readonly appliedPlanCursor: WorkCursor | null
44
- readonly appliedEventCursor: WorkCursor | null
45
- }
46
-
47
34
  export class PgMaterializationSessionState extends ProviderMaterializationSessionState {
48
35
  stagedWorkCount = 0
49
36
  changedObjects = 0
50
37
  changedLinks = 0
51
- appliedPlanCursor: WorkCursor | null = null
52
- appliedEventCursor: WorkCursor | null = null
53
- }
38
+ workAnalyzed = false
39
+ /** Work staged on the session lives in a temp table; a plan-bound session reads its plan's. */
40
+ readonly workTable: string
41
+ readonly workId: string
54
42
 
55
- interface WorkDatabaseRow {
56
- readonly record_key: string
57
- readonly major_order: number
58
- readonly minor_order: number
59
- readonly sort_one: string
60
- readonly sort_two: string
61
- readonly payload: unknown
43
+ constructor(
44
+ header: MaterializationPlanHeader,
45
+ transactionId: object,
46
+ readonly plan: PgReplacementPlan | null
47
+ ) {
48
+ super(header, transactionId)
49
+ this.workTable = plan ? PG_PLAN_WORK_TABLE : PG_MATERIALIZATION_WORK_TABLE
50
+ this.workId = plan ? plan.versionId : this.id
51
+ }
62
52
  }
63
53
 
64
54
  export class PgMaterializationSessions {
@@ -71,7 +61,10 @@ export class PgMaterializationSessions {
71
61
  private readonly context: PgOntologyTransactionContext | null
72
62
  ) {}
73
63
 
74
- async create(header: MaterializationPlanHeader): Promise<PgMaterializationSessionState> {
64
+ async create(
65
+ header: MaterializationPlanHeader,
66
+ plan: PgReplacementPlan | null
67
+ ): Promise<PgMaterializationSessionState> {
75
68
  if (!this.context?.active) {
76
69
  throw new MaterializationConflictError(
77
70
  "effective-state",
@@ -79,7 +72,11 @@ export class PgMaterializationSessions {
79
72
  )
80
73
  }
81
74
  await this.ensureTables()
82
- const session = new PgMaterializationSessionState(structuredClone(header), this.context.id)
75
+ const session = new PgMaterializationSessionState(
76
+ structuredClone(header),
77
+ this.context.id,
78
+ plan
79
+ )
83
80
  this.sessions.set(session.providerToken, session)
84
81
  this.live.add(session)
85
82
  this.context.materializations.register(session.providerToken)
@@ -104,18 +101,15 @@ export class PgMaterializationSessions {
104
101
 
105
102
  async release(session: PgMaterializationSessionState): Promise<void> {
106
103
  if (!session.active) return
107
- await this.deleteWork(session.id)
104
+ // A plan's rows outlive the session: maintenance deletes them once the candidate moved on.
105
+ if (!session.plan) await this.deleteWork(session.id)
108
106
  session.active = false
109
- session.replacement = null
110
107
  this.live.delete(session)
111
108
  this.context?.materializations.complete(session.providerToken)
112
109
  }
113
110
 
114
111
  deactivateAll(): void {
115
- for (const session of this.live) {
116
- session.active = false
117
- session.replacement = null
118
- }
112
+ for (const session of this.live) session.active = false
119
113
  this.live.clear()
120
114
  }
121
115
 
@@ -133,6 +127,7 @@ export class PgMaterializationSessions {
133
127
  minorOrder: columns.minorOrder,
134
128
  sortOne: columns.sortOne,
135
129
  sortTwo: columns.sortTwo,
130
+ occupied: record.kind === "cardinality" ? record.occupied : null,
136
131
  record,
137
132
  }
138
133
  })
@@ -146,12 +141,12 @@ export class PgMaterializationSessions {
146
141
  SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, payload)}::jsonb)
147
142
  )
148
143
  INSERT INTO ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} (
149
- session_id, record_key, unique_key, kind, lane,
150
- major_order, minor_order, sort_one, sort_two, payload
144
+ work_id, record_key, unique_key, kind, lane,
145
+ major_order, minor_order, sort_one, sort_two, cardinality_occupied, payload
151
146
  )
152
147
  SELECT ${session.id}, value->>'recordKey', value->>'uniqueKey', value->>'kind',
153
148
  value->>'lane', (value->>'majorOrder')::integer, (value->>'minorOrder')::integer,
154
- value->>'sortOne', value->>'sortTwo', value->'record'
149
+ value->>'sortOne', value->>'sortTwo', (value->>'occupied')::boolean, value->'record'
155
150
  FROM staged
156
151
  `
157
152
  } catch (error) {
@@ -163,222 +158,96 @@ export class PgMaterializationSessions {
163
158
  session.stagedWorkCount += records.length
164
159
  }
165
160
 
166
- async *stream(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
167
- const session = this.require(input.session)
168
- const stream = session.workStreams[input.order]
169
- if (stream.started) {
170
- throw new MaterializationConflictError(
171
- "effective-state",
172
- `Materialization ${input.order} work may only be streamed once per session.`
173
- )
174
- }
175
- stream.started = true
176
- if (!session.workSealed && session.stagedWorkCount >= 10_000) {
177
- // Planning has finished writing work. Refresh once before draining a large population;
178
- // small edits avoid the extra database round trip.
179
- await this.sql`ANALYZE ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}`
180
- }
181
- session.workSealed = true
182
- let cursor: WorkCursor | null = null
183
- while (true) {
184
- this.require(input.session)
185
- const rows = await this.readLane(input.order, session.id, cursor, input.pageRows)
186
- if (rows.length === 0) break
187
- const records = rows.map((row) =>
188
- structuredClone(row.payload)
189
- ) as MaterializationWorkPage["records"]
190
- cursor = workCursor(rows[rows.length - 1]!)
191
- stream.emittedCount += records.length
192
- yield { records }
193
- }
194
- this.require(input.session)
195
- stream.completed = true
161
+ /**
162
+ * Refreshes the work table's statistics once planning has finished writing a large population,
163
+ * before set-based reads join it; small edits avoid the extra round trip.
164
+ */
165
+ async analyzeSealedWork(session: PgMaterializationSessionState): Promise<void> {
166
+ if (session.workAnalyzed) return
167
+ session.workAnalyzed = true
168
+ // A plan's shared tables are analyzed as it is planned, outside any commit transaction.
169
+ if (session.plan || session.stagedWorkCount < 10_000) return
170
+ await this.sql`ANALYZE ${this.sql(session.workTable)}`
196
171
  }
197
172
 
198
- async readObjectExistence(
173
+ /** Staged object upserts and deletes of profiled types, or of objects with a stored vector. */
174
+ async *vectorChangePages(
199
175
  session: PgMaterializationSessionState,
200
- refs: readonly { readonly objectTypeId: string; readonly primaryId: string }[]
201
- ): Promise<
202
- readonly {
203
- readonly ref: { readonly objectTypeId: string; readonly primaryId: string }
204
- readonly exists: boolean
205
- }[]
206
- > {
207
- if (refs.length === 0) return []
208
- const keys = refs.map(
209
- (ref) => `object-existence:${JSON.stringify([ref.objectTypeId, ref.primaryId])}`
210
- )
211
- // OFFSET 0 keeps each unique-key lookup parameterized instead of letting the planner
212
- // flatten the lateral join into a scan of the entire temporary work table.
213
- const rows = await this.sql<{ readonly unique_key: string; readonly payload: unknown }[]>`
214
- SELECT work.unique_key, work.payload
215
- FROM unnest(${this.sql.array(keys)}::text[]) AS requested(unique_key)
216
- CROSS JOIN LATERAL (
217
- SELECT unique_key, payload FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
218
- WHERE session_id = ${session.id} AND unique_key = requested.unique_key
219
- OFFSET 0
220
- ) AS work
221
- `
222
- const found = new Map(rows.map((row) => [row.unique_key, row.payload] as const))
223
- return keys.flatMap((key) => {
224
- const payload = found.get(key)
225
- if (!payload) return []
226
- // prepareMaterializationWork owns this unique-key namespace. Filtering kind in SQL
227
- // makes stale temporary-table statistics favor a scan over exact indexed lookups.
228
- const record = structuredClone(payload) as MaterializationWorkRecord
229
- if (record.kind !== "object-existence") {
230
- invalidCorrelation("Object existence work has an invalid kind.")
176
+ objectTypeIds: readonly string[],
177
+ pageRows: number
178
+ ): AsyncIterable<readonly MaterializationVectorChange[]> {
179
+ if (objectTypeIds.length === 0) {
180
+ const [stored] = await this.sql`
181
+ SELECT 1 FROM object_vectors WHERE project_id = ${session.header.commit.projectId} LIMIT 1
182
+ `
183
+ if (!stored) return
184
+ }
185
+ // Each kind is one range of the lane index, paged in its order.
186
+ for (const kind of ["object-delete", "object-upsert"] as const) {
187
+ let after: readonly [string, string, string] = ["", "", ""]
188
+ while (true) {
189
+ this.require(session.publicSession())
190
+ const rows = await this.sql<
191
+ {
192
+ readonly sort_one: string
193
+ readonly sort_two: string
194
+ readonly record_key: string
195
+ readonly payload: unknown
196
+ }[]
197
+ >`
198
+ WITH changes AS (
199
+ SELECT sort_one, sort_two, record_key, payload,
200
+ COALESCE(payload->'item'->'value'->'row'->'ref', payload->'item'->'value'->'ref')
201
+ AS ref
202
+ FROM ${this.sql(session.workTable)}
203
+ WHERE work_id = ${session.workId} AND lane = 'apply'
204
+ AND major_order = ${materializationApplyPhase(kind)}
205
+ AND minor_order = ${materializationPlanKindRank(kind)}
206
+ AND (sort_one, sort_two, record_key) > (${after[0]}, ${after[1]}, ${after[2]})
207
+ )
208
+ SELECT sort_one, sort_two, record_key, payload FROM changes
209
+ WHERE ref->>'objectTypeId' = ANY(${this.sql.array([...objectTypeIds])}::text[])
210
+ OR EXISTS (
211
+ SELECT 1 FROM object_vectors
212
+ WHERE project_id = ${session.header.commit.projectId}
213
+ AND object_type_id = changes.ref->>'objectTypeId'
214
+ AND primary_id = changes.ref->>'primaryId'
215
+ )
216
+ ORDER BY sort_one, sort_two, record_key
217
+ LIMIT ${pageRows}
218
+ `
219
+ if (rows.length === 0) break
220
+ const last = rows[rows.length - 1]!
221
+ after = [last.sort_one, last.sort_two, last.record_key]
222
+ yield rows.map(
223
+ (row) => (row.payload as MaterializationPlanWorkRecord).item
224
+ ) as MaterializationVectorChange[]
231
225
  }
232
- return [{ ref: record.ref, exists: record.exists }]
233
- })
234
- }
235
-
236
- async prepareChunkSequence(
237
- session: PgMaterializationSessionState,
238
- chunk: MaterializationPlanChunk
239
- ): Promise<PgChunkSequenceProgress> {
240
- const items = materializationPlanItems(chunk)
241
- const planRows = await this.readLane(
242
- "apply",
243
- session.id,
244
- session.appliedPlanCursor,
245
- items.length
246
- )
247
- const expectedItems = planRows.map(
248
- (row) =>
249
- (
250
- structuredClone(row.payload) as Extract<
251
- MaterializationWorkRecord,
252
- { readonly kind: "plan" }
253
- >
254
- ).item
255
- )
256
- const eventRows = await this.readLane(
257
- "event",
258
- session.id,
259
- session.appliedEventCursor,
260
- chunk.outbox.length
261
- )
262
- const expectedEvents = eventRows.map(
263
- (row) => structuredClone(row.payload) as MaterializationEventWorkRecord
264
- )
265
- const progress = correlateMaterializationChunk(
266
- session,
267
- chunk,
268
- items,
269
- expectedItems,
270
- expectedEvents
271
- )
272
-
273
- return {
274
- ...progress,
275
- appliedPlanCursor:
276
- planRows.length === 0
277
- ? session.appliedPlanCursor
278
- : workCursor(planRows[planRows.length - 1]!),
279
- appliedEventCursor:
280
- eventRows.length === 0
281
- ? session.appliedEventCursor
282
- : workCursor(eventRows[eventRows.length - 1]!),
283
226
  }
284
227
  }
285
228
 
286
- commitChunkSequence(
287
- session: PgMaterializationSessionState,
288
- progress: PgChunkSequenceProgress
289
- ): void {
290
- session.appliedPlanCount = progress.appliedPlanCount
291
- session.appliedOutboxCount = progress.appliedOutboxCount
292
- session.appliedPlanCursor = progress.appliedPlanCursor
293
- session.appliedEventCursor = progress.appliedEventCursor
294
- }
295
-
296
- async count(session: PgMaterializationSessionState, kind: string): Promise<number> {
297
- const [row] = await this.sql<{ readonly count: number | string }[]>`
298
- SELECT COUNT(*) AS count FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
299
- WHERE session_id = ${session.id} AND kind = ${kind}
229
+ async hasCardinalityWork(session: PgMaterializationSessionState): Promise<boolean> {
230
+ const [row] = await this.sql`
231
+ SELECT 1 FROM ${this.sql(session.workTable)}
232
+ WHERE work_id = ${session.workId} AND lane = 'cardinality'
233
+ LIMIT 1
300
234
  `
301
- return Number(row?.count ?? 0)
302
- }
303
-
304
- async laneCounts(session: PgMaterializationSessionState): Promise<{
305
- readonly apply: number
306
- readonly cardinality: number
307
- readonly event: number
308
- }> {
309
- const rows = await this.sql<
310
- { readonly lane: "apply" | "cardinality" | "event"; readonly count: number | string }[]
311
- >`
312
- SELECT lane, COUNT(*) AS count FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
313
- WHERE session_id = ${session.id} AND lane <> 'none'
314
- GROUP BY lane
315
- `
316
- const counts = { apply: 0, cardinality: 0, event: 0 }
317
- for (const row of rows) counts[row.lane] = Number(row.count)
318
- return counts
235
+ return row !== undefined
319
236
  }
320
237
 
321
238
  async assertClassificationCoverage(session: PgMaterializationSessionState): Promise<void> {
322
- if (!session.replacement) return
323
- const includeObjects = session.replacement.projectionKind === "object"
324
- const [mismatch] = await this.sql<{ readonly marker: number }[]>`
325
- WITH expected AS (
326
- SELECT entity_kind, identity_key
327
- FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)}
328
- WHERE session_id = ${session.id} AND diff_required
329
- AND (entity_kind = 'link' OR ${includeObjects})
330
- ), actual AS (
331
- SELECT payload->>'entityKind' AS entity_kind,
332
- payload->>'identityKey' AS identity_key
333
- FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
334
- WHERE session_id = ${session.id} AND kind = 'classification'
335
- AND payload->>'entityKind' IN ('object', 'link')
336
- ), differences AS (
337
- (SELECT * FROM expected EXCEPT SELECT * FROM actual)
338
- UNION ALL
339
- (SELECT * FROM actual EXCEPT SELECT * FROM expected)
340
- ), invalid AS (
341
- SELECT 1 AS marker FROM differences
342
- UNION ALL
343
- SELECT 1 AS marker FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
344
- WHERE session_id = ${session.id} AND kind = 'classification'
345
- AND payload->>'entityKind' = 'point'
346
- )
347
- SELECT marker FROM invalid LIMIT 1
348
- `
349
- if (mismatch) {
350
- invalidCorrelation(
351
- "Projection replacement classification coverage does not match its streamed state."
352
- )
353
- }
239
+ if (session.plan) await assertReplacementPlanCoverage(this.sql, session.plan)
354
240
  }
355
241
 
356
- private async readLane(
357
- lane: StreamMaterializationWorkInput["order"],
358
- sessionId: string,
359
- cursor: WorkCursor | null,
360
- limit: number
361
- ): Promise<WorkDatabaseRow[]> {
362
- if (limit === 0) return []
363
- const after = cursor
364
- ? this.sql`AND (major_order, minor_order, sort_one, sort_two, record_key) >
365
- (${cursor.majorOrder}, ${cursor.minorOrder}, ${cursor.sortOne}, ${cursor.sortTwo},
366
- ${cursor.recordKey})`
367
- : this.sql``
368
- return this.sql<WorkDatabaseRow[]>`
369
- SELECT record_key, major_order, minor_order, sort_one, sort_two, payload
370
- FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
371
- WHERE session_id = ${sessionId} AND lane = ${lane} ${after}
372
- ORDER BY major_order, minor_order, sort_one, sort_two, record_key
373
- LIMIT ${limit}
374
- `
242
+ async projectionCounts(session: PgMaterializationSessionState) {
243
+ return session.plan ? replacementPlanCounts(this.sql, session.plan.versionId) : null
375
244
  }
376
245
 
377
246
  private async ensureTables(): Promise<void> {
378
247
  if (this.tablesReady) return
379
248
  await this.sql`
380
249
  CREATE TEMP TABLE ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} (
381
- session_id TEXT NOT NULL,
250
+ work_id TEXT NOT NULL,
382
251
  record_key TEXT COLLATE "C" NOT NULL,
383
252
  unique_key TEXT NOT NULL,
384
253
  kind TEXT NOT NULL,
@@ -387,31 +256,16 @@ export class PgMaterializationSessions {
387
256
  minor_order INTEGER NOT NULL,
388
257
  sort_one TEXT COLLATE "C" NOT NULL,
389
258
  sort_two TEXT COLLATE "C" NOT NULL,
259
+ cardinality_occupied BOOLEAN,
390
260
  payload JSONB NOT NULL,
391
- PRIMARY KEY (session_id, record_key),
392
- UNIQUE (session_id, unique_key)
261
+ PRIMARY KEY (work_id, record_key),
262
+ UNIQUE (work_id, unique_key)
393
263
  ) ON COMMIT DROP
394
264
  `
395
265
  await this.sql`
396
266
  CREATE INDEX ontology_materialization_work_lane
397
267
  ON ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} (
398
- session_id, lane, major_order, minor_order, sort_one, sort_two, record_key
399
- )
400
- `
401
- await this.sql`
402
- CREATE TEMP TABLE ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
403
- session_id TEXT NOT NULL,
404
- entity_kind TEXT NOT NULL,
405
- identity_key TEXT COLLATE "C" NOT NULL,
406
- sort_key TEXT COLLATE "C" NOT NULL,
407
- diff_required BOOLEAN NOT NULL,
408
- PRIMARY KEY (session_id, entity_kind, identity_key)
409
- ) ON COMMIT DROP
410
- `
411
- await this.sql`
412
- CREATE INDEX ontology_replacement_work_order
413
- ON ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
414
- session_id, entity_kind, sort_key, identity_key
268
+ work_id, lane, major_order, minor_order, sort_one, sort_two, record_key
415
269
  )
416
270
  `
417
271
  this.tablesReady = true
@@ -420,20 +274,7 @@ export class PgMaterializationSessions {
420
274
  private async deleteWork(sessionId: string): Promise<void> {
421
275
  if (!this.tablesReady) return
422
276
  await this.sql`
423
- DELETE FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} WHERE session_id = ${sessionId}
424
- `
425
- await this.sql`
426
- DELETE FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)} WHERE session_id = ${sessionId}
277
+ DELETE FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} WHERE work_id = ${sessionId}
427
278
  `
428
279
  }
429
280
  }
430
-
431
- function workCursor(row: WorkDatabaseRow): WorkCursor {
432
- return {
433
- majorOrder: row.major_order,
434
- minorOrder: row.minor_order,
435
- sortOne: row.sort_one,
436
- sortTwo: row.sort_two,
437
- recordKey: row.record_key,
438
- }
439
- }