@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,509 @@
1
+ import {
2
+ linkRefKey,
3
+ MaterializationConflictError,
4
+ objectRefKey,
5
+ telemetryPointKey,
6
+ } from "@sixb/core/internal/materialization"
7
+ import {
8
+ effectiveConflict,
9
+ type OverrideEntity,
10
+ overrideEntityColumns as overrideColumns,
11
+ } from "@sixb/core/internal/ontology-storage-provider"
12
+ import type { MaterializationPlanChunk } from "@sixb/core/storage"
13
+ import type { SQLClient } from "../pg-client"
14
+ import { assertTimestamp, jsonParameter } from "./shared"
15
+
16
+ /** Applies one already-validated exact plan chunk in bounded PostgreSQL batches. */
17
+ export class PgMaterializationWriter {
18
+ constructor(private readonly sql: SQLClient) {}
19
+
20
+ async apply(projectId: string, commitId: string, chunk: MaterializationPlanChunk): Promise<void> {
21
+ await this.applyOverrides(projectId, chunk)
22
+ await this.applyEffective(projectId, chunk)
23
+ await this.applyTimeseries(projectId, chunk)
24
+ await this.applyOutbox(projectId, commitId, chunk)
25
+ }
26
+
27
+ private async applyOverrides(projectId: string, chunk: MaterializationPlanChunk): Promise<void> {
28
+ const upserts = [
29
+ ...chunk.overrides.objectUpserts.map((item) =>
30
+ overrideWrite({ kind: "object", ref: item.ref }, item)
31
+ ),
32
+ ...chunk.overrides.linkUpserts.map((item) =>
33
+ overrideWrite({ kind: "link", ref: item.ref }, item)
34
+ ),
35
+ ]
36
+ const inserts = upserts.filter((item) => item.expectedLastCommitId === null)
37
+ if (inserts.length > 0) {
38
+ const rows = await this.sql<{ readonly entity_kind: "object" | "link" }[]>`
39
+ WITH staged AS (
40
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, inserts)}::jsonb)
41
+ )
42
+ INSERT INTO ontology_overrides (
43
+ project_id, entity_kind, entity_key, entity_sort_key,
44
+ object_type_id, primary_id, source_type_id, source_primary_id,
45
+ link_id, target_type_id, target_primary_id,
46
+ value, last_commit_id, updated_at
47
+ )
48
+ SELECT ${projectId}, value->>'kind', value->'entityKey', value->>'sortKey',
49
+ value->>'objectTypeId', value->>'primaryId', value->>'sourceTypeId',
50
+ value->>'sourcePrimaryId', value->>'linkId', value->>'targetTypeId',
51
+ value->>'targetPrimaryId', value->'value', value->>'lastCommitId',
52
+ (value->>'updatedAt')::timestamptz
53
+ FROM staged
54
+ ON CONFLICT DO NOTHING
55
+ RETURNING entity_kind
56
+ `
57
+ if (rows.length !== inserts.length) throw overrideConflict(inserts[0]!.kind)
58
+ }
59
+
60
+ const updates = upserts.filter((item) => item.expectedLastCommitId !== null)
61
+ if (updates.length > 0) {
62
+ const rows = await this.sql<{ readonly entity_kind: "object" | "link" }[]>`
63
+ WITH staged AS (
64
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, updates)}::jsonb)
65
+ )
66
+ UPDATE ontology_overrides AS overrides
67
+ SET value = staged.value->'value', last_commit_id = staged.value->>'lastCommitId',
68
+ updated_at = (staged.value->>'updatedAt')::timestamptz
69
+ FROM staged
70
+ WHERE overrides.project_id = ${projectId}
71
+ AND overrides.entity_kind = staged.value->>'kind'
72
+ AND overrides.entity_key = staged.value->'entityKey'
73
+ AND overrides.last_commit_id = staged.value->>'expectedLastCommitId'
74
+ RETURNING overrides.entity_kind
75
+ `
76
+ if (rows.length !== updates.length) throw overrideConflict(updates[0]!.kind)
77
+ }
78
+
79
+ const deletes = [
80
+ ...chunk.overrides.objectDeletes.map((item) => ({
81
+ kind: "object" as const,
82
+ entityKey: JSON.parse(objectRefKey(item.ref)) as unknown,
83
+ expectedLastCommitId: item.expectedLastCommitId,
84
+ })),
85
+ ...chunk.overrides.linkDeletes.map((item) => ({
86
+ kind: "link" as const,
87
+ entityKey: JSON.parse(linkRefKey(item.ref)) as unknown,
88
+ expectedLastCommitId: item.expectedLastCommitId,
89
+ })),
90
+ ]
91
+ if (deletes.length > 0) {
92
+ const rows = await this.sql<{ readonly entity_kind: "object" | "link" }[]>`
93
+ WITH staged AS (
94
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, deletes)}::jsonb)
95
+ )
96
+ DELETE FROM ontology_overrides AS overrides USING staged
97
+ WHERE overrides.project_id = ${projectId}
98
+ AND overrides.entity_kind = staged.value->>'kind'
99
+ AND overrides.entity_key = staged.value->'entityKey'
100
+ AND overrides.last_commit_id = staged.value->>'expectedLastCommitId'
101
+ RETURNING overrides.entity_kind
102
+ `
103
+ if (rows.length !== deletes.length) throw overrideConflict(deletes[0]!.kind)
104
+ }
105
+ }
106
+
107
+ private async applyEffective(projectId: string, chunk: MaterializationPlanChunk): Promise<void> {
108
+ const linkDeletes = chunk.effective.linkDeletes.map((item) => ({
109
+ sourceTypeId: item.ref.source.objectTypeId,
110
+ sourceId: item.ref.source.primaryId,
111
+ linkId: item.ref.linkId,
112
+ targetTypeId: item.ref.target.objectTypeId,
113
+ targetId: item.ref.target.primaryId,
114
+ expectedLastCommitId: item.expected.lastCommitId,
115
+ }))
116
+ if (linkDeletes.length > 0) {
117
+ const rows = await this.sql<{ readonly source_type_id: string }[]>`
118
+ WITH staged AS (
119
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, linkDeletes)}::jsonb)
120
+ )
121
+ DELETE FROM links AS effective USING staged
122
+ WHERE effective.project_id = ${projectId}
123
+ AND effective.source_type_id = staged.value->>'sourceTypeId'
124
+ AND effective.source_id = staged.value->>'sourceId'
125
+ AND effective.link_id = staged.value->>'linkId'
126
+ AND effective.target_type_id = staged.value->>'targetTypeId'
127
+ AND effective.target_id = staged.value->>'targetId'
128
+ AND effective.last_commit_id = staged.value->>'expectedLastCommitId'
129
+ RETURNING effective.source_type_id
130
+ `
131
+ if (rows.length !== linkDeletes.length) throw effectiveLinkConflict(linkDeletes[0]!)
132
+ }
133
+
134
+ const objectDeletes = chunk.effective.objectDeletes.map((item) => ({
135
+ objectTypeId: item.ref.objectTypeId,
136
+ primaryId: item.ref.primaryId,
137
+ expectedVersion: item.expected.version,
138
+ expectedLastCommitId: item.expected.lastCommitId,
139
+ }))
140
+ if (objectDeletes.length > 0) {
141
+ const rows = await this.sql<{ readonly object_type_id: string }[]>`
142
+ WITH staged AS (
143
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, objectDeletes)}::jsonb)
144
+ )
145
+ DELETE FROM objects AS effective USING staged
146
+ WHERE effective.project_id = ${projectId}
147
+ AND effective.object_type_id = staged.value->>'objectTypeId'
148
+ AND effective.primary_id = staged.value->>'primaryId'
149
+ AND effective.version = (staged.value->>'expectedVersion')::integer
150
+ AND effective.last_commit_id = staged.value->>'expectedLastCommitId'
151
+ RETURNING effective.object_type_id
152
+ `
153
+ if (rows.length !== objectDeletes.length) throw effectiveObjectConflict(objectDeletes[0]!)
154
+ }
155
+
156
+ await this.applyObjects(projectId, chunk.effective.objectUpserts)
157
+ await this.applyLinks(projectId, chunk.effective.linkUpserts)
158
+ }
159
+
160
+ private async applyObjects(
161
+ projectId: string,
162
+ items: MaterializationPlanChunk["effective"]["objectUpserts"]
163
+ ): Promise<void> {
164
+ const payload = items.map(({ row, expected }) => ({
165
+ objectTypeId: row.ref.objectTypeId,
166
+ primaryId: row.ref.primaryId,
167
+ properties: row.properties,
168
+ createdAt: row.createdAt,
169
+ updatedAt: row.updatedAt,
170
+ version: row.version,
171
+ lastCommitId: row.lastCommitId,
172
+ expectedExists: expected.exists,
173
+ expectedVersion: expected.exists ? expected.version : null,
174
+ expectedLastCommitId: expected.exists ? expected.lastCommitId : null,
175
+ }))
176
+ const inserts = payload.filter((item) => !item.expectedExists)
177
+ if (inserts.length > 0) {
178
+ const rows = await this.sql<{ readonly object_type_id: string }[]>`
179
+ WITH staged AS (
180
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, inserts)}::jsonb)
181
+ )
182
+ INSERT INTO objects (
183
+ project_id, object_type_id, primary_id, properties, created_at,
184
+ updated_at, version, last_commit_id
185
+ )
186
+ SELECT ${projectId}, value->>'objectTypeId', value->>'primaryId',
187
+ value->'properties', (value->>'createdAt')::timestamptz,
188
+ (value->>'updatedAt')::timestamptz, (value->>'version')::integer,
189
+ value->>'lastCommitId'
190
+ FROM staged
191
+ ON CONFLICT DO NOTHING
192
+ RETURNING object_type_id
193
+ `
194
+ if (rows.length !== inserts.length) {
195
+ throw effectiveConflict(`Expected object ${objectIdentityKey(inserts[0]!)} to be absent.`)
196
+ }
197
+ }
198
+
199
+ const updates = payload.filter((item) => item.expectedExists)
200
+ if (updates.length > 0) {
201
+ const rows = await this.sql<{ readonly object_type_id: string }[]>`
202
+ WITH staged AS (
203
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, updates)}::jsonb)
204
+ )
205
+ UPDATE objects AS effective
206
+ SET properties = staged.value->'properties',
207
+ created_at = (staged.value->>'createdAt')::timestamptz,
208
+ updated_at = (staged.value->>'updatedAt')::timestamptz,
209
+ version = (staged.value->>'version')::integer,
210
+ last_commit_id = staged.value->>'lastCommitId'
211
+ FROM staged
212
+ WHERE effective.project_id = ${projectId}
213
+ AND effective.object_type_id = staged.value->>'objectTypeId'
214
+ AND effective.primary_id = staged.value->>'primaryId'
215
+ AND effective.version = (staged.value->>'expectedVersion')::integer
216
+ AND effective.last_commit_id = staged.value->>'expectedLastCommitId'
217
+ RETURNING effective.object_type_id
218
+ `
219
+ if (rows.length !== updates.length) throw effectiveObjectConflict(updates[0]!)
220
+ }
221
+ }
222
+
223
+ private async applyLinks(
224
+ projectId: string,
225
+ items: MaterializationPlanChunk["effective"]["linkUpserts"]
226
+ ): Promise<void> {
227
+ const payload = items.map(({ row, expected }) => ({
228
+ sourceTypeId: row.ref.source.objectTypeId,
229
+ sourceId: row.ref.source.primaryId,
230
+ linkId: row.ref.linkId,
231
+ targetTypeId: row.ref.target.objectTypeId,
232
+ targetId: row.ref.target.primaryId,
233
+ ...(row.properties === undefined ? {} : { properties: row.properties }),
234
+ createdAt: row.createdAt,
235
+ updatedAt: row.updatedAt,
236
+ lastCommitId: row.lastCommitId,
237
+ expectedExists: expected.exists,
238
+ expectedLastCommitId: expected.exists ? expected.lastCommitId : null,
239
+ }))
240
+ const inserts = payload.filter((item) => !item.expectedExists)
241
+ if (inserts.length > 0) {
242
+ const rows = await this.sql<{ readonly source_type_id: string }[]>`
243
+ WITH staged AS (
244
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, inserts)}::jsonb)
245
+ )
246
+ INSERT INTO links (
247
+ project_id, source_type_id, source_id, link_id, target_type_id, target_id,
248
+ properties, created_at, updated_at, last_commit_id
249
+ )
250
+ SELECT ${projectId}, value->>'sourceTypeId', value->>'sourceId', value->>'linkId',
251
+ value->>'targetTypeId', value->>'targetId',
252
+ CASE WHEN value ? 'properties' THEN value->'properties' ELSE NULL END,
253
+ (value->>'createdAt')::timestamptz, (value->>'updatedAt')::timestamptz,
254
+ value->>'lastCommitId'
255
+ FROM staged
256
+ ON CONFLICT DO NOTHING
257
+ RETURNING source_type_id
258
+ `
259
+ if (rows.length !== inserts.length) {
260
+ throw effectiveConflict(`Expected link ${linkIdentityKey(inserts[0]!)} to be absent.`)
261
+ }
262
+ }
263
+
264
+ const updates = payload.filter((item) => item.expectedExists)
265
+ if (updates.length > 0) {
266
+ const rows = await this.sql<{ readonly source_type_id: string }[]>`
267
+ WITH staged AS (
268
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, updates)}::jsonb)
269
+ )
270
+ UPDATE links AS effective
271
+ SET properties = CASE
272
+ WHEN staged.value ? 'properties' THEN staged.value->'properties'
273
+ ELSE NULL
274
+ END,
275
+ created_at = (staged.value->>'createdAt')::timestamptz,
276
+ updated_at = (staged.value->>'updatedAt')::timestamptz,
277
+ last_commit_id = staged.value->>'lastCommitId'
278
+ FROM staged
279
+ WHERE effective.project_id = ${projectId}
280
+ AND effective.source_type_id = staged.value->>'sourceTypeId'
281
+ AND effective.source_id = staged.value->>'sourceId'
282
+ AND effective.link_id = staged.value->>'linkId'
283
+ AND effective.target_type_id = staged.value->>'targetTypeId'
284
+ AND effective.target_id = staged.value->>'targetId'
285
+ AND effective.last_commit_id = staged.value->>'expectedLastCommitId'
286
+ RETURNING effective.source_type_id
287
+ `
288
+ if (rows.length !== updates.length) throw effectiveLinkConflict(updates[0]!)
289
+ }
290
+ }
291
+
292
+ private async applyTimeseries(projectId: string, chunk: MaterializationPlanChunk): Promise<void> {
293
+ const payload = chunk.timeseries.pointUpserts.map(({ point, expected }) => ({
294
+ objectTypeId: point.series.object.objectTypeId,
295
+ objectId: point.series.object.primaryId,
296
+ propertyId: point.series.propertyId,
297
+ value: point.value,
298
+ unit: point.unit ?? null,
299
+ at: point.at,
300
+ lastCommitId: point.lastCommitId,
301
+ expectedLastCommitId: expected.lastCommitId,
302
+ }))
303
+ const inserts = payload.filter((item) => item.expectedLastCommitId === null)
304
+ if (inserts.length > 0) {
305
+ const rows = await this.sql<{ readonly object_type_id: string }[]>`
306
+ WITH staged AS (
307
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, inserts)}::jsonb)
308
+ ), written AS (
309
+ INSERT INTO timeseries (
310
+ project_id, object_type_id, object_id, property_id,
311
+ value, unit, at, last_commit_id
312
+ )
313
+ SELECT ${projectId}, value->>'objectTypeId', value->>'objectId',
314
+ value->>'propertyId', value->'value', value->>'unit',
315
+ (value->>'at')::timestamptz, value->>'lastCommitId'
316
+ FROM staged
317
+ ON CONFLICT DO NOTHING
318
+ RETURNING *
319
+ ), latest_candidates AS (
320
+ SELECT DISTINCT ON (project_id, object_type_id, object_id, property_id)
321
+ project_id, object_type_id, object_id, property_id,
322
+ value, unit, at, last_commit_id
323
+ FROM written
324
+ ORDER BY project_id, object_type_id, object_id, property_id, at DESC
325
+ ), latest AS (
326
+ INSERT INTO timeseries_latest (
327
+ project_id, object_type_id, object_id, property_id,
328
+ value, unit, at, last_commit_id
329
+ )
330
+ SELECT project_id, object_type_id, object_id, property_id,
331
+ value, unit, at, last_commit_id
332
+ FROM latest_candidates
333
+ ON CONFLICT (project_id, object_type_id, object_id, property_id)
334
+ DO UPDATE SET value = EXCLUDED.value, unit = EXCLUDED.unit, at = EXCLUDED.at,
335
+ last_commit_id = EXCLUDED.last_commit_id
336
+ WHERE EXCLUDED.at >= timeseries_latest.at
337
+ RETURNING object_type_id
338
+ )
339
+ SELECT object_type_id, (SELECT COUNT(*) FROM latest) AS latest_count FROM written
340
+ `
341
+ if (rows.length !== inserts.length) throw pointConflict(inserts[0]!)
342
+ }
343
+
344
+ const updates = payload.filter((item) => item.expectedLastCommitId !== null)
345
+ if (updates.length > 0) {
346
+ const rows = await this.sql<{ readonly object_type_id: string }[]>`
347
+ WITH staged AS (
348
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, updates)}::jsonb)
349
+ ), written AS (
350
+ UPDATE timeseries AS points
351
+ SET value = staged.value->'value', unit = staged.value->>'unit',
352
+ last_commit_id = staged.value->>'lastCommitId'
353
+ FROM staged
354
+ WHERE points.project_id = ${projectId}
355
+ AND points.object_type_id = staged.value->>'objectTypeId'
356
+ AND points.object_id = staged.value->>'objectId'
357
+ AND points.property_id = staged.value->>'propertyId'
358
+ AND points.at = (staged.value->>'at')::timestamptz
359
+ AND points.last_commit_id = staged.value->>'expectedLastCommitId'
360
+ RETURNING points.*
361
+ ), latest_candidates AS (
362
+ SELECT DISTINCT ON (project_id, object_type_id, object_id, property_id)
363
+ project_id, object_type_id, object_id, property_id,
364
+ value, unit, at, last_commit_id
365
+ FROM written
366
+ ORDER BY project_id, object_type_id, object_id, property_id, at DESC
367
+ ), latest AS (
368
+ INSERT INTO timeseries_latest (
369
+ project_id, object_type_id, object_id, property_id,
370
+ value, unit, at, last_commit_id
371
+ )
372
+ SELECT project_id, object_type_id, object_id, property_id,
373
+ value, unit, at, last_commit_id
374
+ FROM latest_candidates
375
+ ON CONFLICT (project_id, object_type_id, object_id, property_id)
376
+ DO UPDATE SET value = EXCLUDED.value, unit = EXCLUDED.unit, at = EXCLUDED.at,
377
+ last_commit_id = EXCLUDED.last_commit_id
378
+ WHERE EXCLUDED.at >= timeseries_latest.at
379
+ RETURNING object_type_id
380
+ )
381
+ SELECT object_type_id, (SELECT COUNT(*) FROM latest) AS latest_count FROM written
382
+ `
383
+ if (rows.length !== updates.length) throw pointConflict(updates[0]!)
384
+ }
385
+ }
386
+
387
+ private async applyOutbox(
388
+ projectId: string,
389
+ commitId: string,
390
+ chunk: MaterializationPlanChunk
391
+ ): Promise<void> {
392
+ if (chunk.outbox.length === 0) return
393
+ const payload = chunk.outbox.map((item) => {
394
+ assertTimestamp(item.availableAt, "Outbox availableAt")
395
+ assertTimestamp(item.createdAt, "Outbox createdAt")
396
+ return {
397
+ id: item.envelope.id,
398
+ commitOrdinal: item.envelope.commitOrdinal,
399
+ envelope: item.envelope,
400
+ availableAt: item.availableAt,
401
+ createdAt: item.createdAt,
402
+ }
403
+ })
404
+ const rows = await this.sql<{ readonly id: string }[]>`
405
+ WITH staged AS (
406
+ SELECT value FROM jsonb_array_elements(${jsonParameter(this.sql, payload)}::jsonb)
407
+ )
408
+ INSERT INTO ontology_outbox (
409
+ project_id, id, commit_id, commit_ordinal, envelope,
410
+ available_at, attempts, lease_id, lease_expires_at,
411
+ published_at, last_error, created_at
412
+ )
413
+ SELECT ${projectId}, value->>'id', ${commitId},
414
+ (value->>'commitOrdinal')::bigint, value->'envelope',
415
+ (value->>'availableAt')::timestamptz, 0, NULL, NULL, NULL, NULL,
416
+ (value->>'createdAt')::timestamptz
417
+ FROM staged
418
+ ON CONFLICT DO NOTHING
419
+ RETURNING id
420
+ `
421
+ if (rows.length !== payload.length) {
422
+ const inserted = new Set(rows.map((row) => row.id))
423
+ const duplicate = payload.find((item) => !inserted.has(item.id)) ?? payload[0]!
424
+ throw effectiveConflict(`Duplicate outbox event '${duplicate.id}'.`)
425
+ }
426
+ }
427
+ }
428
+
429
+ function overrideWrite(
430
+ entity: OverrideEntity,
431
+ item: {
432
+ readonly value: unknown
433
+ readonly lastCommitId: string
434
+ readonly updatedAt: string
435
+ readonly expectedLastCommitId: string | null
436
+ }
437
+ ) {
438
+ const key = entity.kind === "object" ? objectRefKey(entity.ref) : linkRefKey(entity.ref)
439
+ const columns = overrideColumns(entity)
440
+ return {
441
+ kind: entity.kind,
442
+ entityKey: JSON.parse(key) as unknown,
443
+ ...columns,
444
+ value: item.value,
445
+ lastCommitId: item.lastCommitId,
446
+ updatedAt: item.updatedAt,
447
+ expectedLastCommitId: item.expectedLastCommitId,
448
+ }
449
+ }
450
+
451
+ function overrideConflict(kind: "object" | "link"): MaterializationConflictError {
452
+ return effectiveConflict(`Expected ${kind} override changed.`)
453
+ }
454
+
455
+ function objectIdentityKey(item: {
456
+ readonly objectTypeId: string
457
+ readonly primaryId: string
458
+ }): string {
459
+ return objectRefKey({ objectTypeId: item.objectTypeId, primaryId: item.primaryId })
460
+ }
461
+
462
+ function linkIdentityKey(item: {
463
+ readonly sourceTypeId: string
464
+ readonly sourceId: string
465
+ readonly linkId: string
466
+ readonly targetTypeId: string
467
+ readonly targetId: string
468
+ }): string {
469
+ return linkRefKey({
470
+ source: { objectTypeId: item.sourceTypeId, primaryId: item.sourceId },
471
+ linkId: item.linkId,
472
+ target: { objectTypeId: item.targetTypeId, primaryId: item.targetId },
473
+ })
474
+ }
475
+
476
+ function effectiveObjectConflict(item: {
477
+ readonly objectTypeId: string
478
+ readonly primaryId: string
479
+ }): MaterializationConflictError {
480
+ return effectiveConflict(`Expected object ${objectIdentityKey(item)} changed.`)
481
+ }
482
+
483
+ function effectiveLinkConflict(item: {
484
+ readonly sourceTypeId: string
485
+ readonly sourceId: string
486
+ readonly linkId: string
487
+ readonly targetTypeId: string
488
+ readonly targetId: string
489
+ }): MaterializationConflictError {
490
+ return effectiveConflict(`Expected link ${linkIdentityKey(item)} changed.`)
491
+ }
492
+
493
+ function pointConflict(item: {
494
+ readonly objectTypeId: string
495
+ readonly objectId: string
496
+ readonly propertyId: string
497
+ readonly at: string
498
+ }): MaterializationConflictError {
499
+ return new MaterializationConflictError(
500
+ "timeseries-point",
501
+ `Telemetry point ${telemetryPointKey(
502
+ {
503
+ object: { objectTypeId: item.objectTypeId, primaryId: item.objectId },
504
+ propertyId: item.propertyId,
505
+ },
506
+ item.at
507
+ )} changed.`
508
+ )
509
+ }