@sixb/pg 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- package/src/transactions.ts +58 -0
|
@@ -0,0 +1,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
|
+
}
|