@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,216 @@
1
+ import {
2
+ MaterializationConflictError,
3
+ MaterializationValidationError,
4
+ } from "@sixb/core/internal/materialization"
5
+ import type {
6
+ ClaimedOntologyOutboxRow,
7
+ ClaimOntologyOutboxInput,
8
+ CompleteOntologyOutboxLeaseInput,
9
+ OntologyOutboxStorage,
10
+ OntologyOutboxSummary,
11
+ PurgePublishedOntologyOutboxInput,
12
+ RescheduleOntologyOutboxLeaseInput,
13
+ SummarizeOntologyOutboxInput,
14
+ } from "@sixb/core/storage"
15
+ import {
16
+ assertNonblank,
17
+ assertPositiveInteger,
18
+ assertTimestamp,
19
+ outboxRecord,
20
+ type PgOntologyOutboxRow,
21
+ type PgRootOperation,
22
+ } from "./shared"
23
+
24
+ export class PgOntologyOutboxStorage implements OntologyOutboxStorage {
25
+ constructor(private readonly runRootOperation: PgRootOperation) {}
26
+
27
+ async claim(input: ClaimOntologyOutboxInput): Promise<readonly ClaimedOntologyOutboxRow[]> {
28
+ return this.runRootOperation(async (sql) => {
29
+ assertNonblank(input.projectId, "Ontology outbox project id")
30
+ assertPositiveInteger(input.limit, "Ontology outbox limit")
31
+ const now = assertTimestamp(input.now, "Ontology outbox claim time")
32
+ const leaseExpiry = assertTimestamp(input.leaseExpiresAt, "Ontology outbox lease expiry")
33
+ assertNonblank(input.leaseId, "Ontology outbox lease id")
34
+ if (leaseExpiry <= now) {
35
+ throw new MaterializationValidationError(
36
+ "Ontology outbox lease expiry must be later than the claim time."
37
+ )
38
+ }
39
+
40
+ const rows = await sql<PgOntologyOutboxRow[]>`
41
+ WITH candidates AS (
42
+ SELECT project_id, id
43
+ FROM ontology_outbox
44
+ WHERE project_id = ${input.projectId}
45
+ AND published_at IS NULL
46
+ AND available_at <= ${input.now}
47
+ AND (lease_expires_at IS NULL OR lease_expires_at <= ${input.now})
48
+ ORDER BY created_at, commit_id, commit_ordinal
49
+ LIMIT ${input.limit}
50
+ FOR UPDATE SKIP LOCKED
51
+ ), claimed AS (
52
+ UPDATE ontology_outbox AS outbox
53
+ SET attempts = outbox.attempts + 1,
54
+ lease_id = ${input.leaseId},
55
+ lease_expires_at = ${input.leaseExpiresAt}
56
+ FROM candidates
57
+ WHERE outbox.project_id = candidates.project_id AND outbox.id = candidates.id
58
+ RETURNING outbox.id AS row_id, outbox.envelope, outbox.available_at,
59
+ outbox.attempts, outbox.lease_id, outbox.lease_expires_at,
60
+ outbox.published_at, outbox.last_error, outbox.created_at,
61
+ outbox.commit_id, outbox.commit_ordinal
62
+ )
63
+ SELECT * FROM claimed ORDER BY created_at, commit_id, commit_ordinal
64
+ `
65
+ return rows.map((row) => {
66
+ const record = outboxRecord(row)
67
+ if (record.leaseId === null || record.leaseExpiresAt === null) {
68
+ throw new MaterializationConflictError(
69
+ "outbox-lease",
70
+ "Ontology outbox claim returned an unpaired lease."
71
+ )
72
+ }
73
+ return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt }
74
+ })
75
+ })
76
+ }
77
+
78
+ async markPublished(input: CompleteOntologyOutboxLeaseInput): Promise<void> {
79
+ await this.runRootOperation(async (sql) => {
80
+ assertLeaseInput(input)
81
+ assertTimestamp(input.publishedAt, "Ontology outbox publishedAt")
82
+ const ids = validateLeaseIds(input.ids)
83
+ if (ids.length === 0) return
84
+ const rows = await sql<{ readonly id: string }[]>`
85
+ WITH leased AS MATERIALIZED (
86
+ SELECT id FROM ontology_outbox
87
+ WHERE project_id = ${input.projectId}
88
+ AND id = ANY(${sql.array(ids)}::text[])
89
+ AND lease_id = ${input.leaseId}
90
+ AND lease_expires_at IS NOT NULL
91
+ FOR UPDATE
92
+ ), eligible AS (
93
+ SELECT id FROM leased
94
+ WHERE (SELECT COUNT(*) FROM leased) = ${ids.length}
95
+ )
96
+ UPDATE ontology_outbox AS outbox
97
+ SET published_at = ${input.publishedAt}, lease_id = NULL, lease_expires_at = NULL
98
+ FROM eligible
99
+ WHERE outbox.project_id = ${input.projectId} AND outbox.id = eligible.id
100
+ RETURNING outbox.id
101
+ `
102
+ if (rows.length !== ids.length) throw leaseConflict()
103
+ })
104
+ }
105
+
106
+ async reschedule(input: RescheduleOntologyOutboxLeaseInput): Promise<void> {
107
+ await this.runRootOperation(async (sql) => {
108
+ assertLeaseInput(input)
109
+ assertTimestamp(input.availableAt, "Ontology outbox availableAt")
110
+ const ids = validateLeaseIds(input.ids)
111
+ if (ids.length === 0) return
112
+ const rows = await sql<{ readonly id: string }[]>`
113
+ WITH leased AS MATERIALIZED (
114
+ SELECT id FROM ontology_outbox
115
+ WHERE project_id = ${input.projectId}
116
+ AND id = ANY(${sql.array(ids)}::text[])
117
+ AND lease_id = ${input.leaseId}
118
+ AND lease_expires_at IS NOT NULL
119
+ FOR UPDATE
120
+ ), eligible AS (
121
+ SELECT id FROM leased
122
+ WHERE (SELECT COUNT(*) FROM leased) = ${ids.length}
123
+ )
124
+ UPDATE ontology_outbox AS outbox
125
+ SET available_at = ${input.availableAt}, last_error = ${input.error},
126
+ lease_id = NULL, lease_expires_at = NULL
127
+ FROM eligible
128
+ WHERE outbox.project_id = ${input.projectId} AND outbox.id = eligible.id
129
+ RETURNING outbox.id
130
+ `
131
+ if (rows.length !== ids.length) throw leaseConflict()
132
+ })
133
+ }
134
+
135
+ async purgePublished(input: PurgePublishedOntologyOutboxInput): Promise<number> {
136
+ return this.runRootOperation(async (sql) => {
137
+ assertNonblank(input.projectId, "Ontology outbox project id")
138
+ assertPositiveInteger(input.limit, "Ontology outbox limit")
139
+ assertTimestamp(input.publishedBefore, "Ontology outbox purge cutoff")
140
+ const rows = await sql<{ readonly id: string }[]>`
141
+ WITH selected AS (
142
+ SELECT project_id, id
143
+ FROM ontology_outbox
144
+ WHERE project_id = ${input.projectId}
145
+ AND published_at IS NOT NULL
146
+ AND published_at < ${input.publishedBefore}
147
+ ORDER BY published_at, id
148
+ LIMIT ${input.limit}
149
+ FOR UPDATE SKIP LOCKED
150
+ )
151
+ DELETE FROM ontology_outbox AS outbox
152
+ USING selected
153
+ WHERE outbox.project_id = selected.project_id AND outbox.id = selected.id
154
+ RETURNING outbox.id
155
+ `
156
+ return rows.length
157
+ })
158
+ }
159
+
160
+ async summarize(input: SummarizeOntologyOutboxInput): Promise<OntologyOutboxSummary> {
161
+ return this.runRootOperation(async (sql) => {
162
+ assertNonblank(input.projectId, "Ontology outbox project id")
163
+ const [summary] = await sql<
164
+ {
165
+ readonly pending_count: number
166
+ readonly oldest_pending_at: Date | string | null
167
+ readonly retrying_count: number
168
+ readonly max_attempts: number
169
+ }[]
170
+ >`
171
+ SELECT COUNT(*)::int AS pending_count,
172
+ MIN(created_at) AS oldest_pending_at,
173
+ COUNT(*) FILTER (WHERE attempts > 0)::int AS retrying_count,
174
+ COALESCE(MAX(attempts), 0)::int AS max_attempts
175
+ FROM ontology_outbox
176
+ WHERE project_id = ${input.projectId} AND published_at IS NULL
177
+ `
178
+ return {
179
+ pendingCount: summary?.pending_count ?? 0,
180
+ oldestPendingAt: toOptionalIsoString(summary?.oldest_pending_at),
181
+ retryingCount: summary?.retrying_count ?? 0,
182
+ maxAttempts: summary?.max_attempts ?? 0,
183
+ }
184
+ })
185
+ }
186
+ }
187
+
188
+ function toOptionalIsoString(value: Date | string | null | undefined): string | null {
189
+ if (value === null || value === undefined) return null
190
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
191
+ }
192
+
193
+ function validateLeaseIds(ids: readonly string[]): string[] {
194
+ const seen = new Set<string>()
195
+ for (const id of ids) {
196
+ assertNonblank(id, "Ontology outbox event id")
197
+ if (seen.has(id)) {
198
+ throw new MaterializationValidationError(`Ontology outbox lease batch repeats event '${id}'.`)
199
+ }
200
+ seen.add(id)
201
+ }
202
+ return [...seen]
203
+ }
204
+
205
+ function assertLeaseInput(input: {
206
+ readonly projectId: string
207
+ readonly ids: readonly string[]
208
+ readonly leaseId: string
209
+ }): void {
210
+ assertNonblank(input.projectId, "Ontology outbox project id")
211
+ assertNonblank(input.leaseId, "Ontology outbox lease id")
212
+ }
213
+
214
+ function leaseConflict(): MaterializationConflictError {
215
+ return new MaterializationConflictError("outbox-lease", "Ontology outbox lease does not match.")
216
+ }
@@ -0,0 +1,293 @@
1
+ import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
2
+ import { MaterializationConflictError } from "@sixb/core/internal/materialization"
3
+ import type {
4
+ AssertSourceMaterializationExecutionInput,
5
+ OntologyCommitRecord,
6
+ OntologyCommitWrite,
7
+ OntologyOutboxRecord,
8
+ OntologySourceRecord,
9
+ StageSourceAssertion,
10
+ } from "@sixb/core/storage"
11
+ import type { SQLClient } from "../pg-client"
12
+
13
+ export {
14
+ assertNonblank,
15
+ assertNonnegativeInteger,
16
+ assertPositiveInteger,
17
+ assertTimestamp,
18
+ canonicalJson,
19
+ linkRefFromColumns,
20
+ objectRefFromColumns,
21
+ originColumns,
22
+ originWhere,
23
+ sourceEntityKey,
24
+ } from "@sixb/core/internal/ontology-storage-provider"
25
+
26
+ export type PgRootOperation = <T>(run: (sql: SQLClient) => Promise<T>) => Promise<T>
27
+
28
+ type PgJsonValue = Parameters<SQLClient["json"]>[0]
29
+
30
+ export interface PgOntologyCommitRow {
31
+ readonly project_id: string
32
+ readonly id: string
33
+ readonly idempotency_key: string
34
+ readonly request_hash: string
35
+ readonly origin_kind: string
36
+ readonly origin_run_id: string | null
37
+ readonly origin_batch_ordinal: number | string | null
38
+ readonly origin: unknown
39
+ readonly actor: unknown | null
40
+ readonly ontology_revision: string
41
+ readonly projection_revision: string | null
42
+ readonly ownership_hash: string | null
43
+ readonly intent: unknown
44
+ readonly result: unknown
45
+ readonly committed_at: Date | string
46
+ }
47
+
48
+ export interface PgOntologySourceRow {
49
+ readonly project_id: string
50
+ readonly source_id: string
51
+ readonly materialization_id: string
52
+ readonly projection_run_id: string
53
+ readonly projection_kind: "object" | "link"
54
+ readonly protocol: "replacement"
55
+ readonly status: OntologySourceRecord["status"]
56
+ readonly execution_token: string | null
57
+ readonly dataset_id: string
58
+ readonly dataset_version_id: string
59
+ readonly dataset_version_created_at: Date | string
60
+ readonly projection_revision: string
61
+ readonly ownership_hash: string
62
+ readonly ontology_revision: string
63
+ readonly root_count: number | string | null
64
+ readonly assertion_count: number | string | null
65
+ readonly created_at: Date | string
66
+ readonly ready_at: Date | string | null
67
+ readonly activated_at: Date | string | null
68
+ readonly terminal_at: Date | string | null
69
+ readonly last_commit_id: string | null
70
+ readonly updated_at: Date | string
71
+ }
72
+
73
+ export interface PgOntologySourceAssertionRow {
74
+ readonly project_id: string
75
+ readonly source_id: string
76
+ readonly materialization_id: string
77
+ readonly entity_kind: "object" | "link"
78
+ readonly entity_key: unknown
79
+ readonly entity_sort_key: string
80
+ readonly root_kind: "object" | "link"
81
+ readonly root_key: unknown
82
+ readonly root_sort_key: string
83
+ readonly staging_ordinal: number | string
84
+ readonly root: unknown
85
+ readonly assertion: unknown
86
+ }
87
+
88
+ export interface PgOntologyOverrideRow {
89
+ readonly entity_kind: "object" | "link"
90
+ readonly value: unknown
91
+ readonly last_commit_id: string
92
+ readonly updated_at: Date | string
93
+ }
94
+
95
+ export interface PgOntologyOutboxRow {
96
+ readonly envelope: unknown
97
+ readonly available_at: Date | string
98
+ readonly attempts: number | string
99
+ readonly lease_id: string | null
100
+ readonly lease_expires_at: Date | string | null
101
+ readonly published_at: Date | string | null
102
+ readonly last_error: string | null
103
+ readonly created_at: Date | string
104
+ }
105
+
106
+ export function jsonParameter(sql: SQLClient, value: unknown): ReturnType<SQLClient["json"]> {
107
+ return sql.json(value as PgJsonValue)
108
+ }
109
+
110
+ export function jsonKeyParameter(
111
+ sql: SQLClient,
112
+ canonicalKey: string
113
+ ): ReturnType<SQLClient["json"]> {
114
+ return jsonParameter(sql, JSON.parse(canonicalKey))
115
+ }
116
+
117
+ export function toIsoString(value: Date | string): string {
118
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
119
+ }
120
+
121
+ export function optionalIsoString(value: Date | string | null): string | null {
122
+ return value === null ? null : toIsoString(value)
123
+ }
124
+
125
+ export function databaseSafeInteger(value: number | string, label: string): number {
126
+ const result = Number(value)
127
+ if (!Number.isSafeInteger(result) || result < 0) {
128
+ throw new MaterializationConflictError(
129
+ "effective-state",
130
+ `${label} is outside the nonnegative safe-integer range.`
131
+ )
132
+ }
133
+ return result
134
+ }
135
+
136
+ export function sourceRecord(row: PgOntologySourceRow): OntologySourceRecord {
137
+ return {
138
+ projectId: row.project_id,
139
+ source: { projectionId: row.source_id },
140
+ materializationId: row.materialization_id,
141
+ projectionRunId: row.projection_run_id,
142
+ projectionKind: row.projection_kind,
143
+ protocol: row.protocol,
144
+ status: row.status,
145
+ executionToken: row.execution_token,
146
+ datasetVersion: {
147
+ datasetId: row.dataset_id,
148
+ versionId: row.dataset_version_id,
149
+ createdAt: toIsoString(row.dataset_version_created_at),
150
+ },
151
+ projectionRevision: row.projection_revision,
152
+ ownershipHash: row.ownership_hash,
153
+ ontologyRevision: row.ontology_revision,
154
+ rootCount:
155
+ row.root_count === null ? null : databaseSafeInteger(row.root_count, "Source root count"),
156
+ assertionCount:
157
+ row.assertion_count === null
158
+ ? null
159
+ : databaseSafeInteger(row.assertion_count, "Source assertion count"),
160
+ createdAt: toIsoString(row.created_at),
161
+ readyAt: optionalIsoString(row.ready_at),
162
+ activatedAt: optionalIsoString(row.activated_at),
163
+ terminalAt: optionalIsoString(row.terminal_at),
164
+ lastCommitId: row.last_commit_id,
165
+ updatedAt: toIsoString(row.updated_at),
166
+ }
167
+ }
168
+
169
+ export function sourceAssertion(row: PgOntologySourceAssertionRow): StageSourceAssertion {
170
+ return {
171
+ root: structuredClone(row.root) as ProjectionEntityRef,
172
+ assertion: structuredClone(row.assertion) as StageSourceAssertion["assertion"],
173
+ stagingOrdinal: databaseSafeInteger(row.staging_ordinal, "Source staging ordinal"),
174
+ }
175
+ }
176
+
177
+ export function commitRecord(row: PgOntologyCommitRow): OntologyCommitRecord {
178
+ const base = {
179
+ projectId: row.project_id,
180
+ id: row.id,
181
+ idempotencyKey: row.idempotency_key,
182
+ requestHash: row.request_hash,
183
+ origin: structuredClone(row.origin) as OntologyCommitWrite["origin"],
184
+ ...(row.actor === null
185
+ ? {}
186
+ : {
187
+ actor: structuredClone(row.actor) as NonNullable<OntologyCommitWrite["actor"]>,
188
+ }),
189
+ ontologyRevision: row.ontology_revision,
190
+ ...(row.projection_revision === null ? {} : { projectionRevision: row.projection_revision }),
191
+ ...(row.ownership_hash === null ? {} : { ownershipHash: row.ownership_hash }),
192
+ committedAt: toIsoString(row.committed_at),
193
+ }
194
+ const intent = structuredClone(row.intent) as OntologyCommitWrite["intent"]
195
+ const result = structuredClone(row.result) as OntologyCommitRecord["result"]
196
+ return { ...base, intent, result } as OntologyCommitRecord
197
+ }
198
+
199
+ export function outboxRecord(row: PgOntologyOutboxRow): OntologyOutboxRecord {
200
+ return {
201
+ envelope: structuredClone(row.envelope) as OntologyOutboxRecord["envelope"],
202
+ availableAt: toIsoString(row.available_at),
203
+ attempts: databaseSafeInteger(row.attempts, "Ontology outbox attempts"),
204
+ leaseId: row.lease_id,
205
+ leaseExpiresAt: optionalIsoString(row.lease_expires_at),
206
+ publishedAt: optionalIsoString(row.published_at),
207
+ lastError: row.last_error,
208
+ createdAt: toIsoString(row.created_at),
209
+ }
210
+ }
211
+
212
+ export async function assertProjectionExecution(
213
+ sql: SQLClient,
214
+ input: {
215
+ readonly projectId: string
216
+ readonly sourceId: string
217
+ readonly projectionRunId: string
218
+ readonly executionToken: string
219
+ readonly identity?: AssertSourceMaterializationExecutionInput["identity"]
220
+ }
221
+ ): Promise<void> {
222
+ const [run] = await sql<
223
+ {
224
+ readonly projection_id: string
225
+ readonly projection_kind: string
226
+ readonly status: string
227
+ readonly materialization_protocol: string | null
228
+ readonly execution_token: string | null
229
+ readonly dataset_id: string
230
+ readonly dataset_version_id: string
231
+ readonly dataset_version_created_at: Date | string | null
232
+ readonly ontology_revision: string | null
233
+ readonly projection_revision: string | null
234
+ readonly ownership_hash: string | null
235
+ }[]
236
+ >`
237
+ SELECT projection_id, projection_kind, status, materialization_protocol, execution_token,
238
+ dataset_id, dataset_version_id, dataset_version_created_at,
239
+ ontology_revision, projection_revision, ownership_hash
240
+ FROM projection_runs
241
+ WHERE project_id = ${input.projectId} AND id = ${input.projectionRunId}
242
+ FOR UPDATE
243
+ `
244
+ if (!run || run.status !== "running") {
245
+ throw new MaterializationConflictError(
246
+ "run-correlation",
247
+ `Projection run '${input.projectionRunId}' is missing or is not running.`
248
+ )
249
+ }
250
+ if (run.projection_id !== input.sourceId || run.materialization_protocol !== "replacement") {
251
+ throw new MaterializationConflictError(
252
+ "run-correlation",
253
+ `Projection run '${input.projectionRunId}' does not own replacement source '${input.sourceId}'.`
254
+ )
255
+ }
256
+ if (run.execution_token !== input.executionToken) {
257
+ throw new MaterializationConflictError(
258
+ "execution-lost",
259
+ `Projection run '${input.projectionRunId}' execution token is stale.`
260
+ )
261
+ }
262
+ const identity = input.identity
263
+ if (
264
+ identity &&
265
+ (run.projection_kind !== identity.projectionKind ||
266
+ run.materialization_protocol !== identity.protocol ||
267
+ run.dataset_id !== identity.datasetVersion.datasetId ||
268
+ run.dataset_version_id !== identity.datasetVersion.versionId ||
269
+ run.dataset_version_created_at === null ||
270
+ toIsoString(run.dataset_version_created_at) !== identity.datasetVersion.createdAt ||
271
+ run.ontology_revision !== identity.ontologyRevision ||
272
+ run.projection_revision !== identity.projectionRevision ||
273
+ run.ownership_hash !== identity.ownershipHash)
274
+ ) {
275
+ throw new MaterializationConflictError(
276
+ "run-correlation",
277
+ `Projection run '${input.projectionRunId}' immutable source identity does not match.`
278
+ )
279
+ }
280
+ }
281
+
282
+ export function requireRow<T>(
283
+ row: T | undefined,
284
+ kind: ConstructorParameters<typeof MaterializationConflictError>[0],
285
+ message: string
286
+ ): T {
287
+ if (!row) throw new MaterializationConflictError(kind, message)
288
+ return row
289
+ }
290
+
291
+ export function ontologyLockKey(kind: string, ...parts: readonly string[]): string {
292
+ return `ontology:${kind}:${JSON.stringify(parts)}`
293
+ }