@sixb/pg 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- package/src/transactions.ts +58 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { MaterializationConflictError } from "@sixb/core/internal/materialization"
|
|
2
|
+
import {
|
|
3
|
+
correlateMaterializationChunk,
|
|
4
|
+
duplicateMaterializationWork as duplicateWork,
|
|
5
|
+
invalidCorrelation,
|
|
6
|
+
materializationPlanItems,
|
|
7
|
+
ProviderMaterializationSessionState,
|
|
8
|
+
type ProviderMaterializationTransactionLifecycle,
|
|
9
|
+
prepareMaterializationWork,
|
|
10
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
11
|
+
import type {
|
|
12
|
+
MaterializationEventWorkRecord,
|
|
13
|
+
MaterializationPlanChunk,
|
|
14
|
+
MaterializationPlanHeader,
|
|
15
|
+
MaterializationSession,
|
|
16
|
+
MaterializationWorkPage,
|
|
17
|
+
MaterializationWorkRecord,
|
|
18
|
+
StageMaterializationWorkInput,
|
|
19
|
+
StreamMaterializationWorkInput,
|
|
20
|
+
} from "@sixb/core/storage"
|
|
21
|
+
import type { SQLClient } from "../pg-client"
|
|
22
|
+
import { PG_MATERIALIZATION_WORK_TABLE, PG_REPLACEMENT_WORK_TABLE } from "./materialization-state"
|
|
23
|
+
import { jsonParameter } from "./shared"
|
|
24
|
+
|
|
25
|
+
export interface PgOntologyTransactionContext {
|
|
26
|
+
readonly id: object
|
|
27
|
+
readonly materializations: ProviderMaterializationTransactionLifecycle
|
|
28
|
+
active: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WorkCursor {
|
|
32
|
+
readonly majorOrder: number
|
|
33
|
+
readonly minorOrder: number
|
|
34
|
+
readonly sortOne: string
|
|
35
|
+
readonly sortTwo: string
|
|
36
|
+
readonly recordKey: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PgChunkSequenceProgress {
|
|
40
|
+
readonly appliedPlanCount: number
|
|
41
|
+
readonly appliedOutboxCount: number
|
|
42
|
+
readonly appliedPlanCursor: WorkCursor | null
|
|
43
|
+
readonly appliedEventCursor: WorkCursor | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class PgMaterializationSessionState extends ProviderMaterializationSessionState {
|
|
47
|
+
appliedPlanCursor: WorkCursor | null = null
|
|
48
|
+
appliedEventCursor: WorkCursor | null = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface WorkDatabaseRow {
|
|
52
|
+
readonly record_key: string
|
|
53
|
+
readonly major_order: number
|
|
54
|
+
readonly minor_order: number
|
|
55
|
+
readonly sort_one: string
|
|
56
|
+
readonly sort_two: string
|
|
57
|
+
readonly payload: unknown
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class PgMaterializationSessions {
|
|
61
|
+
private readonly sessions = new WeakMap<object, PgMaterializationSessionState>()
|
|
62
|
+
private readonly live = new Set<PgMaterializationSessionState>()
|
|
63
|
+
private tablesReady = false
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
private readonly sql: SQLClient,
|
|
67
|
+
private readonly context: PgOntologyTransactionContext | null
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
async create(header: MaterializationPlanHeader): Promise<PgMaterializationSessionState> {
|
|
71
|
+
if (!this.context?.active) {
|
|
72
|
+
throw new MaterializationConflictError(
|
|
73
|
+
"effective-state",
|
|
74
|
+
"Materialization sessions require an active storage transaction."
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
await this.ensureTables()
|
|
78
|
+
const session = new PgMaterializationSessionState(structuredClone(header), this.context.id)
|
|
79
|
+
this.sessions.set(session.providerToken, session)
|
|
80
|
+
this.live.add(session)
|
|
81
|
+
this.context.materializations.register(session.providerToken)
|
|
82
|
+
return session
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
require(session: MaterializationSession): PgMaterializationSessionState {
|
|
86
|
+
const value = this.sessions.get(session.providerToken)
|
|
87
|
+
if (
|
|
88
|
+
!value ||
|
|
89
|
+
!value.active ||
|
|
90
|
+
!this.context?.active ||
|
|
91
|
+
value.transactionId !== this.context.id
|
|
92
|
+
) {
|
|
93
|
+
throw new MaterializationConflictError(
|
|
94
|
+
"effective-state",
|
|
95
|
+
"Materialization session is inactive."
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
return value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async release(session: PgMaterializationSessionState): Promise<void> {
|
|
102
|
+
if (!session.active) return
|
|
103
|
+
await this.deleteWork(session.id)
|
|
104
|
+
session.active = false
|
|
105
|
+
session.replacement = null
|
|
106
|
+
this.live.delete(session)
|
|
107
|
+
this.context?.materializations.complete(session.providerToken)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
deactivateAll(): void {
|
|
111
|
+
for (const session of this.live) {
|
|
112
|
+
session.active = false
|
|
113
|
+
session.replacement = null
|
|
114
|
+
}
|
|
115
|
+
this.live.clear()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async stage(input: StageMaterializationWorkInput): Promise<void> {
|
|
119
|
+
const session = this.require(input.session)
|
|
120
|
+
const records = prepareMaterializationWork(session, input)
|
|
121
|
+
if (records.length === 0) return
|
|
122
|
+
const keys = records.map(({ record }) => record.recordKey)
|
|
123
|
+
const uniqueKeys = records.map(({ uniqueKey }) => uniqueKey)
|
|
124
|
+
|
|
125
|
+
const [duplicate] = await this.sql<{ readonly record_key: string }[]>`
|
|
126
|
+
SELECT record_key
|
|
127
|
+
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
128
|
+
WHERE session_id = ${session.id}
|
|
129
|
+
AND (
|
|
130
|
+
record_key = ANY(${this.sql.array(keys)}::text[])
|
|
131
|
+
OR unique_key = ANY(${this.sql.array(uniqueKeys)}::text[])
|
|
132
|
+
)
|
|
133
|
+
LIMIT 1
|
|
134
|
+
`
|
|
135
|
+
if (duplicate) throw duplicateWork(duplicate.record_key)
|
|
136
|
+
|
|
137
|
+
const payload = records.map(({ record, uniqueKey, columns }) => {
|
|
138
|
+
return {
|
|
139
|
+
recordKey: record.recordKey,
|
|
140
|
+
uniqueKey,
|
|
141
|
+
kind: record.kind,
|
|
142
|
+
lane: columns.lane,
|
|
143
|
+
majorOrder: columns.majorOrder,
|
|
144
|
+
minorOrder: columns.minorOrder,
|
|
145
|
+
sortOne: columns.sortOne,
|
|
146
|
+
sortTwo: columns.sortTwo,
|
|
147
|
+
record,
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
await this.sql`
|
|
151
|
+
WITH staged AS (
|
|
152
|
+
SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, payload)}::jsonb)
|
|
153
|
+
)
|
|
154
|
+
INSERT INTO ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} (
|
|
155
|
+
session_id, record_key, unique_key, kind, lane,
|
|
156
|
+
major_order, minor_order, sort_one, sort_two, payload
|
|
157
|
+
)
|
|
158
|
+
SELECT ${session.id}, value->>'recordKey', value->>'uniqueKey', value->>'kind',
|
|
159
|
+
value->>'lane', (value->>'majorOrder')::integer, (value->>'minorOrder')::integer,
|
|
160
|
+
value->>'sortOne', value->>'sortTwo', value->'record'
|
|
161
|
+
FROM staged
|
|
162
|
+
`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async *stream(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
|
|
166
|
+
const session = this.require(input.session)
|
|
167
|
+
const stream = session.workStreams[input.order]
|
|
168
|
+
if (stream.started) {
|
|
169
|
+
throw new MaterializationConflictError(
|
|
170
|
+
"effective-state",
|
|
171
|
+
`Materialization ${input.order} work may only be streamed once per session.`
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
stream.started = true
|
|
175
|
+
session.workSealed = true
|
|
176
|
+
let cursor: WorkCursor | null = null
|
|
177
|
+
while (true) {
|
|
178
|
+
this.require(input.session)
|
|
179
|
+
const rows = await this.readLane(input.order, session.id, cursor, input.pageRows)
|
|
180
|
+
if (rows.length === 0) break
|
|
181
|
+
const records = rows.map((row) =>
|
|
182
|
+
structuredClone(row.payload)
|
|
183
|
+
) as MaterializationWorkPage["records"]
|
|
184
|
+
cursor = workCursor(rows[rows.length - 1]!)
|
|
185
|
+
stream.emittedCount += records.length
|
|
186
|
+
yield { records }
|
|
187
|
+
}
|
|
188
|
+
this.require(input.session)
|
|
189
|
+
stream.completed = true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async readObjectExistence(
|
|
193
|
+
session: PgMaterializationSessionState,
|
|
194
|
+
refs: readonly { readonly objectTypeId: string; readonly primaryId: string }[]
|
|
195
|
+
): Promise<
|
|
196
|
+
readonly {
|
|
197
|
+
readonly ref: { readonly objectTypeId: string; readonly primaryId: string }
|
|
198
|
+
readonly exists: boolean
|
|
199
|
+
}[]
|
|
200
|
+
> {
|
|
201
|
+
if (refs.length === 0) return []
|
|
202
|
+
const keys = refs.map(
|
|
203
|
+
(ref) => `object-existence:${JSON.stringify([ref.objectTypeId, ref.primaryId])}`
|
|
204
|
+
)
|
|
205
|
+
const rows = await this.sql<{ readonly unique_key: string; readonly payload: unknown }[]>`
|
|
206
|
+
SELECT unique_key, payload FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
207
|
+
WHERE session_id = ${session.id}
|
|
208
|
+
AND unique_key = ANY(${this.sql.array(keys)}::text[])
|
|
209
|
+
AND kind = 'object-existence'
|
|
210
|
+
`
|
|
211
|
+
const found = new Map(rows.map((row) => [row.unique_key, row.payload] as const))
|
|
212
|
+
return keys.flatMap((key) => {
|
|
213
|
+
const payload = found.get(key)
|
|
214
|
+
if (!payload) return []
|
|
215
|
+
const record = structuredClone(payload) as Extract<
|
|
216
|
+
MaterializationWorkRecord,
|
|
217
|
+
{ readonly kind: "object-existence" }
|
|
218
|
+
>
|
|
219
|
+
return [{ ref: record.ref, exists: record.exists }]
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async prepareChunkSequence(
|
|
224
|
+
session: PgMaterializationSessionState,
|
|
225
|
+
chunk: MaterializationPlanChunk
|
|
226
|
+
): Promise<PgChunkSequenceProgress> {
|
|
227
|
+
const items = materializationPlanItems(chunk)
|
|
228
|
+
const planRows = await this.readLane(
|
|
229
|
+
"apply",
|
|
230
|
+
session.id,
|
|
231
|
+
session.appliedPlanCursor,
|
|
232
|
+
items.length
|
|
233
|
+
)
|
|
234
|
+
const expectedItems = planRows.map(
|
|
235
|
+
(row) =>
|
|
236
|
+
(
|
|
237
|
+
structuredClone(row.payload) as Extract<
|
|
238
|
+
MaterializationWorkRecord,
|
|
239
|
+
{ readonly kind: "plan" }
|
|
240
|
+
>
|
|
241
|
+
).item
|
|
242
|
+
)
|
|
243
|
+
const eventRows = await this.readLane(
|
|
244
|
+
"event",
|
|
245
|
+
session.id,
|
|
246
|
+
session.appliedEventCursor,
|
|
247
|
+
chunk.outbox.length
|
|
248
|
+
)
|
|
249
|
+
const expectedEvents = eventRows.map(
|
|
250
|
+
(row) => structuredClone(row.payload) as MaterializationEventWorkRecord
|
|
251
|
+
)
|
|
252
|
+
const progress = correlateMaterializationChunk(
|
|
253
|
+
session,
|
|
254
|
+
chunk,
|
|
255
|
+
items,
|
|
256
|
+
expectedItems,
|
|
257
|
+
expectedEvents
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
...progress,
|
|
262
|
+
appliedPlanCursor:
|
|
263
|
+
planRows.length === 0
|
|
264
|
+
? session.appliedPlanCursor
|
|
265
|
+
: workCursor(planRows[planRows.length - 1]!),
|
|
266
|
+
appliedEventCursor:
|
|
267
|
+
eventRows.length === 0
|
|
268
|
+
? session.appliedEventCursor
|
|
269
|
+
: workCursor(eventRows[eventRows.length - 1]!),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
commitChunkSequence(
|
|
274
|
+
session: PgMaterializationSessionState,
|
|
275
|
+
progress: PgChunkSequenceProgress
|
|
276
|
+
): void {
|
|
277
|
+
session.appliedPlanCount = progress.appliedPlanCount
|
|
278
|
+
session.appliedOutboxCount = progress.appliedOutboxCount
|
|
279
|
+
session.appliedPlanCursor = progress.appliedPlanCursor
|
|
280
|
+
session.appliedEventCursor = progress.appliedEventCursor
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async count(session: PgMaterializationSessionState, kind: string): Promise<number> {
|
|
284
|
+
const [row] = await this.sql<{ readonly count: number | string }[]>`
|
|
285
|
+
SELECT COUNT(*) AS count FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
286
|
+
WHERE session_id = ${session.id} AND kind = ${kind}
|
|
287
|
+
`
|
|
288
|
+
return Number(row?.count ?? 0)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async laneCounts(session: PgMaterializationSessionState): Promise<{
|
|
292
|
+
readonly apply: number
|
|
293
|
+
readonly cardinality: number
|
|
294
|
+
readonly event: number
|
|
295
|
+
}> {
|
|
296
|
+
const rows = await this.sql<
|
|
297
|
+
{ readonly lane: "apply" | "cardinality" | "event"; readonly count: number | string }[]
|
|
298
|
+
>`
|
|
299
|
+
SELECT lane, COUNT(*) AS count FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
300
|
+
WHERE session_id = ${session.id} AND lane <> 'none'
|
|
301
|
+
GROUP BY lane
|
|
302
|
+
`
|
|
303
|
+
const counts = { apply: 0, cardinality: 0, event: 0 }
|
|
304
|
+
for (const row of rows) counts[row.lane] = Number(row.count)
|
|
305
|
+
return counts
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async assertClassificationCoverage(session: PgMaterializationSessionState): Promise<void> {
|
|
309
|
+
if (!session.replacement) return
|
|
310
|
+
const includeObjects = session.replacement.projectionKind === "object"
|
|
311
|
+
const [mismatch] = await this.sql<{ readonly marker: number }[]>`
|
|
312
|
+
WITH expected AS (
|
|
313
|
+
SELECT entity_kind, identity_key
|
|
314
|
+
FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)}
|
|
315
|
+
WHERE session_id = ${session.id} AND diff_required
|
|
316
|
+
AND (entity_kind = 'link' OR ${includeObjects})
|
|
317
|
+
), actual AS (
|
|
318
|
+
SELECT payload->>'entityKind' AS entity_kind,
|
|
319
|
+
payload->>'identityKey' AS identity_key
|
|
320
|
+
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
321
|
+
WHERE session_id = ${session.id} AND kind = 'classification'
|
|
322
|
+
AND payload->>'entityKind' IN ('object', 'link')
|
|
323
|
+
), differences AS (
|
|
324
|
+
(SELECT * FROM expected EXCEPT SELECT * FROM actual)
|
|
325
|
+
UNION ALL
|
|
326
|
+
(SELECT * FROM actual EXCEPT SELECT * FROM expected)
|
|
327
|
+
), invalid AS (
|
|
328
|
+
SELECT 1 AS marker FROM differences
|
|
329
|
+
UNION ALL
|
|
330
|
+
SELECT 1 AS marker FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
331
|
+
WHERE session_id = ${session.id} AND kind = 'classification'
|
|
332
|
+
AND payload->>'entityKind' = 'point'
|
|
333
|
+
)
|
|
334
|
+
SELECT marker FROM invalid LIMIT 1
|
|
335
|
+
`
|
|
336
|
+
if (mismatch) {
|
|
337
|
+
invalidCorrelation(
|
|
338
|
+
"Projection replacement classification coverage does not match its streamed state."
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private async readLane(
|
|
344
|
+
lane: StreamMaterializationWorkInput["order"],
|
|
345
|
+
sessionId: string,
|
|
346
|
+
cursor: WorkCursor | null,
|
|
347
|
+
limit: number
|
|
348
|
+
): Promise<WorkDatabaseRow[]> {
|
|
349
|
+
if (limit === 0) return []
|
|
350
|
+
const after = cursor
|
|
351
|
+
? this.sql`AND (major_order, minor_order, sort_one, sort_two, record_key) >
|
|
352
|
+
(${cursor.majorOrder}, ${cursor.minorOrder}, ${cursor.sortOne}, ${cursor.sortTwo},
|
|
353
|
+
${cursor.recordKey})`
|
|
354
|
+
: this.sql``
|
|
355
|
+
return this.sql<WorkDatabaseRow[]>`
|
|
356
|
+
SELECT record_key, major_order, minor_order, sort_one, sort_two, payload
|
|
357
|
+
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
358
|
+
WHERE session_id = ${sessionId} AND lane = ${lane} ${after}
|
|
359
|
+
ORDER BY major_order, minor_order, sort_one, sort_two, record_key
|
|
360
|
+
LIMIT ${limit}
|
|
361
|
+
`
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async ensureTables(): Promise<void> {
|
|
365
|
+
if (this.tablesReady) return
|
|
366
|
+
await this.sql`
|
|
367
|
+
CREATE TEMP TABLE ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} (
|
|
368
|
+
session_id TEXT NOT NULL,
|
|
369
|
+
record_key TEXT COLLATE "C" NOT NULL,
|
|
370
|
+
unique_key TEXT NOT NULL,
|
|
371
|
+
kind TEXT NOT NULL,
|
|
372
|
+
lane TEXT NOT NULL,
|
|
373
|
+
major_order INTEGER NOT NULL,
|
|
374
|
+
minor_order INTEGER NOT NULL,
|
|
375
|
+
sort_one TEXT COLLATE "C" NOT NULL,
|
|
376
|
+
sort_two TEXT COLLATE "C" NOT NULL,
|
|
377
|
+
payload JSONB NOT NULL,
|
|
378
|
+
PRIMARY KEY (session_id, record_key),
|
|
379
|
+
UNIQUE (session_id, unique_key)
|
|
380
|
+
) ON COMMIT DROP
|
|
381
|
+
`
|
|
382
|
+
await this.sql`
|
|
383
|
+
CREATE INDEX ontology_materialization_work_lane
|
|
384
|
+
ON ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} (
|
|
385
|
+
session_id, lane, major_order, minor_order, sort_one, sort_two, record_key
|
|
386
|
+
)
|
|
387
|
+
`
|
|
388
|
+
await this.sql`
|
|
389
|
+
CREATE TEMP TABLE ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
|
|
390
|
+
session_id TEXT NOT NULL,
|
|
391
|
+
entity_kind TEXT NOT NULL,
|
|
392
|
+
identity_key TEXT COLLATE "C" NOT NULL,
|
|
393
|
+
sort_key TEXT COLLATE "C" NOT NULL,
|
|
394
|
+
diff_required BOOLEAN NOT NULL,
|
|
395
|
+
PRIMARY KEY (session_id, entity_kind, identity_key)
|
|
396
|
+
) ON COMMIT DROP
|
|
397
|
+
`
|
|
398
|
+
await this.sql`
|
|
399
|
+
CREATE INDEX ontology_replacement_work_order
|
|
400
|
+
ON ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
|
|
401
|
+
session_id, entity_kind, sort_key, identity_key
|
|
402
|
+
)
|
|
403
|
+
`
|
|
404
|
+
this.tablesReady = true
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async deleteWork(sessionId: string): Promise<void> {
|
|
408
|
+
if (!this.tablesReady) return
|
|
409
|
+
await this.sql`
|
|
410
|
+
DELETE FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)} WHERE session_id = ${sessionId}
|
|
411
|
+
`
|
|
412
|
+
await this.sql`
|
|
413
|
+
DELETE FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)} WHERE session_id = ${sessionId}
|
|
414
|
+
`
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function workCursor(row: WorkDatabaseRow): WorkCursor {
|
|
419
|
+
return {
|
|
420
|
+
majorOrder: row.major_order,
|
|
421
|
+
minorOrder: row.minor_order,
|
|
422
|
+
sortOne: row.sort_one,
|
|
423
|
+
sortTwo: row.sort_two,
|
|
424
|
+
recordKey: row.record_key,
|
|
425
|
+
}
|
|
426
|
+
}
|