@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,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
|
+
}
|