@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,4 +1,3 @@
|
|
|
1
|
-
import type { EffectiveChangeCounts } from "@sixb/core/internal/materialization"
|
|
2
1
|
import {
|
|
3
2
|
assertPinnedDatasetWatermark,
|
|
4
3
|
linkRefKey,
|
|
@@ -17,32 +16,30 @@ import {
|
|
|
17
16
|
assertExpectedObjectRevision,
|
|
18
17
|
assertMaterializationFinalizationCorrelation,
|
|
19
18
|
assertMaterializationHeader,
|
|
20
|
-
assertMaterializationLaneCompletion,
|
|
21
19
|
assertPageRows,
|
|
22
|
-
assertPlanChunkCorrelations,
|
|
23
20
|
assertSourceActivationCorrelation,
|
|
21
|
+
beginMaterializationApply,
|
|
22
|
+
beginMaterializationVectorChanges,
|
|
23
|
+
cardinalityViolation,
|
|
24
24
|
invalidCorrelation,
|
|
25
25
|
sameNonnegativeCounts as sameCounts,
|
|
26
26
|
uniqueSorted,
|
|
27
27
|
} from "@sixb/core/internal/ontology-storage-provider"
|
|
28
28
|
import type {
|
|
29
|
-
|
|
29
|
+
AppliedMaterialization,
|
|
30
|
+
ApplyMaterializationInput,
|
|
30
31
|
ApplyMaterializationResult,
|
|
31
32
|
FinalizeMaterializationInput,
|
|
32
|
-
MaterializationObjectExistence,
|
|
33
33
|
MaterializationPlanHeader,
|
|
34
34
|
MaterializationSession,
|
|
35
35
|
MaterializationStatePage,
|
|
36
|
-
|
|
36
|
+
MaterializationVectorChangePage,
|
|
37
37
|
OntologyCommitRecord,
|
|
38
38
|
OntologyMaterializationStorage,
|
|
39
|
-
ReadMaterializationObjectExistenceInput,
|
|
40
39
|
SourceActivationWrite,
|
|
41
|
-
SourceReplacementStatePage,
|
|
42
40
|
StageMaterializationWorkInput,
|
|
43
41
|
StreamMaterializationStateInput,
|
|
44
|
-
|
|
45
|
-
StreamSourceReplacementStateInput,
|
|
42
|
+
StreamMaterializationVectorChangesInput,
|
|
46
43
|
} from "@sixb/core/storage"
|
|
47
44
|
import type { SQLClient } from "../pg-client"
|
|
48
45
|
import { isUniqueViolation } from "../storage-errors"
|
|
@@ -52,12 +49,9 @@ import {
|
|
|
52
49
|
PgMaterializationSessions,
|
|
53
50
|
type PgOntologyTransactionContext,
|
|
54
51
|
} from "./materialization-session"
|
|
55
|
-
import {
|
|
56
|
-
linkSortExpression,
|
|
57
|
-
PG_MATERIALIZATION_WORK_TABLE,
|
|
58
|
-
PgMaterializationStateReader,
|
|
59
|
-
} from "./materialization-state"
|
|
52
|
+
import { linkSortExpression, PgMaterializationStateReader } from "./materialization-state"
|
|
60
53
|
import { PgMaterializationWriter } from "./materialization-writer"
|
|
54
|
+
import { boundReplacementPlan } from "./replacement-plans"
|
|
61
55
|
import {
|
|
62
56
|
assertProjectionExecution,
|
|
63
57
|
commitRecord,
|
|
@@ -70,17 +64,6 @@ import {
|
|
|
70
64
|
} from "./shared"
|
|
71
65
|
import { activateSourceRoots } from "./source-roots"
|
|
72
66
|
|
|
73
|
-
interface ProjectionCountsRow {
|
|
74
|
-
readonly object_classifications: number | string
|
|
75
|
-
readonly link_classifications: number | string
|
|
76
|
-
readonly objects_created: number | string
|
|
77
|
-
readonly objects_updated: number | string
|
|
78
|
-
readonly objects_deleted: number | string
|
|
79
|
-
readonly links_created: number | string
|
|
80
|
-
readonly links_updated: number | string
|
|
81
|
-
readonly links_deleted: number | string
|
|
82
|
-
}
|
|
83
|
-
|
|
84
67
|
interface TelemetrySummaryRow {
|
|
85
68
|
readonly classified_points: number | string
|
|
86
69
|
readonly points_created: number | string
|
|
@@ -114,7 +97,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
114
97
|
|
|
115
98
|
async begin(input: MaterializationPlanHeader): Promise<MaterializationSession> {
|
|
116
99
|
assertMaterializationHeader(input)
|
|
117
|
-
const session = await this.sessions.create(
|
|
100
|
+
const session = await this.sessions.create(
|
|
101
|
+
input,
|
|
102
|
+
input.plan ? await boundReplacementPlan(this.sql, input) : null
|
|
103
|
+
)
|
|
118
104
|
try {
|
|
119
105
|
await lockAdvisoryKeys(this.sql, materializationLockKeys(input))
|
|
120
106
|
await this.assertCommitAbsent(input)
|
|
@@ -220,128 +206,43 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
220
206
|
}
|
|
221
207
|
}
|
|
222
208
|
|
|
223
|
-
async *streamSourceReplacementState(
|
|
224
|
-
input: StreamSourceReplacementStateInput
|
|
225
|
-
): AsyncIterable<SourceReplacementStatePage> {
|
|
226
|
-
const session = this.sessions.require(input.session)
|
|
227
|
-
assertPageRows(input.pageRows)
|
|
228
|
-
const replacement = await this.requireReplacement(session, input)
|
|
229
|
-
const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId)
|
|
230
|
-
if (input.entityKind === "object") {
|
|
231
|
-
if (replacement.projectionKind !== "object") {
|
|
232
|
-
throw new MaterializationConflictError(
|
|
233
|
-
"source-materialization",
|
|
234
|
-
"Link projection replacement cannot stream object state."
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
if (replacement.objectStreamStarted) {
|
|
238
|
-
throw new MaterializationConflictError(
|
|
239
|
-
"effective-state",
|
|
240
|
-
"Replacement object state may only be streamed once per session."
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
replacement.objectStreamStarted = true
|
|
244
|
-
for await (const identities of reader.replacementIdentities({
|
|
245
|
-
sessionId: session.id,
|
|
246
|
-
sourceId: replacement.sourceId,
|
|
247
|
-
candidateMaterializationId: replacement.candidateMaterializationId,
|
|
248
|
-
previousMaterializationId: replacement.previousMaterializationId,
|
|
249
|
-
incremental: replacement.incremental,
|
|
250
|
-
kind: "object",
|
|
251
|
-
pageRows: input.pageRows,
|
|
252
|
-
})) {
|
|
253
|
-
this.sessions.require(input.session)
|
|
254
|
-
const refs = identities.map((identity) => {
|
|
255
|
-
if (identity.kind !== "object") {
|
|
256
|
-
invalidCorrelation("Object replacement returned a link identity.")
|
|
257
|
-
}
|
|
258
|
-
return identity.ref
|
|
259
|
-
})
|
|
260
|
-
const objects = await reader.replacementObjectStates(
|
|
261
|
-
replacement.sourceId,
|
|
262
|
-
replacement.candidateMaterializationId,
|
|
263
|
-
refs
|
|
264
|
-
)
|
|
265
|
-
yield { objects, links: [] }
|
|
266
|
-
}
|
|
267
|
-
this.sessions.require(input.session)
|
|
268
|
-
replacement.objectStreamCompleted = true
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
|
|
273
|
-
throw new MaterializationConflictError(
|
|
274
|
-
"effective-state",
|
|
275
|
-
"Object projection replacement must fully stream object state before link state."
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
if (replacement.linkStreamStarted) {
|
|
279
|
-
throw new MaterializationConflictError(
|
|
280
|
-
"effective-state",
|
|
281
|
-
"Replacement link state may only be streamed once per session."
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
replacement.linkStreamStarted = true
|
|
285
|
-
const materializationIds = [
|
|
286
|
-
replacement.candidateMaterializationId,
|
|
287
|
-
...(replacement.previousMaterializationId ? [replacement.previousMaterializationId] : []),
|
|
288
|
-
]
|
|
289
|
-
for await (const identities of reader.replacementIdentities({
|
|
290
|
-
sessionId: session.id,
|
|
291
|
-
sourceId: replacement.sourceId,
|
|
292
|
-
candidateMaterializationId: replacement.candidateMaterializationId,
|
|
293
|
-
previousMaterializationId: replacement.previousMaterializationId,
|
|
294
|
-
incremental: replacement.incremental,
|
|
295
|
-
kind: "link",
|
|
296
|
-
pageRows: input.pageRows,
|
|
297
|
-
})) {
|
|
298
|
-
this.sessions.require(input.session)
|
|
299
|
-
const linkIdentities = identities.map((identity) => {
|
|
300
|
-
if (identity.kind !== "link") {
|
|
301
|
-
invalidCorrelation("Link replacement returned an object identity.")
|
|
302
|
-
}
|
|
303
|
-
return identity
|
|
304
|
-
})
|
|
305
|
-
const links = await reader.replacementLinkStates(
|
|
306
|
-
replacement.sourceId,
|
|
307
|
-
replacement.candidateMaterializationId,
|
|
308
|
-
materializationIds,
|
|
309
|
-
linkIdentities,
|
|
310
|
-
replacement.incremental
|
|
311
|
-
)
|
|
312
|
-
yield { objects: [], links }
|
|
313
|
-
}
|
|
314
|
-
this.sessions.require(input.session)
|
|
315
|
-
replacement.linkStreamCompleted = true
|
|
316
|
-
}
|
|
317
|
-
|
|
318
209
|
async stageWork(input: StageMaterializationWorkInput): Promise<void> {
|
|
319
210
|
await this.sessions.stage(input)
|
|
320
211
|
}
|
|
321
212
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async readObjectExistence(
|
|
328
|
-
input: ReadMaterializationObjectExistenceInput
|
|
329
|
-
): Promise<readonly MaterializationObjectExistence[]> {
|
|
213
|
+
async *streamVectorChanges(
|
|
214
|
+
input: StreamMaterializationVectorChangesInput
|
|
215
|
+
): AsyncIterable<MaterializationVectorChangePage> {
|
|
330
216
|
const session = this.sessions.require(input.session)
|
|
331
|
-
|
|
217
|
+
assertPageRows(input.pageRows)
|
|
218
|
+
beginMaterializationVectorChanges(session)
|
|
219
|
+
await this.sessions.analyzeSealedWork(session)
|
|
220
|
+
for await (const items of this.sessions.vectorChangePages(
|
|
221
|
+
session,
|
|
222
|
+
input.objectTypeIds,
|
|
223
|
+
input.pageRows
|
|
224
|
+
)) {
|
|
225
|
+
yield { items }
|
|
226
|
+
}
|
|
332
227
|
}
|
|
333
228
|
|
|
334
|
-
async
|
|
229
|
+
async apply(input: ApplyMaterializationInput): Promise<AppliedMaterialization> {
|
|
335
230
|
const session = this.sessions.require(input.session)
|
|
231
|
+
beginMaterializationApply(session)
|
|
232
|
+
await this.sessions.analyzeSealedWork(session)
|
|
233
|
+
await this.assertStagedCardinality(session)
|
|
336
234
|
const { commit } = session.header
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
235
|
+
const applied = await this.writer.applyStaged({
|
|
236
|
+
workTable: session.workTable,
|
|
237
|
+
workId: session.workId,
|
|
238
|
+
projectId: commit.projectId,
|
|
239
|
+
commitId: commit.id,
|
|
240
|
+
committedAt: commit.committedAt,
|
|
241
|
+
})
|
|
242
|
+
session.changedObjects = applied.objectWrites
|
|
243
|
+
session.changedLinks = applied.linkWrites
|
|
244
|
+
session.appliedEventCount = applied.eventCount
|
|
245
|
+
return { eventCount: applied.eventCount }
|
|
345
246
|
}
|
|
346
247
|
|
|
347
248
|
async finalize(input: FinalizeMaterializationInput): Promise<ApplyMaterializationResult> {
|
|
@@ -353,8 +254,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
353
254
|
}
|
|
354
255
|
// The next sparse projection must see the distribution of a newly published bulk load.
|
|
355
256
|
// With empty/stale statistics, a link lookup can scan every edge sharing its target.
|
|
356
|
-
|
|
357
|
-
|
|
257
|
+
await refreshMaterializedStatistics(this.sql, "objects", session.changedObjects)
|
|
258
|
+
await refreshMaterializedStatistics(this.sql, "links", session.changedLinks)
|
|
358
259
|
const record = await this.insertCommit(session.header, input)
|
|
359
260
|
await this.sessions.release(session)
|
|
360
261
|
return { commit: record }
|
|
@@ -364,67 +265,6 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
364
265
|
this.sessions.deactivateAll()
|
|
365
266
|
}
|
|
366
267
|
|
|
367
|
-
private async requireReplacement(
|
|
368
|
-
session: PgMaterializationSessionState,
|
|
369
|
-
input: StreamSourceReplacementStateInput
|
|
370
|
-
): Promise<NonNullable<PgMaterializationSessionState["replacement"]>> {
|
|
371
|
-
if (session.replacement) {
|
|
372
|
-
if (
|
|
373
|
-
session.replacement.sourceId !== input.source.projectionId ||
|
|
374
|
-
session.replacement.candidateMaterializationId !== input.candidateMaterializationId
|
|
375
|
-
) {
|
|
376
|
-
throw new MaterializationConflictError(
|
|
377
|
-
"source-materialization",
|
|
378
|
-
"Materialization session already owns another replacement union."
|
|
379
|
-
)
|
|
380
|
-
}
|
|
381
|
-
return session.replacement
|
|
382
|
-
}
|
|
383
|
-
const projectId = session.header.commit.projectId
|
|
384
|
-
const candidate = await this.getSource(
|
|
385
|
-
projectId,
|
|
386
|
-
input.source.projectionId,
|
|
387
|
-
input.candidateMaterializationId,
|
|
388
|
-
true
|
|
389
|
-
)
|
|
390
|
-
if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
|
|
391
|
-
throw new MaterializationConflictError(
|
|
392
|
-
"source-materialization",
|
|
393
|
-
`Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
await assertProjectionExecution(this.sql, {
|
|
397
|
-
projectId,
|
|
398
|
-
sourceId: input.source.projectionId,
|
|
399
|
-
projectionRunId: candidate.projection_run_id,
|
|
400
|
-
executionToken: candidate.execution_token,
|
|
401
|
-
})
|
|
402
|
-
const previous = await this.getActiveSource(projectId, input.source.projectionId, true)
|
|
403
|
-
if (
|
|
404
|
-
candidate.protocol !== "replacement" ||
|
|
405
|
-
(previous &&
|
|
406
|
-
(previous.protocol !== candidate.protocol ||
|
|
407
|
-
previous.projection_kind !== candidate.projection_kind))
|
|
408
|
-
) {
|
|
409
|
-
throw new MaterializationConflictError(
|
|
410
|
-
"source-materialization",
|
|
411
|
-
"Source replacement kind or protocol does not match its active materialization."
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
session.replacement = {
|
|
415
|
-
sourceId: input.source.projectionId,
|
|
416
|
-
candidateMaterializationId: input.candidateMaterializationId,
|
|
417
|
-
previousMaterializationId: previous?.materialization_id ?? null,
|
|
418
|
-
incremental: candidate.base_materialization_id !== null,
|
|
419
|
-
projectionKind: candidate.projection_kind,
|
|
420
|
-
objectStreamStarted: false,
|
|
421
|
-
objectStreamCompleted: false,
|
|
422
|
-
linkStreamStarted: false,
|
|
423
|
-
linkStreamCompleted: false,
|
|
424
|
-
}
|
|
425
|
-
return session.replacement
|
|
426
|
-
}
|
|
427
|
-
|
|
428
268
|
private async assertCommitAbsent(header: MaterializationPlanHeader): Promise<void> {
|
|
429
269
|
const [duplicateIdentity] = await this.sql<
|
|
430
270
|
{ readonly id: string; readonly idempotency_key: string }[]
|
|
@@ -518,10 +358,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
518
358
|
const { commit } = session.header
|
|
519
359
|
const { result } = input.finalization
|
|
520
360
|
assertMaterializationFinalizationCorrelation(session, input)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (laneCounts.cardinality > 0) await this.assertFinalCardinality(session)
|
|
524
|
-
const eventCount = laneCounts.event
|
|
361
|
+
if (await this.sessions.hasCardinalityWork(session)) await this.assertFinalCardinality(session)
|
|
362
|
+
const eventCount = session.appliedEventCount
|
|
525
363
|
const [outbox] = await this.sql<
|
|
526
364
|
{
|
|
527
365
|
readonly count: number | string
|
|
@@ -558,12 +396,45 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
558
396
|
if (commit.intent.kind === "projection") {
|
|
559
397
|
if (result.kind !== "projection") return
|
|
560
398
|
await this.sessions.assertClassificationCoverage(session)
|
|
561
|
-
|
|
399
|
+
const counts = await this.sessions.projectionCounts(session)
|
|
400
|
+
if (!counts || !sameCounts(result.counts, counts)) {
|
|
562
401
|
invalidCorrelation("Projection result counts do not correlate with finalized work.")
|
|
563
402
|
}
|
|
564
403
|
}
|
|
565
404
|
}
|
|
566
405
|
|
|
406
|
+
/** Rejects a cardinality-one scope that two staged occupants claim, effective view first. */
|
|
407
|
+
private async assertStagedCardinality(session: PgMaterializationSessionState): Promise<void> {
|
|
408
|
+
// Cardinality records are unique per view, scope and link: two occupied in one scope is the
|
|
409
|
+
// violation, and only its scope needs reading back.
|
|
410
|
+
const [violation] = await this.sql<
|
|
411
|
+
{ readonly major_order: number; readonly sort_one: string }[]
|
|
412
|
+
>`
|
|
413
|
+
SELECT major_order, sort_one
|
|
414
|
+
FROM ${this.sql(session.workTable)}
|
|
415
|
+
WHERE work_id = ${session.workId} AND lane = 'cardinality'
|
|
416
|
+
AND cardinality_occupied
|
|
417
|
+
GROUP BY major_order, sort_one
|
|
418
|
+
HAVING COUNT(*) > 1
|
|
419
|
+
ORDER BY major_order, sort_one
|
|
420
|
+
LIMIT 1
|
|
421
|
+
`
|
|
422
|
+
if (!violation) return
|
|
423
|
+
const [scope] = await this.sql<{ readonly source_type_id: string; readonly link_id: string }[]>`
|
|
424
|
+
SELECT payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
|
|
425
|
+
payload->'ref'->>'linkId' AS link_id
|
|
426
|
+
FROM ${this.sql(session.workTable)}
|
|
427
|
+
WHERE work_id = ${session.workId} AND lane = 'cardinality'
|
|
428
|
+
AND major_order = ${violation.major_order} AND sort_one = ${violation.sort_one}
|
|
429
|
+
LIMIT 1
|
|
430
|
+
`
|
|
431
|
+
throw cardinalityViolation(
|
|
432
|
+
violation.major_order === 0 ? "effective" : "candidate",
|
|
433
|
+
scope?.source_type_id ?? "",
|
|
434
|
+
scope?.link_id ?? ""
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
567
438
|
private async assertFinalCardinality(session: PgMaterializationSessionState): Promise<void> {
|
|
568
439
|
// A mixed work table can hide a few occupied scopes among millions of other records.
|
|
569
440
|
// Even ANALYZE can miss that population. Isolate it before planning the final join so
|
|
@@ -574,21 +445,19 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
574
445
|
await this.sql.unsafe(
|
|
575
446
|
`CREATE TEMP TABLE ${table} ON COMMIT DROP AS
|
|
576
447
|
SELECT sort_one AS scope_sort_key, sort_two AS link_sort_key,
|
|
577
|
-
|
|
448
|
+
cardinality_occupied AS occupied,
|
|
578
449
|
payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
|
|
579
450
|
payload->'ref'->'source'->>'primaryId' AS source_id,
|
|
580
451
|
payload->'ref'->>'linkId' AS link_id
|
|
581
|
-
FROM ${
|
|
582
|
-
WHERE
|
|
583
|
-
[session.
|
|
452
|
+
FROM ${session.workTable}
|
|
453
|
+
WHERE work_id = $1 AND lane = 'cardinality' AND major_order = 0`,
|
|
454
|
+
[session.workId]
|
|
584
455
|
)
|
|
585
456
|
await this.sql.unsafe(`ANALYZE ${table}`)
|
|
586
|
-
|
|
457
|
+
// Apply already rejected a scope with two staged occupants; this checks what it wrote.
|
|
458
|
+
const [violation] = await this.sql.unsafe<{ readonly mismatch: number }[]>(
|
|
587
459
|
`WITH work AS (
|
|
588
460
|
SELECT * FROM ${table}
|
|
589
|
-
), duplicate AS (
|
|
590
|
-
SELECT scope_sort_key FROM work WHERE occupied
|
|
591
|
-
GROUP BY scope_sort_key HAVING COUNT(*) > 1
|
|
592
461
|
), scopes AS (
|
|
593
462
|
SELECT DISTINCT scope_sort_key, source_type_id, source_id, link_id FROM work
|
|
594
463
|
), expected AS (
|
|
@@ -604,16 +473,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
604
473
|
UNION ALL
|
|
605
474
|
(SELECT * FROM actual EXCEPT SELECT * FROM expected)
|
|
606
475
|
)
|
|
607
|
-
SELECT
|
|
608
|
-
UNION ALL
|
|
609
|
-
SELECT 'mismatch'::text AS reason FROM differences
|
|
610
|
-
LIMIT 1`,
|
|
476
|
+
SELECT 1 AS mismatch FROM differences LIMIT 1`,
|
|
611
477
|
[session.header.commit.projectId]
|
|
612
478
|
)
|
|
613
479
|
await this.sql.unsafe(`DROP TABLE ${table}`)
|
|
614
|
-
if (violation?.reason === "duplicate") {
|
|
615
|
-
invalidCorrelation("Materialization cardinality work violates cardinality-one.")
|
|
616
|
-
}
|
|
617
480
|
if (violation) {
|
|
618
481
|
invalidCorrelation(
|
|
619
482
|
"Materialization cardinality work does not match the final effective link scope."
|
|
@@ -621,65 +484,6 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
621
484
|
}
|
|
622
485
|
}
|
|
623
486
|
|
|
624
|
-
private async projectionCounts(
|
|
625
|
-
session: PgMaterializationSessionState
|
|
626
|
-
): Promise<EffectiveChangeCounts> {
|
|
627
|
-
const [row] = await this.sql<ProjectionCountsRow[]>`
|
|
628
|
-
SELECT
|
|
629
|
-
COUNT(*) FILTER (
|
|
630
|
-
WHERE kind = 'classification' AND payload->>'entityKind' = 'object'
|
|
631
|
-
) AS object_classifications,
|
|
632
|
-
COUNT(*) FILTER (
|
|
633
|
-
WHERE kind = 'classification' AND payload->>'entityKind' = 'link'
|
|
634
|
-
) AS link_classifications,
|
|
635
|
-
COUNT(*) FILTER (
|
|
636
|
-
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
|
|
637
|
-
AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
638
|
-
) AS objects_created,
|
|
639
|
-
COUNT(*) FILTER (
|
|
640
|
-
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
|
|
641
|
-
AND (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
642
|
-
) AS objects_updated,
|
|
643
|
-
COUNT(*) FILTER (
|
|
644
|
-
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-delete'
|
|
645
|
-
) AS objects_deleted,
|
|
646
|
-
COUNT(*) FILTER (
|
|
647
|
-
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
|
|
648
|
-
AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
649
|
-
) AS links_created,
|
|
650
|
-
COUNT(*) FILTER (
|
|
651
|
-
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
|
|
652
|
-
AND (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
653
|
-
) AS links_updated,
|
|
654
|
-
COUNT(*) FILTER (
|
|
655
|
-
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-delete'
|
|
656
|
-
) AS links_deleted
|
|
657
|
-
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
658
|
-
WHERE session_id = ${session.id}
|
|
659
|
-
`
|
|
660
|
-
const objectsCreated = databaseCount(row?.objects_created)
|
|
661
|
-
const objectsUpdated = databaseCount(row?.objects_updated)
|
|
662
|
-
const objectsDeleted = databaseCount(row?.objects_deleted)
|
|
663
|
-
const linksCreated = databaseCount(row?.links_created)
|
|
664
|
-
const linksUpdated = databaseCount(row?.links_updated)
|
|
665
|
-
const linksDeleted = databaseCount(row?.links_deleted)
|
|
666
|
-
return {
|
|
667
|
-
objectsCreated,
|
|
668
|
-
objectsUpdated,
|
|
669
|
-
objectsDeleted,
|
|
670
|
-
objectsUnchanged:
|
|
671
|
-
databaseCount(row?.object_classifications) -
|
|
672
|
-
objectsCreated -
|
|
673
|
-
objectsUpdated -
|
|
674
|
-
objectsDeleted,
|
|
675
|
-
linksCreated,
|
|
676
|
-
linksUpdated,
|
|
677
|
-
linksDeleted,
|
|
678
|
-
linksUnchanged:
|
|
679
|
-
databaseCount(row?.link_classifications) - linksCreated - linksUpdated - linksDeleted,
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
487
|
private async telemetrySummary(session: PgMaterializationSessionState, pointCount: number) {
|
|
684
488
|
const [row] = await this.sql<TelemetrySummaryRow[]>`
|
|
685
489
|
SELECT
|
|
@@ -697,8 +501,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
697
501
|
COUNT(*) FILTER (
|
|
698
502
|
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
|
|
699
503
|
) AS latest_objects_changed
|
|
700
|
-
FROM ${this.sql(
|
|
701
|
-
WHERE
|
|
504
|
+
FROM ${this.sql(session.workTable)}
|
|
505
|
+
WHERE work_id = ${session.workId}
|
|
702
506
|
`
|
|
703
507
|
const pointsCreated = databaseCount(row?.points_created)
|
|
704
508
|
const pointsUpdated = databaseCount(row?.points_updated)
|
|
@@ -828,20 +632,17 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
|
|
|
828
632
|
): Promise<OntologyCommitRecord> {
|
|
829
633
|
const { commit } = header
|
|
830
634
|
const origin = originColumns(commit.origin)
|
|
831
|
-
const requestedBy =
|
|
832
|
-
commit.requestedBy === undefined ? null : jsonParameter(this.sql, commit.requestedBy)
|
|
833
635
|
const rows = await this.sql<PgOntologyCommitRow[]>`
|
|
834
636
|
INSERT INTO ontology_commits (
|
|
835
637
|
project_id, id, idempotency_key, request_hash, execution_id,
|
|
836
|
-
origin_kind, origin_run_id, origin_batch_ordinal, origin,
|
|
638
|
+
origin_kind, origin_run_id, origin_batch_ordinal, origin,
|
|
837
639
|
ontology_revision, projection_revision, ownership_hash,
|
|
838
640
|
intent, result, committed_at
|
|
839
641
|
) VALUES (
|
|
840
642
|
${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
|
|
841
643
|
${commit.executionId},
|
|
842
644
|
${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
|
|
843
|
-
${jsonParameter(this.sql, commit.origin)}, ${
|
|
844
|
-
${jsonParameter(this.sql, commit.executor)}, ${commit.ontologyRevision},
|
|
645
|
+
${jsonParameter(this.sql, commit.origin)}, ${commit.ontologyRevision},
|
|
845
646
|
${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
|
|
846
647
|
${jsonParameter(this.sql, commit.intent)},
|
|
847
648
|
${jsonParameter(this.sql, input.finalization.result)}, ${commit.committedAt}
|
|
@@ -935,3 +736,19 @@ function materializationLockKeys(header: MaterializationPlanHeader): string[] {
|
|
|
935
736
|
function databaseCount(value: number | string | undefined): number {
|
|
936
737
|
return Number(value ?? 0)
|
|
937
738
|
}
|
|
739
|
+
|
|
740
|
+
/** Keep the first bulk publication queryable without analyzing every small subsequent delta.
|
|
741
|
+
* Expression indexes make full ANALYZE expensive. Only a substantial current publication
|
|
742
|
+
* forces synchronous work. Autovacuum accumulates smaller writes separately; its modification
|
|
743
|
+
* count can still include a previous transaction's manually analyzed writes at this point. */
|
|
744
|
+
async function refreshMaterializedStatistics(
|
|
745
|
+
sql: SQLClient,
|
|
746
|
+
table: "objects" | "links",
|
|
747
|
+
changed: number
|
|
748
|
+
): Promise<void> {
|
|
749
|
+
if (changed < 1_000) return
|
|
750
|
+
const [state] = await sql<{ rows: number }[]>`
|
|
751
|
+
SELECT reltuples AS rows FROM pg_class WHERE oid=${table}::regclass`
|
|
752
|
+
if (!state || state.rows <= 0 || changed >= Math.max(10_000, state.rows * 0.1))
|
|
753
|
+
await sql.unsafe(`ANALYZE ${table}`)
|
|
754
|
+
}
|
|
@@ -2,7 +2,12 @@ import { serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
|
2
2
|
import {
|
|
3
3
|
MaterializationConflictError,
|
|
4
4
|
MaterializationValidationError,
|
|
5
|
+
type OntologyMaterializationEventCommit,
|
|
5
6
|
} from "@sixb/core/internal/materialization"
|
|
7
|
+
import {
|
|
8
|
+
claimedOutboxRows,
|
|
9
|
+
type OutboxEventContext,
|
|
10
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
6
11
|
import type {
|
|
7
12
|
ClaimedOntologyOutboxRow,
|
|
8
13
|
ClaimOntologyOutboxInput,
|
|
@@ -14,15 +19,24 @@ import type {
|
|
|
14
19
|
SummarizeOntologyOutboxInput,
|
|
15
20
|
} from "@sixb/core/storage"
|
|
16
21
|
import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage"
|
|
22
|
+
import { type PgExecutionRow, pgExecutionRecord } from "../pg-execution-storage"
|
|
17
23
|
import {
|
|
18
24
|
assertNonblank,
|
|
19
25
|
assertPositiveInteger,
|
|
20
26
|
assertTimestamp,
|
|
21
|
-
|
|
27
|
+
claimedOutboxDraft,
|
|
22
28
|
type PgOntologyOutboxRow,
|
|
23
29
|
type PgRootOperation,
|
|
30
|
+
toIsoString,
|
|
24
31
|
} from "./shared"
|
|
25
32
|
|
|
33
|
+
/** A commit's own fields, prefixed, beside every column of the execution it ran under. */
|
|
34
|
+
interface PgOutboxContextRow extends PgExecutionRow {
|
|
35
|
+
readonly commit_id: string
|
|
36
|
+
readonly commit_committed_at: Date | string
|
|
37
|
+
readonly commit_origin: OntologyMaterializationEventCommit["origin"]
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
27
41
|
constructor(private readonly runRootOperation: PgRootOperation) {}
|
|
28
42
|
|
|
@@ -57,23 +71,28 @@ export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
|
57
71
|
lease_expires_at = ${input.leaseExpiresAt}
|
|
58
72
|
FROM candidates
|
|
59
73
|
WHERE outbox.project_id = candidates.project_id AND outbox.id = candidates.id
|
|
60
|
-
RETURNING outbox.id
|
|
61
|
-
outbox.attempts, outbox.lease_id, outbox.lease_expires_at,
|
|
62
|
-
outbox.published_at, outbox.last_failure, outbox.created_at
|
|
63
|
-
outbox.commit_id, outbox.commit_ordinal
|
|
74
|
+
RETURNING outbox.id, outbox.commit_id, outbox.commit_ordinal, outbox.event,
|
|
75
|
+
outbox.available_at, outbox.attempts, outbox.lease_id, outbox.lease_expires_at,
|
|
76
|
+
outbox.published_at, outbox.last_failure, outbox.created_at
|
|
64
77
|
)
|
|
65
78
|
SELECT * FROM claimed ORDER BY created_at, commit_id, commit_ordinal
|
|
66
79
|
`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
if (rows.length === 0) return []
|
|
81
|
+
// Each commit and its execution are read once, however many of its events the batch holds.
|
|
82
|
+
const commitIds = [...new Set(rows.map((row) => row.commit_id))]
|
|
83
|
+
const contexts = await sql<PgOutboxContextRow[]>`
|
|
84
|
+
SELECT commits.id AS commit_id, commits.committed_at AS commit_committed_at,
|
|
85
|
+
commits.origin AS commit_origin, executions.*
|
|
86
|
+
FROM ontology_commits AS commits
|
|
87
|
+
JOIN executions
|
|
88
|
+
ON executions.project_id = commits.project_id AND executions.id = commits.execution_id
|
|
89
|
+
WHERE commits.project_id = ${input.projectId}
|
|
90
|
+
AND commits.id = ANY(${sql.array(commitIds)}::text[])
|
|
91
|
+
`
|
|
92
|
+
const contextOf = new Map(
|
|
93
|
+
contexts.map((row) => [row.commit_id, outboxEventContext(input.projectId, row)])
|
|
94
|
+
)
|
|
95
|
+
return claimedOutboxRows(rows.map(claimedOutboxDraft), (commitId) => contextOf.get(commitId))
|
|
77
96
|
})
|
|
78
97
|
}
|
|
79
98
|
|
|
@@ -192,6 +211,18 @@ export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
|
|
|
192
211
|
}
|
|
193
212
|
}
|
|
194
213
|
|
|
214
|
+
function outboxEventContext(projectId: string, row: PgOutboxContextRow): OutboxEventContext {
|
|
215
|
+
return {
|
|
216
|
+
commit: {
|
|
217
|
+
projectId,
|
|
218
|
+
id: row.commit_id,
|
|
219
|
+
committedAt: toIsoString(row.commit_committed_at),
|
|
220
|
+
origin: row.commit_origin,
|
|
221
|
+
},
|
|
222
|
+
execution: pgExecutionRecord(row),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
function toOptionalIsoString(value: Date | string | null | undefined): string | null {
|
|
196
227
|
if (value === null || value === undefined) return null
|
|
197
228
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
|