@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.
- package/README.md +125 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3202 -1474
- package/dist/index.js.map +32 -25
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/aggregate-admission.d.ts +10 -0
- package/dist/objects/aggregate-admission.d.ts.map +1 -0
- package/dist/objects/preparation-lock.d.ts +6 -0
- package/dist/objects/preparation-lock.d.ts.map +1 -0
- package/dist/objects/prepare-queries.d.ts +5 -0
- package/dist/objects/prepare-queries.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +19 -0
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts +43 -0
- package/dist/objects/query-indexes.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +7 -1
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +1 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/objects/text-count-index.d.ts +24 -0
- package/dist/objects/text-count-index.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +18 -38
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +3 -26
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +51 -9
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +5 -7
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/outbox.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts +36 -0
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +11 -5
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
- package/dist/ontology-storage/source-roots.d.ts +14 -1
- package/dist/ontology-storage/source-roots.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +1 -2
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-limit-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +2 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +28 -0
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-file-download-grant-storage.d.ts +11 -0
- package/dist/pg-file-download-grant-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/pg-rules-storage.d.ts.map +1 -1
- package/dist/transactions.d.ts +6 -2
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +53 -3
- package/src/migrations/053-replacement-plans.sql +72 -0
- package/src/migrations/054-slim-ontology-outbox.sql +108 -0
- package/src/migrations/055-object-query-preparation.sql +11 -0
- package/src/migrations/056-commit-touches.sql +41 -0
- package/src/migrations/057-audio-transcription.sql +10 -0
- package/src/migrations/058-reranking-model-kind.sql +11 -0
- package/src/migrations/059-file-download-grants.sql +16 -0
- package/src/migrations/060-drained-source-versions.sql +11 -0
- package/src/migrations.ts +20 -0
- package/src/objects/aggregate-admission.ts +21 -0
- package/src/objects/preparation-lock.ts +15 -0
- package/src/objects/prepare-queries.ts +181 -0
- package/src/objects/query-compiler.ts +405 -115
- package/src/objects/query-indexes.ts +348 -0
- package/src/objects/reader.ts +140 -51
- package/src/objects/storage.ts +15 -3
- package/src/objects/text-count-index.ts +455 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +116 -275
- package/src/ontology-storage/materialization-state.ts +8 -259
- package/src/ontology-storage/materialization-writer.ts +618 -508
- package/src/ontology-storage/materializations.ts +106 -289
- package/src/ontology-storage/outbox.ts +46 -15
- package/src/ontology-storage/replacement-plans.ts +1110 -0
- package/src/ontology-storage/shared.ts +27 -15
- package/src/ontology-storage/source-cleanup.ts +22 -10
- package/src/ontology-storage/source-roots.ts +35 -8
- package/src/ontology-storage/sources.ts +0 -30
- package/src/pg-ai-cost-storage.ts +12 -0
- package/src/pg-ai-limit-storage.ts +13 -2
- package/src/pg-ai-usage-storage.ts +8 -0
- package/src/pg-execution-storage.ts +8 -3
- package/src/pg-file-download-grant-storage.ts +76 -0
- package/src/pg-projection-run-storage.ts +6 -1
- package/src/pg-rules-storage.ts +7 -3
- 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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
173
|
+
/** Staged object upserts and deletes of profiled types, or of objects with a stored vector. */
|
|
174
|
+
async *vectorChangePages(
|
|
199
175
|
session: PgMaterializationSessionState,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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 (
|
|
392
|
-
UNIQUE (
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|