@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,1109 @@
1
+ import type {
2
+ EffectiveLinkSnapshot,
3
+ EffectiveObjectSnapshot,
4
+ OntologyLinkRef,
5
+ OntologyObjectRef,
6
+ ProjectionEntityRef,
7
+ TelemetrySeriesRef,
8
+ } from "@sixb/core/internal/materialization"
9
+ import {
10
+ linkRefKey,
11
+ linkScopeSortKey,
12
+ MaterializationConflictError,
13
+ objectRefKey,
14
+ projectionEntityKey,
15
+ telemetryPointKey,
16
+ telemetryPointSortKey,
17
+ } from "@sixb/core/internal/materialization"
18
+ import {
19
+ appendScopeSnapshot,
20
+ finishScopeAccumulator,
21
+ startScopeAccumulator,
22
+ } from "@sixb/core/internal/ontology-storage-provider"
23
+ import type {
24
+ MaterializationLinkScopeState,
25
+ MaterializationLinkState,
26
+ MaterializationObjectState,
27
+ SourceReplacementLinkState,
28
+ SourceReplacementObjectState,
29
+ StoredLinkOverride,
30
+ StoredObjectOverride,
31
+ StoredSourceAssertion,
32
+ StoredSourceLinkAssertion,
33
+ StoredSourceObjectAssertion,
34
+ StoredTelemetryPoint,
35
+ } from "@sixb/core/storage"
36
+ import type { SQLClient } from "../pg-client"
37
+ import {
38
+ databaseSafeInteger,
39
+ jsonKeyParameter,
40
+ jsonParameter,
41
+ linkRefFromColumns,
42
+ objectRefFromColumns,
43
+ type PgOntologyOverrideRow,
44
+ type PgOntologySourceAssertionRow,
45
+ toIsoString,
46
+ } from "./shared"
47
+
48
+ export const PG_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work"
49
+ export const PG_REPLACEMENT_WORK_TABLE = "ontology_replacement_work"
50
+
51
+ interface EffectiveObjectRow {
52
+ readonly object_type_id: string
53
+ readonly primary_id: string
54
+ readonly properties: unknown
55
+ readonly created_at: Date | string
56
+ readonly updated_at: Date | string
57
+ readonly version: number
58
+ readonly last_commit_id: string | null
59
+ }
60
+
61
+ interface EffectiveLinkRow {
62
+ readonly source_type_id: string
63
+ readonly source_id: string
64
+ readonly link_id: string
65
+ readonly target_type_id: string
66
+ readonly target_id: string
67
+ readonly properties: unknown | null
68
+ readonly created_at: Date | string
69
+ readonly updated_at: Date | string
70
+ readonly last_commit_id: string | null
71
+ }
72
+
73
+ interface TelemetryRow {
74
+ readonly object_type_id: string
75
+ readonly object_id: string
76
+ readonly property_id: string
77
+ readonly value: unknown
78
+ readonly unit: string | null
79
+ readonly at: Date | string
80
+ readonly last_commit_id: string | null
81
+ }
82
+
83
+ interface LinkIdentityRow {
84
+ readonly source_type_id: string
85
+ readonly source_id: string
86
+ readonly link_id: string
87
+ readonly target_type_id: string
88
+ readonly target_id: string
89
+ readonly sort_key: string
90
+ }
91
+
92
+ interface LinkScopeRow extends EffectiveLinkRow {
93
+ readonly scope_sort_key: string
94
+ readonly link_sort_key: string
95
+ }
96
+
97
+ interface ObjectOverrideRow extends PgOntologyOverrideRow {
98
+ readonly object_type_id: string
99
+ readonly primary_id: string
100
+ }
101
+
102
+ interface LinkOverrideRow extends PgOntologyOverrideRow {
103
+ readonly source_type_id: string
104
+ readonly source_primary_id: string
105
+ readonly link_id: string
106
+ readonly target_type_id: string
107
+ readonly target_primary_id: string
108
+ }
109
+
110
+ export type ReplacementIdentity =
111
+ | {
112
+ readonly kind: "object"
113
+ readonly ref: OntologyObjectRef
114
+ readonly sortKey: string
115
+ readonly diffRequired: true
116
+ }
117
+ | {
118
+ readonly kind: "link"
119
+ readonly ref: OntologyLinkRef
120
+ readonly sortKey: string
121
+ readonly diffRequired: boolean
122
+ }
123
+
124
+ interface ReplacementSources {
125
+ readonly owned: ReadonlySet<string>
126
+ readonly byMaterialization: ReadonlyMap<string, ReadonlyMap<string, StoredSourceAssertion>>
127
+ }
128
+
129
+ interface ReplacementIdentityInput {
130
+ readonly sessionId: string
131
+ readonly sourceId: string
132
+ readonly candidateMaterializationId: string
133
+ readonly previousMaterializationId: string | null
134
+ readonly kind: "object" | "link"
135
+ readonly pageRows: number
136
+ }
137
+
138
+ interface ReplacementWorkRow {
139
+ readonly entity_kind: "object" | "link"
140
+ readonly identity_key: string
141
+ readonly sort_key: string
142
+ readonly diff_required: boolean
143
+ }
144
+
145
+ export class PgMaterializationStateReader {
146
+ constructor(
147
+ private readonly sql: SQLClient,
148
+ readonly projectId: string
149
+ ) {}
150
+
151
+ async objectState(ref: OntologyObjectRef): Promise<MaterializationObjectState> {
152
+ const [state] = await this.objectStates([ref])
153
+ return state!
154
+ }
155
+
156
+ async objectStates(
157
+ refs: readonly OntologyObjectRef[]
158
+ ): Promise<readonly MaterializationObjectState[]> {
159
+ if (refs.length === 0) return []
160
+ const requested = refs.map((ref) => ({
161
+ object_type_id: ref.objectTypeId,
162
+ primary_id: ref.primaryId,
163
+ }))
164
+ const requestedParameter = jsonParameter(this.sql, requested)
165
+ const [effectiveRows, sourceRows, overrideRows, telemetryRows] = await Promise.all([
166
+ this.sql<EffectiveObjectRow[]>`
167
+ WITH requested AS (
168
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
169
+ AS requested_values(object_type_id TEXT, primary_id TEXT)
170
+ )
171
+ SELECT objects.* FROM objects
172
+ JOIN requested USING (object_type_id, primary_id)
173
+ WHERE objects.project_id = ${this.projectId}
174
+ `,
175
+ this.sql<PgOntologySourceAssertionRow[]>`
176
+ WITH requested AS (
177
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
178
+ AS requested_values(object_type_id TEXT, primary_id TEXT)
179
+ )
180
+ SELECT rows.*
181
+ FROM ontology_source_rows AS rows
182
+ JOIN requested USING (object_type_id, primary_id)
183
+ JOIN ontology_sources AS sources
184
+ ON sources.project_id = rows.project_id
185
+ AND sources.source_id = rows.source_id
186
+ AND sources.materialization_id = rows.materialization_id
187
+ WHERE rows.project_id = ${this.projectId}
188
+ AND rows.entity_kind = 'object'
189
+ AND sources.status = 'active'
190
+ `,
191
+ this.sql<ObjectOverrideRow[]>`
192
+ WITH requested AS (
193
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
194
+ AS requested_values(object_type_id TEXT, primary_id TEXT)
195
+ )
196
+ SELECT overrides.* FROM ontology_overrides AS overrides
197
+ JOIN requested USING (object_type_id, primary_id)
198
+ WHERE overrides.project_id = ${this.projectId}
199
+ AND overrides.entity_kind = 'object'
200
+ `,
201
+ this.sql<TelemetryRow[]>`
202
+ WITH requested AS (
203
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
204
+ AS requested_values(object_type_id TEXT, primary_id TEXT)
205
+ )
206
+ SELECT latest.*
207
+ FROM timeseries_latest AS latest
208
+ JOIN requested
209
+ ON requested.object_type_id = latest.object_type_id
210
+ AND requested.primary_id = latest.object_id
211
+ WHERE latest.project_id = ${this.projectId}
212
+ `,
213
+ ])
214
+
215
+ const effective = new Map(
216
+ effectiveRows.map((row) => [objectRefKey(objectRefFromColumns(row)), row] as const)
217
+ )
218
+ const sources = activeSourceMap(sourceRows)
219
+ const overrides = new Map(
220
+ overrideRows.map(
221
+ (row) => [objectRefKey(objectRefFromColumns(row)), storedObjectOverride(row)] as const
222
+ )
223
+ )
224
+ const telemetry = new Map<string, StoredTelemetryPoint[]>()
225
+ for (const row of telemetryRows) {
226
+ const key = objectRefKey({ objectTypeId: row.object_type_id, primaryId: row.object_id })
227
+ const points = telemetry.get(key) ?? []
228
+ points.push(storedPoint(row))
229
+ telemetry.set(key, points)
230
+ }
231
+ for (const points of telemetry.values()) {
232
+ points.sort((left, right) =>
233
+ telemetryPointSortKey(left.series, left.at).localeCompare(
234
+ telemetryPointSortKey(right.series, right.at)
235
+ )
236
+ )
237
+ }
238
+
239
+ return refs.map((ref) => {
240
+ const key = objectRefKey(ref)
241
+ const effectiveRow = effective.get(key)
242
+ return {
243
+ ref: structuredClone(ref),
244
+ source: sourceObject(sources.get(projectionEntityKey({ kind: "object", ref }))),
245
+ override: overrides.get(key) ?? null,
246
+ effective: effectiveRow ? effectiveObjectSnapshot(effectiveRow) : null,
247
+ latestTelemetry: telemetry.get(key) ?? [],
248
+ }
249
+ })
250
+ }
251
+
252
+ async linkState(ref: OntologyLinkRef): Promise<MaterializationLinkState> {
253
+ const [state] = await this.linkStates([ref])
254
+ return state!
255
+ }
256
+
257
+ async linkStates(refs: readonly OntologyLinkRef[]): Promise<readonly MaterializationLinkState[]> {
258
+ if (refs.length === 0) return []
259
+ const requested = refs.map((ref) => ({
260
+ source_type_id: ref.source.objectTypeId,
261
+ source_id: ref.source.primaryId,
262
+ source_primary_id: ref.source.primaryId,
263
+ link_id: ref.linkId,
264
+ target_type_id: ref.target.objectTypeId,
265
+ target_id: ref.target.primaryId,
266
+ target_primary_id: ref.target.primaryId,
267
+ }))
268
+ const requestedParameter = jsonParameter(this.sql, requested)
269
+ const [effectiveRows, sourceRows, overrideRows] = await Promise.all([
270
+ this.sql<EffectiveLinkRow[]>`
271
+ WITH requested AS (
272
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
273
+ source_type_id TEXT, source_id TEXT, link_id TEXT,
274
+ target_type_id TEXT, target_id TEXT
275
+ )
276
+ )
277
+ SELECT links.* FROM links
278
+ JOIN requested USING (source_type_id, source_id, link_id, target_type_id, target_id)
279
+ WHERE links.project_id = ${this.projectId}
280
+ `,
281
+ this.sql<PgOntologySourceAssertionRow[]>`
282
+ WITH requested AS (
283
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
284
+ source_type_id TEXT, source_primary_id TEXT, link_id TEXT,
285
+ target_type_id TEXT, target_primary_id TEXT
286
+ )
287
+ )
288
+ SELECT rows.*
289
+ FROM ontology_source_rows AS rows
290
+ JOIN requested USING (
291
+ source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
292
+ )
293
+ JOIN ontology_sources AS sources
294
+ ON sources.project_id = rows.project_id
295
+ AND sources.source_id = rows.source_id
296
+ AND sources.materialization_id = rows.materialization_id
297
+ WHERE rows.project_id = ${this.projectId}
298
+ AND rows.entity_kind = 'link'
299
+ AND sources.status = 'active'
300
+ `,
301
+ this.sql<LinkOverrideRow[]>`
302
+ WITH requested AS (
303
+ SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
304
+ source_type_id TEXT, source_primary_id TEXT, link_id TEXT,
305
+ target_type_id TEXT, target_primary_id TEXT
306
+ )
307
+ )
308
+ SELECT overrides.* FROM ontology_overrides AS overrides
309
+ JOIN requested USING (
310
+ source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
311
+ )
312
+ WHERE overrides.project_id = ${this.projectId}
313
+ AND overrides.entity_kind = 'link'
314
+ `,
315
+ ])
316
+ const effective = new Map(
317
+ effectiveRows.map((row) => [linkRefKey(linkRefFromColumns(row)), row] as const)
318
+ )
319
+ const sources = activeSourceMap(sourceRows)
320
+ const overrides = new Map(
321
+ overrideRows.map(
322
+ (row) => [linkRefKey(linkRefFromOverrideColumns(row)), storedLinkOverride(row)] as const
323
+ )
324
+ )
325
+ return refs.map((ref) => {
326
+ const key = linkRefKey(ref)
327
+ const effectiveRow = effective.get(key)
328
+ return {
329
+ ref: structuredClone(ref),
330
+ source: sourceLink(sources.get(projectionEntityKey({ kind: "link", ref }))),
331
+ override: overrides.get(key) ?? null,
332
+ effective: effectiveRow ? effectiveLinkSnapshot(effectiveRow) : null,
333
+ }
334
+ })
335
+ }
336
+
337
+ async exactPoint(
338
+ series: TelemetrySeriesRef,
339
+ at: string,
340
+ lock = false
341
+ ): Promise<StoredTelemetryPoint | null> {
342
+ return (await this.exactPoints([{ series, at }], lock))[0] ?? null
343
+ }
344
+
345
+ async exactPoints(
346
+ points: readonly { readonly series: TelemetrySeriesRef; readonly at: string }[],
347
+ lock = false
348
+ ): Promise<readonly StoredTelemetryPoint[]> {
349
+ if (points.length === 0) return []
350
+ const requested = points.map(({ series, at }) => ({
351
+ object_type_id: series.object.objectTypeId,
352
+ object_id: series.object.primaryId,
353
+ property_id: series.propertyId,
354
+ at,
355
+ }))
356
+ const lockFragment = lock ? this.sql`FOR UPDATE OF timeseries` : this.sql``
357
+ const rows = await this.sql<TelemetryRow[]>`
358
+ WITH requested AS (
359
+ SELECT DISTINCT * FROM jsonb_to_recordset(${jsonParameter(this.sql, requested)}) AS requested_values(
360
+ object_type_id TEXT, object_id TEXT, property_id TEXT, at TIMESTAMPTZ
361
+ )
362
+ )
363
+ SELECT timeseries.* FROM timeseries
364
+ JOIN requested USING (object_type_id, object_id, property_id, at)
365
+ WHERE timeseries.project_id = ${this.projectId}
366
+ ${lockFragment}
367
+ `
368
+ const found = new Map(
369
+ rows.map((row) => {
370
+ const point = storedPoint(row)
371
+ return [telemetryPointKey(point.series, point.at), point] as const
372
+ })
373
+ )
374
+ return points.flatMap((point) => {
375
+ const stored = found.get(telemetryPointKey(point.series, point.at))
376
+ return stored ? [stored] : []
377
+ })
378
+ }
379
+
380
+ async linkScopes(
381
+ scopes: readonly { readonly source: OntologyObjectRef; readonly linkId: string }[]
382
+ ): Promise<readonly MaterializationLinkScopeState[]> {
383
+ if (scopes.length === 0) return []
384
+ const requested = scopes.map(({ source, linkId }) => ({
385
+ scope_sort_key: linkScopeSortKey(source, linkId),
386
+ source_type_id: source.objectTypeId,
387
+ source_id: source.primaryId,
388
+ link_id: linkId,
389
+ }))
390
+ const accumulators = new Map(
391
+ scopes.map(({ source, linkId }) => {
392
+ const key = linkScopeSortKey(source, linkId)
393
+ return [key, startScopeAccumulator(source, linkId, key)] as const
394
+ })
395
+ )
396
+ let scopeCursor: string | null = null
397
+ let linkCursor: string | null = null
398
+ while (true) {
399
+ const rows: LinkScopeRow[] = await this.sql`
400
+ WITH requested AS (
401
+ SELECT DISTINCT * FROM jsonb_to_recordset(${jsonParameter(this.sql, requested)})
402
+ AS requested_values(
403
+ scope_sort_key TEXT, source_type_id TEXT, source_id TEXT, link_id TEXT
404
+ )
405
+ ), selected AS (
406
+ SELECT requested.scope_sort_key, links.*,
407
+ ${this.sql.unsafe(linkSortExpression("links"))} AS link_sort_key
408
+ FROM links
409
+ JOIN requested USING (source_type_id, source_id, link_id)
410
+ WHERE links.project_id = ${this.projectId}
411
+ )
412
+ SELECT * FROM selected
413
+ WHERE (${scopeCursor}::text IS NULL OR (scope_sort_key, link_sort_key) >
414
+ (${scopeCursor}, ${linkCursor}))
415
+ ORDER BY scope_sort_key, link_sort_key
416
+ LIMIT 500
417
+ `
418
+ for (const row of rows) {
419
+ const accumulator = accumulators.get(row.scope_sort_key)
420
+ if (!accumulator) {
421
+ throw new MaterializationConflictError(
422
+ "effective-state",
423
+ `Unexpected link scope '${row.scope_sort_key}' returned by storage.`
424
+ )
425
+ }
426
+ appendScopeSnapshot(accumulator, effectiveLinkSnapshot(row))
427
+ }
428
+ if (rows.length < 500) break
429
+ const last: LinkScopeRow = rows[rows.length - 1]!
430
+ scopeCursor = last.scope_sort_key
431
+ linkCursor = last.link_sort_key
432
+ }
433
+ return scopes.map(({ source, linkId }) =>
434
+ finishScopeAccumulator(accumulators.get(linkScopeSortKey(source, linkId))!)
435
+ )
436
+ }
437
+
438
+ async linkScope(
439
+ source: OntologyObjectRef,
440
+ linkId: string
441
+ ): Promise<MaterializationLinkScopeState> {
442
+ const [scope] = await this.linkScopes([{ source, linkId }])
443
+ if (!scope) {
444
+ throw new MaterializationConflictError(
445
+ "effective-state",
446
+ "Link scope lookup returned no row."
447
+ )
448
+ }
449
+ return scope
450
+ }
451
+
452
+ async *incidentLinks(
453
+ objects: readonly OntologyObjectRef[],
454
+ pageRows: number
455
+ ): AsyncIterable<OntologyLinkRef[]> {
456
+ if (objects.length === 0) return
457
+ const requested = objects.map((ref) => ({
458
+ object_type_id: ref.objectTypeId,
459
+ primary_id: ref.primaryId,
460
+ }))
461
+ let cursor: string | null = null
462
+ while (true) {
463
+ const rows: LinkIdentityRow[] = await this.sql`
464
+ WITH requested AS (
465
+ SELECT DISTINCT *
466
+ FROM jsonb_to_recordset(${jsonParameter(this.sql, requested)})
467
+ AS requested_values(object_type_id TEXT, primary_id TEXT)
468
+ ), authority_links AS (
469
+ SELECT links.source_type_id, links.source_id, links.link_id,
470
+ links.target_type_id, links.target_id
471
+ FROM links
472
+ JOIN requested
473
+ ON requested.object_type_id = links.source_type_id
474
+ AND requested.primary_id = links.source_id
475
+ WHERE links.project_id = ${this.projectId}
476
+ UNION
477
+ SELECT links.source_type_id, links.source_id, links.link_id,
478
+ links.target_type_id, links.target_id
479
+ FROM links
480
+ JOIN requested
481
+ ON requested.object_type_id = links.target_type_id
482
+ AND requested.primary_id = links.target_id
483
+ WHERE links.project_id = ${this.projectId}
484
+ UNION
485
+ SELECT overrides.source_type_id, overrides.source_primary_id AS source_id,
486
+ overrides.link_id, overrides.target_type_id,
487
+ overrides.target_primary_id AS target_id
488
+ FROM ontology_overrides AS overrides
489
+ JOIN requested
490
+ ON requested.object_type_id = overrides.source_type_id
491
+ AND requested.primary_id = overrides.source_primary_id
492
+ WHERE overrides.project_id = ${this.projectId} AND overrides.entity_kind = 'link'
493
+ UNION
494
+ SELECT overrides.source_type_id, overrides.source_primary_id AS source_id,
495
+ overrides.link_id, overrides.target_type_id,
496
+ overrides.target_primary_id AS target_id
497
+ FROM ontology_overrides AS overrides
498
+ JOIN requested
499
+ ON requested.object_type_id = overrides.target_type_id
500
+ AND requested.primary_id = overrides.target_primary_id
501
+ WHERE overrides.project_id = ${this.projectId} AND overrides.entity_kind = 'link'
502
+ UNION
503
+ SELECT rows.source_type_id, rows.source_primary_id AS source_id,
504
+ rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
505
+ FROM ontology_source_rows AS rows
506
+ JOIN ontology_sources AS sources
507
+ ON sources.project_id = rows.project_id
508
+ AND sources.source_id = rows.source_id
509
+ AND sources.materialization_id = rows.materialization_id
510
+ JOIN requested
511
+ ON requested.object_type_id = rows.source_type_id
512
+ AND requested.primary_id = rows.source_primary_id
513
+ WHERE rows.project_id = ${this.projectId}
514
+ AND rows.entity_kind = 'link'
515
+ AND sources.status = 'active'
516
+ UNION
517
+ SELECT rows.source_type_id, rows.source_primary_id AS source_id,
518
+ rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
519
+ FROM ontology_source_rows AS rows
520
+ JOIN ontology_sources AS sources
521
+ ON sources.project_id = rows.project_id
522
+ AND sources.source_id = rows.source_id
523
+ AND sources.materialization_id = rows.materialization_id
524
+ JOIN requested
525
+ ON requested.object_type_id = rows.target_type_id
526
+ AND requested.primary_id = rows.target_primary_id
527
+ WHERE rows.project_id = ${this.projectId}
528
+ AND rows.entity_kind = 'link'
529
+ AND sources.status = 'active'
530
+ ), selected AS (
531
+ SELECT DISTINCT links.*,
532
+ ${this.sql.unsafe(linkSortExpression("links"))} AS sort_key
533
+ FROM authority_links AS links
534
+ )
535
+ SELECT * FROM selected
536
+ WHERE (${cursor}::text IS NULL OR sort_key > ${cursor})
537
+ ORDER BY sort_key
538
+ LIMIT ${pageRows}
539
+ `
540
+ if (rows.length === 0) break
541
+ yield rows.map(linkRefFromColumns)
542
+ cursor = rows[rows.length - 1]?.sort_key ?? null
543
+ }
544
+ }
545
+
546
+ async *replacementIdentities(
547
+ input: ReplacementIdentityInput
548
+ ): AsyncIterable<ReplacementIdentity[]> {
549
+ await this.prepareReplacementIdentities(input)
550
+ let cursor: string | null = null
551
+ while (true) {
552
+ const rows: ReplacementWorkRow[] = await this.sql<ReplacementWorkRow[]>`
553
+ SELECT entity_kind, identity_key, sort_key, diff_required
554
+ FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)}
555
+ WHERE session_id = ${input.sessionId} AND entity_kind = ${input.kind}
556
+ AND (${cursor}::text IS NULL OR sort_key > ${cursor})
557
+ ORDER BY sort_key
558
+ LIMIT ${input.pageRows}
559
+ `
560
+ if (rows.length === 0) break
561
+ yield rows.map(replacementIdentity)
562
+ cursor = rows[rows.length - 1]?.sort_key ?? null
563
+ }
564
+ }
565
+
566
+ async replacementObjectStates(
567
+ sourceId: string,
568
+ candidateMaterializationId: string,
569
+ refs: readonly OntologyObjectRef[]
570
+ ): Promise<readonly SourceReplacementObjectState[]> {
571
+ const base = await this.objectStates(refs)
572
+ const candidates = await this.replacementSources(
573
+ sourceId,
574
+ [candidateMaterializationId],
575
+ refs.map((ref) => ({ kind: "object" as const, ref }))
576
+ )
577
+ return base.map((state) => ({
578
+ ref: state.ref,
579
+ candidateSource: sourceObject(
580
+ candidates.byMaterialization
581
+ .get(candidateMaterializationId)
582
+ ?.get(projectionEntityKey({ kind: "object", ref: state.ref }))
583
+ ),
584
+ override: state.override,
585
+ effective: state.effective,
586
+ latestTelemetry: state.latestTelemetry,
587
+ }))
588
+ }
589
+
590
+ async replacementLinkStates(
591
+ sourceId: string,
592
+ candidateMaterializationId: string,
593
+ materializationIds: readonly string[],
594
+ identities: readonly Extract<ReplacementIdentity, { readonly kind: "link" }>[]
595
+ ): Promise<readonly SourceReplacementLinkState[]> {
596
+ const refs = identities.map((identity) => identity.ref)
597
+ const base = await this.linkStates(refs)
598
+ const replacements = await this.replacementSources(
599
+ sourceId,
600
+ materializationIds,
601
+ refs.map((ref) => ({ kind: "link" as const, ref }))
602
+ )
603
+ const candidate = replacements.byMaterialization.get(candidateMaterializationId)
604
+ return base.map((state, index) => {
605
+ const key = projectionEntityKey({ kind: "link", ref: state.ref })
606
+ return {
607
+ ref: state.ref,
608
+ candidateSource: replacements.owned.has(key)
609
+ ? sourceLink(candidate?.get(key))
610
+ : state.source,
611
+ override: state.override,
612
+ effective: state.effective,
613
+ diffRequired: identities[index]!.diffRequired,
614
+ }
615
+ })
616
+ }
617
+
618
+ async effectiveObjectRevision(
619
+ ref: OntologyObjectRef,
620
+ lock = false
621
+ ): Promise<{ readonly version: number; readonly lastCommitId: string | null } | null> {
622
+ return (await this.effectiveObjectRevisions([ref], lock)).get(objectRefKey(ref)) ?? null
623
+ }
624
+
625
+ async effectiveObjectRevisions(
626
+ refs: readonly OntologyObjectRef[],
627
+ lock = false
628
+ ): Promise<
629
+ ReadonlyMap<string, { readonly version: number; readonly lastCommitId: string | null }>
630
+ > {
631
+ if (refs.length === 0) return new Map()
632
+ const requested = refs.map((ref) => ({
633
+ object_type_id: ref.objectTypeId,
634
+ primary_id: ref.primaryId,
635
+ }))
636
+ const lockFragment = lock ? this.sql`FOR UPDATE OF objects` : this.sql``
637
+ const rows = await this.sql<
638
+ {
639
+ readonly object_type_id: string
640
+ readonly primary_id: string
641
+ readonly version: number
642
+ readonly last_commit_id: string | null
643
+ }[]
644
+ >`
645
+ WITH requested AS (
646
+ SELECT DISTINCT * FROM jsonb_to_recordset(${jsonParameter(this.sql, requested)})
647
+ AS requested_values(object_type_id TEXT, primary_id TEXT)
648
+ )
649
+ SELECT objects.object_type_id, objects.primary_id, objects.version,
650
+ objects.last_commit_id
651
+ FROM objects JOIN requested USING (object_type_id, primary_id)
652
+ WHERE objects.project_id = ${this.projectId}
653
+ ${lockFragment}
654
+ `
655
+ return new Map(
656
+ rows.map(
657
+ (row) =>
658
+ [
659
+ objectRefKey(objectRefFromColumns(row)),
660
+ { version: row.version, lastCommitId: row.last_commit_id },
661
+ ] as const
662
+ )
663
+ )
664
+ }
665
+
666
+ async effectiveLinkLastCommit(
667
+ ref: OntologyLinkRef,
668
+ lock = false
669
+ ): Promise<string | null | undefined> {
670
+ return (await this.effectiveLinkLastCommits([ref], lock)).get(linkRefKey(ref))
671
+ }
672
+
673
+ async effectiveLinkLastCommits(
674
+ refs: readonly OntologyLinkRef[],
675
+ lock = false
676
+ ): Promise<ReadonlyMap<string, string | null>> {
677
+ if (refs.length === 0) return new Map()
678
+ const requested = refs.map((ref) => ({
679
+ source_type_id: ref.source.objectTypeId,
680
+ source_id: ref.source.primaryId,
681
+ link_id: ref.linkId,
682
+ target_type_id: ref.target.objectTypeId,
683
+ target_id: ref.target.primaryId,
684
+ }))
685
+ const lockFragment = lock ? this.sql`FOR UPDATE OF links` : this.sql``
686
+ const rows = await this.sql<EffectiveLinkRow[]>`
687
+ WITH requested AS (
688
+ SELECT DISTINCT * FROM jsonb_to_recordset(${jsonParameter(this.sql, requested)})
689
+ AS requested_values(
690
+ source_type_id TEXT, source_id TEXT, link_id TEXT,
691
+ target_type_id TEXT, target_id TEXT
692
+ )
693
+ )
694
+ SELECT links.* FROM links
695
+ JOIN requested USING (source_type_id, source_id, link_id, target_type_id, target_id)
696
+ WHERE links.project_id = ${this.projectId}
697
+ ${lockFragment}
698
+ `
699
+ return new Map(
700
+ rows.map((row) => [linkRefKey(linkRefFromColumns(row)), row.last_commit_id] as const)
701
+ )
702
+ }
703
+
704
+ async overrideLastCommit(kind: "object" | "link", entityKey: string): Promise<string | null> {
705
+ const [row] = await this.sql<{ readonly last_commit_id: string }[]>`
706
+ SELECT last_commit_id FROM ontology_overrides
707
+ WHERE project_id = ${this.projectId}
708
+ AND entity_kind = ${kind}
709
+ AND entity_key = ${jsonKeyParameter(this.sql, entityKey)}
710
+ `
711
+ return row?.last_commit_id ?? null
712
+ }
713
+
714
+ private async replacementSources(
715
+ sourceId: string,
716
+ materializationIds: readonly string[],
717
+ refs: readonly ProjectionEntityRef[]
718
+ ): Promise<ReplacementSources> {
719
+ if (materializationIds.length === 0 || refs.length === 0) {
720
+ return { owned: new Set(), byMaterialization: new Map() }
721
+ }
722
+ const keys = refs.map((ref) => ({
723
+ entity_kind: ref.kind,
724
+ entity_key: JSON.parse(projectionEntityKey(ref)) as unknown,
725
+ }))
726
+ const rows = await this.sql<PgOntologySourceAssertionRow[]>`
727
+ WITH requested AS (
728
+ SELECT *
729
+ FROM jsonb_to_recordset(${jsonParameter(this.sql, keys)})
730
+ AS requested_values(entity_kind TEXT, entity_key JSONB)
731
+ )
732
+ SELECT rows.* FROM ontology_source_rows AS rows
733
+ JOIN requested USING (entity_kind, entity_key)
734
+ WHERE rows.project_id = ${this.projectId}
735
+ AND rows.source_id = ${sourceId}
736
+ AND rows.materialization_id = ANY(
737
+ ${this.sql.array([...new Set(materializationIds)])}::text[]
738
+ )
739
+ `
740
+ const owned = new Set<string>()
741
+ const byMaterialization = new Map<string, Map<string, StoredSourceAssertion>>()
742
+ for (const row of rows) {
743
+ const source = storedSource(row)
744
+ const key = projectionEntityKey(source.assertion)
745
+ owned.add(key)
746
+ const materialization = byMaterialization.get(row.materialization_id) ?? new Map()
747
+ materialization.set(key, source)
748
+ byMaterialization.set(row.materialization_id, materialization)
749
+ }
750
+ return { owned, byMaterialization }
751
+ }
752
+
753
+ private async prepareReplacementIdentities(input: ReplacementIdentityInput): Promise<void> {
754
+ if (input.kind === "object") {
755
+ await this.prepareReplacementObjects(input)
756
+ return
757
+ }
758
+ await this.prepareReplacementLinks(input)
759
+ }
760
+
761
+ private async prepareReplacementObjects(input: ReplacementIdentityInput): Promise<void> {
762
+ const materializationIds = [
763
+ input.candidateMaterializationId,
764
+ ...(input.previousMaterializationId ? [input.previousMaterializationId] : []),
765
+ ]
766
+ await this.sql`
767
+ WITH selected AS (
768
+ SELECT DISTINCT object_type_id, primary_id,
769
+ ${this.sql.unsafe(objectKeyExpression("rows"))} AS identity_key,
770
+ ${this.sql.unsafe(objectSortExpression("rows"))} AS sort_key
771
+ FROM ontology_source_rows AS rows
772
+ WHERE project_id = ${this.projectId}
773
+ AND source_id = ${input.sourceId}
774
+ AND entity_kind = 'object'
775
+ AND materialization_id = ANY(${this.sql.array(materializationIds)}::text[])
776
+ )
777
+ INSERT INTO ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
778
+ session_id, entity_kind, identity_key, sort_key, diff_required
779
+ )
780
+ SELECT ${input.sessionId}, 'object', identity_key, sort_key, TRUE
781
+ FROM selected
782
+ ON CONFLICT (session_id, entity_kind, identity_key) DO UPDATE SET
783
+ diff_required = ${this.sql(PG_REPLACEMENT_WORK_TABLE)}.diff_required
784
+ OR EXCLUDED.diff_required
785
+ `
786
+ }
787
+
788
+ private async prepareReplacementLinks(input: ReplacementIdentityInput): Promise<void> {
789
+ const materializationIds = [
790
+ input.candidateMaterializationId,
791
+ ...(input.previousMaterializationId ? [input.previousMaterializationId] : []),
792
+ ]
793
+ await this.sql`
794
+ WITH incident_objects AS (
795
+ SELECT DISTINCT payload->'ref'->>'objectTypeId' AS object_type_id,
796
+ payload->'ref'->>'primaryId' AS primary_id
797
+ FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
798
+ WHERE session_id = ${input.sessionId} AND kind = 'incident-object'
799
+ ), replacement_links AS (
800
+ SELECT source_type_id, source_primary_id AS source_id, link_id,
801
+ target_type_id, target_primary_id AS target_id
802
+ FROM ontology_source_rows
803
+ WHERE project_id = ${this.projectId}
804
+ AND source_id = ${input.sourceId}
805
+ AND entity_kind = 'link'
806
+ AND materialization_id = ANY(${this.sql.array(materializationIds)}::text[])
807
+ ), incident_links AS (
808
+ SELECT links.source_type_id, links.source_id, links.link_id,
809
+ links.target_type_id, links.target_id
810
+ FROM links
811
+ JOIN incident_objects
812
+ ON incident_objects.object_type_id = links.source_type_id
813
+ AND incident_objects.primary_id = links.source_id
814
+ WHERE links.project_id = ${this.projectId}
815
+ UNION
816
+ SELECT links.source_type_id, links.source_id, links.link_id,
817
+ links.target_type_id, links.target_id
818
+ FROM links
819
+ JOIN incident_objects
820
+ ON incident_objects.object_type_id = links.target_type_id
821
+ AND incident_objects.primary_id = links.target_id
822
+ WHERE links.project_id = ${this.projectId}
823
+ UNION
824
+ SELECT overrides.source_type_id, overrides.source_primary_id AS source_id,
825
+ overrides.link_id, overrides.target_type_id,
826
+ overrides.target_primary_id AS target_id
827
+ FROM ontology_overrides AS overrides
828
+ JOIN incident_objects
829
+ ON incident_objects.object_type_id = overrides.source_type_id
830
+ AND incident_objects.primary_id = overrides.source_primary_id
831
+ WHERE overrides.project_id = ${this.projectId} AND overrides.entity_kind = 'link'
832
+ UNION
833
+ SELECT overrides.source_type_id, overrides.source_primary_id AS source_id,
834
+ overrides.link_id, overrides.target_type_id,
835
+ overrides.target_primary_id AS target_id
836
+ FROM ontology_overrides AS overrides
837
+ JOIN incident_objects
838
+ ON incident_objects.object_type_id = overrides.target_type_id
839
+ AND incident_objects.primary_id = overrides.target_primary_id
840
+ WHERE overrides.project_id = ${this.projectId} AND overrides.entity_kind = 'link'
841
+ UNION
842
+ SELECT rows.source_type_id, rows.source_primary_id AS source_id,
843
+ rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
844
+ FROM ontology_source_rows AS rows
845
+ JOIN ontology_sources AS sources
846
+ ON sources.project_id = rows.project_id
847
+ AND sources.source_id = rows.source_id
848
+ AND sources.materialization_id = rows.materialization_id
849
+ JOIN incident_objects
850
+ ON incident_objects.object_type_id = rows.source_type_id
851
+ AND incident_objects.primary_id = rows.source_primary_id
852
+ WHERE rows.project_id = ${this.projectId}
853
+ AND rows.entity_kind = 'link'
854
+ AND sources.status = 'active'
855
+ UNION
856
+ SELECT rows.source_type_id, rows.source_primary_id AS source_id,
857
+ rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
858
+ FROM ontology_source_rows AS rows
859
+ JOIN ontology_sources AS sources
860
+ ON sources.project_id = rows.project_id
861
+ AND sources.source_id = rows.source_id
862
+ AND sources.materialization_id = rows.materialization_id
863
+ JOIN incident_objects
864
+ ON incident_objects.object_type_id = rows.target_type_id
865
+ AND incident_objects.primary_id = rows.target_primary_id
866
+ WHERE rows.project_id = ${this.projectId}
867
+ AND rows.entity_kind = 'link'
868
+ AND sources.status = 'active'
869
+ ), diff_links AS (
870
+ SELECT * FROM replacement_links
871
+ UNION SELECT * FROM incident_links
872
+ ), affected_scopes AS (
873
+ SELECT DISTINCT source_type_id, source_id, link_id FROM diff_links
874
+ ), all_links AS (
875
+ SELECT diff_links.*, TRUE AS diff_required FROM diff_links
876
+ UNION ALL
877
+ SELECT links.source_type_id, links.source_id, links.link_id,
878
+ links.target_type_id, links.target_id, FALSE AS diff_required
879
+ FROM links
880
+ JOIN affected_scopes USING (source_type_id, source_id, link_id)
881
+ WHERE links.project_id = ${this.projectId}
882
+ ), selected AS (
883
+ SELECT source_type_id, source_id, link_id, target_type_id, target_id,
884
+ ${this.sql.unsafe(linkKeyExpression("all_links"))} AS identity_key,
885
+ ${this.sql.unsafe(linkSortExpression("all_links"))} AS sort_key,
886
+ BOOL_OR(diff_required) AS diff_required
887
+ FROM all_links
888
+ GROUP BY source_type_id, source_id, link_id, target_type_id, target_id
889
+ )
890
+ INSERT INTO ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
891
+ session_id, entity_kind, identity_key, sort_key, diff_required
892
+ )
893
+ SELECT ${input.sessionId}, 'link', identity_key, sort_key, diff_required
894
+ FROM selected
895
+ ON CONFLICT (session_id, entity_kind, identity_key) DO UPDATE SET
896
+ diff_required = ${this.sql(PG_REPLACEMENT_WORK_TABLE)}.diff_required
897
+ OR EXCLUDED.diff_required
898
+ `
899
+ }
900
+ }
901
+
902
+ function replacementIdentity(row: ReplacementWorkRow): ReplacementIdentity {
903
+ const parts = JSON.parse(row.identity_key) as string[]
904
+ if (row.entity_kind === "object") {
905
+ return {
906
+ kind: "object",
907
+ ref: { objectTypeId: parts[0]!, primaryId: parts[1]! },
908
+ sortKey: row.sort_key,
909
+ diffRequired: true,
910
+ }
911
+ }
912
+ return {
913
+ kind: "link",
914
+ ref: {
915
+ source: { objectTypeId: parts[0]!, primaryId: parts[1]! },
916
+ linkId: parts[2]!,
917
+ target: { objectTypeId: parts[3]!, primaryId: parts[4]! },
918
+ },
919
+ sortKey: row.sort_key,
920
+ diffRequired: row.diff_required,
921
+ }
922
+ }
923
+
924
+ function activeSourceMap(
925
+ rows: readonly PgOntologySourceAssertionRow[]
926
+ ): ReadonlyMap<string, StoredSourceAssertion> {
927
+ const result = new Map<string, StoredSourceAssertion>()
928
+ for (const row of rows) {
929
+ const source = storedSource(row)
930
+ const key = projectionEntityKey(source.assertion)
931
+ if (result.has(key)) {
932
+ throw new MaterializationConflictError(
933
+ "source-materialization",
934
+ `Multiple active sources assert ${key}.`
935
+ )
936
+ }
937
+ result.set(key, source)
938
+ }
939
+ return result
940
+ }
941
+
942
+ function sourceObject(
943
+ source: StoredSourceAssertion | undefined
944
+ ): StoredSourceObjectAssertion | null {
945
+ return source?.assertion.kind === "object" ? (source as StoredSourceObjectAssertion) : null
946
+ }
947
+
948
+ function sourceLink(source: StoredSourceAssertion | undefined): StoredSourceLinkAssertion | null {
949
+ return source?.assertion.kind === "link" ? (source as StoredSourceLinkAssertion) : null
950
+ }
951
+
952
+ function storedObjectOverride(row: ObjectOverrideRow): StoredObjectOverride {
953
+ const ref = objectRefFromColumns(row)
954
+ return {
955
+ ref,
956
+ value: structuredClone(row.value) as StoredObjectOverride["value"],
957
+ lastCommitId: row.last_commit_id,
958
+ updatedAt: toIsoString(row.updated_at),
959
+ }
960
+ }
961
+
962
+ function storedLinkOverride(row: LinkOverrideRow): StoredLinkOverride {
963
+ const ref = linkRefFromOverrideColumns(row)
964
+ return {
965
+ ref,
966
+ value: structuredClone(row.value) as StoredLinkOverride["value"],
967
+ lastCommitId: row.last_commit_id,
968
+ updatedAt: toIsoString(row.updated_at),
969
+ }
970
+ }
971
+
972
+ function linkRefFromOverrideColumns(row: LinkOverrideRow): OntologyLinkRef {
973
+ return {
974
+ source: { objectTypeId: row.source_type_id, primaryId: row.source_primary_id },
975
+ linkId: row.link_id,
976
+ target: { objectTypeId: row.target_type_id, primaryId: row.target_primary_id },
977
+ }
978
+ }
979
+
980
+ function storedSource(row: PgOntologySourceAssertionRow): StoredSourceAssertion {
981
+ return {
982
+ source: { projectionId: row.source_id },
983
+ materializationId: row.materialization_id,
984
+ root: structuredClone(row.root) as StoredSourceAssertion["root"],
985
+ assertion: structuredClone(row.assertion) as StoredSourceAssertion["assertion"],
986
+ stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal"),
987
+ } as StoredSourceAssertion
988
+ }
989
+
990
+ function effectiveObjectSnapshot(row: EffectiveObjectRow): EffectiveObjectSnapshot {
991
+ if (!row.last_commit_id) {
992
+ throw new MaterializationConflictError(
993
+ "effective-state",
994
+ `Effective object ${row.object_type_id}:${row.primary_id} lacks materializer provenance.`
995
+ )
996
+ }
997
+ return {
998
+ ref: objectRefFromColumns(row),
999
+ properties: structuredClone(row.properties) as EffectiveObjectSnapshot["properties"],
1000
+ version: row.version,
1001
+ createdAt: toIsoString(row.created_at),
1002
+ updatedAt: toIsoString(row.updated_at),
1003
+ lastCommitId: row.last_commit_id,
1004
+ }
1005
+ }
1006
+
1007
+ function effectiveLinkSnapshot(row: EffectiveLinkRow): EffectiveLinkSnapshot {
1008
+ if (!row.last_commit_id) {
1009
+ throw new MaterializationConflictError(
1010
+ "effective-state",
1011
+ `Effective link ${linkRefKey(linkRefFromColumns(row))} lacks materializer provenance.`
1012
+ )
1013
+ }
1014
+ return {
1015
+ ref: linkRefFromColumns(row),
1016
+ ...(row.properties === null
1017
+ ? {}
1018
+ : {
1019
+ properties: structuredClone(row.properties) as NonNullable<
1020
+ EffectiveLinkSnapshot["properties"]
1021
+ >,
1022
+ }),
1023
+ createdAt: toIsoString(row.created_at),
1024
+ updatedAt: toIsoString(row.updated_at),
1025
+ lastCommitId: row.last_commit_id,
1026
+ }
1027
+ }
1028
+
1029
+ function storedPoint(row: TelemetryRow): StoredTelemetryPoint {
1030
+ const at = toIsoString(row.at)
1031
+ if (!row.last_commit_id) {
1032
+ throw new MaterializationConflictError(
1033
+ "timeseries-point",
1034
+ `Telemetry point ${telemetryPointSortKey(
1035
+ {
1036
+ object: { objectTypeId: row.object_type_id, primaryId: row.object_id },
1037
+ propertyId: row.property_id,
1038
+ },
1039
+ at
1040
+ )} lacks materializer provenance.`
1041
+ )
1042
+ }
1043
+ return {
1044
+ series: {
1045
+ object: { objectTypeId: row.object_type_id, primaryId: row.object_id },
1046
+ propertyId: row.property_id,
1047
+ },
1048
+ value: structuredClone(row.value) as StoredTelemetryPoint["value"],
1049
+ ...(row.unit === null ? {} : { unit: row.unit }),
1050
+ at,
1051
+ lastCommitId: row.last_commit_id,
1052
+ }
1053
+ }
1054
+
1055
+ export function objectSortExpression(alias?: string): string {
1056
+ const prefix = alias ? `${alias}.` : ""
1057
+ return utf8SortExpression([
1058
+ `to_jsonb(${prefix}object_type_id)::text`,
1059
+ `to_jsonb(${prefix}primary_id)::text`,
1060
+ ])
1061
+ }
1062
+
1063
+ function objectKeyExpression(alias?: string): string {
1064
+ const prefix = alias ? `${alias}.` : ""
1065
+ return jsonTupleExpression([
1066
+ `to_jsonb(${prefix}object_type_id)::text`,
1067
+ `to_jsonb(${prefix}primary_id)::text`,
1068
+ ])
1069
+ }
1070
+
1071
+ export function linkSortExpression(alias?: string): string {
1072
+ const prefix = alias ? `${alias}.` : ""
1073
+ return utf8SortExpression([
1074
+ `to_jsonb(${prefix}source_type_id)::text`,
1075
+ `to_jsonb(${prefix}source_id)::text`,
1076
+ `to_jsonb(${prefix}link_id)::text`,
1077
+ `to_jsonb(${prefix}target_type_id)::text`,
1078
+ `to_jsonb(${prefix}target_id)::text`,
1079
+ ])
1080
+ }
1081
+
1082
+ function linkKeyExpression(alias?: string): string {
1083
+ const prefix = alias ? `${alias}.` : ""
1084
+ return jsonTupleExpression([
1085
+ `to_jsonb(${prefix}source_type_id)::text`,
1086
+ `to_jsonb(${prefix}source_id)::text`,
1087
+ `to_jsonb(${prefix}link_id)::text`,
1088
+ `to_jsonb(${prefix}target_type_id)::text`,
1089
+ `to_jsonb(${prefix}target_id)::text`,
1090
+ ])
1091
+ }
1092
+
1093
+ export function pointSortExpression(alias?: string): string {
1094
+ const prefix = alias ? `${alias}.` : ""
1095
+ return utf8SortExpression([
1096
+ `to_jsonb(${prefix}object_type_id)::text`,
1097
+ `to_jsonb(${prefix}object_id)::text`,
1098
+ `to_jsonb(${prefix}property_id)::text`,
1099
+ `to_jsonb(to_char(${prefix}at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'))::text`,
1100
+ ])
1101
+ }
1102
+
1103
+ function utf8SortExpression(parts: readonly string[]): string {
1104
+ return `encode(convert_to(${jsonTupleExpression(parts)}, 'UTF8'), 'hex')`
1105
+ }
1106
+
1107
+ function jsonTupleExpression(parts: readonly string[]): string {
1108
+ return `concat('[', ${parts.join(", ',', ")}, ']')`
1109
+ }