@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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,939 @@
1
+ import type {
2
+ EffectiveChangeCounts,
3
+ ExpectedLinkRevision,
4
+ ExpectedObjectRevision,
5
+ } from "@sixb/core/internal/materialization"
6
+ import {
7
+ assertPinnedDatasetWatermark,
8
+ linkRefKey,
9
+ linkRefSortKey,
10
+ linkScopeSortKey,
11
+ MaterializationConflictError,
12
+ objectRefKey,
13
+ objectRefSortKey,
14
+ telemetryPointKey,
15
+ telemetryPointSortKey,
16
+ } from "@sixb/core/internal/materialization"
17
+ import {
18
+ assertMaterializationFinalizationCorrelation,
19
+ assertMaterializationHeader,
20
+ assertMaterializationLaneCompletion,
21
+ assertPageRows,
22
+ assertPlanChunkCorrelations,
23
+ assertSourceActivationCorrelation,
24
+ invalidCorrelation,
25
+ sameNonnegativeCounts as sameCounts,
26
+ uniqueSorted,
27
+ } from "@sixb/core/internal/ontology-storage-provider"
28
+ import type {
29
+ ApplyMaterializationChunkInput,
30
+ ApplyMaterializationResult,
31
+ FinalizeMaterializationInput,
32
+ MaterializationObjectExistence,
33
+ MaterializationPlanHeader,
34
+ MaterializationSession,
35
+ MaterializationStatePage,
36
+ MaterializationWorkPage,
37
+ OntologyCommitRecord,
38
+ OntologyMaterializationStorage,
39
+ ReadMaterializationObjectExistenceInput,
40
+ SourceActivationWrite,
41
+ SourceReplacementStatePage,
42
+ StageMaterializationWorkInput,
43
+ StreamMaterializationStateInput,
44
+ StreamMaterializationWorkInput,
45
+ StreamSourceReplacementStateInput,
46
+ } from "@sixb/core/storage"
47
+ import type { SQLClient } from "../pg-client"
48
+ import { isUniqueViolation } from "../storage-errors"
49
+ import { lockAdvisoryKeys } from "../transactions"
50
+ import {
51
+ type PgMaterializationSessionState,
52
+ PgMaterializationSessions,
53
+ type PgOntologyTransactionContext,
54
+ } from "./materialization-session"
55
+ import {
56
+ linkSortExpression,
57
+ PG_MATERIALIZATION_WORK_TABLE,
58
+ PgMaterializationStateReader,
59
+ } from "./materialization-state"
60
+ import { PgMaterializationWriter } from "./materialization-writer"
61
+ import {
62
+ assertProjectionExecution,
63
+ commitRecord,
64
+ jsonParameter,
65
+ ontologyLockKey,
66
+ originColumns,
67
+ type PgOntologyCommitRow,
68
+ type PgOntologySourceRow,
69
+ toIsoString,
70
+ } from "./shared"
71
+
72
+ interface ProjectionCountsRow {
73
+ readonly object_classifications: number | string
74
+ readonly link_classifications: number | string
75
+ readonly objects_created: number | string
76
+ readonly objects_updated: number | string
77
+ readonly objects_deleted: number | string
78
+ readonly links_created: number | string
79
+ readonly links_updated: number | string
80
+ readonly links_deleted: number | string
81
+ }
82
+
83
+ interface TelemetrySummaryRow {
84
+ readonly classified_points: number | string
85
+ readonly points_created: number | string
86
+ readonly points_updated: number | string
87
+ readonly latest_objects_changed: number | string
88
+ }
89
+
90
+ export class PgOntologyMaterializationStorage implements OntologyMaterializationStorage {
91
+ private readonly sessions: PgMaterializationSessions
92
+ private readonly writer: PgMaterializationWriter
93
+
94
+ constructor(
95
+ private readonly sql: SQLClient,
96
+ context: PgOntologyTransactionContext | null
97
+ ) {
98
+ this.sessions = new PgMaterializationSessions(sql, context)
99
+ this.writer = new PgMaterializationWriter(sql)
100
+ }
101
+
102
+ async begin(input: MaterializationPlanHeader): Promise<MaterializationSession> {
103
+ assertMaterializationHeader(input)
104
+ const session = await this.sessions.create(input)
105
+ try {
106
+ await lockAdvisoryKeys(this.sql, materializationLockKeys(input))
107
+ await this.assertCommitAbsent(input)
108
+ const reader = new PgMaterializationStateReader(this.sql, input.commit.projectId)
109
+ await this.assertSources(input.expected.sources, input.commit.projectId)
110
+ const objectRevisions = await reader.effectiveObjectRevisions(
111
+ input.expected.objects.map((expected) => expected.ref),
112
+ true
113
+ )
114
+ for (const expected of input.expected.objects) {
115
+ this.assertObject(objectRevisions.get(objectRefKey(expected.ref)) ?? null, expected)
116
+ }
117
+ const linkRevisions = await reader.effectiveLinkLastCommits(
118
+ input.expected.links.map((expected) => expected.ref),
119
+ true
120
+ )
121
+ for (const expected of input.expected.links) {
122
+ this.assertLink(linkRevisions.get(linkRefKey(expected.ref)), expected)
123
+ }
124
+ const linkScopes = await reader.linkScopes(input.expected.linkScopes)
125
+ for (const [index, expected] of input.expected.linkScopes.entries()) {
126
+ if (linkScopes[index]?.fingerprint !== expected.fingerprint) {
127
+ throw new MaterializationConflictError(
128
+ "effective-state",
129
+ `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`
130
+ )
131
+ }
132
+ }
133
+ const points = await reader.exactPoints(input.expected.points, true)
134
+ const pointRevisions = new Map(
135
+ points.map(
136
+ (point) => [telemetryPointKey(point.series, point.at), point.lastCommitId] as const
137
+ )
138
+ )
139
+ for (const expected of input.expected.points) {
140
+ if (
141
+ (pointRevisions.get(telemetryPointKey(expected.series, expected.at)) ?? null) !==
142
+ expected.lastCommitId
143
+ ) {
144
+ throw new MaterializationConflictError(
145
+ "timeseries-point",
146
+ `Telemetry point ${telemetryPointKey(expected.series, expected.at)} changed.`
147
+ )
148
+ }
149
+ }
150
+ return session.publicSession()
151
+ } catch (error) {
152
+ await this.sessions.release(session)
153
+ throw error
154
+ }
155
+ }
156
+
157
+ async *streamState(
158
+ input: StreamMaterializationStateInput
159
+ ): AsyncIterable<MaterializationStatePage> {
160
+ const session = this.sessions.require(input.session)
161
+ assertPageRows(input.pageRows)
162
+ const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId)
163
+ for await (const request of input.requests) {
164
+ this.sessions.require(input.session)
165
+ const objects = uniqueSorted(request.objects, objectRefKey, objectRefSortKey)
166
+ for (let offset = 0; offset < objects.length; offset += input.pageRows) {
167
+ const states = await reader.objectStates(objects.slice(offset, offset + input.pageRows))
168
+ yield { objects: states, links: [], linkScopes: [], points: [] }
169
+ }
170
+ const links = uniqueSorted(request.links, linkRefKey, linkRefSortKey)
171
+ for (let offset = 0; offset < links.length; offset += input.pageRows) {
172
+ const states = await reader.linkStates(links.slice(offset, offset + input.pageRows))
173
+ yield { objects: [], links: states, linkScopes: [], points: [] }
174
+ }
175
+ for await (const refs of reader.incidentLinks(request.incidentObjects, input.pageRows)) {
176
+ this.sessions.require(input.session)
177
+ yield {
178
+ objects: [],
179
+ links: await reader.linkStates(refs),
180
+ linkScopes: [],
181
+ points: [],
182
+ }
183
+ }
184
+ const scopes = uniqueSorted(
185
+ request.linkScopes,
186
+ (scope) =>
187
+ JSON.stringify([scope.source.objectTypeId, scope.source.primaryId, scope.linkId]),
188
+ (scope) => linkScopeSortKey(scope.source, scope.linkId)
189
+ )
190
+ for (let offset = 0; offset < scopes.length; offset += input.pageRows) {
191
+ yield {
192
+ objects: [],
193
+ links: [],
194
+ linkScopes: await reader.linkScopes(scopes.slice(offset, offset + input.pageRows)),
195
+ points: [],
196
+ }
197
+ }
198
+ const points = uniqueSorted(
199
+ request.points,
200
+ (point) => telemetryPointKey(point.series, point.at),
201
+ (point) => telemetryPointSortKey(point.series, point.at)
202
+ )
203
+ for (let offset = 0; offset < points.length; offset += input.pageRows) {
204
+ const stored = await reader.exactPoints(points.slice(offset, offset + input.pageRows))
205
+ if (stored.length > 0) {
206
+ yield { objects: [], links: [], linkScopes: [], points: stored }
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ async *streamSourceReplacementState(
213
+ input: StreamSourceReplacementStateInput
214
+ ): AsyncIterable<SourceReplacementStatePage> {
215
+ const session = this.sessions.require(input.session)
216
+ assertPageRows(input.pageRows)
217
+ const replacement = await this.requireReplacement(session, input)
218
+ const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId)
219
+ if (input.entityKind === "object") {
220
+ if (replacement.projectionKind !== "object") {
221
+ throw new MaterializationConflictError(
222
+ "source-materialization",
223
+ "Link projection replacement cannot stream object state."
224
+ )
225
+ }
226
+ if (replacement.objectStreamStarted) {
227
+ throw new MaterializationConflictError(
228
+ "effective-state",
229
+ "Replacement object state may only be streamed once per session."
230
+ )
231
+ }
232
+ replacement.objectStreamStarted = true
233
+ for await (const identities of reader.replacementIdentities({
234
+ sessionId: session.id,
235
+ sourceId: replacement.sourceId,
236
+ candidateMaterializationId: replacement.candidateMaterializationId,
237
+ previousMaterializationId: replacement.previousMaterializationId,
238
+ kind: "object",
239
+ pageRows: input.pageRows,
240
+ })) {
241
+ this.sessions.require(input.session)
242
+ const refs = identities.map((identity) => {
243
+ if (identity.kind !== "object") {
244
+ invalidCorrelation("Object replacement returned a link identity.")
245
+ }
246
+ return identity.ref
247
+ })
248
+ const objects = await reader.replacementObjectStates(
249
+ replacement.sourceId,
250
+ replacement.candidateMaterializationId,
251
+ refs
252
+ )
253
+ yield { objects, links: [] }
254
+ }
255
+ this.sessions.require(input.session)
256
+ replacement.objectStreamCompleted = true
257
+ return
258
+ }
259
+
260
+ if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
261
+ throw new MaterializationConflictError(
262
+ "effective-state",
263
+ "Object projection replacement must fully stream object state before link state."
264
+ )
265
+ }
266
+ if (replacement.linkStreamStarted) {
267
+ throw new MaterializationConflictError(
268
+ "effective-state",
269
+ "Replacement link state may only be streamed once per session."
270
+ )
271
+ }
272
+ replacement.linkStreamStarted = true
273
+ const materializationIds = [
274
+ replacement.candidateMaterializationId,
275
+ ...(replacement.previousMaterializationId ? [replacement.previousMaterializationId] : []),
276
+ ]
277
+ for await (const identities of reader.replacementIdentities({
278
+ sessionId: session.id,
279
+ sourceId: replacement.sourceId,
280
+ candidateMaterializationId: replacement.candidateMaterializationId,
281
+ previousMaterializationId: replacement.previousMaterializationId,
282
+ kind: "link",
283
+ pageRows: input.pageRows,
284
+ })) {
285
+ this.sessions.require(input.session)
286
+ const linkIdentities = identities.map((identity) => {
287
+ if (identity.kind !== "link") {
288
+ invalidCorrelation("Link replacement returned an object identity.")
289
+ }
290
+ return identity
291
+ })
292
+ const links = await reader.replacementLinkStates(
293
+ replacement.sourceId,
294
+ replacement.candidateMaterializationId,
295
+ materializationIds,
296
+ linkIdentities
297
+ )
298
+ yield { objects: [], links }
299
+ }
300
+ this.sessions.require(input.session)
301
+ replacement.linkStreamCompleted = true
302
+ }
303
+
304
+ async stageWork(input: StageMaterializationWorkInput): Promise<void> {
305
+ await this.sessions.stage(input)
306
+ }
307
+
308
+ streamWork(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
309
+ assertPageRows(input.pageRows)
310
+ return this.sessions.stream(input)
311
+ }
312
+
313
+ async readObjectExistence(
314
+ input: ReadMaterializationObjectExistenceInput
315
+ ): Promise<readonly MaterializationObjectExistence[]> {
316
+ const session = this.sessions.require(input.session)
317
+ return this.sessions.readObjectExistence(session, input.refs)
318
+ }
319
+
320
+ async applyChunk(input: ApplyMaterializationChunkInput): Promise<void> {
321
+ const session = this.sessions.require(input.session)
322
+ const { commit } = session.header
323
+ assertPlanChunkCorrelations(input.chunk, commit)
324
+ const progress = await this.sessions.prepareChunkSequence(session, input.chunk)
325
+ await this.writer.apply(commit.projectId, commit.id, input.chunk)
326
+ this.sessions.commitChunkSequence(session, progress)
327
+ }
328
+
329
+ async finalize(input: FinalizeMaterializationInput): Promise<ApplyMaterializationResult> {
330
+ const session = this.sessions.require(input.session)
331
+ await this.assertCommitAbsent(session.header)
332
+ await this.assertFinalization(session, input)
333
+ for (const activation of input.finalization.sourceActivations) {
334
+ await this.activateSource(session, activation)
335
+ }
336
+ const record = await this.insertCommit(session.header, input)
337
+ await this.sessions.release(session)
338
+ return { commit: record }
339
+ }
340
+
341
+ deactivateSessions(): void {
342
+ this.sessions.deactivateAll()
343
+ }
344
+
345
+ private async requireReplacement(
346
+ session: PgMaterializationSessionState,
347
+ input: StreamSourceReplacementStateInput
348
+ ): Promise<NonNullable<PgMaterializationSessionState["replacement"]>> {
349
+ if (session.replacement) {
350
+ if (
351
+ session.replacement.sourceId !== input.source.projectionId ||
352
+ session.replacement.candidateMaterializationId !== input.candidateMaterializationId
353
+ ) {
354
+ throw new MaterializationConflictError(
355
+ "source-materialization",
356
+ "Materialization session already owns another replacement union."
357
+ )
358
+ }
359
+ return session.replacement
360
+ }
361
+ const projectId = session.header.commit.projectId
362
+ const candidate = await this.getSource(
363
+ projectId,
364
+ input.source.projectionId,
365
+ input.candidateMaterializationId,
366
+ true
367
+ )
368
+ if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
369
+ throw new MaterializationConflictError(
370
+ "source-materialization",
371
+ `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`
372
+ )
373
+ }
374
+ await assertProjectionExecution(this.sql, {
375
+ projectId,
376
+ sourceId: input.source.projectionId,
377
+ projectionRunId: candidate.projection_run_id,
378
+ executionToken: candidate.execution_token,
379
+ })
380
+ const previous = await this.getActiveSource(projectId, input.source.projectionId, true)
381
+ if (
382
+ candidate.protocol !== "replacement" ||
383
+ (previous &&
384
+ (previous.protocol !== candidate.protocol ||
385
+ previous.projection_kind !== candidate.projection_kind))
386
+ ) {
387
+ throw new MaterializationConflictError(
388
+ "source-materialization",
389
+ "Source replacement kind or protocol does not match its active materialization."
390
+ )
391
+ }
392
+ session.replacement = {
393
+ sourceId: input.source.projectionId,
394
+ candidateMaterializationId: input.candidateMaterializationId,
395
+ previousMaterializationId: previous?.materialization_id ?? null,
396
+ projectionKind: candidate.projection_kind,
397
+ objectStreamStarted: false,
398
+ objectStreamCompleted: false,
399
+ linkStreamStarted: false,
400
+ linkStreamCompleted: false,
401
+ }
402
+ return session.replacement
403
+ }
404
+
405
+ private async assertCommitAbsent(header: MaterializationPlanHeader): Promise<void> {
406
+ const [duplicateIdentity] = await this.sql<
407
+ { readonly id: string; readonly idempotency_key: string }[]
408
+ >`
409
+ SELECT id, idempotency_key
410
+ FROM ontology_commits
411
+ WHERE project_id = ${header.commit.projectId}
412
+ AND (id = ${header.commit.id} OR idempotency_key = ${header.commit.idempotencyKey})
413
+ ORDER BY CASE WHEN id = ${header.commit.id} THEN 0 ELSE 1 END
414
+ LIMIT 1
415
+ FOR UPDATE
416
+ `
417
+ if (duplicateIdentity) {
418
+ throw new MaterializationConflictError(
419
+ "idempotency",
420
+ duplicateIdentity.id === header.commit.id
421
+ ? `Ontology commit '${header.commit.id}' already exists.`
422
+ : "Ontology idempotency key already exists."
423
+ )
424
+ }
425
+
426
+ const origin = originColumns(header.commit.origin)
427
+ if (origin.runId === null) return
428
+ const [duplicateOrigin] =
429
+ origin.batchOrdinal === null
430
+ ? await this.sql<{ readonly id: string }[]>`
431
+ SELECT id FROM ontology_commits
432
+ WHERE project_id = ${header.commit.projectId}
433
+ AND origin_kind = ${origin.kind}
434
+ AND origin_run_id = ${origin.runId}
435
+ AND origin_batch_ordinal IS NULL
436
+ LIMIT 1
437
+ FOR UPDATE
438
+ `
439
+ : await this.sql<{ readonly id: string }[]>`
440
+ SELECT id FROM ontology_commits
441
+ WHERE project_id = ${header.commit.projectId}
442
+ AND origin_kind = ${origin.kind}
443
+ AND origin_run_id = ${origin.runId}
444
+ AND origin_batch_ordinal = ${origin.batchOrdinal}
445
+ LIMIT 1
446
+ FOR UPDATE
447
+ `
448
+ if (duplicateOrigin) {
449
+ throw new MaterializationConflictError(
450
+ "run-correlation",
451
+ "Ontology commit origin already has an authoritative commit."
452
+ )
453
+ }
454
+ }
455
+
456
+ private async assertSources(
457
+ expectedSources: MaterializationPlanHeader["expected"]["sources"],
458
+ projectId: string
459
+ ): Promise<void> {
460
+ if (expectedSources.length === 0) return
461
+ const rows = await this.sql<PgOntologySourceRow[]>`
462
+ SELECT * FROM ontology_sources
463
+ WHERE project_id = ${projectId}
464
+ AND source_id = ANY(
465
+ ${this.sql.array(expectedSources.map((expected) => expected.source.projectionId))}::text[]
466
+ )
467
+ AND status = 'active'
468
+ FOR UPDATE
469
+ `
470
+ const active = new Map(rows.map((row) => [row.source_id, row] as const))
471
+ for (const expected of expectedSources) {
472
+ this.assertSourceRow(expected, active.get(expected.source.projectionId) ?? null)
473
+ }
474
+ }
475
+
476
+ private assertSourceRow(
477
+ expected: MaterializationPlanHeader["expected"]["sources"][number],
478
+ active: PgOntologySourceRow | null
479
+ ): void {
480
+ if (
481
+ (active?.materialization_id ?? null) !== expected.activeMaterializationId ||
482
+ (active?.last_commit_id ?? null) !== expected.lastCommitId
483
+ ) {
484
+ throw new MaterializationConflictError(
485
+ "projection-fence",
486
+ `Source '${expected.source.projectionId}' changed.`
487
+ )
488
+ }
489
+ }
490
+
491
+ private assertObject(
492
+ row: { readonly version: number; readonly lastCommitId: string | null } | null,
493
+ expected: ExpectedObjectRevision
494
+ ): void {
495
+ if (!expected.exists) {
496
+ if (row) {
497
+ throw new MaterializationConflictError(
498
+ "effective-state",
499
+ `Expected object ${objectRefKey(expected.ref)} to be absent.`
500
+ )
501
+ }
502
+ return
503
+ }
504
+ if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
505
+ throw new MaterializationConflictError(
506
+ "effective-state",
507
+ `Expected object ${objectRefKey(expected.ref)} changed.`
508
+ )
509
+ }
510
+ }
511
+
512
+ private assertLink(
513
+ lastCommitId: string | null | undefined,
514
+ expected: ExpectedLinkRevision
515
+ ): void {
516
+ if (!expected.exists) {
517
+ if (lastCommitId !== undefined) {
518
+ throw new MaterializationConflictError(
519
+ "effective-state",
520
+ `Expected link ${linkRefKey(expected.ref)} to be absent.`
521
+ )
522
+ }
523
+ return
524
+ }
525
+ if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
526
+ throw new MaterializationConflictError(
527
+ "effective-state",
528
+ `Expected link ${linkRefKey(expected.ref)} changed.`
529
+ )
530
+ }
531
+ }
532
+
533
+ private async assertFinalization(
534
+ session: PgMaterializationSessionState,
535
+ input: FinalizeMaterializationInput
536
+ ): Promise<void> {
537
+ const { commit } = session.header
538
+ const { result } = input.finalization
539
+ assertMaterializationFinalizationCorrelation(session, input)
540
+ const laneCounts = await this.sessions.laneCounts(session)
541
+ assertMaterializationLaneCompletion(session, laneCounts)
542
+ await this.assertFinalCardinality(session)
543
+ const eventCount = laneCounts.event
544
+ const [outbox] = await this.sql<
545
+ {
546
+ readonly count: number | string
547
+ readonly minimum: number | string | null
548
+ readonly maximum: number | string | null
549
+ }[]
550
+ >`
551
+ SELECT COUNT(*) AS count, MIN(commit_ordinal) AS minimum,
552
+ MAX(commit_ordinal) AS maximum
553
+ FROM ontology_outbox
554
+ WHERE project_id = ${commit.projectId} AND commit_id = ${commit.id}
555
+ `
556
+ const outboxCount = Number(outbox?.count ?? 0)
557
+ const minimum = outbox?.minimum === null ? null : Number(outbox?.minimum)
558
+ const maximum = outbox?.maximum === null ? null : Number(outbox?.maximum)
559
+ if (
560
+ outboxCount !== eventCount ||
561
+ (eventCount > 0 && (minimum !== 0 || maximum !== eventCount - 1))
562
+ ) {
563
+ invalidCorrelation("Outbox event ordinals must be contiguous from zero.")
564
+ }
565
+
566
+ if (commit.intent.kind === "telemetry") {
567
+ const summary = await this.telemetrySummary(session, commit.intent.pointCount)
568
+ if (summary.classifiedPoints !== commit.intent.pointCount) {
569
+ invalidCorrelation(
570
+ "Telemetry point classification coverage does not match the commit intent."
571
+ )
572
+ }
573
+ if (result.kind !== "telemetry" || !sameCounts(result, summary.counts)) {
574
+ invalidCorrelation("Telemetry result counts do not correlate with finalized work.")
575
+ }
576
+ }
577
+ if (commit.intent.kind === "projection") {
578
+ if (result.kind !== "projection") return
579
+ await this.sessions.assertClassificationCoverage(session)
580
+ if (!sameCounts(result.counts, await this.projectionCounts(session))) {
581
+ invalidCorrelation("Projection result counts do not correlate with finalized work.")
582
+ }
583
+ }
584
+ }
585
+
586
+ private async assertFinalCardinality(session: PgMaterializationSessionState): Promise<void> {
587
+ const [violation] = await this.sql<{ readonly reason: "duplicate" | "mismatch" }[]>`
588
+ WITH work AS (
589
+ SELECT payload->>'scopeSortKey' AS scope_sort_key,
590
+ payload->>'linkSortKey' AS link_sort_key,
591
+ (payload->>'occupied')::boolean AS occupied,
592
+ payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
593
+ payload->'ref'->'source'->>'primaryId' AS source_id,
594
+ payload->'ref'->>'linkId' AS link_id
595
+ FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
596
+ WHERE session_id = ${session.id} AND kind = 'cardinality'
597
+ ), duplicate AS (
598
+ SELECT scope_sort_key FROM work WHERE occupied
599
+ GROUP BY scope_sort_key HAVING COUNT(*) > 1
600
+ ), scopes AS (
601
+ SELECT DISTINCT scope_sort_key, source_type_id, source_id, link_id FROM work
602
+ ), expected AS (
603
+ SELECT scope_sort_key, link_sort_key FROM work WHERE occupied
604
+ ), actual AS (
605
+ SELECT scopes.scope_sort_key,
606
+ ${this.sql.unsafe(linkSortExpression("links"))} AS link_sort_key
607
+ FROM scopes
608
+ JOIN links USING (source_type_id, source_id, link_id)
609
+ WHERE links.project_id = ${session.header.commit.projectId}
610
+ ), differences AS (
611
+ (SELECT * FROM expected EXCEPT SELECT * FROM actual)
612
+ UNION ALL
613
+ (SELECT * FROM actual EXCEPT SELECT * FROM expected)
614
+ )
615
+ SELECT 'duplicate'::text AS reason FROM duplicate
616
+ UNION ALL
617
+ SELECT 'mismatch'::text AS reason FROM differences
618
+ LIMIT 1
619
+ `
620
+ if (violation?.reason === "duplicate") {
621
+ invalidCorrelation("Materialization cardinality work violates cardinality-one.")
622
+ }
623
+ if (violation) {
624
+ invalidCorrelation(
625
+ "Materialization cardinality work does not match the final effective link scope."
626
+ )
627
+ }
628
+ }
629
+
630
+ private async projectionCounts(
631
+ session: PgMaterializationSessionState
632
+ ): Promise<EffectiveChangeCounts> {
633
+ const [row] = await this.sql<ProjectionCountsRow[]>`
634
+ SELECT
635
+ COUNT(*) FILTER (
636
+ WHERE kind = 'classification' AND payload->>'entityKind' = 'object'
637
+ ) AS object_classifications,
638
+ COUNT(*) FILTER (
639
+ WHERE kind = 'classification' AND payload->>'entityKind' = 'link'
640
+ ) AS link_classifications,
641
+ COUNT(*) FILTER (
642
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
643
+ AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
644
+ ) AS objects_created,
645
+ COUNT(*) FILTER (
646
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
647
+ AND (payload->'item'->'value'->'expected'->>'exists')::boolean
648
+ ) AS objects_updated,
649
+ COUNT(*) FILTER (
650
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-delete'
651
+ ) AS objects_deleted,
652
+ COUNT(*) FILTER (
653
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
654
+ AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
655
+ ) AS links_created,
656
+ COUNT(*) FILTER (
657
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
658
+ AND (payload->'item'->'value'->'expected'->>'exists')::boolean
659
+ ) AS links_updated,
660
+ COUNT(*) FILTER (
661
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-delete'
662
+ ) AS links_deleted
663
+ FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
664
+ WHERE session_id = ${session.id}
665
+ `
666
+ const objectsCreated = databaseCount(row?.objects_created)
667
+ const objectsUpdated = databaseCount(row?.objects_updated)
668
+ const objectsDeleted = databaseCount(row?.objects_deleted)
669
+ const linksCreated = databaseCount(row?.links_created)
670
+ const linksUpdated = databaseCount(row?.links_updated)
671
+ const linksDeleted = databaseCount(row?.links_deleted)
672
+ return {
673
+ objectsCreated,
674
+ objectsUpdated,
675
+ objectsDeleted,
676
+ objectsUnchanged:
677
+ databaseCount(row?.object_classifications) -
678
+ objectsCreated -
679
+ objectsUpdated -
680
+ objectsDeleted,
681
+ linksCreated,
682
+ linksUpdated,
683
+ linksDeleted,
684
+ linksUnchanged:
685
+ databaseCount(row?.link_classifications) - linksCreated - linksUpdated - linksDeleted,
686
+ }
687
+ }
688
+
689
+ private async telemetrySummary(session: PgMaterializationSessionState, pointCount: number) {
690
+ const [row] = await this.sql<TelemetrySummaryRow[]>`
691
+ SELECT
692
+ COUNT(*) FILTER (
693
+ WHERE kind = 'classification' AND payload->>'entityKind' = 'point'
694
+ ) AS classified_points,
695
+ COUNT(*) FILTER (
696
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'point-upsert'
697
+ AND payload->'item'->'value'->'expected'->>'lastCommitId' IS NULL
698
+ ) AS points_created,
699
+ COUNT(*) FILTER (
700
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'point-upsert'
701
+ AND payload->'item'->'value'->'expected'->>'lastCommitId' IS NOT NULL
702
+ ) AS points_updated,
703
+ COUNT(*) FILTER (
704
+ WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
705
+ ) AS latest_objects_changed
706
+ FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
707
+ WHERE session_id = ${session.id}
708
+ `
709
+ const pointsCreated = databaseCount(row?.points_created)
710
+ const pointsUpdated = databaseCount(row?.points_updated)
711
+ return {
712
+ classifiedPoints: databaseCount(row?.classified_points),
713
+ counts: {
714
+ pointsCreated,
715
+ pointsUpdated,
716
+ pointsUnchanged: pointCount - pointsCreated - pointsUpdated,
717
+ latestObjectsChanged: databaseCount(row?.latest_objects_changed),
718
+ },
719
+ }
720
+ }
721
+
722
+ private async activateSource(
723
+ session: PgMaterializationSessionState,
724
+ activation: SourceActivationWrite
725
+ ): Promise<void> {
726
+ const { commit } = session.header
727
+ assertSourceActivationCorrelation(session, activation)
728
+ const candidate = await this.getSource(
729
+ commit.projectId,
730
+ activation.source.projectionId,
731
+ activation.materializationId,
732
+ true
733
+ )
734
+ if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
735
+ throw new MaterializationConflictError(
736
+ "source-materialization",
737
+ "Source activation candidate is missing or is not ready."
738
+ )
739
+ }
740
+ await assertProjectionExecution(this.sql, {
741
+ projectId: commit.projectId,
742
+ sourceId: activation.source.projectionId,
743
+ projectionRunId: activation.execution.projectionRunId,
744
+ executionToken: activation.execution.executionToken,
745
+ })
746
+ if (
747
+ candidate.projection_run_id !== activation.execution.projectionRunId ||
748
+ candidate.execution_token !== activation.execution.executionToken ||
749
+ candidate.projection_kind !== activation.projectionKind ||
750
+ candidate.protocol !== activation.protocol ||
751
+ candidate.dataset_id !== activation.datasetVersion.datasetId ||
752
+ candidate.dataset_version_id !== activation.datasetVersion.versionId ||
753
+ toIsoString(candidate.dataset_version_created_at) !== activation.datasetVersion.createdAt ||
754
+ candidate.projection_revision !== activation.projectionRevision ||
755
+ candidate.ownership_hash !== activation.ownershipHash ||
756
+ candidate.ontology_revision !== activation.ontologyRevision ||
757
+ candidate.ready_at === null ||
758
+ activation.updatedAt < toIsoString(candidate.ready_at)
759
+ ) {
760
+ invalidCorrelation("Source activation does not match its ready candidate identity.")
761
+ }
762
+ const previous = await this.getActiveSource(
763
+ commit.projectId,
764
+ activation.source.projectionId,
765
+ true
766
+ )
767
+ this.assertSourceRow(activation.expected, previous)
768
+ if (previous) {
769
+ assertPinnedDatasetWatermark(
770
+ {
771
+ datasetId: previous.dataset_id,
772
+ versionId: previous.dataset_version_id,
773
+ createdAt: toIsoString(previous.dataset_version_created_at),
774
+ },
775
+ activation.datasetVersion,
776
+ "Source activation"
777
+ )
778
+ if (activation.updatedAt < toIsoString(previous.updated_at)) {
779
+ invalidCorrelation("Source activation cannot precede the active materialization update.")
780
+ }
781
+ const superseded = await this.sql<{ readonly materialization_id: string }[]>`
782
+ UPDATE ontology_sources
783
+ SET status = 'superseded', execution_token = NULL,
784
+ terminal_at = ${activation.updatedAt}, updated_at = ${activation.updatedAt}
785
+ WHERE project_id = ${commit.projectId}
786
+ AND source_id = ${activation.source.projectionId}
787
+ AND materialization_id = ${previous.materialization_id}
788
+ AND status = 'active'
789
+ AND last_commit_id IS NOT DISTINCT FROM ${activation.expected.lastCommitId}
790
+ RETURNING materialization_id
791
+ `
792
+ if (superseded.length !== 1) {
793
+ throw new MaterializationConflictError(
794
+ "projection-fence",
795
+ `Source '${activation.source.projectionId}' changed.`
796
+ )
797
+ }
798
+ }
799
+ let activated: readonly { readonly materialization_id: string }[]
800
+ try {
801
+ activated = await this.sql<{ readonly materialization_id: string }[]>`
802
+ UPDATE ontology_sources
803
+ SET status = 'active', execution_token = NULL,
804
+ activated_at = ${activation.updatedAt}, last_commit_id = ${activation.lastCommitId},
805
+ updated_at = ${activation.updatedAt}
806
+ WHERE project_id = ${commit.projectId}
807
+ AND source_id = ${activation.source.projectionId}
808
+ AND materialization_id = ${activation.materializationId}
809
+ AND status = 'ready'
810
+ AND execution_token = ${activation.execution.executionToken}
811
+ RETURNING materialization_id
812
+ `
813
+ } catch (error) {
814
+ if (isUniqueViolation(error)) {
815
+ throw new MaterializationConflictError(
816
+ "projection-fence",
817
+ `Source '${activation.source.projectionId}' changed.`
818
+ )
819
+ }
820
+ throw error
821
+ }
822
+ if (activated.length !== 1) {
823
+ throw new MaterializationConflictError(
824
+ "source-materialization",
825
+ "Source activation candidate changed."
826
+ )
827
+ }
828
+ }
829
+
830
+ private async insertCommit(
831
+ header: MaterializationPlanHeader,
832
+ input: FinalizeMaterializationInput
833
+ ): Promise<OntologyCommitRecord> {
834
+ const { commit } = header
835
+ const origin = originColumns(commit.origin)
836
+ const actor = commit.actor === undefined ? null : jsonParameter(this.sql, commit.actor)
837
+ const rows = await this.sql<PgOntologyCommitRow[]>`
838
+ INSERT INTO ontology_commits (
839
+ project_id, id, idempotency_key, request_hash,
840
+ origin_kind, origin_run_id, origin_batch_ordinal, origin, actor,
841
+ ontology_revision, projection_revision, ownership_hash,
842
+ intent, result, committed_at
843
+ ) VALUES (
844
+ ${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
845
+ ${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
846
+ ${jsonParameter(this.sql, commit.origin)}, ${actor}, ${commit.ontologyRevision},
847
+ ${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
848
+ ${jsonParameter(this.sql, commit.intent)},
849
+ ${jsonParameter(this.sql, input.finalization.result)}, ${commit.committedAt}
850
+ )
851
+ ON CONFLICT DO NOTHING
852
+ RETURNING *
853
+ `
854
+ if (!rows[0]) {
855
+ await this.assertCommitAbsent(header)
856
+ throw new MaterializationConflictError(
857
+ "idempotency",
858
+ "Ontology commit identity already exists."
859
+ )
860
+ }
861
+ return commitRecord(rows[0])
862
+ }
863
+
864
+ private async getActiveSource(
865
+ projectId: string,
866
+ sourceId: string,
867
+ lock = false
868
+ ): Promise<PgOntologySourceRow | null> {
869
+ const lockFragment = lock ? this.sql`FOR UPDATE` : this.sql``
870
+ const [row] = await this.sql<PgOntologySourceRow[]>`
871
+ SELECT * FROM ontology_sources
872
+ WHERE project_id = ${projectId} AND source_id = ${sourceId} AND status = 'active'
873
+ ${lockFragment}
874
+ `
875
+ return row ?? null
876
+ }
877
+
878
+ private async getSource(
879
+ projectId: string,
880
+ sourceId: string,
881
+ materializationId: string,
882
+ lock = false
883
+ ): Promise<PgOntologySourceRow | null> {
884
+ const lockFragment = lock ? this.sql`FOR UPDATE` : this.sql``
885
+ const [row] = await this.sql<PgOntologySourceRow[]>`
886
+ SELECT * FROM ontology_sources
887
+ WHERE project_id = ${projectId}
888
+ AND source_id = ${sourceId}
889
+ AND materialization_id = ${materializationId}
890
+ ${lockFragment}
891
+ `
892
+ return row ?? null
893
+ }
894
+ }
895
+
896
+ function materializationLockKeys(header: MaterializationPlanHeader): string[] {
897
+ const { commit, expected } = header
898
+ const origin = originColumns(commit.origin)
899
+ return [
900
+ ontologyLockKey("commit-id", commit.projectId, commit.id),
901
+ ontologyLockKey("commit-idempotency", commit.projectId, commit.idempotencyKey),
902
+ ...(origin.runId === null
903
+ ? []
904
+ : [
905
+ ontologyLockKey(
906
+ "commit-origin",
907
+ commit.projectId,
908
+ origin.kind,
909
+ origin.runId,
910
+ origin.batchOrdinal === null ? "" : String(origin.batchOrdinal)
911
+ ),
912
+ ]),
913
+ ...expected.sources.map((value) =>
914
+ ontologyLockKey("source", commit.projectId, value.source.projectionId)
915
+ ),
916
+ ...expected.objects.map((value) =>
917
+ ontologyLockKey("object", commit.projectId, objectRefKey(value.ref))
918
+ ),
919
+ ...expected.links.map((value) =>
920
+ ontologyLockKey("link", commit.projectId, linkRefKey(value.ref))
921
+ ),
922
+ ...expected.linkScopes.map((value) =>
923
+ ontologyLockKey(
924
+ "link-scope",
925
+ commit.projectId,
926
+ value.source.objectTypeId,
927
+ value.source.primaryId,
928
+ value.linkId
929
+ )
930
+ ),
931
+ ...expected.points.map((value) =>
932
+ ontologyLockKey("point", commit.projectId, telemetryPointKey(value.series, value.at))
933
+ ),
934
+ ]
935
+ }
936
+
937
+ function databaseCount(value: number | string | undefined): number {
938
+ return Number(value ?? 0)
939
+ }