@opengeni/db 0.28.1 → 0.31.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 (187) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/environment-crypto.d.ts +8 -4
  35. package/dist/generated-images.d.ts +139 -0
  36. package/dist/index.d.ts +958 -126
  37. package/dist/index.js +31204 -12172
  38. package/dist/index.js.map +1 -1
  39. package/dist/insights.d.ts +51 -4
  40. package/dist/integration-bindings.d.ts +70 -0
  41. package/dist/integration-features.d.ts +99 -0
  42. package/dist/interaction-revisions.d.ts +3 -0
  43. package/dist/interaction-schema.d.ts +5372 -0
  44. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  45. package/dist/knowledge-source-sync.d.ts +274 -0
  46. package/dist/lossless-columns.d.ts +58 -0
  47. package/dist/lossless-json.d.ts +39 -0
  48. package/dist/memory-domain.d.ts +3 -6
  49. package/dist/memory-governance.d.ts +2 -2
  50. package/dist/memory-slack-delivery.d.ts +101 -0
  51. package/dist/new-session-drafts.d.ts +2 -2
  52. package/dist/pack-components.d.ts +76 -0
  53. package/dist/pack-installations.d.ts +86 -0
  54. package/dist/persistence-errors.d.ts +7 -14
  55. package/dist/plugin-packages.d.ts +126 -0
  56. package/dist/preference-registry.d.ts +1 -1
  57. package/dist/provision-roles.d.ts +9 -1
  58. package/dist/provision-roles.js +1 -1
  59. package/dist/runtime-posture.d.ts +18 -6
  60. package/dist/schema.d.ts +19122 -8731
  61. package/dist/schema.js +173 -1
  62. package/dist/scoped-knowledge.d.ts +97 -1
  63. package/dist/session-control.d.ts +4 -4
  64. package/dist/session-queue-commands.d.ts +19 -11
  65. package/dist/session-realtime-context.d.ts +2 -2
  66. package/dist/session-realtime-ledger.d.ts +4 -4
  67. package/dist/session-realtime-mirror.d.ts +2 -2
  68. package/dist/session-realtime-state.d.ts +1 -1
  69. package/dist/session-realtime-terminal.d.ts +8 -1
  70. package/dist/session-realtime.d.ts +7 -7
  71. package/dist/session-tool-call-settlement.d.ts +1 -1
  72. package/dist/slack-user-link-access.d.ts +54 -0
  73. package/dist/transcription-recordings-schema.d.ts +44 -6
  74. package/dist/transcription-recordings.d.ts +3 -3
  75. package/dist/turn-initiator.d.ts +2 -2
  76. package/dist/video-generation.d.ts +216 -0
  77. package/dist/video-generation.js +66 -0
  78. package/dist/video-generation.js.map +1 -0
  79. package/dist/workspace-artifacts.d.ts +1 -1
  80. package/dist/workspace-instruction-policies.d.ts +1 -1
  81. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  82. package/dist/workspace-learning-policy.d.ts +161 -0
  83. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  84. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  85. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  86. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  87. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  88. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  89. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  90. package/drizzle/0181_connected_machine_removal.sql +52 -0
  91. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  92. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  93. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  94. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  95. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  96. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  97. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  98. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  99. package/drizzle/0190_timeline_annotations.sql +31 -0
  100. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  101. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  102. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  103. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  104. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  105. package/drizzle/0196_rig_provider_images.sql +152 -0
  106. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  107. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  108. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  109. package/drizzle/0201_company_profile_authority.sql +849 -0
  110. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  111. package/drizzle/0203_durable_video_generation.sql +353 -0
  112. package/drizzle/0204_video_generation_funding.sql +44 -0
  113. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  114. package/drizzle/0206_browser_sessions.sql +646 -0
  115. package/drizzle/0207_browser_identities.sql +379 -0
  116. package/drizzle/0208_attached_browser_devices.sql +165 -0
  117. package/drizzle/0209_computer_sessions.sql +575 -0
  118. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  119. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  120. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  121. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  122. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  123. package/drizzle/0215_capabilities_platform.sql +980 -0
  124. package/drizzle/0216_pack_component_ownership.sql +235 -0
  125. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  126. package/package.json +18 -5
  127. package/src/attached-browser-devices.ts +371 -0
  128. package/src/attempt-tool-catalogs.ts +131 -0
  129. package/src/browser-auth.ts +1896 -0
  130. package/src/browser-identities.ts +1168 -0
  131. package/src/browser-sessions.ts +2005 -0
  132. package/src/browser-state-artifacts.ts +59 -0
  133. package/src/capability-components.ts +166 -0
  134. package/src/capability-integrations.ts +1663 -0
  135. package/src/codemode-operations.ts +674 -0
  136. package/src/company-profile-schema.ts +143 -0
  137. package/src/company-profile.ts +968 -0
  138. package/src/computer-sessions.ts +1130 -0
  139. package/src/connection-token-resolver.ts +270 -25
  140. package/src/database.ts +434 -24
  141. package/src/editable-artifact-durable-export.ts +1201 -0
  142. package/src/editable-artifact-materialization.ts +893 -0
  143. package/src/editable-artifacts-schema.ts +879 -0
  144. package/src/editable-artifacts.ts +5404 -0
  145. package/src/environment-crypto.ts +22 -9
  146. package/src/generated-images.ts +740 -0
  147. package/src/index.ts +10117 -2730
  148. package/src/insights.ts +160 -13
  149. package/src/integration-bindings.ts +455 -0
  150. package/src/integration-features.ts +859 -0
  151. package/src/interaction-revisions.ts +40 -0
  152. package/src/interaction-schema.ts +1303 -0
  153. package/src/knowledge-source-sync-schema.ts +192 -0
  154. package/src/knowledge-source-sync.ts +1922 -0
  155. package/src/lossless-columns.ts +35 -0
  156. package/src/lossless-json.ts +322 -0
  157. package/src/memory-domain.ts +5 -44
  158. package/src/memory-slack-delivery.ts +801 -0
  159. package/src/pack-components.ts +954 -0
  160. package/src/pack-installations.ts +929 -0
  161. package/src/persistence-errors.ts +26 -34
  162. package/src/plugin-packages.ts +1186 -0
  163. package/src/provision-roles.ts +302 -1
  164. package/src/runtime-posture-cli.ts +9 -0
  165. package/src/runtime-posture.ts +514 -5
  166. package/src/schema.ts +5177 -2957
  167. package/src/scoped-knowledge-schema.ts +1 -1
  168. package/src/scoped-knowledge.ts +655 -3
  169. package/src/session-control.ts +133 -81
  170. package/src/session-queue-commands.ts +511 -247
  171. package/src/session-realtime-context.ts +20 -7
  172. package/src/session-realtime-ledger.ts +32 -10
  173. package/src/session-realtime-mirror.ts +4 -2
  174. package/src/session-realtime-terminal.ts +89 -21
  175. package/src/session-realtime.ts +10 -9
  176. package/src/session-tool-call-settlement.ts +38 -15
  177. package/src/slack-user-link-access.ts +702 -0
  178. package/src/transcription-recordings-schema.ts +5 -2
  179. package/src/transcription-recordings.ts +70 -40
  180. package/src/video-generation.ts +1452 -0
  181. package/src/workspace-learning-policy-schema.ts +139 -0
  182. package/src/workspace-learning-policy.ts +595 -0
  183. package/dist/chunk-DBS5HPEW.js.map +0 -1
  184. package/dist/chunk-HT5T62CC.js +0 -6803
  185. package/dist/chunk-HT5T62CC.js.map +0 -1
  186. package/dist/event-payload-sanitizer.d.ts +0 -32
  187. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,3853 @@
1
+ -- deployment-mode: rolling
2
+ -- Additive durable authority for collaborative editable artifacts. Canonical
3
+ -- model bytes remain in object storage; Postgres owns scoped identity,
4
+ -- idempotency, causality, delivery order, immutable history, and job/outbox
5
+ -- state. No existing table or behavior is changed by this migration.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '10min';
9
+
10
+ CREATE SCHEMA IF NOT EXISTS opengeni_private;
11
+
12
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_frontier_valid(value jsonb)
13
+ RETURNS boolean
14
+ LANGUAGE plpgsql
15
+ IMMUTABLE
16
+ SET search_path = pg_catalog, pg_temp
17
+ AS $body$
18
+ DECLARE entry jsonb;
19
+ DECLARE replica text;
20
+ DECLARE counter_text text;
21
+ DECLARE previous_replica text;
22
+ BEGIN
23
+ IF value IS NULL
24
+ OR jsonb_typeof(value) IS DISTINCT FROM 'array'
25
+ OR jsonb_array_length(value) > 65536
26
+ OR pg_column_size(value) > 1048576
27
+ OR octet_length(value::text) > 1048576
28
+ THEN
29
+ RETURN false;
30
+ END IF;
31
+ FOR entry IN SELECT item FROM jsonb_array_elements(value) AS items(item) LOOP
32
+ IF jsonb_typeof(entry) <> 'object'
33
+ OR (SELECT count(*) FROM jsonb_object_keys(entry)) <> 2
34
+ OR NOT (entry ? 'replicaId' AND entry ? 'counter')
35
+ OR jsonb_typeof(entry->'replicaId') <> 'string'
36
+ OR jsonb_typeof(entry->'counter') <> 'number'
37
+ THEN
38
+ RETURN false;
39
+ END IF;
40
+ replica := entry->>'replicaId';
41
+ counter_text := entry->>'counter';
42
+ IF replica !~ '^[0-9a-f]{16}$' OR replica ~ '^0+$'
43
+ OR counter_text !~ '^[1-9][0-9]{0,15}$'
44
+ OR counter_text::numeric > 9007199254740991
45
+ OR (previous_replica IS NOT NULL
46
+ AND convert_to(replica, 'UTF8') <= convert_to(previous_replica, 'UTF8'))
47
+ THEN
48
+ RETURN false;
49
+ END IF;
50
+ previous_replica := replica;
51
+ END LOOP;
52
+ RETURN true;
53
+ EXCEPTION WHEN others THEN
54
+ RETURN false;
55
+ END;
56
+ $body$;
57
+
58
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_id_array_valid(value jsonb)
59
+ RETURNS boolean
60
+ LANGUAGE sql
61
+ IMMUTABLE
62
+ SET search_path = pg_catalog, pg_temp
63
+ AS $body$
64
+ SELECT CASE
65
+ WHEN value IS NULL OR jsonb_typeof(value) IS DISTINCT FROM 'array' THEN false
66
+ ELSE jsonb_array_length(value) <= 10000
67
+ AND pg_column_size(value) <= 1048576
68
+ AND octet_length(value::text) <= 1048576
69
+ AND NOT EXISTS (
70
+ SELECT 1
71
+ FROM jsonb_array_elements(value) AS entries(entry)
72
+ WHERE jsonb_typeof(entry) <> 'string'
73
+ OR entry #>> '{}' !~ '^[0-9a-f]{32}$'
74
+ OR entry #>> '{}' ~ '^0+$'
75
+ )
76
+ AND (
77
+ SELECT count(*) = count(DISTINCT entry #>> '{}')
78
+ FROM jsonb_array_elements(value) AS entries(entry)
79
+ )
80
+ AND NOT EXISTS (
81
+ SELECT 1
82
+ FROM (
83
+ SELECT entry #>> '{}' AS current_value,
84
+ lag(entry #>> '{}') OVER (ORDER BY ordinal) AS previous_value
85
+ FROM jsonb_array_elements(value) WITH ORDINALITY AS entries(entry, ordinal)
86
+ ) ordered
87
+ WHERE previous_value IS NOT NULL
88
+ AND convert_to(previous_value, 'UTF8') >= convert_to(current_value, 'UTF8')
89
+ )
90
+ END;
91
+ $body$;
92
+
93
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_frontier_dominates(
94
+ candidate jsonb,
95
+ required jsonb
96
+ ) RETURNS boolean
97
+ LANGUAGE plpgsql
98
+ IMMUTABLE
99
+ SET search_path = pg_catalog, pg_temp
100
+ AS $body$
101
+ BEGIN
102
+ IF NOT opengeni_private.editable_artifact_frontier_valid(candidate)
103
+ OR NOT opengeni_private.editable_artifact_frontier_valid(required)
104
+ THEN
105
+ RETURN false;
106
+ END IF;
107
+ RETURN NOT EXISTS (
108
+ SELECT 1
109
+ FROM jsonb_array_elements(required) AS required_entries(entry)
110
+ LEFT JOIN jsonb_array_elements(candidate) AS candidate_entries(candidate_entry)
111
+ ON candidate_entry->>'replicaId' = entry->>'replicaId'
112
+ WHERE candidate_entry IS NULL
113
+ OR (candidate_entry->>'counter')::bigint < (entry->>'counter')::bigint
114
+ );
115
+ END;
116
+ $body$;
117
+
118
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_frontier_merge_dot_equals(
119
+ result_frontier jsonb,
120
+ left_frontier jsonb,
121
+ right_frontier jsonb,
122
+ dot_replica_id text,
123
+ dot_counter bigint
124
+ ) RETURNS boolean
125
+ LANGUAGE plpgsql
126
+ IMMUTABLE
127
+ SET search_path = pg_catalog, pg_temp
128
+ AS $body$
129
+ DECLARE matches boolean;
130
+ BEGIN
131
+ IF NOT opengeni_private.editable_artifact_frontier_valid(result_frontier)
132
+ OR NOT opengeni_private.editable_artifact_frontier_valid(left_frontier)
133
+ OR NOT opengeni_private.editable_artifact_frontier_valid(right_frontier)
134
+ OR dot_replica_id !~ '^[0-9a-f]{16}$'
135
+ OR dot_replica_id ~ '^0+$'
136
+ OR dot_counter NOT BETWEEN 0 AND 9007199254740991
137
+ THEN
138
+ RETURN false;
139
+ END IF;
140
+ WITH entries AS (
141
+ SELECT entry->>'replicaId' AS replica_id, (entry->>'counter')::bigint AS counter
142
+ FROM jsonb_array_elements(left_frontier) AS left_entries(entry)
143
+ UNION ALL
144
+ SELECT entry->>'replicaId', (entry->>'counter')::bigint
145
+ FROM jsonb_array_elements(right_frontier) AS right_entries(entry)
146
+ UNION ALL
147
+ SELECT dot_replica_id, dot_counter WHERE dot_counter > 0
148
+ ), merged AS (
149
+ SELECT replica_id, max(counter) AS counter
150
+ FROM entries
151
+ GROUP BY replica_id
152
+ ), canonical AS (
153
+ SELECT coalesce(
154
+ jsonb_agg(
155
+ jsonb_build_object('replicaId', replica_id, 'counter', counter)
156
+ ORDER BY convert_to(replica_id, 'UTF8')
157
+ ),
158
+ '[]'::jsonb
159
+ ) AS value
160
+ FROM merged
161
+ )
162
+ SELECT result_frontier = canonical.value INTO matches FROM canonical;
163
+ RETURN matches;
164
+ END;
165
+ $body$;
166
+
167
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_object_has_exact_keys(
168
+ value jsonb,
169
+ expected text[]
170
+ ) RETURNS boolean
171
+ LANGUAGE sql
172
+ IMMUTABLE
173
+ SET search_path = pg_catalog, pg_temp
174
+ AS $body$
175
+ SELECT jsonb_typeof(value) = 'object'
176
+ AND (SELECT array_agg(key ORDER BY convert_to(key, 'UTF8'))
177
+ FROM jsonb_object_keys(value) AS keys(key))
178
+ = (SELECT array_agg(key ORDER BY convert_to(key, 'UTF8'))
179
+ FROM unnest(expected) AS keys(key));
180
+ $body$;
181
+
182
+ -- Match JavaScript boundedIdentity for PostgreSQL-representable text: 1..256
183
+ -- UTF-16 code units, no ECMAScript trim character at either edge, and the
184
+ -- redundant 1024-byte UTF-8 safety envelope. PostgreSQL UTF-8 text already
185
+ -- excludes unpaired UTF-16 surrogates.
186
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_identity_valid(value text)
187
+ RETURNS boolean
188
+ LANGUAGE plpgsql
189
+ IMMUTABLE
190
+ SET search_path = pg_catalog, pg_temp
191
+ AS $body$
192
+ DECLARE code_units integer;
193
+ BEGIN
194
+ IF value IS NULL OR value = '' OR octet_length(convert_to(value, 'UTF8')) > 1024
195
+ OR value <> btrim(
196
+ value,
197
+ U&'\0009\000A\000B\000C\000D\0020\00A0\1680\2000\2001\2002\2003\2004\2005\2006\2007\2008\2009\200A\2028\2029\202F\205F\3000\FEFF'
198
+ )
199
+ THEN
200
+ RETURN false;
201
+ END IF;
202
+ SELECT sum(CASE WHEN ascii(character) > 65535 THEN 2 ELSE 1 END)::integer
203
+ INTO code_units
204
+ FROM unnest(string_to_array(value, NULL)) AS characters(character);
205
+ RETURN code_units BETWEEN 1 AND 256;
206
+ EXCEPTION WHEN others THEN
207
+ RETURN false;
208
+ END;
209
+ $body$;
210
+
211
+ -- PostgreSQL marks convert_to(text, name) STABLE because the target encoding is
212
+ -- an argument. This fixed-UTF8 wrapper is genuinely immutable and therefore
213
+ -- safe in stored generated digest columns.
214
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_text_sha256(value text)
215
+ RETURNS bytea
216
+ LANGUAGE sql
217
+ IMMUTABLE
218
+ STRICT
219
+ SET search_path = pg_catalog, pg_temp
220
+ AS $body$
221
+ SELECT pg_catalog.sha256(pg_catalog.convert_to(value, 'UTF8'));
222
+ $body$;
223
+
224
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_actor_key_matches(
225
+ value text,
226
+ actor_kind text,
227
+ actor_subject_id text,
228
+ agent_session_id text,
229
+ agent_turn_id text,
230
+ agent_attempt_id text,
231
+ agent_generation integer,
232
+ service_name text
233
+ ) RETURNS boolean
234
+ LANGUAGE plpgsql
235
+ IMMUTABLE
236
+ SET search_path = pg_catalog, pg_temp
237
+ AS $body$
238
+ DECLARE parsed jsonb;
239
+ BEGIN
240
+ IF octet_length(convert_to(value, 'UTF8')) NOT BETWEEN 1 AND 8192 THEN
241
+ RETURN false;
242
+ END IF;
243
+ IF NOT opengeni_private.editable_artifact_identity_valid(actor_subject_id) THEN
244
+ RETURN false;
245
+ END IF;
246
+ parsed := value::jsonb;
247
+ IF actor_kind = 'human' THEN
248
+ RETURN parsed = jsonb_build_array('human', actor_subject_id)
249
+ AND value = '[' || to_json('human'::text)::text || ','
250
+ || to_json(actor_subject_id)::text || ']';
251
+ ELSIF actor_kind = 'agent' THEN
252
+ IF NOT opengeni_private.editable_artifact_identity_valid(agent_session_id)
253
+ OR NOT opengeni_private.editable_artifact_identity_valid(agent_turn_id)
254
+ OR NOT opengeni_private.editable_artifact_identity_valid(agent_attempt_id)
255
+ THEN
256
+ RETURN false;
257
+ END IF;
258
+ RETURN parsed = jsonb_build_array(
259
+ 'agent', actor_subject_id, agent_session_id, agent_turn_id,
260
+ agent_attempt_id, agent_generation
261
+ ) AND value = '[' || to_json('agent'::text)::text || ','
262
+ || to_json(actor_subject_id)::text || ',' || to_json(agent_session_id)::text
263
+ || ',' || to_json(agent_turn_id)::text || ',' || to_json(agent_attempt_id)::text
264
+ || ',' || agent_generation::text || ']';
265
+ ELSIF actor_kind = 'service' THEN
266
+ IF NOT opengeni_private.editable_artifact_identity_valid(service_name) THEN
267
+ RETURN false;
268
+ END IF;
269
+ RETURN parsed = jsonb_build_array('service', actor_subject_id, service_name)
270
+ AND value = '[' || to_json('service'::text)::text || ','
271
+ || to_json(actor_subject_id)::text || ',' || to_json(service_name)::text || ']';
272
+ END IF;
273
+ RETURN false;
274
+ EXCEPTION WHEN others THEN
275
+ RETURN false;
276
+ END;
277
+ $body$;
278
+
279
+ CREATE OR REPLACE FUNCTION opengeni_private.reject_editable_artifact_history_mutation()
280
+ RETURNS trigger
281
+ LANGUAGE plpgsql
282
+ SET search_path = pg_catalog, pg_temp
283
+ AS $body$
284
+ BEGIN
285
+ RAISE EXCEPTION '% is immutable; use an explicit maintenance migration', TG_TABLE_NAME
286
+ USING ERRCODE = '55000';
287
+ END;
288
+ $body$;
289
+
290
+ CREATE TABLE "editable_artifact_scope_authorization_heads" (
291
+ "account_id" uuid NOT NULL,
292
+ "workspace_id" uuid NOT NULL,
293
+ "create_revision" bigint NOT NULL DEFAULT 1,
294
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
295
+ CONSTRAINT "editable_artifact_scope_authorization_heads_pk"
296
+ PRIMARY KEY ("account_id", "workspace_id"),
297
+ CONSTRAINT "editable_artifact_scope_authorization_heads_workspace_fk"
298
+ FOREIGN KEY ("workspace_id", "account_id")
299
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
300
+ CONSTRAINT "editable_artifact_scope_authorization_heads_revision_chk" CHECK (
301
+ "create_revision" BETWEEN 1 AND 9007199254740991
302
+ )
303
+ );
304
+
305
+ CREATE TABLE "editable_artifacts" (
306
+ "account_id" uuid NOT NULL,
307
+ "workspace_id" uuid NOT NULL,
308
+ "id" text NOT NULL,
309
+ "modality" text NOT NULL,
310
+ "title" text NOT NULL,
311
+ "lifecycle_state" text NOT NULL DEFAULT 'active',
312
+ "authorization_revision" bigint NOT NULL,
313
+ "head_sequence" bigint NOT NULL DEFAULT 0,
314
+ "causal_frontier" jsonb,
315
+ "state_hash" text NOT NULL,
316
+ "current_snapshot_id" text,
317
+ "created_by_subject_id" text NOT NULL,
318
+ "created_at" timestamptz NOT NULL DEFAULT now(),
319
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
320
+ CONSTRAINT "editable_artifacts_pk" PRIMARY KEY ("account_id", "workspace_id", "id"),
321
+ CONSTRAINT "editable_artifacts_workspace_fk"
322
+ FOREIGN KEY ("workspace_id", "account_id")
323
+ REFERENCES "workspaces"("id", "account_id") ON DELETE RESTRICT,
324
+ CONSTRAINT "editable_artifacts_identity_chk" CHECK (
325
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
326
+ ),
327
+ CONSTRAINT "editable_artifacts_modality_chk" CHECK (
328
+ "modality" IN ('spreadsheet', 'presentation', 'document')
329
+ ),
330
+ CONSTRAINT "editable_artifacts_title_chk" CHECK (
331
+ octet_length(convert_to("title", 'UTF8')) BETWEEN 1 AND 512
332
+ AND "title" = btrim("title")
333
+ ),
334
+ CONSTRAINT "editable_artifacts_lifecycle_chk" CHECK (
335
+ "lifecycle_state" IN ('active', 'archived')
336
+ ),
337
+ CONSTRAINT "editable_artifacts_authorization_revision_chk" CHECK (
338
+ "authorization_revision" BETWEEN 1 AND 9007199254740991
339
+ ),
340
+ CONSTRAINT "editable_artifacts_head_chk" CHECK (
341
+ "head_sequence" BETWEEN 0 AND 9007199254740991
342
+ ),
343
+ CONSTRAINT "editable_artifacts_frontier_chk" CHECK (
344
+ ("modality" = 'spreadsheet'
345
+ AND opengeni_private.editable_artifact_frontier_valid("causal_frontier"))
346
+ OR ("modality" IN ('document', 'presentation') AND "causal_frontier" IS NULL)
347
+ ),
348
+ CONSTRAINT "editable_artifacts_hash_chk" CHECK (
349
+ "state_hash" ~ '^sha256:[0-9a-f]{64}$'
350
+ ),
351
+ CONSTRAINT "editable_artifacts_snapshot_id_chk" CHECK (
352
+ "current_snapshot_id" IS NULL OR (
353
+ "current_snapshot_id" ~ '^[0-9a-f]{32}$'
354
+ AND "current_snapshot_id" !~ '^0+$'
355
+ )
356
+ ),
357
+ CONSTRAINT "editable_artifacts_actor_chk" CHECK (
358
+ opengeni_private.editable_artifact_identity_valid("created_by_subject_id")
359
+ )
360
+ );
361
+
362
+ CREATE INDEX "editable_artifacts_workspace_timeline_idx"
363
+ ON "editable_artifacts" ("workspace_id", "lifecycle_state", "updated_at" DESC, "id");
364
+
365
+ CREATE TABLE "editable_artifact_transactions" (
366
+ "account_id" uuid NOT NULL,
367
+ "workspace_id" uuid NOT NULL,
368
+ "artifact_id" text NOT NULL,
369
+ "modality" text NOT NULL,
370
+ "id" text NOT NULL,
371
+ "client_transaction_id" text NOT NULL,
372
+ "previous_local_transaction_id" text,
373
+ "request_hash" text NOT NULL,
374
+ "intent_hash" text NOT NULL,
375
+ "intent_envelope_version" integer NOT NULL,
376
+ "intent_protocol_version" integer NOT NULL,
377
+ "command_protocol_version" integer NOT NULL,
378
+ "intent_byte_size" integer NOT NULL,
379
+ "intent_bytes" bytea NOT NULL,
380
+ "parent_head_sequence" bigint NOT NULL,
381
+ "sequence_start" bigint NOT NULL,
382
+ "sequence_end" bigint NOT NULL,
383
+ "prior_state_hash" text NOT NULL,
384
+ "causal_base" jsonb,
385
+ "resolved_causal_base" jsonb,
386
+ "resulting_causal_frontier" jsonb,
387
+ "selective_undo_targets" jsonb,
388
+ "state_hash" text NOT NULL,
389
+ "operation_count" integer,
390
+ "operation_ids" jsonb,
391
+ "actor_kind" text NOT NULL,
392
+ "actor_subject_id" text NOT NULL,
393
+ "actor_key" text NOT NULL,
394
+ "actor_key_digest" bytea GENERATED ALWAYS AS (
395
+ opengeni_private.editable_artifact_text_sha256("actor_key")
396
+ ) STORED,
397
+ "replica_id" text NOT NULL,
398
+ "replica_counter" bigint NOT NULL,
399
+ "agent_session_id" text,
400
+ "agent_turn_id" text,
401
+ "agent_attempt_id" text,
402
+ "agent_generation" integer,
403
+ "service_name" text,
404
+ "kernel_version" text NOT NULL,
405
+ "model_schema_version" integer NOT NULL,
406
+ "operation_protocol_version" integer,
407
+ "commit_protocol_version" integer,
408
+ "prior_native_revision" bigint,
409
+ "native_revision" bigint,
410
+ "command_count" integer,
411
+ "native_receipt_byte_size" integer,
412
+ "native_receipt_hash" text,
413
+ "native_receipt_bytes" bytea,
414
+ "committed_transaction_byte_size" integer NOT NULL,
415
+ "committed_transaction_hash" text NOT NULL,
416
+ "committed_transaction_bytes" bytea NOT NULL,
417
+ "committed_at" timestamptz NOT NULL DEFAULT now(),
418
+ CONSTRAINT "editable_artifact_transactions_pk"
419
+ PRIMARY KEY ("account_id", "workspace_id", "artifact_id", "id"),
420
+ CONSTRAINT "editable_artifact_transactions_artifact_fk"
421
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
422
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
423
+ CONSTRAINT "editable_artifact_transactions_id_chk" CHECK (
424
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
425
+ ),
426
+ CONSTRAINT "editable_artifact_transactions_modality_chk" CHECK (
427
+ "modality" IN ('spreadsheet', 'presentation', 'document')
428
+ ),
429
+ CONSTRAINT "editable_artifact_transactions_client_id_chk" CHECK (
430
+ octet_length("client_transaction_id") BETWEEN 1 AND 200
431
+ AND "client_transaction_id" ~ '^[A-Za-z0-9._:-]+$'
432
+ AND ("previous_local_transaction_id" IS NULL OR (
433
+ octet_length("previous_local_transaction_id") BETWEEN 1 AND 200
434
+ AND "previous_local_transaction_id" ~ '^[A-Za-z0-9._:-]+$'
435
+ AND "previous_local_transaction_id" <> "client_transaction_id"
436
+ ))
437
+ ),
438
+ CONSTRAINT "editable_artifact_transactions_intent_chk" CHECK (
439
+ "request_hash" ~ '^sha256:[0-9a-f]{64}$'
440
+ AND "intent_hash" = "request_hash"
441
+ AND "intent_hash" = 'sha256:' || encode(sha256("intent_bytes"), 'hex')
442
+ AND "intent_envelope_version" = 1
443
+ AND "intent_protocol_version" = 1
444
+ AND "command_protocol_version" > 0
445
+ AND "intent_byte_size" BETWEEN 8 AND 5242880
446
+ AND octet_length("intent_bytes") = "intent_byte_size"
447
+ AND substring("intent_bytes" FROM 1 FOR 8) = convert_to('OGATX001', 'UTF8')
448
+ ),
449
+ CONSTRAINT "editable_artifact_transactions_sequence_chk" CHECK (
450
+ "parent_head_sequence" BETWEEN 0 AND 9007199254740991
451
+ AND "sequence_start" = "parent_head_sequence" + 1
452
+ AND "sequence_end" <= 9007199254740991
453
+ AND (("modality" = 'spreadsheet'
454
+ AND "operation_count" BETWEEN 1 AND 4096
455
+ AND "sequence_end" = "parent_head_sequence" + "operation_count")
456
+ OR ("modality" IN ('document', 'presentation')
457
+ AND "operation_count" IS NULL
458
+ AND "sequence_end" = "sequence_start"))
459
+ ),
460
+ CONSTRAINT "editable_artifact_transactions_frontiers_chk" CHECK (
461
+ ("modality" = 'spreadsheet'
462
+ AND opengeni_private.editable_artifact_frontier_valid("causal_base")
463
+ AND opengeni_private.editable_artifact_frontier_valid("resolved_causal_base")
464
+ AND opengeni_private.editable_artifact_frontier_valid("resulting_causal_frontier")
465
+ AND opengeni_private.editable_artifact_id_array_valid("operation_ids")
466
+ AND jsonb_array_length("operation_ids") = "operation_count"
467
+ AND opengeni_private.editable_artifact_id_array_valid("selective_undo_targets"))
468
+ OR ("modality" IN ('document', 'presentation')
469
+ AND "causal_base" IS NULL AND "resolved_causal_base" IS NULL
470
+ AND "resulting_causal_frontier" IS NULL AND "operation_ids" IS NULL
471
+ AND "selective_undo_targets" IS NULL)
472
+ ),
473
+ CONSTRAINT "editable_artifact_transactions_result_chk" CHECK (
474
+ "prior_state_hash" ~ '^sha256:[0-9a-f]{64}$'
475
+ AND "state_hash" ~ '^sha256:[0-9a-f]{64}$'
476
+ AND octet_length("kernel_version") BETWEEN 1 AND 512
477
+ AND "model_schema_version" > 0
478
+ AND "committed_transaction_byte_size" BETWEEN 1 AND 8388608
479
+ AND octet_length("committed_transaction_bytes") = "committed_transaction_byte_size"
480
+ AND "committed_transaction_hash" ~ '^sha256:[0-9a-f]{64}$'
481
+ AND "committed_transaction_hash" =
482
+ 'sha256:' || encode(sha256("committed_transaction_bytes"), 'hex')
483
+ AND (("modality" = 'spreadsheet'
484
+ AND "operation_protocol_version" = 1
485
+ AND "commit_protocol_version" IS NULL
486
+ AND "prior_native_revision" IS NULL AND "native_revision" IS NULL
487
+ AND "command_count" IS NULL AND "native_receipt_byte_size" IS NULL
488
+ AND "native_receipt_hash" IS NULL AND "native_receipt_bytes" IS NULL
489
+ AND substring("committed_transaction_bytes" FROM 1 FOR 8) = convert_to('OGACO001', 'UTF8'))
490
+ OR ("modality" IN ('document', 'presentation')
491
+ AND "operation_protocol_version" IS NULL
492
+ AND "commit_protocol_version" = 1
493
+ AND "prior_native_revision" BETWEEN 0 AND 9007199254740991
494
+ AND (("modality" = 'document' AND (
495
+ ("native_revision" = "prior_native_revision" AND "state_hash" = "prior_state_hash")
496
+ OR ("native_revision" = "prior_native_revision" + 1
497
+ AND "state_hash" <> "prior_state_hash")
498
+ )) OR ("modality" = 'presentation'
499
+ AND "native_revision" = "prior_native_revision" + 1
500
+ AND "state_hash" <> "prior_state_hash"))
501
+ AND "command_count" BETWEEN 1 AND 4096
502
+ AND "native_receipt_byte_size" BETWEEN 1 AND 524288
503
+ AND octet_length("native_receipt_bytes") = "native_receipt_byte_size"
504
+ AND "native_receipt_hash" ~ '^sha256:[0-9a-f]{64}$'
505
+ AND "native_receipt_hash" = 'sha256:' || encode(sha256("native_receipt_bytes"), 'hex')
506
+ AND substring("committed_transaction_bytes" FROM 1 FOR 8) = convert_to('OGAST001', 'UTF8')
507
+ AND get_byte("committed_transaction_bytes", 12) =
508
+ CASE "modality" WHEN 'document' THEN 1 ELSE 2 END
509
+ AND get_byte("committed_transaction_bytes", 13) = 0
510
+ AND get_byte("committed_transaction_bytes", 14) = 0
511
+ AND get_byte("committed_transaction_bytes", 15) = 0
512
+ AND substring("native_receipt_bytes" FROM 1 FOR 8) = convert_to(
513
+ CASE "modality" WHEN 'document' THEN 'OGADR001' ELSE 'OGAPR001' END,
514
+ 'UTF8'
515
+ )))
516
+ ),
517
+ CONSTRAINT "editable_artifact_transactions_replica_chk" CHECK (
518
+ "replica_id" ~ '^[0-9a-f]{16}$' AND "replica_id" !~ '^0+$'
519
+ AND "replica_counter" BETWEEN 1 AND 9007199254740991
520
+ AND (("replica_counter" = 1 AND "previous_local_transaction_id" IS NULL)
521
+ OR ("replica_counter" > 1 AND "previous_local_transaction_id" IS NOT NULL))
522
+ ),
523
+ CONSTRAINT "editable_artifact_transactions_actor_chk" CHECK (
524
+ opengeni_private.editable_artifact_identity_valid("actor_subject_id")
525
+ AND (
526
+ ("actor_kind" = 'human'
527
+ AND "agent_session_id" IS NULL AND "agent_turn_id" IS NULL
528
+ AND "agent_attempt_id" IS NULL AND "agent_generation" IS NULL
529
+ AND "service_name" IS NULL)
530
+ OR
531
+ ("actor_kind" = 'agent'
532
+ AND "agent_session_id" IS NOT NULL
533
+ AND "agent_turn_id" IS NOT NULL
534
+ AND "agent_attempt_id" IS NOT NULL
535
+ AND "agent_generation" IS NOT NULL
536
+ AND opengeni_private.editable_artifact_identity_valid("agent_session_id")
537
+ AND opengeni_private.editable_artifact_identity_valid("agent_turn_id")
538
+ AND opengeni_private.editable_artifact_identity_valid("agent_attempt_id")
539
+ AND "agent_generation" BETWEEN 0 AND 2147483647
540
+ AND "service_name" IS NULL)
541
+ OR
542
+ ("actor_kind" = 'service'
543
+ AND "agent_session_id" IS NULL AND "agent_turn_id" IS NULL
544
+ AND "agent_attempt_id" IS NULL AND "agent_generation" IS NULL
545
+ AND "service_name" IS NOT NULL
546
+ AND opengeni_private.editable_artifact_identity_valid("service_name"))
547
+ )
548
+ AND opengeni_private.editable_artifact_actor_key_matches(
549
+ "actor_key", "actor_kind", "actor_subject_id", "agent_session_id",
550
+ "agent_turn_id", "agent_attempt_id", "agent_generation", "service_name"
551
+ ) IS TRUE
552
+ ),
553
+ CONSTRAINT "editable_artifact_transactions_actor_client_uq"
554
+ UNIQUE (
555
+ "account_id", "workspace_id", "artifact_id", "actor_key_digest",
556
+ "client_transaction_id"
557
+ ),
558
+ CONSTRAINT "editable_artifact_transactions_replica_counter_uq"
559
+ UNIQUE ("account_id", "workspace_id", "artifact_id", "replica_id", "replica_counter"),
560
+ CONSTRAINT "editable_artifact_transactions_predecessor_identity_uq"
561
+ UNIQUE (
562
+ "account_id", "workspace_id", "artifact_id", "actor_key_digest", "replica_id",
563
+ "client_transaction_id"
564
+ ),
565
+ CONSTRAINT "editable_artifact_transactions_exact_receipt_authority_uq"
566
+ UNIQUE (
567
+ "account_id", "workspace_id", "artifact_id", "id", "actor_key_digest",
568
+ "client_transaction_id", "request_hash"
569
+ ),
570
+ CONSTRAINT "editable_artifact_transactions_sequence_uq"
571
+ UNIQUE ("account_id", "workspace_id", "artifact_id", "sequence_start", "sequence_end")
572
+ );
573
+
574
+ ALTER TABLE "editable_artifact_transactions"
575
+ ADD CONSTRAINT "editable_artifact_transactions_predecessor_fk"
576
+ FOREIGN KEY (
577
+ "account_id", "workspace_id", "artifact_id", "actor_key_digest", "replica_id",
578
+ "previous_local_transaction_id"
579
+ ) REFERENCES "editable_artifact_transactions" (
580
+ "account_id", "workspace_id", "artifact_id", "actor_key_digest", "replica_id",
581
+ "client_transaction_id"
582
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
583
+
584
+ CREATE INDEX "editable_artifact_transactions_replay_idx"
585
+ ON "editable_artifact_transactions" ("workspace_id", "artifact_id", "sequence_start");
586
+
587
+ CREATE TABLE "editable_artifact_operations" (
588
+ "account_id" uuid NOT NULL,
589
+ "workspace_id" uuid NOT NULL,
590
+ "artifact_id" text NOT NULL,
591
+ "transaction_id" text NOT NULL,
592
+ "operation_id" text NOT NULL,
593
+ "operation_index" integer NOT NULL,
594
+ "sequence" bigint NOT NULL,
595
+ "dot_replica_id" text NOT NULL,
596
+ "dot_counter" bigint NOT NULL,
597
+ "actor_key" text NOT NULL,
598
+ "created_at" timestamptz NOT NULL DEFAULT now(),
599
+ CONSTRAINT "editable_artifact_operations_pk" PRIMARY KEY (
600
+ "account_id", "workspace_id", "artifact_id", "transaction_id",
601
+ "operation_index"
602
+ ),
603
+ CONSTRAINT "editable_artifact_operations_transaction_fk"
604
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "transaction_id")
605
+ REFERENCES "editable_artifact_transactions"(
606
+ "account_id", "workspace_id", "artifact_id", "id"
607
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
608
+ CONSTRAINT "editable_artifact_operations_ids_chk" CHECK (
609
+ "transaction_id" ~ '^[0-9a-f]{32}$' AND "transaction_id" !~ '^0+$'
610
+ AND "operation_id" ~ '^[0-9a-f]{32}$' AND "operation_id" !~ '^0+$'
611
+ ),
612
+ CONSTRAINT "editable_artifact_operations_position_chk" CHECK (
613
+ "operation_index" BETWEEN 0 AND 4095
614
+ AND "sequence" BETWEEN 1 AND 9007199254740991
615
+ ),
616
+ CONSTRAINT "editable_artifact_operations_dot_chk" CHECK (
617
+ "dot_replica_id" ~ '^[0-9a-f]{16}$' AND "dot_replica_id" !~ '^0+$'
618
+ AND "dot_counter" BETWEEN 1 AND 9007199254740991
619
+ AND octet_length(convert_to("actor_key", 'UTF8')) BETWEEN 1 AND 8192
620
+ )
621
+ );
622
+
623
+ CREATE UNIQUE INDEX "editable_artifact_operations_sequence_uq"
624
+ ON "editable_artifact_operations"
625
+ ("account_id", "workspace_id", "artifact_id", "sequence");
626
+ CREATE UNIQUE INDEX "editable_artifact_operations_operation_uq"
627
+ ON "editable_artifact_operations"
628
+ ("account_id", "workspace_id", "artifact_id", "operation_id");
629
+ CREATE UNIQUE INDEX "editable_artifact_operations_dot_operation_uq"
630
+ ON "editable_artifact_operations"
631
+ ("account_id", "workspace_id", "artifact_id", "dot_replica_id", "dot_counter", "operation_index");
632
+ CREATE INDEX "editable_artifact_operations_replay_idx"
633
+ ON "editable_artifact_operations" ("workspace_id", "artifact_id", "sequence");
634
+
635
+ CREATE TABLE "editable_artifact_idempotency_receipts" (
636
+ "account_id" uuid NOT NULL,
637
+ "workspace_id" uuid NOT NULL,
638
+ "id" text NOT NULL,
639
+ "artifact_id" text NOT NULL,
640
+ "operation_kind" text NOT NULL,
641
+ "authority_key" text NOT NULL,
642
+ "authority_key_digest" bytea GENERATED ALWAYS AS (
643
+ opengeni_private.editable_artifact_text_sha256("authority_key")
644
+ ) STORED,
645
+ "idempotency_key" text NOT NULL,
646
+ "request_hash" text NOT NULL,
647
+ "resource_type" text NOT NULL,
648
+ "resource_id" text NOT NULL,
649
+ "server_transaction_id" text,
650
+ "result" jsonb NOT NULL,
651
+ "created_at" timestamptz NOT NULL DEFAULT now(),
652
+ CONSTRAINT "editable_artifact_idempotency_receipts_pk"
653
+ PRIMARY KEY ("account_id", "workspace_id", "id"),
654
+ CONSTRAINT "editable_artifact_idempotency_receipts_artifact_fk"
655
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
656
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
657
+ CONSTRAINT "editable_artifact_idempotency_receipts_transaction_fk"
658
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "server_transaction_id")
659
+ REFERENCES "editable_artifact_transactions"(
660
+ "account_id", "workspace_id", "artifact_id", "id"
661
+ ) ON DELETE RESTRICT,
662
+ CONSTRAINT "editable_artifact_idempotency_receipts_exact_transaction_fk"
663
+ FOREIGN KEY (
664
+ "account_id", "workspace_id", "artifact_id", "server_transaction_id",
665
+ "authority_key_digest", "idempotency_key", "request_hash"
666
+ ) REFERENCES "editable_artifact_transactions"(
667
+ "account_id", "workspace_id", "artifact_id", "id", "actor_key_digest",
668
+ "client_transaction_id", "request_hash"
669
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
670
+ CONSTRAINT "editable_artifact_idempotency_receipts_identity_chk" CHECK (
671
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
672
+ AND "artifact_id" ~ '^[0-9a-f]{32}$' AND "artifact_id" !~ '^0+$'
673
+ AND "resource_id" ~ '^[0-9a-f]{32}$' AND "resource_id" !~ '^0+$'
674
+ AND "request_hash" ~ '^sha256:[0-9a-f]{64}$'
675
+ ),
676
+ CONSTRAINT "editable_artifact_idempotency_receipts_key_chk" CHECK (
677
+ "operation_kind" IN ('create', 'import', 'edit', 'snapshot', 'materialize')
678
+ AND octet_length(convert_to("authority_key", 'UTF8')) BETWEEN 1 AND 8192
679
+ AND octet_length("idempotency_key") BETWEEN 1 AND 256
680
+ AND "idempotency_key" = btrim("idempotency_key")
681
+ ),
682
+ CONSTRAINT "editable_artifact_idempotency_receipts_resource_chk" CHECK (
683
+ "resource_type" IN ('artifact', 'transaction', 'snapshot', 'materialization_job')
684
+ AND (
685
+ ("operation_kind" = 'create' AND "resource_type" = 'artifact'
686
+ AND "resource_id" = "artifact_id" AND "server_transaction_id" IS NULL)
687
+ OR ("operation_kind" = 'edit' AND "resource_type" = 'transaction'
688
+ AND "resource_id" = "server_transaction_id")
689
+ OR ("operation_kind" = 'snapshot' AND "resource_type" = 'snapshot'
690
+ AND "server_transaction_id" IS NULL)
691
+ OR ("operation_kind" = 'materialize' AND "resource_type" = 'materialization_job'
692
+ AND "server_transaction_id" IS NULL)
693
+ OR ("operation_kind" = 'import' AND "resource_type" = 'artifact'
694
+ AND "resource_id" = "artifact_id" AND "server_transaction_id" IS NULL)
695
+ )
696
+ ),
697
+ CONSTRAINT "editable_artifact_idempotency_receipts_result_chk" CHECK (
698
+ jsonb_typeof("result") = 'object'
699
+ AND octet_length("result"::text) <= 65536
700
+ )
701
+ );
702
+
703
+ CREATE UNIQUE INDEX "editable_artifact_idempotency_receipts_creation_request_uq"
704
+ ON "editable_artifact_idempotency_receipts"
705
+ (
706
+ "account_id", "workspace_id", "operation_kind", "authority_key_digest",
707
+ "idempotency_key"
708
+ )
709
+ WHERE "operation_kind" IN ('create', 'import');
710
+ CREATE UNIQUE INDEX "editable_artifact_idempotency_receipts_artifact_request_uq"
711
+ ON "editable_artifact_idempotency_receipts"
712
+ (
713
+ "account_id", "workspace_id", "artifact_id", "operation_kind",
714
+ "authority_key_digest", "idempotency_key"
715
+ )
716
+ WHERE "operation_kind" IN ('edit', 'snapshot', 'materialize');
717
+ CREATE UNIQUE INDEX "editable_artifact_idempotency_receipts_origin_uq"
718
+ ON "editable_artifact_idempotency_receipts"
719
+ ("account_id", "workspace_id", "artifact_id")
720
+ WHERE "operation_kind" IN ('create', 'import') AND "resource_type" = 'artifact';
721
+
722
+ CREATE INDEX "editable_artifact_idempotency_receipts_transaction_idx"
723
+ ON "editable_artifact_idempotency_receipts"
724
+ ("workspace_id", "artifact_id", "server_transaction_id");
725
+
726
+ CREATE TABLE "editable_artifact_undo_claims" (
727
+ "account_id" uuid NOT NULL,
728
+ "workspace_id" uuid NOT NULL,
729
+ "artifact_id" text NOT NULL,
730
+ "target_operation_id" text NOT NULL,
731
+ "claiming_transaction_id" text NOT NULL,
732
+ "created_at" timestamptz NOT NULL DEFAULT now(),
733
+ CONSTRAINT "editable_artifact_undo_claims_pk" PRIMARY KEY (
734
+ "account_id", "workspace_id", "artifact_id", "target_operation_id"
735
+ ),
736
+ CONSTRAINT "editable_artifact_undo_claims_transaction_fk"
737
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "claiming_transaction_id")
738
+ REFERENCES "editable_artifact_transactions"(
739
+ "account_id", "workspace_id", "artifact_id", "id"
740
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
741
+ CONSTRAINT "editable_artifact_undo_claims_ids_chk" CHECK (
742
+ "target_operation_id" ~ '^[0-9a-f]{32}$' AND "target_operation_id" !~ '^0+$'
743
+ AND "claiming_transaction_id" ~ '^[0-9a-f]{32}$'
744
+ AND "claiming_transaction_id" !~ '^0+$'
745
+ )
746
+ );
747
+
748
+ CREATE INDEX "editable_artifact_undo_claims_transaction_idx"
749
+ ON "editable_artifact_undo_claims"
750
+ (
751
+ "account_id", "workspace_id", "artifact_id", "claiming_transaction_id",
752
+ "target_operation_id"
753
+ );
754
+
755
+ CREATE TABLE "editable_artifact_sequence_checkpoints" (
756
+ "account_id" uuid NOT NULL,
757
+ "workspace_id" uuid NOT NULL,
758
+ "artifact_id" text NOT NULL,
759
+ "modality" text NOT NULL,
760
+ "head_sequence" bigint NOT NULL,
761
+ "transaction_id" text,
762
+ "causal_frontier" jsonb,
763
+ "native_revision" bigint,
764
+ "state_hash" text NOT NULL,
765
+ "created_at" timestamptz NOT NULL DEFAULT now(),
766
+ CONSTRAINT "editable_artifact_sequence_checkpoints_pk" PRIMARY KEY (
767
+ "account_id", "workspace_id", "artifact_id", "head_sequence"
768
+ ),
769
+ CONSTRAINT "editable_artifact_sequence_checkpoints_artifact_fk"
770
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
771
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
772
+ CONSTRAINT "editable_artifact_sequence_checkpoints_transaction_fk"
773
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "transaction_id")
774
+ REFERENCES "editable_artifact_transactions"(
775
+ "account_id", "workspace_id", "artifact_id", "id"
776
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
777
+ CONSTRAINT "editable_artifact_sequence_checkpoints_shape_chk" CHECK (
778
+ "head_sequence" BETWEEN 0 AND 9007199254740991
779
+ AND (("head_sequence" = 0 AND "transaction_id" IS NULL)
780
+ OR ("head_sequence" > 0 AND "transaction_id" IS NOT NULL))
781
+ AND (("modality" = 'spreadsheet'
782
+ AND opengeni_private.editable_artifact_frontier_valid("causal_frontier")
783
+ AND "native_revision" IS NULL)
784
+ OR ("modality" IN ('document', 'presentation')
785
+ AND "causal_frontier" IS NULL
786
+ AND "native_revision" BETWEEN 0 AND 9007199254740991))
787
+ AND "state_hash" ~ '^sha256:[0-9a-f]{64}$'
788
+ )
789
+ );
790
+
791
+ CREATE UNIQUE INDEX "editable_artifact_sequence_checkpoints_transaction_uq"
792
+ ON "editable_artifact_sequence_checkpoints"
793
+ ("account_id", "workspace_id", "artifact_id", "transaction_id")
794
+ WHERE "transaction_id" IS NOT NULL;
795
+
796
+ ALTER TABLE "editable_artifact_transactions"
797
+ ADD CONSTRAINT "editable_artifact_transactions_parent_checkpoint_fk"
798
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "parent_head_sequence")
799
+ REFERENCES "editable_artifact_sequence_checkpoints"(
800
+ "account_id", "workspace_id", "artifact_id", "head_sequence"
801
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
802
+
803
+ CREATE TABLE "editable_artifact_blob_refs" (
804
+ "account_id" uuid NOT NULL,
805
+ "workspace_id" uuid NOT NULL,
806
+ "artifact_id" text NOT NULL,
807
+ "id" text NOT NULL,
808
+ "kind" text NOT NULL,
809
+ "object_reference" text NOT NULL,
810
+ "byte_size" bigint NOT NULL,
811
+ "content_hash" text NOT NULL,
812
+ "mime_type" text NOT NULL,
813
+ "created_at" timestamptz NOT NULL DEFAULT now(),
814
+ CONSTRAINT "editable_artifact_blob_refs_pk" PRIMARY KEY (
815
+ "account_id", "workspace_id", "artifact_id", "id"
816
+ ),
817
+ CONSTRAINT "editable_artifact_blob_refs_artifact_fk"
818
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
819
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
820
+ CONSTRAINT "editable_artifact_blob_refs_identity_chk" CHECK (
821
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
822
+ AND "kind" IN (
823
+ 'snapshot', 'original_import', 'media', 'loss_envelope', 'materialization'
824
+ )
825
+ ),
826
+ CONSTRAINT "editable_artifact_blob_refs_facts_chk" CHECK (
827
+ octet_length("object_reference") BETWEEN 1 AND 1024
828
+ AND "object_reference" = btrim("object_reference")
829
+ AND "byte_size" BETWEEN 1 AND 9007199254740991
830
+ AND "content_hash" ~ '^sha256:[0-9a-f]{64}$'
831
+ AND octet_length("mime_type") BETWEEN 1 AND 256
832
+ AND "mime_type" = btrim("mime_type")
833
+ ),
834
+ CONSTRAINT "editable_artifact_blob_refs_content_uq" UNIQUE (
835
+ "account_id", "workspace_id", "artifact_id", "kind", "content_hash"
836
+ ),
837
+ CONSTRAINT "editable_artifact_blob_refs_exact_facts_uq" UNIQUE (
838
+ "account_id", "workspace_id", "artifact_id", "id", "byte_size",
839
+ "content_hash", "mime_type"
840
+ )
841
+ );
842
+
843
+ CREATE TABLE "editable_artifact_snapshots" (
844
+ "account_id" uuid NOT NULL,
845
+ "workspace_id" uuid NOT NULL,
846
+ "artifact_id" text NOT NULL,
847
+ "modality" text NOT NULL,
848
+ "id" text NOT NULL,
849
+ "blob_ref_id" text NOT NULL,
850
+ "byte_size" bigint NOT NULL,
851
+ "content_hash" text NOT NULL,
852
+ "mime_type" text NOT NULL DEFAULT 'application/vnd.opengeni.editable-artifact-snapshot',
853
+ "covered_head_sequence" bigint NOT NULL,
854
+ "covered_causal_frontier" jsonb,
855
+ "state_hash" text NOT NULL,
856
+ "model_schema_version" integer NOT NULL,
857
+ "operation_protocol_version" integer,
858
+ "kernel_version" text NOT NULL,
859
+ "crdt_state_version" integer,
860
+ "native_revision" bigint,
861
+ "verified_at" timestamptz NOT NULL,
862
+ "published_at" timestamptz NOT NULL DEFAULT now(),
863
+ CONSTRAINT "editable_artifact_snapshots_pk" PRIMARY KEY (
864
+ "account_id", "workspace_id", "artifact_id", "id"
865
+ ),
866
+ CONSTRAINT "editable_artifact_snapshots_artifact_fk"
867
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
868
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
869
+ CONSTRAINT "editable_artifact_snapshots_blob_fk"
870
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "blob_ref_id")
871
+ REFERENCES "editable_artifact_blob_refs"(
872
+ "account_id", "workspace_id", "artifact_id", "id"
873
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
874
+ CONSTRAINT "editable_artifact_snapshots_blob_facts_fk"
875
+ FOREIGN KEY (
876
+ "account_id", "workspace_id", "artifact_id", "blob_ref_id", "byte_size",
877
+ "content_hash", "mime_type"
878
+ ) REFERENCES "editable_artifact_blob_refs"(
879
+ "account_id", "workspace_id", "artifact_id", "id", "byte_size",
880
+ "content_hash", "mime_type"
881
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
882
+ CONSTRAINT "editable_artifact_snapshots_checkpoint_fk"
883
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "covered_head_sequence")
884
+ REFERENCES "editable_artifact_sequence_checkpoints"(
885
+ "account_id", "workspace_id", "artifact_id", "head_sequence"
886
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
887
+ CONSTRAINT "editable_artifact_snapshots_identity_chk" CHECK (
888
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
889
+ AND "blob_ref_id" ~ '^[0-9a-f]{32}$' AND "blob_ref_id" !~ '^0+$'
890
+ ),
891
+ CONSTRAINT "editable_artifact_snapshots_facts_chk" CHECK (
892
+ "byte_size" BETWEEN 1 AND 67108864
893
+ AND "content_hash" ~ '^sha256:[0-9a-f]{64}$'
894
+ AND "mime_type" = 'application/vnd.opengeni.editable-artifact-snapshot'
895
+ AND "covered_head_sequence" BETWEEN 0 AND 9007199254740991
896
+ AND "state_hash" ~ '^sha256:[0-9a-f]{64}$'
897
+ AND "model_schema_version" > 0
898
+ AND (("modality" = 'spreadsheet'
899
+ AND opengeni_private.editable_artifact_frontier_valid("covered_causal_frontier")
900
+ AND "operation_protocol_version" > 0
901
+ AND "crdt_state_version" > 0
902
+ AND "native_revision" IS NULL)
903
+ OR ("modality" IN ('document', 'presentation')
904
+ AND "covered_causal_frontier" IS NULL
905
+ AND "operation_protocol_version" IS NULL
906
+ AND "crdt_state_version" IS NULL
907
+ AND "native_revision" BETWEEN 0 AND 9007199254740991))
908
+ AND octet_length("kernel_version") BETWEEN 1 AND 512
909
+ AND "verified_at" <= "published_at"
910
+ ),
911
+ CONSTRAINT "editable_artifact_snapshots_content_uq" UNIQUE (
912
+ "account_id", "workspace_id", "artifact_id", "content_hash", "covered_head_sequence"
913
+ ),
914
+ CONSTRAINT "editable_artifact_snapshots_source_manifest_uq" UNIQUE (
915
+ "account_id", "workspace_id", "artifact_id", "id", "covered_head_sequence", "state_hash"
916
+ )
917
+ );
918
+
919
+ CREATE INDEX "editable_artifact_snapshots_coverage_idx"
920
+ ON "editable_artifact_snapshots"
921
+ ("workspace_id", "artifact_id", "covered_head_sequence" DESC);
922
+
923
+ ALTER TABLE "editable_artifacts"
924
+ ADD CONSTRAINT "editable_artifacts_current_snapshot_fk"
925
+ FOREIGN KEY ("account_id", "workspace_id", "id", "current_snapshot_id")
926
+ REFERENCES "editable_artifact_snapshots"(
927
+ "account_id", "workspace_id", "artifact_id", "id"
928
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
929
+
930
+ CREATE TABLE "editable_artifact_versions" (
931
+ "account_id" uuid NOT NULL,
932
+ "workspace_id" uuid NOT NULL,
933
+ "artifact_id" text NOT NULL,
934
+ "id" text NOT NULL,
935
+ "snapshot_id" text,
936
+ "head_sequence" bigint NOT NULL,
937
+ "causal_frontier" jsonb NOT NULL,
938
+ "state_hash" text NOT NULL,
939
+ "name" text NOT NULL,
940
+ "pinned" boolean NOT NULL DEFAULT true,
941
+ "created_by_subject_id" text NOT NULL,
942
+ "created_at" timestamptz NOT NULL DEFAULT now(),
943
+ CONSTRAINT "editable_artifact_versions_pk" PRIMARY KEY (
944
+ "account_id", "workspace_id", "artifact_id", "id"
945
+ ),
946
+ CONSTRAINT "editable_artifact_versions_artifact_fk"
947
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
948
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
949
+ CONSTRAINT "editable_artifact_versions_snapshot_fk"
950
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "snapshot_id")
951
+ REFERENCES "editable_artifact_snapshots"(
952
+ "account_id", "workspace_id", "artifact_id", "id"
953
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
954
+ CONSTRAINT "editable_artifact_versions_checkpoint_fk"
955
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "head_sequence")
956
+ REFERENCES "editable_artifact_sequence_checkpoints"(
957
+ "account_id", "workspace_id", "artifact_id", "head_sequence"
958
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
959
+ CONSTRAINT "editable_artifact_versions_identity_chk" CHECK (
960
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
961
+ AND ("snapshot_id" IS NULL OR (
962
+ "snapshot_id" ~ '^[0-9a-f]{32}$' AND "snapshot_id" !~ '^0+$'
963
+ ))
964
+ ),
965
+ CONSTRAINT "editable_artifact_versions_facts_chk" CHECK (
966
+ "head_sequence" BETWEEN 0 AND 9007199254740991
967
+ AND opengeni_private.editable_artifact_frontier_valid("causal_frontier")
968
+ AND "state_hash" ~ '^sha256:[0-9a-f]{64}$'
969
+ AND octet_length(convert_to("name", 'UTF8')) BETWEEN 1 AND 256
970
+ AND "name" = btrim("name")
971
+ AND opengeni_private.editable_artifact_identity_valid("created_by_subject_id")
972
+ )
973
+ );
974
+
975
+ CREATE INDEX "editable_artifact_versions_timeline_idx"
976
+ ON "editable_artifact_versions" ("workspace_id", "artifact_id", "created_at" DESC, "id");
977
+
978
+ CREATE TABLE "editable_artifact_materialization_jobs" (
979
+ "account_id" uuid NOT NULL,
980
+ "workspace_id" uuid NOT NULL,
981
+ "artifact_id" text NOT NULL,
982
+ "id" text NOT NULL,
983
+ "version_id" text,
984
+ "input_snapshot_id" text NOT NULL,
985
+ "target_head_sequence" bigint NOT NULL,
986
+ "state_hash" text NOT NULL,
987
+ "format" text NOT NULL,
988
+ "normalized_options" text NOT NULL,
989
+ "options_hash" text GENERATED ALWAYS AS (
990
+ 'sha256:' || encode(
991
+ opengeni_private.editable_artifact_text_sha256("normalized_options"), 'hex'
992
+ )
993
+ ) STORED,
994
+ "codec_id" text NOT NULL,
995
+ "codec_version" text NOT NULL,
996
+ "kernel_version" text NOT NULL,
997
+ "font_registry_hash" text NOT NULL,
998
+ "policy_hash" text NOT NULL,
999
+ "state" text NOT NULL DEFAULT 'pending',
1000
+ "attempt_count" integer NOT NULL DEFAULT 0,
1001
+ "lease_owner" text,
1002
+ "lease_expires_at" timestamptz,
1003
+ "settled_by_owner" text,
1004
+ "error_code" text,
1005
+ "created_at" timestamptz NOT NULL DEFAULT now(),
1006
+ "started_at" timestamptz,
1007
+ "completed_at" timestamptz,
1008
+ CONSTRAINT "editable_artifact_materialization_jobs_pk" PRIMARY KEY (
1009
+ "account_id", "workspace_id", "artifact_id", "id"
1010
+ ),
1011
+ CONSTRAINT "editable_artifact_materialization_jobs_artifact_fk"
1012
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
1013
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
1014
+ CONSTRAINT "editable_artifact_materialization_jobs_version_fk"
1015
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "version_id")
1016
+ REFERENCES "editable_artifact_versions"(
1017
+ "account_id", "workspace_id", "artifact_id", "id"
1018
+ ) ON DELETE RESTRICT,
1019
+ CONSTRAINT "editable_artifact_materialization_jobs_checkpoint_fk"
1020
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "target_head_sequence")
1021
+ REFERENCES "editable_artifact_sequence_checkpoints"(
1022
+ "account_id", "workspace_id", "artifact_id", "head_sequence"
1023
+ ) ON DELETE RESTRICT,
1024
+ CONSTRAINT "editable_artifact_materialization_jobs_source_snapshot_fk"
1025
+ FOREIGN KEY (
1026
+ "account_id", "workspace_id", "artifact_id", "input_snapshot_id",
1027
+ "target_head_sequence", "state_hash"
1028
+ ) REFERENCES "editable_artifact_snapshots"(
1029
+ "account_id", "workspace_id", "artifact_id", "id",
1030
+ "covered_head_sequence", "state_hash"
1031
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
1032
+ CONSTRAINT "editable_artifact_materialization_jobs_identity_chk" CHECK (
1033
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
1034
+ AND "input_snapshot_id" ~ '^[0-9a-f]{32}$' AND "input_snapshot_id" !~ '^0+$'
1035
+ AND ("version_id" IS NULL OR (
1036
+ "version_id" ~ '^[0-9a-f]{32}$' AND "version_id" !~ '^0+$'
1037
+ ))
1038
+ ),
1039
+ CONSTRAINT "editable_artifact_materialization_jobs_cache_facts_chk" CHECK (
1040
+ "target_head_sequence" BETWEEN 0 AND 9007199254740991
1041
+ AND "state_hash" ~ '^sha256:[0-9a-f]{64}$'
1042
+ AND "format" IN ('xlsx', 'pptx', 'docx', 'pdf', 'png', 'webp')
1043
+ AND "options_hash" ~ '^sha256:[0-9a-f]{64}$'
1044
+ AND "font_registry_hash" ~ '^sha256:[0-9a-f]{64}$'
1045
+ AND "policy_hash" ~ '^sha256:[0-9a-f]{64}$'
1046
+ AND octet_length(convert_to("normalized_options", 'UTF8')) BETWEEN 2 AND 262144
1047
+ AND "normalized_options" = btrim("normalized_options")
1048
+ AND jsonb_typeof("normalized_options"::jsonb) = 'object'
1049
+ AND octet_length(convert_to("codec_id", 'UTF8')) BETWEEN 1 AND 128
1050
+ AND "codec_id" = btrim("codec_id")
1051
+ AND octet_length("codec_version") BETWEEN 1 AND 128
1052
+ AND "codec_version" = btrim("codec_version")
1053
+ AND octet_length("kernel_version") BETWEEN 1 AND 512
1054
+ AND "kernel_version" = btrim("kernel_version")
1055
+ ),
1056
+ CONSTRAINT "editable_artifact_materialization_jobs_state_chk" CHECK (
1057
+ "state" IN ('pending', 'running', 'succeeded', 'failed')
1058
+ AND "attempt_count" BETWEEN 0 AND 1000
1059
+ AND ("lease_owner" IS NULL) = ("lease_expires_at" IS NULL)
1060
+ AND ("lease_owner" IS NULL OR octet_length("lease_owner") BETWEEN 1 AND 256)
1061
+ AND ("settled_by_owner" IS NULL
1062
+ OR octet_length("settled_by_owner") BETWEEN 1 AND 256)
1063
+ AND ("error_code" IS NULL OR octet_length("error_code") BETWEEN 1 AND 256)
1064
+ AND (("state" = 'pending' AND "attempt_count" = 0
1065
+ AND "lease_owner" IS NULL AND "settled_by_owner" IS NULL
1066
+ AND "started_at" IS NULL AND "completed_at" IS NULL)
1067
+ OR ("state" = 'running' AND "attempt_count" > 0
1068
+ AND "lease_owner" IS NOT NULL AND "started_at" IS NOT NULL
1069
+ AND "lease_expires_at" > "started_at" AND "settled_by_owner" IS NULL
1070
+ AND "completed_at" IS NULL)
1071
+ OR ("state" IN ('succeeded', 'failed')
1072
+ AND "attempt_count" > 0 AND "started_at" IS NOT NULL
1073
+ AND "completed_at" IS NOT NULL AND "completed_at" >= "started_at"
1074
+ AND "settled_by_owner" IS NOT NULL
1075
+ AND "lease_owner" IS NULL AND "lease_expires_at" IS NULL))
1076
+ AND (("state" = 'failed' AND "error_code" IS NOT NULL)
1077
+ OR ("state" <> 'failed' AND "error_code" IS NULL))
1078
+ ),
1079
+ CONSTRAINT "editable_artifact_materialization_jobs_cache_uq" UNIQUE (
1080
+ "account_id", "workspace_id", "artifact_id", "input_snapshot_id", "state_hash", "format",
1081
+ "options_hash", "codec_id", "codec_version", "kernel_version",
1082
+ "font_registry_hash", "policy_hash"
1083
+ )
1084
+ );
1085
+
1086
+ CREATE INDEX "editable_artifact_materialization_jobs_claim_idx"
1087
+ ON "editable_artifact_materialization_jobs" (
1088
+ (coalesce("lease_expires_at", "created_at")), "created_at",
1089
+ "account_id", "workspace_id", "artifact_id", "id"
1090
+ )
1091
+ WHERE "state" IN ('pending', 'running');
1092
+
1093
+ CREATE TABLE "editable_artifact_materialization_results" (
1094
+ "account_id" uuid NOT NULL,
1095
+ "workspace_id" uuid NOT NULL,
1096
+ "artifact_id" text NOT NULL,
1097
+ "id" text NOT NULL,
1098
+ "job_id" text NOT NULL,
1099
+ "blob_ref_id" text NOT NULL,
1100
+ "byte_size" bigint NOT NULL,
1101
+ "content_hash" text NOT NULL,
1102
+ "mime_type" text NOT NULL,
1103
+ "verified_at" timestamptz NOT NULL,
1104
+ "created_at" timestamptz NOT NULL DEFAULT now(),
1105
+ CONSTRAINT "editable_artifact_materialization_results_pk" PRIMARY KEY (
1106
+ "account_id", "workspace_id", "artifact_id", "id"
1107
+ ),
1108
+ CONSTRAINT "editable_artifact_materialization_results_job_fk"
1109
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "job_id")
1110
+ REFERENCES "editable_artifact_materialization_jobs"(
1111
+ "account_id", "workspace_id", "artifact_id", "id"
1112
+ ) ON DELETE RESTRICT,
1113
+ CONSTRAINT "editable_artifact_materialization_results_blob_fk"
1114
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "blob_ref_id")
1115
+ REFERENCES "editable_artifact_blob_refs"(
1116
+ "account_id", "workspace_id", "artifact_id", "id"
1117
+ ) ON DELETE RESTRICT,
1118
+ CONSTRAINT "editable_artifact_materialization_results_blob_facts_fk"
1119
+ FOREIGN KEY (
1120
+ "account_id", "workspace_id", "artifact_id", "blob_ref_id", "byte_size",
1121
+ "content_hash", "mime_type"
1122
+ ) REFERENCES "editable_artifact_blob_refs"(
1123
+ "account_id", "workspace_id", "artifact_id", "id", "byte_size",
1124
+ "content_hash", "mime_type"
1125
+ ) ON DELETE RESTRICT,
1126
+ CONSTRAINT "editable_artifact_materialization_results_identity_chk" CHECK (
1127
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
1128
+ AND "job_id" ~ '^[0-9a-f]{32}$' AND "job_id" !~ '^0+$'
1129
+ AND "blob_ref_id" ~ '^[0-9a-f]{32}$' AND "blob_ref_id" !~ '^0+$'
1130
+ ),
1131
+ CONSTRAINT "editable_artifact_materialization_results_facts_chk" CHECK (
1132
+ "byte_size" BETWEEN 1 AND 9007199254740991
1133
+ AND "content_hash" ~ '^sha256:[0-9a-f]{64}$'
1134
+ AND octet_length("mime_type") BETWEEN 1 AND 256
1135
+ AND "mime_type" = btrim("mime_type")
1136
+ AND "verified_at" <= "created_at"
1137
+ ),
1138
+ CONSTRAINT "editable_artifact_materialization_results_job_uq" UNIQUE (
1139
+ "account_id", "workspace_id", "artifact_id", "job_id"
1140
+ )
1141
+ );
1142
+
1143
+ CREATE TABLE "editable_artifact_live_outbox" (
1144
+ "account_id" uuid NOT NULL,
1145
+ "workspace_id" uuid NOT NULL,
1146
+ "artifact_id" text NOT NULL,
1147
+ "id" text NOT NULL,
1148
+ "transaction_id" text,
1149
+ "snapshot_id" text,
1150
+ "event_kind" text NOT NULL,
1151
+ "event" jsonb NOT NULL,
1152
+ "state" text NOT NULL DEFAULT 'pending',
1153
+ "attempt_count" integer NOT NULL DEFAULT 0,
1154
+ "lease_owner" text,
1155
+ "lease_expires_at" timestamptz,
1156
+ "next_attempt_at" timestamptz NOT NULL DEFAULT now(),
1157
+ "last_error_code" text,
1158
+ "published_at" timestamptz,
1159
+ "dead_lettered_at" timestamptz,
1160
+ "created_at" timestamptz NOT NULL DEFAULT now(),
1161
+ CONSTRAINT "editable_artifact_live_outbox_pk" PRIMARY KEY (
1162
+ "account_id", "workspace_id", "artifact_id", "id"
1163
+ ),
1164
+ CONSTRAINT "editable_artifact_live_outbox_artifact_fk"
1165
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
1166
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
1167
+ CONSTRAINT "editable_artifact_live_outbox_transaction_fk"
1168
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "transaction_id")
1169
+ REFERENCES "editable_artifact_transactions"(
1170
+ "account_id", "workspace_id", "artifact_id", "id"
1171
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
1172
+ CONSTRAINT "editable_artifact_live_outbox_snapshot_fk"
1173
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "snapshot_id")
1174
+ REFERENCES "editable_artifact_snapshots"(
1175
+ "account_id", "workspace_id", "artifact_id", "id"
1176
+ ) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
1177
+ CONSTRAINT "editable_artifact_live_outbox_identity_chk" CHECK (
1178
+ "id" ~ '^[0-9a-f]{32}$' AND "id" !~ '^0+$'
1179
+ ),
1180
+ CONSTRAINT "editable_artifact_live_outbox_event_chk" CHECK (
1181
+ "event_kind" IN ('transaction_committed', 'snapshot_published')
1182
+ AND jsonb_typeof("event") = 'object'
1183
+ AND octet_length("event"::text) <= 65536
1184
+ AND jsonb_typeof("event"->'kind') = 'string'
1185
+ AND jsonb_typeof("event"->'schemaVersion') = 'number'
1186
+ AND "event"->>'kind' = "event_kind"
1187
+ AND "event"->>'schemaVersion' = '1'
1188
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1189
+ "event"->'scope', ARRAY['accountId', 'workspaceId']
1190
+ )
1191
+ AND "event"#>>'{scope,accountId}' = "account_id"::text
1192
+ AND "event"#>>'{scope,workspaceId}' = "workspace_id"::text
1193
+ AND jsonb_typeof("event"->'artifactId') = 'string'
1194
+ AND "event"->>'artifactId' = "artifact_id"
1195
+ AND jsonb_typeof("event"->'modality') = 'string'
1196
+ AND "event"->>'modality' IN ('spreadsheet', 'document', 'presentation')
1197
+ AND (("event_kind" = 'transaction_committed'
1198
+ AND "transaction_id" IS NOT NULL AND "snapshot_id" IS NULL
1199
+ AND (("event"->>'modality' = 'spreadsheet'
1200
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1201
+ "event", ARRAY[
1202
+ 'artifactId', 'committedAt', 'kind', 'modality', 'operationProtocolVersion',
1203
+ 'schemaVersion', 'scope', 'sequenceEnd',
1204
+ 'sequenceStart', 'serverTransactionId', 'stateHash'
1205
+ ]
1206
+ )
1207
+ AND jsonb_typeof("event"->'operationProtocolVersion') = 'number'
1208
+ AND ("event"->>'operationProtocolVersion') ~ '^[1-9][0-9]{0,9}$')
1209
+ OR ("event"->>'modality' IN ('document', 'presentation')
1210
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1211
+ "event", ARRAY[
1212
+ 'artifactId', 'commitProtocolVersion', 'committedAt', 'kind', 'modality',
1213
+ 'schemaVersion', 'scope', 'sequenceEnd',
1214
+ 'sequenceStart', 'serverTransactionId', 'stateHash'
1215
+ ]
1216
+ )
1217
+ AND jsonb_typeof("event"->'commitProtocolVersion') = 'number'
1218
+ AND ("event"->>'commitProtocolVersion') ~ '^[1-9][0-9]{0,9}$'))
1219
+ AND jsonb_typeof("event"->'serverTransactionId') = 'string'
1220
+ AND "event"->>'serverTransactionId' = "transaction_id")
1221
+ OR ("event_kind" = 'snapshot_published'
1222
+ AND "transaction_id" IS NULL AND "snapshot_id" IS NOT NULL
1223
+ AND (("event"->>'modality' = 'spreadsheet'
1224
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1225
+ "event", ARRAY[
1226
+ 'artifactId', 'coveredHeadSequence', 'kind', 'modality',
1227
+ 'operationProtocolVersion', 'publishedAt', 'schemaVersion',
1228
+ 'scope', 'snapshotId', 'stateHash'
1229
+ ]
1230
+ )
1231
+ AND jsonb_typeof("event"->'operationProtocolVersion') = 'number'
1232
+ AND ("event"->>'operationProtocolVersion') ~ '^[1-9][0-9]{0,9}$')
1233
+ OR ("event"->>'modality' IN ('document', 'presentation')
1234
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1235
+ "event", ARRAY[
1236
+ 'artifactId', 'coveredHeadSequence', 'kind', 'modality',
1237
+ 'publishedAt', 'schemaVersion', 'scope', 'snapshotId', 'stateHash'
1238
+ ]
1239
+ )))
1240
+ AND jsonb_typeof("event"->'snapshotId') = 'string'
1241
+ AND "event"->>'snapshotId' = "snapshot_id"))
1242
+ AND jsonb_typeof("event"->'stateHash') = 'string'
1243
+ AND "event"->>'stateHash' ~ '^sha256:[0-9a-f]{64}$'
1244
+ AND (("event_kind" = 'transaction_committed'
1245
+ AND jsonb_typeof("event"->'sequenceStart') = 'number'
1246
+ AND jsonb_typeof("event"->'sequenceEnd') = 'number'
1247
+ AND ("event"->>'sequenceStart') ~ '^[1-9][0-9]{0,15}$'
1248
+ AND ("event"->>'sequenceEnd') ~ '^[1-9][0-9]{0,15}$'
1249
+ AND jsonb_typeof("event"->'committedAt') = 'string'
1250
+ AND "event"->>'committedAt'
1251
+ ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$')
1252
+ OR ("event_kind" = 'snapshot_published'
1253
+ AND jsonb_typeof("event"->'coveredHeadSequence') = 'number'
1254
+ AND ("event"->>'coveredHeadSequence') ~ '^(0|[1-9][0-9]{0,15})$'
1255
+ AND jsonb_typeof("event"->'publishedAt') = 'string'
1256
+ AND "event"->>'publishedAt'
1257
+ ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$'))
1258
+ ),
1259
+ CONSTRAINT "editable_artifact_live_outbox_state_chk" CHECK (
1260
+ "state" IN ('pending', 'publishing', 'published', 'dead_lettered')
1261
+ AND "attempt_count" BETWEEN 0 AND 1000000
1262
+ AND ("lease_owner" IS NULL) = ("lease_expires_at" IS NULL)
1263
+ AND ("lease_owner" IS NULL OR octet_length("lease_owner") BETWEEN 1 AND 256)
1264
+ AND ("last_error_code" IS NULL OR octet_length("last_error_code") BETWEEN 1 AND 256)
1265
+ AND (("state" = 'pending' AND "lease_owner" IS NULL
1266
+ AND "published_at" IS NULL AND "dead_lettered_at" IS NULL)
1267
+ OR ("state" = 'publishing' AND "lease_owner" IS NOT NULL
1268
+ AND "published_at" IS NULL AND "dead_lettered_at" IS NULL)
1269
+ OR ("state" = 'published' AND "lease_owner" IS NULL
1270
+ AND "published_at" IS NOT NULL AND "dead_lettered_at" IS NULL
1271
+ AND "last_error_code" IS NULL)
1272
+ OR ("state" = 'dead_lettered' AND "lease_owner" IS NULL
1273
+ AND "published_at" IS NULL AND "dead_lettered_at" IS NOT NULL
1274
+ AND "last_error_code" IN ('invalid_hint', 'oversized_hint', 'attempts_exhausted')))
1275
+ )
1276
+ );
1277
+
1278
+ CREATE UNIQUE INDEX "editable_artifact_live_outbox_id_uq"
1279
+ ON "editable_artifact_live_outbox" ("id");
1280
+ CREATE UNIQUE INDEX "editable_artifact_live_outbox_transaction_uq"
1281
+ ON "editable_artifact_live_outbox"
1282
+ ("account_id", "workspace_id", "artifact_id", "transaction_id")
1283
+ WHERE "event_kind" = 'transaction_committed';
1284
+ CREATE UNIQUE INDEX "editable_artifact_live_outbox_snapshot_uq"
1285
+ ON "editable_artifact_live_outbox"
1286
+ ("account_id", "workspace_id", "artifact_id", "snapshot_id")
1287
+ WHERE "event_kind" = 'snapshot_published';
1288
+
1289
+ CREATE INDEX "editable_artifact_live_outbox_pending_claim_idx"
1290
+ ON "editable_artifact_live_outbox" ("next_attempt_at", "created_at", "id")
1291
+ WHERE "state" = 'pending';
1292
+ CREATE INDEX "editable_artifact_live_outbox_publishing_claim_idx"
1293
+ ON "editable_artifact_live_outbox" ("lease_expires_at", "created_at", "id")
1294
+ WHERE "state" = 'publishing';
1295
+
1296
+ CREATE TABLE "editable_artifact_replica_leases" (
1297
+ "account_id" uuid NOT NULL,
1298
+ "workspace_id" uuid NOT NULL,
1299
+ "artifact_id" text NOT NULL,
1300
+ "modality" text NOT NULL,
1301
+ "replica_id" text NOT NULL,
1302
+ "actor_key" text NOT NULL,
1303
+ "applied_head_sequence" bigint NOT NULL,
1304
+ "causal_frontier" jsonb,
1305
+ "native_revision" bigint,
1306
+ "lease_expires_at" timestamptz NOT NULL,
1307
+ "last_seen_at" timestamptz NOT NULL DEFAULT now(),
1308
+ "revoked_at" timestamptz,
1309
+ "created_at" timestamptz NOT NULL DEFAULT now(),
1310
+ CONSTRAINT "editable_artifact_replica_leases_pk" PRIMARY KEY (
1311
+ "account_id", "workspace_id", "artifact_id", "replica_id"
1312
+ ),
1313
+ CONSTRAINT "editable_artifact_replica_leases_artifact_fk"
1314
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id")
1315
+ REFERENCES "editable_artifacts"("account_id", "workspace_id", "id") ON DELETE RESTRICT,
1316
+ CONSTRAINT "editable_artifact_replica_leases_checkpoint_fk"
1317
+ FOREIGN KEY ("account_id", "workspace_id", "artifact_id", "applied_head_sequence")
1318
+ REFERENCES "editable_artifact_sequence_checkpoints"(
1319
+ "account_id", "workspace_id", "artifact_id", "head_sequence"
1320
+ ) ON DELETE RESTRICT,
1321
+ CONSTRAINT "editable_artifact_replica_leases_replica_chk" CHECK (
1322
+ "replica_id" ~ '^[0-9a-f]{16}$' AND "replica_id" !~ '^0+$'
1323
+ AND octet_length(convert_to("actor_key", 'UTF8')) BETWEEN 1 AND 8192
1324
+ AND (("modality" = 'spreadsheet'
1325
+ AND opengeni_private.editable_artifact_frontier_valid("causal_frontier")
1326
+ AND "native_revision" IS NULL)
1327
+ OR ("modality" IN ('document', 'presentation')
1328
+ AND "causal_frontier" IS NULL
1329
+ AND "native_revision" BETWEEN 0 AND 9007199254740991))
1330
+ ),
1331
+ CONSTRAINT "editable_artifact_replica_leases_cursor_chk" CHECK (
1332
+ "applied_head_sequence" BETWEEN 0 AND 9007199254740991
1333
+ AND "last_seen_at" >= "created_at"
1334
+ AND "lease_expires_at" > "last_seen_at"
1335
+ AND ("revoked_at" IS NULL OR "revoked_at" >= "created_at")
1336
+ )
1337
+ );
1338
+
1339
+ CREATE INDEX "editable_artifact_replica_leases_expiry_idx"
1340
+ ON "editable_artifact_replica_leases" ("lease_expires_at", "replica_id")
1341
+ WHERE "revoked_at" IS NULL;
1342
+
1343
+ CREATE OR REPLACE FUNCTION opengeni_private.validate_editable_artifact_transaction_commit()
1344
+ RETURNS trigger
1345
+ LANGUAGE plpgsql
1346
+ SET search_path = pg_catalog, pg_temp
1347
+ AS $body$
1348
+ DECLARE tx record;
1349
+ DECLARE parent_checkpoint record;
1350
+ DECLARE predecessor record;
1351
+ DECLARE predecessor_resolved_causal_base jsonb := '[]'::jsonb;
1352
+ DECLARE parent_replica_counter bigint;
1353
+ DECLARE resolved_replica_counter bigint;
1354
+ DECLARE result_replica_counter bigint;
1355
+ DECLARE operation_rows integer;
1356
+ DECLARE undo_rows integer;
1357
+ DECLARE artifact_modality text;
1358
+ BEGIN
1359
+ PERFORM set_config(
1360
+ 'search_path', 'pg_catalog,' || quote_ident(TG_TABLE_SCHEMA) || ',pg_temp', true
1361
+ );
1362
+ SELECT * INTO tx FROM editable_artifact_transactions candidate
1363
+ WHERE candidate.account_id = NEW.account_id
1364
+ AND candidate.workspace_id = NEW.workspace_id
1365
+ AND candidate.artifact_id = NEW.artifact_id
1366
+ AND candidate.id = NEW.id;
1367
+
1368
+ IF NOT FOUND THEN
1369
+ RAISE EXCEPTION 'editable artifact transaction dependency is missing'
1370
+ USING ERRCODE = '23503';
1371
+ END IF;
1372
+
1373
+ SELECT artifact.modality INTO artifact_modality
1374
+ FROM editable_artifacts artifact
1375
+ WHERE artifact.account_id = tx.account_id
1376
+ AND artifact.workspace_id = tx.workspace_id
1377
+ AND artifact.id = tx.artifact_id;
1378
+ IF artifact_modality IS NULL OR artifact_modality <> tx.modality THEN
1379
+ RAISE EXCEPTION 'transaction modality differs from its durable artifact'
1380
+ USING ERRCODE = '23514';
1381
+ END IF;
1382
+
1383
+ IF tx.replica_counter > 1 THEN
1384
+ SELECT * INTO predecessor
1385
+ FROM editable_artifact_transactions candidate
1386
+ WHERE candidate.account_id = tx.account_id
1387
+ AND candidate.workspace_id = tx.workspace_id
1388
+ AND candidate.artifact_id = tx.artifact_id
1389
+ AND candidate.actor_key = tx.actor_key
1390
+ AND candidate.replica_id = tx.replica_id
1391
+ AND candidate.client_transaction_id = tx.previous_local_transaction_id;
1392
+ IF NOT FOUND
1393
+ OR predecessor.replica_counter <> tx.replica_counter - 1
1394
+ OR predecessor.sequence_end > tx.parent_head_sequence
1395
+ THEN
1396
+ RAISE EXCEPTION 'transaction predecessor is not the prior committed local replica counter'
1397
+ USING ERRCODE = '23514';
1398
+ END IF;
1399
+ predecessor_resolved_causal_base := predecessor.resolved_causal_base;
1400
+ END IF;
1401
+
1402
+ SELECT * INTO parent_checkpoint
1403
+ FROM editable_artifact_sequence_checkpoints checkpoint
1404
+ WHERE checkpoint.account_id = tx.account_id
1405
+ AND checkpoint.workspace_id = tx.workspace_id
1406
+ AND checkpoint.artifact_id = tx.artifact_id
1407
+ AND checkpoint.head_sequence = tx.parent_head_sequence;
1408
+ IF NOT FOUND THEN
1409
+ RAISE EXCEPTION 'transaction parent checkpoint is missing'
1410
+ USING ERRCODE = '23514';
1411
+ END IF;
1412
+
1413
+ IF parent_checkpoint.modality <> tx.modality
1414
+ OR parent_checkpoint.state_hash <> tx.prior_state_hash
1415
+ THEN
1416
+ RAISE EXCEPTION 'transaction parent checkpoint modality or state differs'
1417
+ USING ERRCODE = '23514';
1418
+ END IF;
1419
+
1420
+ IF tx.modality = 'spreadsheet' THEN
1421
+ SELECT (entry->>'counter')::bigint INTO resolved_replica_counter
1422
+ FROM jsonb_array_elements(tx.resolved_causal_base) AS entries(entry)
1423
+ WHERE entry->>'replicaId' = tx.replica_id;
1424
+ SELECT (entry->>'counter')::bigint INTO result_replica_counter
1425
+ FROM jsonb_array_elements(tx.resulting_causal_frontier) AS entries(entry)
1426
+ WHERE entry->>'replicaId' = tx.replica_id;
1427
+ SELECT (entry->>'counter')::bigint INTO parent_replica_counter
1428
+ FROM jsonb_array_elements(parent_checkpoint.causal_frontier) AS entries(entry)
1429
+ WHERE entry->>'replicaId' = tx.replica_id;
1430
+ IF coalesce(parent_replica_counter, 0) <> tx.replica_counter - 1
1431
+ OR coalesce(resolved_replica_counter, 0) <> tx.replica_counter - 1
1432
+ OR result_replica_counter IS DISTINCT FROM tx.replica_counter
1433
+ OR NOT opengeni_private.editable_artifact_frontier_merge_dot_equals(
1434
+ tx.resolved_causal_base, tx.causal_base, predecessor_resolved_causal_base,
1435
+ tx.replica_id, tx.replica_counter - 1
1436
+ )
1437
+ OR NOT opengeni_private.editable_artifact_frontier_merge_dot_equals(
1438
+ tx.resulting_causal_frontier, parent_checkpoint.causal_frontier,
1439
+ tx.resolved_causal_base, tx.replica_id, tx.replica_counter
1440
+ )
1441
+ OR NOT opengeni_private.editable_artifact_frontier_dominates(
1442
+ parent_checkpoint.causal_frontier, tx.resolved_causal_base
1443
+ )
1444
+ THEN
1445
+ RAISE EXCEPTION 'transaction causal frontiers do not match parent, predecessor, and dot'
1446
+ USING ERRCODE = '23514';
1447
+ END IF;
1448
+ ELSIF parent_checkpoint.native_revision <> tx.prior_native_revision THEN
1449
+ RAISE EXCEPTION 'serialized transaction native revision differs from its parent checkpoint'
1450
+ USING ERRCODE = '23514';
1451
+ END IF;
1452
+
1453
+ SELECT count(*) INTO operation_rows
1454
+ FROM editable_artifact_operations operation
1455
+ WHERE operation.account_id = tx.account_id
1456
+ AND operation.workspace_id = tx.workspace_id
1457
+ AND operation.artifact_id = tx.artifact_id
1458
+ AND operation.transaction_id = tx.id;
1459
+ IF (tx.modality = 'spreadsheet' AND operation_rows <> tx.operation_count)
1460
+ OR (tx.modality <> 'spreadsheet' AND operation_rows <> 0)
1461
+ THEN
1462
+ RAISE EXCEPTION 'transaction operation coverage is incomplete'
1463
+ USING ERRCODE = '23514';
1464
+ END IF;
1465
+
1466
+ IF tx.modality = 'spreadsheet' AND EXISTS (
1467
+ SELECT 1
1468
+ FROM editable_artifact_operations operation
1469
+ WHERE operation.account_id = tx.account_id
1470
+ AND operation.workspace_id = tx.workspace_id
1471
+ AND operation.artifact_id = tx.artifact_id
1472
+ AND operation.transaction_id = tx.id
1473
+ AND (
1474
+ operation.sequence <> tx.sequence_start + operation.operation_index
1475
+ OR operation.operation_index >= tx.operation_count
1476
+ OR operation.operation_id <> tx.operation_ids ->> operation.operation_index
1477
+ OR operation.dot_replica_id <> tx.replica_id
1478
+ OR operation.dot_counter <> tx.replica_counter
1479
+ OR operation.actor_key <> tx.actor_key
1480
+ OR operation.created_at <> tx.committed_at
1481
+ )
1482
+ ) THEN
1483
+ RAISE EXCEPTION 'operation metadata differs from canonical transaction projection'
1484
+ USING ERRCODE = '23514';
1485
+ END IF;
1486
+
1487
+ SELECT count(*) INTO undo_rows
1488
+ FROM editable_artifact_undo_claims claim
1489
+ WHERE claim.account_id = tx.account_id
1490
+ AND claim.workspace_id = tx.workspace_id
1491
+ AND claim.artifact_id = tx.artifact_id
1492
+ AND claim.claiming_transaction_id = tx.id;
1493
+ IF (tx.modality <> 'spreadsheet' AND undo_rows <> 0)
1494
+ OR (tx.modality = 'spreadsheet' AND (
1495
+ undo_rows <> jsonb_array_length(tx.selective_undo_targets)
1496
+ OR EXISTS (
1497
+ WITH targets AS MATERIALIZED (
1498
+ SELECT operation_id
1499
+ FROM jsonb_array_elements_text(tx.selective_undo_targets) target(operation_id)
1500
+ ), frontier AS MATERIALIZED (
1501
+ SELECT entry->>'replicaId' AS replica_id,
1502
+ (entry->>'counter')::bigint AS counter
1503
+ FROM jsonb_array_elements(tx.resolved_causal_base) AS entries(entry)
1504
+ )
1505
+ SELECT 1
1506
+ FROM targets target
1507
+ LEFT JOIN editable_artifact_undo_claims claim
1508
+ ON claim.account_id = tx.account_id
1509
+ AND claim.workspace_id = tx.workspace_id
1510
+ AND claim.artifact_id = tx.artifact_id
1511
+ AND claim.claiming_transaction_id = tx.id
1512
+ AND claim.target_operation_id = target.operation_id
1513
+ LEFT JOIN editable_artifact_operations operation
1514
+ ON operation.account_id = tx.account_id
1515
+ AND operation.workspace_id = tx.workspace_id
1516
+ AND operation.artifact_id = tx.artifact_id
1517
+ AND operation.operation_id = target.operation_id
1518
+ LEFT JOIN frontier
1519
+ ON frontier.replica_id = operation.dot_replica_id
1520
+ WHERE claim.target_operation_id IS NULL
1521
+ OR operation.operation_id IS NULL
1522
+ OR operation.actor_key <> tx.actor_key
1523
+ OR operation.sequence > tx.parent_head_sequence
1524
+ OR coalesce(frontier.counter, 0) < operation.dot_counter
1525
+ )))
1526
+ THEN
1527
+ RAISE EXCEPTION 'selective undo claims do not exactly match durable targets'
1528
+ USING ERRCODE = '23514';
1529
+ END IF;
1530
+
1531
+ IF NOT EXISTS (
1532
+ SELECT 1 FROM editable_artifact_sequence_checkpoints checkpoint
1533
+ WHERE checkpoint.account_id = tx.account_id
1534
+ AND checkpoint.workspace_id = tx.workspace_id
1535
+ AND checkpoint.artifact_id = tx.artifact_id
1536
+ AND checkpoint.head_sequence = tx.sequence_end
1537
+ AND checkpoint.transaction_id = tx.id
1538
+ AND checkpoint.modality = tx.modality
1539
+ AND ((tx.modality = 'spreadsheet'
1540
+ AND checkpoint.causal_frontier = tx.resulting_causal_frontier
1541
+ AND checkpoint.native_revision IS NULL)
1542
+ OR (tx.modality IN ('document', 'presentation')
1543
+ AND checkpoint.causal_frontier IS NULL
1544
+ AND checkpoint.native_revision = tx.native_revision))
1545
+ AND checkpoint.state_hash = tx.state_hash
1546
+ ) THEN
1547
+ RAISE EXCEPTION 'transaction terminal checkpoint is missing or inconsistent'
1548
+ USING ERRCODE = '23514';
1549
+ END IF;
1550
+
1551
+ IF NOT EXISTS (
1552
+ SELECT 1 FROM editable_artifact_live_outbox outbox
1553
+ WHERE outbox.account_id = tx.account_id
1554
+ AND outbox.workspace_id = tx.workspace_id
1555
+ AND outbox.artifact_id = tx.artifact_id
1556
+ AND outbox.event_kind = 'transaction_committed'
1557
+ AND outbox.transaction_id = tx.id
1558
+ ) THEN
1559
+ RAISE EXCEPTION 'transaction committed outbox event is missing'
1560
+ USING ERRCODE = '23514';
1561
+ END IF;
1562
+
1563
+ IF NOT EXISTS (
1564
+ SELECT 1 FROM editable_artifact_idempotency_receipts receipt
1565
+ WHERE receipt.account_id = tx.account_id
1566
+ AND receipt.workspace_id = tx.workspace_id
1567
+ AND receipt.artifact_id = tx.artifact_id
1568
+ AND receipt.operation_kind = 'edit'
1569
+ AND receipt.authority_key = tx.actor_key
1570
+ AND receipt.idempotency_key = tx.client_transaction_id
1571
+ AND receipt.request_hash = tx.request_hash
1572
+ AND receipt.resource_type = 'transaction'
1573
+ AND receipt.resource_id = tx.id
1574
+ AND receipt.server_transaction_id = tx.id
1575
+ ) THEN
1576
+ RAISE EXCEPTION 'transaction idempotency receipt is missing or inconsistent'
1577
+ USING ERRCODE = '23514';
1578
+ END IF;
1579
+
1580
+ IF NOT EXISTS (
1581
+ SELECT 1 FROM editable_artifacts artifact
1582
+ WHERE artifact.account_id = tx.account_id
1583
+ AND artifact.workspace_id = tx.workspace_id
1584
+ AND artifact.id = tx.artifact_id
1585
+ AND artifact.lifecycle_state = 'active'
1586
+ AND artifact.head_sequence >= tx.sequence_end
1587
+ ) THEN
1588
+ RAISE EXCEPTION 'artifact head was not advanced through committed transaction'
1589
+ USING ERRCODE = '23514';
1590
+ END IF;
1591
+ RETURN NULL;
1592
+ END;
1593
+ $body$;
1594
+
1595
+ CREATE CONSTRAINT TRIGGER editable_artifact_transactions_commit_guard
1596
+ AFTER INSERT ON "editable_artifact_transactions"
1597
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
1598
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_transaction_commit();
1599
+
1600
+ -- The aggregate validator above runs exactly once per transaction header. These
1601
+ -- constant-cost guards prevent immutable children from being appended after the
1602
+ -- owning transaction has published its head, without rescanning the transaction
1603
+ -- once per operation projection row.
1604
+ CREATE OR REPLACE FUNCTION opengeni_private.guard_editable_artifact_transaction_child_insert()
1605
+ RETURNS trigger
1606
+ LANGUAGE plpgsql
1607
+ SET search_path = pg_catalog, pg_temp
1608
+ AS $body$
1609
+ DECLARE owner_transaction_id text;
1610
+ DECLARE owner_sequence_end bigint;
1611
+ DECLARE owner_modality text;
1612
+ DECLARE published_head bigint;
1613
+ BEGIN
1614
+ PERFORM set_config(
1615
+ 'search_path', 'pg_catalog,' || quote_ident(TG_TABLE_SCHEMA) || ',pg_temp', true
1616
+ );
1617
+ IF TG_TABLE_NAME = 'editable_artifact_operations' THEN
1618
+ owner_transaction_id := NEW.transaction_id;
1619
+ ELSIF TG_TABLE_NAME = 'editable_artifact_idempotency_receipts' THEN
1620
+ IF NEW.operation_kind <> 'edit' THEN RETURN NEW; END IF;
1621
+ owner_transaction_id := NEW.server_transaction_id;
1622
+ ELSIF TG_TABLE_NAME = 'editable_artifact_undo_claims' THEN
1623
+ owner_transaction_id := NEW.claiming_transaction_id;
1624
+ ELSIF TG_TABLE_NAME = 'editable_artifact_sequence_checkpoints' THEN
1625
+ IF NEW.transaction_id IS NULL THEN RETURN NEW; END IF;
1626
+ owner_transaction_id := NEW.transaction_id;
1627
+ ELSIF TG_TABLE_NAME = 'editable_artifact_live_outbox' THEN
1628
+ IF NEW.event_kind <> 'transaction_committed' THEN RETURN NEW; END IF;
1629
+ owner_transaction_id := NEW.transaction_id;
1630
+ END IF;
1631
+
1632
+ SELECT transaction.sequence_end, transaction.modality
1633
+ INTO owner_sequence_end, owner_modality
1634
+ FROM editable_artifact_transactions transaction
1635
+ WHERE transaction.account_id = NEW.account_id
1636
+ AND transaction.workspace_id = NEW.workspace_id
1637
+ AND transaction.artifact_id = NEW.artifact_id
1638
+ AND transaction.id = owner_transaction_id;
1639
+ IF NOT FOUND THEN
1640
+ RAISE EXCEPTION 'transaction child must follow its transaction header'
1641
+ USING ERRCODE = '23503';
1642
+ END IF;
1643
+ IF TG_TABLE_NAME IN ('editable_artifact_operations', 'editable_artifact_undo_claims')
1644
+ AND owner_modality <> 'spreadsheet'
1645
+ THEN
1646
+ RAISE EXCEPTION 'serialized artifact transactions cannot receive CRDT children'
1647
+ USING ERRCODE = '23514';
1648
+ END IF;
1649
+
1650
+ SELECT artifact.head_sequence INTO published_head
1651
+ FROM editable_artifacts artifact
1652
+ WHERE artifact.account_id = NEW.account_id
1653
+ AND artifact.workspace_id = NEW.workspace_id
1654
+ AND artifact.id = NEW.artifact_id;
1655
+ IF NOT FOUND THEN
1656
+ RAISE EXCEPTION 'transaction child artifact is missing'
1657
+ USING ERRCODE = '23503';
1658
+ END IF;
1659
+ IF published_head >= owner_sequence_end THEN
1660
+ RAISE EXCEPTION 'committed transaction history cannot receive additional children'
1661
+ USING ERRCODE = '55000';
1662
+ END IF;
1663
+ RETURN NEW;
1664
+ END;
1665
+ $body$;
1666
+
1667
+ CREATE TRIGGER editable_artifact_operations_insert_guard
1668
+ BEFORE INSERT ON "editable_artifact_operations"
1669
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_transaction_child_insert();
1670
+ CREATE TRIGGER editable_artifact_receipts_insert_guard
1671
+ BEFORE INSERT ON "editable_artifact_idempotency_receipts"
1672
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_transaction_child_insert();
1673
+ CREATE TRIGGER editable_artifact_undo_claims_insert_guard
1674
+ BEFORE INSERT ON "editable_artifact_undo_claims"
1675
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_transaction_child_insert();
1676
+ CREATE TRIGGER editable_artifact_checkpoints_insert_guard
1677
+ BEFORE INSERT ON "editable_artifact_sequence_checkpoints"
1678
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_transaction_child_insert();
1679
+ CREATE TRIGGER editable_artifact_live_outbox_transaction_insert_guard
1680
+ BEFORE INSERT ON "editable_artifact_live_outbox"
1681
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_transaction_child_insert();
1682
+
1683
+ CREATE OR REPLACE FUNCTION opengeni_private.validate_editable_artifact_projection()
1684
+ RETURNS trigger
1685
+ LANGUAGE plpgsql
1686
+ SECURITY DEFINER
1687
+ SET search_path = pg_catalog, pg_temp
1688
+ AS $body$
1689
+ DECLARE checkpoint record;
1690
+ DECLARE blob record;
1691
+ DECLARE tx record;
1692
+ DECLARE snapshot record;
1693
+ DECLARE job record;
1694
+ DECLARE artifact_modality text;
1695
+ BEGIN
1696
+ PERFORM set_config(
1697
+ 'search_path', 'pg_catalog,' || quote_ident(TG_TABLE_SCHEMA) || ',pg_temp', true
1698
+ );
1699
+ IF TG_TABLE_NAME = 'editable_artifacts' THEN
1700
+ SELECT * INTO checkpoint FROM editable_artifact_sequence_checkpoints candidate
1701
+ WHERE candidate.account_id = NEW.account_id
1702
+ AND candidate.workspace_id = NEW.workspace_id
1703
+ AND candidate.artifact_id = NEW.id
1704
+ AND candidate.head_sequence = NEW.head_sequence;
1705
+ IF NOT FOUND OR checkpoint.modality <> NEW.modality
1706
+ OR checkpoint.state_hash <> NEW.state_hash
1707
+ OR (NEW.modality = 'spreadsheet'
1708
+ AND checkpoint.causal_frontier IS DISTINCT FROM NEW.causal_frontier)
1709
+ OR (NEW.modality IN ('document', 'presentation')
1710
+ AND checkpoint.native_revision IS NULL)
1711
+ THEN
1712
+ RAISE EXCEPTION 'artifact head lacks an exact durable checkpoint'
1713
+ USING ERRCODE = '23514';
1714
+ END IF;
1715
+ IF TG_OP = 'INSERT' THEN
1716
+ IF NEW.head_sequence <> 0 OR NEW.current_snapshot_id IS NULL THEN
1717
+ RAISE EXCEPTION 'editable artifact must begin at its genesis snapshot'
1718
+ USING ERRCODE = '23514';
1719
+ END IF;
1720
+ IF NOT EXISTS (
1721
+ SELECT 1 FROM editable_artifact_idempotency_receipts receipt
1722
+ WHERE receipt.account_id = NEW.account_id
1723
+ AND receipt.workspace_id = NEW.workspace_id
1724
+ AND receipt.artifact_id = NEW.id
1725
+ AND receipt.operation_kind IN ('create', 'import')
1726
+ AND receipt.resource_type = 'artifact'
1727
+ AND receipt.resource_id = NEW.id
1728
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1729
+ receipt.result, ARRAY['artifactId', 'genesisSnapshotId', 'schemaVersion']
1730
+ )
1731
+ AND jsonb_typeof(receipt.result->'schemaVersion') = 'number'
1732
+ AND receipt.result->>'schemaVersion' = '1'
1733
+ AND jsonb_typeof(receipt.result->'artifactId') = 'string'
1734
+ AND receipt.result->>'artifactId' = NEW.id
1735
+ AND jsonb_typeof(receipt.result->'genesisSnapshotId') = 'string'
1736
+ AND receipt.result->>'genesisSnapshotId' = NEW.current_snapshot_id
1737
+ ) THEN
1738
+ RAISE EXCEPTION 'editable artifact exact genesis receipt is missing'
1739
+ USING ERRCODE = '23514';
1740
+ END IF;
1741
+ END IF;
1742
+ IF NEW.current_snapshot_id IS NOT NULL AND NOT EXISTS (
1743
+ SELECT 1 FROM editable_artifact_snapshots current_snapshot
1744
+ WHERE current_snapshot.account_id = NEW.account_id
1745
+ AND current_snapshot.workspace_id = NEW.workspace_id
1746
+ AND current_snapshot.artifact_id = NEW.id
1747
+ AND current_snapshot.id = NEW.current_snapshot_id
1748
+ AND current_snapshot.modality = NEW.modality
1749
+ AND current_snapshot.covered_head_sequence <= NEW.head_sequence
1750
+ ) THEN
1751
+ RAISE EXCEPTION 'artifact current snapshot is ahead of or outside its head'
1752
+ USING ERRCODE = '23514';
1753
+ END IF;
1754
+ ELSIF TG_TABLE_NAME = 'editable_artifact_snapshots' THEN
1755
+ SELECT * INTO checkpoint FROM editable_artifact_sequence_checkpoints candidate
1756
+ WHERE candidate.account_id = NEW.account_id
1757
+ AND candidate.workspace_id = NEW.workspace_id
1758
+ AND candidate.artifact_id = NEW.artifact_id
1759
+ AND candidate.head_sequence = NEW.covered_head_sequence;
1760
+ SELECT * INTO blob FROM editable_artifact_blob_refs candidate
1761
+ WHERE candidate.account_id = NEW.account_id
1762
+ AND candidate.workspace_id = NEW.workspace_id
1763
+ AND candidate.artifact_id = NEW.artifact_id
1764
+ AND candidate.id = NEW.blob_ref_id;
1765
+ IF checkpoint.head_sequence IS NULL
1766
+ OR checkpoint.modality <> NEW.modality
1767
+ OR (NEW.modality = 'spreadsheet'
1768
+ AND checkpoint.causal_frontier IS DISTINCT FROM NEW.covered_causal_frontier)
1769
+ OR (NEW.modality IN ('document', 'presentation')
1770
+ AND checkpoint.native_revision IS DISTINCT FROM NEW.native_revision)
1771
+ OR checkpoint.state_hash <> NEW.state_hash
1772
+ OR blob.id IS NULL OR blob.kind <> 'snapshot'
1773
+ OR blob.byte_size <> NEW.byte_size OR blob.content_hash <> NEW.content_hash
1774
+ OR blob.mime_type <> NEW.mime_type
1775
+ THEN
1776
+ RAISE EXCEPTION 'snapshot does not exactly match its checkpoint and blob facts'
1777
+ USING ERRCODE = '23514';
1778
+ END IF;
1779
+ IF NOT EXISTS (
1780
+ SELECT 1 FROM editable_artifact_live_outbox outbox
1781
+ WHERE outbox.account_id = NEW.account_id
1782
+ AND outbox.workspace_id = NEW.workspace_id
1783
+ AND outbox.artifact_id = NEW.artifact_id
1784
+ AND outbox.event_kind = 'snapshot_published'
1785
+ AND outbox.snapshot_id = NEW.id
1786
+ ) THEN
1787
+ RAISE EXCEPTION 'snapshot published outbox event is missing'
1788
+ USING ERRCODE = '23514';
1789
+ END IF;
1790
+ IF NOT EXISTS (
1791
+ SELECT 1 FROM editable_artifacts artifact
1792
+ WHERE artifact.account_id = NEW.account_id
1793
+ AND artifact.workspace_id = NEW.workspace_id
1794
+ AND artifact.id = NEW.artifact_id
1795
+ AND artifact.modality = NEW.modality
1796
+ AND artifact.current_snapshot_id = NEW.id
1797
+ ) THEN
1798
+ RAISE EXCEPTION 'published snapshot is not the artifact current snapshot'
1799
+ USING ERRCODE = '23514';
1800
+ END IF;
1801
+ ELSIF TG_TABLE_NAME = 'editable_artifact_versions' THEN
1802
+ SELECT artifact.modality INTO artifact_modality
1803
+ FROM editable_artifacts artifact
1804
+ WHERE artifact.account_id = NEW.account_id
1805
+ AND artifact.workspace_id = NEW.workspace_id
1806
+ AND artifact.id = NEW.artifact_id;
1807
+ SELECT * INTO checkpoint FROM editable_artifact_sequence_checkpoints candidate
1808
+ WHERE candidate.account_id = NEW.account_id
1809
+ AND candidate.workspace_id = NEW.workspace_id
1810
+ AND candidate.artifact_id = NEW.artifact_id
1811
+ AND candidate.head_sequence = NEW.head_sequence;
1812
+ IF artifact_modality <> 'spreadsheet'
1813
+ OR checkpoint.head_sequence IS NULL
1814
+ OR checkpoint.causal_frontier <> NEW.causal_frontier
1815
+ OR checkpoint.state_hash <> NEW.state_hash
1816
+ OR (NEW.snapshot_id IS NOT NULL AND NOT EXISTS (
1817
+ SELECT 1 FROM editable_artifact_snapshots version_snapshot
1818
+ WHERE version_snapshot.account_id = NEW.account_id
1819
+ AND version_snapshot.workspace_id = NEW.workspace_id
1820
+ AND version_snapshot.artifact_id = NEW.artifact_id
1821
+ AND version_snapshot.id = NEW.snapshot_id
1822
+ AND version_snapshot.covered_head_sequence <= NEW.head_sequence
1823
+ AND opengeni_private.editable_artifact_frontier_dominates(
1824
+ NEW.causal_frontier, version_snapshot.covered_causal_frontier
1825
+ )
1826
+ AND (version_snapshot.covered_head_sequence < NEW.head_sequence OR (
1827
+ version_snapshot.covered_causal_frontier = NEW.causal_frontier
1828
+ AND version_snapshot.state_hash = NEW.state_hash
1829
+ ))
1830
+ ))
1831
+ THEN
1832
+ RAISE EXCEPTION 'artifact version does not exactly identify a durable checkpoint'
1833
+ USING ERRCODE = '23514';
1834
+ END IF;
1835
+ ELSIF TG_TABLE_NAME = 'editable_artifact_materialization_results' THEN
1836
+ SELECT * INTO blob FROM editable_artifact_blob_refs candidate
1837
+ WHERE candidate.account_id = NEW.account_id
1838
+ AND candidate.workspace_id = NEW.workspace_id
1839
+ AND candidate.artifact_id = NEW.artifact_id
1840
+ AND candidate.id = NEW.blob_ref_id;
1841
+ SELECT * INTO job FROM editable_artifact_materialization_jobs candidate
1842
+ WHERE candidate.account_id = NEW.account_id
1843
+ AND candidate.workspace_id = NEW.workspace_id
1844
+ AND candidate.artifact_id = NEW.artifact_id
1845
+ AND candidate.id = NEW.job_id;
1846
+ IF blob.id IS NULL OR blob.kind <> 'materialization'
1847
+ OR blob.byte_size <> NEW.byte_size OR blob.content_hash <> NEW.content_hash
1848
+ OR blob.mime_type <> NEW.mime_type
1849
+ OR job.id IS NULL OR job.state <> 'succeeded'
1850
+ OR NEW.mime_type <> (CASE job.format
1851
+ WHEN 'xlsx' THEN 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
1852
+ WHEN 'pptx' THEN 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
1853
+ WHEN 'docx' THEN 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
1854
+ WHEN 'pdf' THEN 'application/pdf'
1855
+ WHEN 'png' THEN 'image/png'
1856
+ WHEN 'webp' THEN 'image/webp'
1857
+ END)
1858
+ THEN
1859
+ RAISE EXCEPTION 'materialization result does not match a succeeded job and blob facts'
1860
+ USING ERRCODE = '23514';
1861
+ END IF;
1862
+ ELSIF TG_TABLE_NAME = 'editable_artifact_materialization_jobs' THEN
1863
+ SELECT * INTO checkpoint FROM editable_artifact_sequence_checkpoints candidate
1864
+ WHERE candidate.account_id = NEW.account_id
1865
+ AND candidate.workspace_id = NEW.workspace_id
1866
+ AND candidate.artifact_id = NEW.artifact_id
1867
+ AND candidate.head_sequence = NEW.target_head_sequence;
1868
+ SELECT * INTO snapshot FROM editable_artifact_snapshots candidate
1869
+ WHERE candidate.account_id = NEW.account_id
1870
+ AND candidate.workspace_id = NEW.workspace_id
1871
+ AND candidate.artifact_id = NEW.artifact_id
1872
+ AND candidate.id = NEW.input_snapshot_id;
1873
+ IF snapshot.id IS NOT NULL THEN
1874
+ SELECT * INTO blob FROM editable_artifact_blob_refs candidate
1875
+ WHERE candidate.account_id = snapshot.account_id
1876
+ AND candidate.workspace_id = snapshot.workspace_id
1877
+ AND candidate.artifact_id = snapshot.artifact_id
1878
+ AND candidate.id = snapshot.blob_ref_id;
1879
+ END IF;
1880
+ SELECT artifact.modality INTO artifact_modality
1881
+ FROM editable_artifacts artifact
1882
+ WHERE artifact.account_id = NEW.account_id
1883
+ AND artifact.workspace_id = NEW.workspace_id
1884
+ AND artifact.id = NEW.artifact_id;
1885
+ IF checkpoint.head_sequence IS NULL OR checkpoint.state_hash <> NEW.state_hash
1886
+ OR snapshot.id IS NULL
1887
+ OR snapshot.covered_head_sequence <> NEW.target_head_sequence
1888
+ OR snapshot.state_hash <> NEW.state_hash
1889
+ OR blob.id IS NULL OR blob.kind <> 'snapshot'
1890
+ OR blob.id <> snapshot.blob_ref_id
1891
+ OR blob.byte_size <> snapshot.byte_size
1892
+ OR blob.content_hash <> snapshot.content_hash
1893
+ OR blob.mime_type <> snapshot.mime_type
1894
+ OR artifact_modality IS NULL
1895
+ OR NOT ((artifact_modality = 'spreadsheet' AND NEW.format IN ('xlsx', 'pdf', 'png', 'webp'))
1896
+ OR (artifact_modality = 'presentation' AND NEW.format IN ('pptx', 'pdf', 'png', 'webp'))
1897
+ OR (artifact_modality = 'document' AND NEW.format IN ('docx', 'pdf', 'png', 'webp')))
1898
+ OR (NEW.version_id IS NOT NULL AND NOT EXISTS (
1899
+ SELECT 1 FROM editable_artifact_versions version
1900
+ WHERE version.account_id = NEW.account_id
1901
+ AND version.workspace_id = NEW.workspace_id
1902
+ AND version.artifact_id = NEW.artifact_id
1903
+ AND version.id = NEW.version_id
1904
+ AND version.head_sequence = NEW.target_head_sequence
1905
+ AND version.state_hash = NEW.state_hash
1906
+ ))
1907
+ THEN
1908
+ RAISE EXCEPTION 'materialization job manifest differs from its pinned source/checkpoint/version'
1909
+ USING ERRCODE = '23514';
1910
+ END IF;
1911
+ IF NOT EXISTS (
1912
+ SELECT 1 FROM editable_artifact_idempotency_receipts receipt
1913
+ WHERE receipt.account_id = NEW.account_id
1914
+ AND receipt.workspace_id = NEW.workspace_id
1915
+ AND receipt.artifact_id = NEW.artifact_id
1916
+ AND receipt.operation_kind = 'materialize'
1917
+ AND receipt.resource_type = 'materialization_job'
1918
+ AND receipt.resource_id = NEW.id
1919
+ AND receipt.server_transaction_id IS NULL
1920
+ ) THEN
1921
+ RAISE EXCEPTION 'materialization job idempotency receipt is missing'
1922
+ USING ERRCODE = '23514';
1923
+ END IF;
1924
+ IF NEW.state = 'succeeded' AND NOT EXISTS (
1925
+ SELECT 1 FROM editable_artifact_materialization_results result
1926
+ WHERE result.account_id = NEW.account_id
1927
+ AND result.workspace_id = NEW.workspace_id
1928
+ AND result.artifact_id = NEW.artifact_id
1929
+ AND result.job_id = NEW.id
1930
+ ) THEN
1931
+ RAISE EXCEPTION 'succeeded materialization job lacks its immutable result'
1932
+ USING ERRCODE = '23514';
1933
+ END IF;
1934
+ ELSIF TG_TABLE_NAME = 'editable_artifact_replica_leases' THEN
1935
+ SELECT artifact.modality INTO artifact_modality
1936
+ FROM editable_artifacts artifact
1937
+ WHERE artifact.account_id = NEW.account_id
1938
+ AND artifact.workspace_id = NEW.workspace_id
1939
+ AND artifact.id = NEW.artifact_id;
1940
+ SELECT * INTO checkpoint FROM editable_artifact_sequence_checkpoints candidate
1941
+ WHERE candidate.account_id = NEW.account_id
1942
+ AND candidate.workspace_id = NEW.workspace_id
1943
+ AND candidate.artifact_id = NEW.artifact_id
1944
+ AND candidate.head_sequence = NEW.applied_head_sequence;
1945
+ IF artifact_modality IS NULL OR NEW.modality <> artifact_modality
1946
+ OR checkpoint.head_sequence IS NULL OR checkpoint.modality <> NEW.modality
1947
+ OR (NEW.modality = 'spreadsheet'
1948
+ AND checkpoint.causal_frontier IS DISTINCT FROM NEW.causal_frontier)
1949
+ OR (NEW.modality IN ('document', 'presentation')
1950
+ AND checkpoint.native_revision IS DISTINCT FROM NEW.native_revision)
1951
+ THEN
1952
+ RAISE EXCEPTION 'replica lease cursor differs from its durable checkpoint'
1953
+ USING ERRCODE = '23514';
1954
+ END IF;
1955
+ ELSIF TG_TABLE_NAME = 'editable_artifact_idempotency_receipts' THEN
1956
+ IF (NEW.resource_type = 'artifact' AND NOT EXISTS (
1957
+ SELECT 1 FROM editable_artifacts artifact
1958
+ WHERE artifact.account_id = NEW.account_id
1959
+ AND artifact.workspace_id = NEW.workspace_id
1960
+ AND artifact.id = NEW.artifact_id
1961
+ AND artifact.id = NEW.resource_id
1962
+ )) OR (NEW.resource_type = 'snapshot' AND NOT EXISTS (
1963
+ SELECT 1 FROM editable_artifact_snapshots receipt_snapshot
1964
+ WHERE receipt_snapshot.account_id = NEW.account_id
1965
+ AND receipt_snapshot.workspace_id = NEW.workspace_id
1966
+ AND receipt_snapshot.artifact_id = NEW.artifact_id
1967
+ AND receipt_snapshot.id = NEW.resource_id
1968
+ )) OR (NEW.resource_type = 'materialization_job' AND NOT EXISTS (
1969
+ SELECT 1 FROM editable_artifact_materialization_jobs receipt_job
1970
+ WHERE receipt_job.account_id = NEW.account_id
1971
+ AND receipt_job.workspace_id = NEW.workspace_id
1972
+ AND receipt_job.artifact_id = NEW.artifact_id
1973
+ AND receipt_job.id = NEW.resource_id
1974
+ ))
1975
+ THEN
1976
+ RAISE EXCEPTION 'idempotency receipt resource does not exist in its exact scope'
1977
+ USING ERRCODE = '23514';
1978
+ END IF;
1979
+ IF NEW.operation_kind IN ('create', 'import') AND NOT EXISTS (
1980
+ SELECT 1 FROM editable_artifacts artifact
1981
+ WHERE artifact.account_id = NEW.account_id
1982
+ AND artifact.workspace_id = NEW.workspace_id
1983
+ AND artifact.id = NEW.artifact_id
1984
+ AND artifact.head_sequence = 0
1985
+ AND artifact.current_snapshot_id IS NOT NULL
1986
+ AND opengeni_private.editable_artifact_object_has_exact_keys(
1987
+ NEW.result, ARRAY['artifactId', 'genesisSnapshotId', 'schemaVersion']
1988
+ )
1989
+ AND jsonb_typeof(NEW.result->'schemaVersion') = 'number'
1990
+ AND NEW.result->>'schemaVersion' = '1'
1991
+ AND jsonb_typeof(NEW.result->'artifactId') = 'string'
1992
+ AND NEW.result->>'artifactId' = artifact.id
1993
+ AND jsonb_typeof(NEW.result->'genesisSnapshotId') = 'string'
1994
+ AND NEW.result->>'genesisSnapshotId' = artifact.current_snapshot_id
1995
+ ) THEN
1996
+ RAISE EXCEPTION 'creation/import receipt differs from exact artifact genesis'
1997
+ USING ERRCODE = '23514';
1998
+ END IF;
1999
+ ELSIF TG_TABLE_NAME = 'editable_artifact_live_outbox' THEN
2000
+ IF NEW.event_kind = 'transaction_committed' THEN
2001
+ SELECT * INTO tx FROM editable_artifact_transactions candidate
2002
+ WHERE candidate.account_id = NEW.account_id
2003
+ AND candidate.workspace_id = NEW.workspace_id
2004
+ AND candidate.artifact_id = NEW.artifact_id
2005
+ AND candidate.id = NEW.transaction_id;
2006
+ IF tx.id IS NULL
2007
+ OR NEW.event->>'modality' <> tx.modality
2008
+ OR NEW.event->>'sequenceStart' <> tx.sequence_start::text
2009
+ OR NEW.event->>'sequenceEnd' <> tx.sequence_end::text
2010
+ OR NEW.event->>'stateHash' <> tx.state_hash
2011
+ OR (tx.modality = 'spreadsheet'
2012
+ AND NEW.event->>'operationProtocolVersion' <> tx.operation_protocol_version::text)
2013
+ OR (tx.modality IN ('document', 'presentation')
2014
+ AND NEW.event->>'commitProtocolVersion' <> tx.commit_protocol_version::text)
2015
+ OR (NEW.event->>'committedAt')::timestamptz <> tx.committed_at
2016
+ OR NEW.created_at <> tx.committed_at
2017
+ THEN
2018
+ RAISE EXCEPTION 'transaction outbox event differs from durable transaction'
2019
+ USING ERRCODE = '23514';
2020
+ END IF;
2021
+ ELSE
2022
+ SELECT * INTO snapshot FROM editable_artifact_snapshots candidate
2023
+ WHERE candidate.account_id = NEW.account_id
2024
+ AND candidate.workspace_id = NEW.workspace_id
2025
+ AND candidate.artifact_id = NEW.artifact_id
2026
+ AND candidate.id = NEW.snapshot_id;
2027
+ IF snapshot.id IS NULL
2028
+ OR NEW.event->>'modality' <> snapshot.modality
2029
+ OR NEW.event->>'coveredHeadSequence' <> snapshot.covered_head_sequence::text
2030
+ OR NEW.event->>'stateHash' <> snapshot.state_hash
2031
+ OR (snapshot.modality = 'spreadsheet'
2032
+ AND NEW.event->>'operationProtocolVersion'
2033
+ <> snapshot.operation_protocol_version::text)
2034
+ OR (NEW.event->>'publishedAt')::timestamptz <> snapshot.published_at
2035
+ OR NEW.created_at <> snapshot.published_at
2036
+ THEN
2037
+ RAISE EXCEPTION 'snapshot outbox event differs from durable snapshot'
2038
+ USING ERRCODE = '23514';
2039
+ END IF;
2040
+ END IF;
2041
+ END IF;
2042
+ RETURN NULL;
2043
+ END;
2044
+ $body$;
2045
+
2046
+ CREATE CONSTRAINT TRIGGER editable_artifacts_projection_guard
2047
+ AFTER INSERT OR UPDATE ON "editable_artifacts"
2048
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2049
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2050
+ CREATE CONSTRAINT TRIGGER editable_artifact_snapshots_projection_guard
2051
+ AFTER INSERT ON "editable_artifact_snapshots"
2052
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2053
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2054
+ CREATE CONSTRAINT TRIGGER editable_artifact_versions_projection_guard
2055
+ AFTER INSERT ON "editable_artifact_versions"
2056
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2057
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2058
+ CREATE CONSTRAINT TRIGGER editable_artifact_materialization_results_projection_guard
2059
+ AFTER INSERT ON "editable_artifact_materialization_results"
2060
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2061
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2062
+ CREATE CONSTRAINT TRIGGER editable_artifact_materialization_jobs_projection_guard
2063
+ AFTER INSERT OR UPDATE ON "editable_artifact_materialization_jobs"
2064
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2065
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2066
+ CREATE CONSTRAINT TRIGGER editable_artifact_replica_leases_projection_guard
2067
+ AFTER INSERT OR UPDATE ON "editable_artifact_replica_leases"
2068
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2069
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2070
+ CREATE CONSTRAINT TRIGGER editable_artifact_idempotency_receipts_projection_guard
2071
+ AFTER INSERT ON "editable_artifact_idempotency_receipts"
2072
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2073
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2074
+ CREATE CONSTRAINT TRIGGER editable_artifact_live_outbox_projection_guard
2075
+ AFTER INSERT ON "editable_artifact_live_outbox"
2076
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
2077
+ EXECUTE FUNCTION opengeni_private.validate_editable_artifact_projection();
2078
+
2079
+ CREATE OR REPLACE FUNCTION opengeni_private.guard_editable_artifact_update()
2080
+ RETURNS trigger
2081
+ LANGUAGE plpgsql
2082
+ SET search_path = pg_catalog, pg_temp
2083
+ AS $body$
2084
+ DECLARE old_snapshot_coverage bigint;
2085
+ DECLARE new_snapshot_coverage bigint;
2086
+ DECLARE table_owner name;
2087
+ BEGIN
2088
+ PERFORM set_config(
2089
+ 'search_path', 'pg_catalog,' || quote_ident(TG_TABLE_SCHEMA) || ',pg_temp', true
2090
+ );
2091
+ IF NEW.account_id <> OLD.account_id OR NEW.workspace_id <> OLD.workspace_id
2092
+ OR NEW.id <> OLD.id OR NEW.modality <> OLD.modality
2093
+ OR NEW.created_by_subject_id <> OLD.created_by_subject_id
2094
+ OR NEW.created_at <> OLD.created_at
2095
+ OR NEW.head_sequence < OLD.head_sequence
2096
+ OR NEW.updated_at < OLD.updated_at
2097
+ OR (NEW.head_sequence = OLD.head_sequence AND (
2098
+ NEW.causal_frontier IS DISTINCT FROM OLD.causal_frontier
2099
+ OR NEW.state_hash <> OLD.state_hash
2100
+ ))
2101
+ THEN
2102
+ RAISE EXCEPTION 'invalid editable artifact head or identity mutation'
2103
+ USING ERRCODE = '23514';
2104
+ END IF;
2105
+ IF NEW.authorization_revision < OLD.authorization_revision THEN
2106
+ RAISE EXCEPTION 'editable artifact authorization revision cannot regress'
2107
+ USING ERRCODE = '23514';
2108
+ END IF;
2109
+ IF NEW.authorization_revision > OLD.authorization_revision THEN
2110
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner) INTO table_owner
2111
+ FROM pg_catalog.pg_class relation
2112
+ WHERE relation.oid = TG_RELID;
2113
+ IF table_owner IS NULL OR current_user <> table_owner THEN
2114
+ RAISE EXCEPTION 'editable artifact authorization revision requires its owner capability'
2115
+ USING ERRCODE = '42501';
2116
+ END IF;
2117
+ IF NEW.title <> OLD.title
2118
+ OR NEW.lifecycle_state <> OLD.lifecycle_state
2119
+ OR NEW.head_sequence <> OLD.head_sequence
2120
+ OR NEW.causal_frontier IS DISTINCT FROM OLD.causal_frontier
2121
+ OR NEW.state_hash <> OLD.state_hash
2122
+ OR NEW.current_snapshot_id IS DISTINCT FROM OLD.current_snapshot_id
2123
+ THEN
2124
+ RAISE EXCEPTION 'editable artifact authorization revision must advance independently'
2125
+ USING ERRCODE = '23514';
2126
+ END IF;
2127
+ END IF;
2128
+ IF OLD.lifecycle_state = 'archived' AND NEW.lifecycle_state <> 'archived' THEN
2129
+ RAISE EXCEPTION 'archived editable artifact cannot be reactivated implicitly'
2130
+ USING ERRCODE = '23514';
2131
+ END IF;
2132
+ IF OLD.lifecycle_state = 'archived' AND (
2133
+ NEW.head_sequence <> OLD.head_sequence
2134
+ OR NEW.causal_frontier IS DISTINCT FROM OLD.causal_frontier
2135
+ OR NEW.state_hash <> OLD.state_hash
2136
+ ) THEN
2137
+ RAISE EXCEPTION 'archived editable artifact authority cannot advance'
2138
+ USING ERRCODE = '23514';
2139
+ END IF;
2140
+ IF NEW.current_snapshot_id IS DISTINCT FROM OLD.current_snapshot_id THEN
2141
+ IF NEW.current_snapshot_id IS NULL THEN
2142
+ RAISE EXCEPTION 'editable artifact current snapshot cannot be cleared'
2143
+ USING ERRCODE = '23514';
2144
+ END IF;
2145
+ SELECT snapshot.covered_head_sequence INTO new_snapshot_coverage
2146
+ FROM editable_artifact_snapshots snapshot
2147
+ WHERE snapshot.account_id = NEW.account_id
2148
+ AND snapshot.workspace_id = NEW.workspace_id
2149
+ AND snapshot.artifact_id = NEW.id
2150
+ AND snapshot.id = NEW.current_snapshot_id;
2151
+ IF NOT FOUND THEN
2152
+ RAISE EXCEPTION 'editable artifact current snapshot is missing'
2153
+ USING ERRCODE = '23503';
2154
+ END IF;
2155
+ IF OLD.current_snapshot_id IS NOT NULL THEN
2156
+ SELECT snapshot.covered_head_sequence INTO old_snapshot_coverage
2157
+ FROM editable_artifact_snapshots snapshot
2158
+ WHERE snapshot.account_id = OLD.account_id
2159
+ AND snapshot.workspace_id = OLD.workspace_id
2160
+ AND snapshot.artifact_id = OLD.id
2161
+ AND snapshot.id = OLD.current_snapshot_id;
2162
+ IF NOT FOUND OR new_snapshot_coverage <= old_snapshot_coverage THEN
2163
+ RAISE EXCEPTION 'editable artifact current snapshot coverage must advance'
2164
+ USING ERRCODE = '23514';
2165
+ END IF;
2166
+ END IF;
2167
+ END IF;
2168
+ RETURN NEW;
2169
+ END;
2170
+ $body$;
2171
+
2172
+ CREATE TRIGGER editable_artifacts_update_guard
2173
+ BEFORE UPDATE ON "editable_artifacts"
2174
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_update();
2175
+
2176
+ CREATE OR REPLACE FUNCTION opengeni_private.guard_editable_artifact_mutable_state()
2177
+ RETURNS trigger
2178
+ LANGUAGE plpgsql
2179
+ SET search_path = pg_catalog, pg_temp
2180
+ AS $body$
2181
+ BEGIN
2182
+ PERFORM set_config(
2183
+ 'search_path', 'pg_catalog,' || quote_ident(TG_TABLE_SCHEMA) || ',pg_temp', true
2184
+ );
2185
+ IF TG_TABLE_NAME = 'editable_artifact_materialization_jobs' THEN
2186
+ IF TG_OP = 'INSERT' THEN
2187
+ IF NEW.state <> 'pending' OR NEW.attempt_count <> 0
2188
+ OR NEW.lease_owner IS NOT NULL OR NEW.lease_expires_at IS NOT NULL
2189
+ OR NEW.settled_by_owner IS NOT NULL OR NEW.error_code IS NOT NULL
2190
+ OR NEW.started_at IS NOT NULL
2191
+ OR NEW.completed_at IS NOT NULL
2192
+ THEN
2193
+ RAISE EXCEPTION 'materialization jobs must begin pending and unleased'
2194
+ USING ERRCODE = '23514';
2195
+ END IF;
2196
+ RETURN NEW;
2197
+ END IF;
2198
+ IF NEW.account_id <> OLD.account_id OR NEW.workspace_id <> OLD.workspace_id
2199
+ OR NEW.artifact_id <> OLD.artifact_id OR NEW.id <> OLD.id
2200
+ OR NEW.version_id IS DISTINCT FROM OLD.version_id
2201
+ OR NEW.input_snapshot_id <> OLD.input_snapshot_id
2202
+ OR NEW.target_head_sequence <> OLD.target_head_sequence
2203
+ OR NEW.state_hash <> OLD.state_hash OR NEW.format <> OLD.format
2204
+ OR NEW.normalized_options <> OLD.normalized_options
2205
+ OR NEW.options_hash <> OLD.options_hash OR NEW.codec_id <> OLD.codec_id
2206
+ OR NEW.codec_version <> OLD.codec_version
2207
+ OR NEW.kernel_version <> OLD.kernel_version
2208
+ OR NEW.font_registry_hash <> OLD.font_registry_hash OR NEW.policy_hash <> OLD.policy_hash
2209
+ OR NEW.created_at <> OLD.created_at
2210
+ THEN
2211
+ RAISE EXCEPTION 'invalid materialization job mutation' USING ERRCODE = '23514';
2212
+ END IF;
2213
+ IF OLD.state IN ('succeeded', 'failed') THEN
2214
+ RAISE EXCEPTION 'terminal materialization job is immutable' USING ERRCODE = '23514';
2215
+ ELSIF OLD.state = 'pending' THEN
2216
+ IF NEW.state <> 'running' OR NEW.attempt_count <> OLD.attempt_count + 1
2217
+ OR NEW.settled_by_owner IS NOT NULL
2218
+ THEN
2219
+ RAISE EXCEPTION 'pending materialization job must be claimed exactly once'
2220
+ USING ERRCODE = '23514';
2221
+ END IF;
2222
+ ELSIF OLD.state = 'running' AND NEW.state = 'running' THEN
2223
+ IF NOT (
2224
+ -- Heartbeats extend one still-live claim without changing its fencing
2225
+ -- token. They cannot resurrect an expired lease or move ownership.
2226
+ (NEW.attempt_count = OLD.attempt_count
2227
+ AND NEW.lease_owner IS NOT DISTINCT FROM OLD.lease_owner
2228
+ AND NEW.started_at IS NOT DISTINCT FROM OLD.started_at
2229
+ AND OLD.lease_expires_at > pg_catalog.statement_timestamp()
2230
+ AND NEW.lease_expires_at > OLD.lease_expires_at
2231
+ AND NEW.settled_by_owner IS NULL)
2232
+ OR
2233
+ -- Reclaim is a distinct transition: only an expired claim may advance
2234
+ -- the attempt-count fence and reset its owner/start/expiration.
2235
+ (NEW.attempt_count = OLD.attempt_count + 1
2236
+ AND OLD.lease_expires_at <= pg_catalog.statement_timestamp()
2237
+ AND NEW.started_at >= OLD.started_at
2238
+ AND NEW.settled_by_owner IS NULL)
2239
+ )
2240
+ THEN
2241
+ RAISE EXCEPTION 'invalid materialization renewal or reclaim transition'
2242
+ USING ERRCODE = '23514';
2243
+ END IF;
2244
+ ELSIF OLD.state = 'running' AND NEW.state IN ('succeeded', 'failed') THEN
2245
+ IF NOT (
2246
+ (NEW.attempt_count = OLD.attempt_count
2247
+ AND NEW.started_at IS NOT DISTINCT FROM OLD.started_at
2248
+ AND NEW.settled_by_owner IS NOT DISTINCT FROM OLD.lease_owner
2249
+ AND OLD.lease_expires_at > pg_catalog.statement_timestamp())
2250
+ OR
2251
+ (NEW.state = 'failed' AND NEW.error_code = 'attempts_exhausted'
2252
+ AND NEW.attempt_count = 1000
2253
+ AND NEW.started_at IS NOT DISTINCT FROM OLD.started_at
2254
+ AND NEW.settled_by_owner IS NOT DISTINCT FROM OLD.lease_owner
2255
+ AND OLD.lease_expires_at <= pg_catalog.statement_timestamp())
2256
+ ) THEN
2257
+ RAISE EXCEPTION 'materialization settlement attempt is inconsistent'
2258
+ USING ERRCODE = '23514';
2259
+ END IF;
2260
+ ELSE
2261
+ RAISE EXCEPTION 'invalid materialization job state transition' USING ERRCODE = '23514';
2262
+ END IF;
2263
+ ELSIF TG_TABLE_NAME = 'editable_artifact_live_outbox' THEN
2264
+ IF TG_OP = 'INSERT' THEN
2265
+ IF NEW.state <> 'pending' OR NEW.attempt_count <> 0
2266
+ OR NEW.lease_owner IS NOT NULL OR NEW.lease_expires_at IS NOT NULL
2267
+ OR NEW.next_attempt_at IS DISTINCT FROM NEW.created_at
2268
+ OR NEW.last_error_code IS NOT NULL OR NEW.published_at IS NOT NULL
2269
+ OR NEW.dead_lettered_at IS NOT NULL
2270
+ THEN
2271
+ RAISE EXCEPTION 'outbox events must begin pending and unleased'
2272
+ USING ERRCODE = '23514';
2273
+ END IF;
2274
+ RETURN NEW;
2275
+ END IF;
2276
+ IF NEW.account_id <> OLD.account_id OR NEW.workspace_id <> OLD.workspace_id
2277
+ OR NEW.artifact_id <> OLD.artifact_id OR NEW.id <> OLD.id
2278
+ OR NEW.transaction_id IS DISTINCT FROM OLD.transaction_id
2279
+ OR NEW.snapshot_id IS DISTINCT FROM OLD.snapshot_id
2280
+ OR NEW.event_kind <> OLD.event_kind OR NEW.event <> OLD.event
2281
+ OR NEW.created_at <> OLD.created_at OR NEW.attempt_count < OLD.attempt_count
2282
+ OR NEW.attempt_count > OLD.attempt_count + 1
2283
+ OR OLD.state IN ('published', 'dead_lettered')
2284
+ THEN
2285
+ RAISE EXCEPTION 'invalid editable artifact outbox mutation' USING ERRCODE = '23514';
2286
+ END IF;
2287
+ ELSIF TG_TABLE_NAME = 'editable_artifact_replica_leases' THEN
2288
+ IF NEW.account_id <> OLD.account_id OR NEW.workspace_id <> OLD.workspace_id
2289
+ OR NEW.artifact_id <> OLD.artifact_id OR NEW.replica_id <> OLD.replica_id
2290
+ OR NEW.modality <> OLD.modality
2291
+ OR NEW.actor_key <> OLD.actor_key OR NEW.created_at <> OLD.created_at
2292
+ OR NEW.applied_head_sequence < OLD.applied_head_sequence
2293
+ OR NEW.last_seen_at < OLD.last_seen_at
2294
+ OR (OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS DISTINCT FROM OLD.revoked_at)
2295
+ THEN
2296
+ RAISE EXCEPTION 'invalid editable artifact replica lease mutation'
2297
+ USING ERRCODE = '23514';
2298
+ END IF;
2299
+ END IF;
2300
+ RETURN NEW;
2301
+ END;
2302
+ $body$;
2303
+
2304
+ CREATE TRIGGER editable_artifact_materialization_jobs_update_guard
2305
+ BEFORE INSERT OR UPDATE ON "editable_artifact_materialization_jobs"
2306
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_mutable_state();
2307
+ CREATE TRIGGER editable_artifact_live_outbox_update_guard
2308
+ BEFORE INSERT OR UPDATE ON "editable_artifact_live_outbox"
2309
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_mutable_state();
2310
+ CREATE TRIGGER editable_artifact_replica_leases_update_guard
2311
+ BEFORE UPDATE ON "editable_artifact_replica_leases"
2312
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_editable_artifact_mutable_state();
2313
+
2314
+ DO $immutable_history$
2315
+ DECLARE table_name text;
2316
+ BEGIN
2317
+ FOREACH table_name IN ARRAY ARRAY[
2318
+ 'editable_artifact_transactions',
2319
+ 'editable_artifact_operations',
2320
+ 'editable_artifact_idempotency_receipts',
2321
+ 'editable_artifact_undo_claims',
2322
+ 'editable_artifact_sequence_checkpoints',
2323
+ 'editable_artifact_blob_refs',
2324
+ 'editable_artifact_snapshots',
2325
+ 'editable_artifact_versions',
2326
+ 'editable_artifact_materialization_results'
2327
+ ] LOOP
2328
+ EXECUTE format(
2329
+ 'CREATE TRIGGER %I BEFORE UPDATE OR DELETE ON %I '
2330
+ || 'FOR EACH ROW EXECUTE FUNCTION '
2331
+ || 'opengeni_private.reject_editable_artifact_history_mutation()',
2332
+ table_name || '_immutable_guard', table_name
2333
+ );
2334
+ END LOOP;
2335
+ END;
2336
+ $immutable_history$;
2337
+
2338
+ -- Validate the explicit/defaulted caller schema before privileged access. The
2339
+ -- public functions default this final argument from current_schema() at their
2340
+ -- call site, before their hardened SECURITY DEFINER search_path takes effect.
2341
+ CREATE OR REPLACE FUNCTION opengeni_private.editable_artifact_scope_matches_context(
2342
+ p_account_id uuid,
2343
+ p_workspace_id uuid
2344
+ ) RETURNS boolean
2345
+ LANGUAGE plpgsql
2346
+ STABLE
2347
+ SET search_path = pg_catalog, pg_temp
2348
+ AS $body$
2349
+ BEGIN
2350
+ IF p_account_id IS NULL OR p_workspace_id IS NULL THEN RETURN false; END IF;
2351
+ RETURN p_account_id = nullif(
2352
+ pg_catalog.current_setting('opengeni.account_id', true), ''
2353
+ )::uuid
2354
+ AND p_workspace_id = nullif(
2355
+ pg_catalog.current_setting('opengeni.workspace_id', true), ''
2356
+ )::uuid;
2357
+ EXCEPTION WHEN others THEN
2358
+ RETURN false;
2359
+ END;
2360
+ $body$;
2361
+
2362
+ CREATE OR REPLACE FUNCTION opengeni_private.resolve_editable_artifact_data_schema(
2363
+ requested_schema name
2364
+ )
2365
+ RETURNS name
2366
+ LANGUAGE plpgsql
2367
+ SECURITY DEFINER
2368
+ SET search_path = pg_catalog, pg_temp
2369
+ AS $body$
2370
+ DECLARE data_schema name;
2371
+ BEGIN
2372
+ IF requested_schema IS NULL THEN
2373
+ RAISE EXCEPTION 'editable artifact data schema is unavailable or untrusted'
2374
+ USING ERRCODE = '42501';
2375
+ END IF;
2376
+ SELECT namespace.nspname INTO data_schema
2377
+ FROM pg_catalog.pg_namespace namespace
2378
+ JOIN pg_catalog.pg_class relation
2379
+ ON relation.relnamespace = namespace.oid
2380
+ AND relation.relname = 'editable_artifact_live_outbox'
2381
+ AND relation.relkind IN ('r', 'p')
2382
+ JOIN pg_catalog.pg_class artifact_relation
2383
+ ON artifact_relation.relnamespace = namespace.oid
2384
+ AND artifact_relation.relname = 'editable_artifacts'
2385
+ AND artifact_relation.relkind IN ('r', 'p')
2386
+ AND artifact_relation.relowner = relation.relowner
2387
+ JOIN pg_catalog.pg_class scope_authority_relation
2388
+ ON scope_authority_relation.relnamespace = namespace.oid
2389
+ AND scope_authority_relation.relname = 'editable_artifact_scope_authorization_heads'
2390
+ AND scope_authority_relation.relkind IN ('r', 'p')
2391
+ AND scope_authority_relation.relowner = relation.relowner
2392
+ JOIN pg_catalog.pg_class job_relation
2393
+ ON job_relation.relnamespace = namespace.oid
2394
+ AND job_relation.relname = 'editable_artifact_materialization_jobs'
2395
+ AND job_relation.relkind IN ('r', 'p')
2396
+ AND job_relation.relowner = relation.relowner
2397
+ JOIN pg_catalog.pg_class result_relation
2398
+ ON result_relation.relnamespace = namespace.oid
2399
+ AND result_relation.relname = 'editable_artifact_materialization_results'
2400
+ AND result_relation.relkind IN ('r', 'p')
2401
+ AND result_relation.relowner = relation.relowner
2402
+ JOIN pg_catalog.pg_class blob_relation
2403
+ ON blob_relation.relnamespace = namespace.oid
2404
+ AND blob_relation.relname = 'editable_artifact_blob_refs'
2405
+ AND blob_relation.relkind IN ('r', 'p')
2406
+ AND blob_relation.relowner = relation.relowner
2407
+ JOIN pg_catalog.pg_roles owner_role
2408
+ ON owner_role.oid = relation.relowner
2409
+ AND owner_role.rolname = current_user
2410
+ WHERE namespace.nspname = requested_schema
2411
+ AND namespace.nspname !~ '^pg_'
2412
+ AND namespace.nspname <> 'information_schema'
2413
+ AND relation.relrowsecurity
2414
+ AND relation.relforcerowsecurity
2415
+ AND artifact_relation.relrowsecurity
2416
+ AND artifact_relation.relforcerowsecurity
2417
+ AND scope_authority_relation.relrowsecurity
2418
+ AND scope_authority_relation.relforcerowsecurity
2419
+ AND job_relation.relrowsecurity
2420
+ AND job_relation.relforcerowsecurity
2421
+ AND result_relation.relrowsecurity
2422
+ AND result_relation.relforcerowsecurity
2423
+ AND blob_relation.relrowsecurity
2424
+ AND blob_relation.relforcerowsecurity
2425
+ AND pg_catalog.has_schema_privilege(session_user, namespace.oid, 'USAGE');
2426
+ IF NOT FOUND THEN
2427
+ RAISE EXCEPTION 'editable artifact data schema is unavailable or untrusted'
2428
+ USING ERRCODE = '42501';
2429
+ END IF;
2430
+ RETURN data_schema;
2431
+ END;
2432
+ $body$;
2433
+
2434
+ CREATE OR REPLACE FUNCTION opengeni_private.ensure_editable_artifact_scope_authorization_head(
2435
+ p_account_id uuid,
2436
+ p_workspace_id uuid,
2437
+ p_data_schema name DEFAULT pg_catalog.current_schema()
2438
+ ) RETURNS bigint
2439
+ LANGUAGE plpgsql
2440
+ SECURITY DEFINER
2441
+ SET search_path = pg_catalog, pg_temp
2442
+ AS $body$
2443
+ DECLARE data_schema name;
2444
+ DECLARE current_revision bigint;
2445
+ BEGIN
2446
+ IF p_account_id IS NULL OR p_workspace_id IS NULL OR p_data_schema IS NULL
2447
+ OR NOT opengeni_private.editable_artifact_scope_matches_context(
2448
+ p_account_id, p_workspace_id
2449
+ )
2450
+ THEN
2451
+ RAISE EXCEPTION 'editable artifact scope authorization context is unavailable'
2452
+ USING ERRCODE = '42501';
2453
+ END IF;
2454
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2455
+ EXECUTE pg_catalog.format($query$
2456
+ INSERT INTO %I.editable_artifact_scope_authorization_heads (
2457
+ account_id, workspace_id, create_revision
2458
+ ) VALUES ($1, $2, 1)
2459
+ ON CONFLICT (account_id, workspace_id) DO NOTHING
2460
+ $query$, data_schema) USING p_account_id, p_workspace_id;
2461
+ EXECUTE pg_catalog.format($query$
2462
+ SELECT create_revision
2463
+ FROM %I.editable_artifact_scope_authorization_heads
2464
+ WHERE account_id = $1 AND workspace_id = $2
2465
+ FOR SHARE
2466
+ $query$, data_schema) INTO current_revision USING p_account_id, p_workspace_id;
2467
+ IF current_revision IS NULL THEN
2468
+ RAISE EXCEPTION 'editable artifact scope authorization head is unavailable'
2469
+ USING ERRCODE = '55000';
2470
+ END IF;
2471
+ RETURN current_revision;
2472
+ END;
2473
+ $body$;
2474
+
2475
+ CREATE OR REPLACE FUNCTION opengeni_private.advance_editable_artifact_scope_authorization_revision(
2476
+ p_account_id uuid,
2477
+ p_workspace_id uuid,
2478
+ p_expected_revision bigint,
2479
+ p_next_revision bigint,
2480
+ p_data_schema name DEFAULT pg_catalog.current_schema()
2481
+ ) RETURNS TABLE (
2482
+ applied boolean,
2483
+ authorization_revision bigint
2484
+ )
2485
+ LANGUAGE plpgsql
2486
+ SECURITY DEFINER
2487
+ SET search_path = pg_catalog, pg_temp
2488
+ AS $body$
2489
+ DECLARE data_schema name;
2490
+ DECLARE current_revision bigint;
2491
+ BEGIN
2492
+ IF p_account_id IS NULL OR p_workspace_id IS NULL OR p_data_schema IS NULL
2493
+ OR p_expected_revision IS NULL
2494
+ OR p_expected_revision NOT BETWEEN 1 AND 9007199254740991
2495
+ OR p_next_revision IS NULL
2496
+ OR p_next_revision NOT BETWEEN 1 AND 9007199254740991
2497
+ OR p_next_revision <= p_expected_revision
2498
+ OR NOT opengeni_private.editable_artifact_scope_matches_context(
2499
+ p_account_id, p_workspace_id
2500
+ )
2501
+ THEN
2502
+ RAISE EXCEPTION 'invalid editable artifact scope authorization revision bounds'
2503
+ USING ERRCODE = '22023';
2504
+ END IF;
2505
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2506
+ -- Ensure the default head exists before the compare-and-swap. Both statements
2507
+ -- remain in the caller transaction, so create's FOR SHARE and this UPDATE
2508
+ -- serialize revocation against genesis publication.
2509
+ PERFORM opengeni_private.ensure_editable_artifact_scope_authorization_head(
2510
+ p_account_id, p_workspace_id, data_schema
2511
+ );
2512
+ EXECUTE pg_catalog.format($query$
2513
+ UPDATE %I.editable_artifact_scope_authorization_heads target
2514
+ SET create_revision = $3,
2515
+ updated_at = GREATEST(target.updated_at, pg_catalog.statement_timestamp())
2516
+ WHERE target.account_id = $1
2517
+ AND target.workspace_id = $2
2518
+ AND target.create_revision = $4
2519
+ RETURNING target.create_revision
2520
+ $query$, data_schema)
2521
+ INTO current_revision
2522
+ USING p_account_id, p_workspace_id, p_next_revision, p_expected_revision;
2523
+ IF current_revision IS NOT NULL THEN
2524
+ RETURN QUERY SELECT true, current_revision;
2525
+ RETURN;
2526
+ END IF;
2527
+ EXECUTE pg_catalog.format($query$
2528
+ SELECT target.create_revision
2529
+ FROM %I.editable_artifact_scope_authorization_heads target
2530
+ WHERE target.account_id = $1 AND target.workspace_id = $2
2531
+ $query$, data_schema) INTO current_revision USING p_account_id, p_workspace_id;
2532
+ RETURN QUERY SELECT false, current_revision;
2533
+ END;
2534
+ $body$;
2535
+
2536
+ -- Authorization policy projection is a separate owner capability. Ordinary
2537
+ -- artifact-head UPDATEs retain their existing ACL but the row trigger rejects
2538
+ -- any authorization revision change unless this SECURITY DEFINER owner path is
2539
+ -- used. Exact expected-revision matching is the CAS fence consumed by edit and
2540
+ -- snapshot authorization checks.
2541
+ CREATE OR REPLACE FUNCTION opengeni_private.advance_editable_artifact_authorization_revision(
2542
+ p_account_id uuid,
2543
+ p_workspace_id uuid,
2544
+ p_artifact_id text,
2545
+ p_expected_revision bigint,
2546
+ p_next_revision bigint,
2547
+ p_data_schema name DEFAULT pg_catalog.current_schema()
2548
+ ) RETURNS TABLE (
2549
+ applied boolean,
2550
+ authorization_revision bigint
2551
+ )
2552
+ LANGUAGE plpgsql
2553
+ SECURITY DEFINER
2554
+ SET search_path = pg_catalog, pg_temp
2555
+ AS $body$
2556
+ DECLARE affected integer;
2557
+ DECLARE current_revision bigint;
2558
+ DECLARE data_schema name;
2559
+ BEGIN
2560
+ IF p_account_id IS NULL OR p_workspace_id IS NULL OR p_artifact_id IS NULL
2561
+ OR p_data_schema IS NULL
2562
+ OR p_artifact_id !~ '^[0-9a-f]{32}$' OR p_artifact_id ~ '^0+$'
2563
+ OR p_expected_revision IS NULL
2564
+ OR p_expected_revision NOT BETWEEN 0 AND 9007199254740991
2565
+ OR p_next_revision IS NULL
2566
+ OR p_next_revision NOT BETWEEN 1 AND 9007199254740991
2567
+ OR p_next_revision <= p_expected_revision
2568
+ THEN
2569
+ RAISE EXCEPTION 'invalid editable artifact authorization revision bounds'
2570
+ USING ERRCODE = '22023';
2571
+ END IF;
2572
+ IF NOT opengeni_private.editable_artifact_scope_matches_context(
2573
+ p_account_id, p_workspace_id
2574
+ ) THEN
2575
+ RAISE EXCEPTION 'editable artifact authorization scope is unavailable'
2576
+ USING ERRCODE = '42501';
2577
+ END IF;
2578
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2579
+ EXECUTE pg_catalog.format($query$
2580
+ UPDATE %I.editable_artifacts target
2581
+ SET authorization_revision = $4,
2582
+ updated_at = GREATEST(
2583
+ target.updated_at, pg_catalog.statement_timestamp()
2584
+ )
2585
+ WHERE target.account_id = $1
2586
+ AND target.workspace_id = $2
2587
+ AND target.id = $3
2588
+ AND target.authorization_revision = $5
2589
+ RETURNING target.authorization_revision
2590
+ $query$, data_schema)
2591
+ INTO current_revision
2592
+ USING p_account_id, p_workspace_id, p_artifact_id, p_next_revision, p_expected_revision;
2593
+ GET DIAGNOSTICS affected = ROW_COUNT;
2594
+ IF affected = 1 THEN
2595
+ RETURN QUERY SELECT true, current_revision;
2596
+ RETURN;
2597
+ END IF;
2598
+ EXECUTE pg_catalog.format($query$
2599
+ SELECT target.authorization_revision
2600
+ FROM %I.editable_artifacts target
2601
+ WHERE target.account_id = $1
2602
+ AND target.workspace_id = $2
2603
+ AND target.id = $3
2604
+ $query$, data_schema)
2605
+ INTO current_revision
2606
+ USING p_account_id, p_workspace_id, p_artifact_id;
2607
+ RETURN QUERY SELECT false, current_revision;
2608
+ END;
2609
+ $body$;
2610
+
2611
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_editable_artifact_live_outbox(
2612
+ p_owner text,
2613
+ p_lease_duration_ms integer,
2614
+ p_limit integer,
2615
+ p_data_schema name
2616
+ ) RETURNS TABLE (
2617
+ outbox_id text,
2618
+ account_id uuid,
2619
+ workspace_id uuid,
2620
+ artifact_id text,
2621
+ transaction_id text,
2622
+ snapshot_id text,
2623
+ event_kind text,
2624
+ event jsonb,
2625
+ state text,
2626
+ attempt_count integer,
2627
+ lease_owner text,
2628
+ lease_expires_at timestamptz,
2629
+ next_attempt_at timestamptz,
2630
+ last_error_code text,
2631
+ published_at timestamptz,
2632
+ dead_lettered_at timestamptz,
2633
+ created_at timestamptz
2634
+ )
2635
+ LANGUAGE plpgsql
2636
+ SECURITY DEFINER
2637
+ SET search_path = pg_catalog, pg_temp
2638
+ AS $body$
2639
+ DECLARE data_schema name;
2640
+ BEGIN
2641
+ IF p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2642
+ OR p_lease_duration_ms IS NULL
2643
+ OR p_lease_duration_ms NOT BETWEEN 1 AND 86400000
2644
+ OR p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 1000
2645
+ OR p_data_schema IS NULL
2646
+ THEN
2647
+ RAISE EXCEPTION 'invalid editable artifact outbox claim bounds'
2648
+ USING ERRCODE = '22023';
2649
+ END IF;
2650
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2651
+ RETURN QUERY EXECUTE pg_catalog.format($query$
2652
+ WITH exhausted_candidates AS MATERIALIZED (
2653
+ SELECT candidate.id
2654
+ FROM %1$I.editable_artifact_live_outbox candidate
2655
+ WHERE candidate.attempt_count >= 1000000
2656
+ AND ((candidate.state = 'pending'
2657
+ AND candidate.next_attempt_at <= pg_catalog.statement_timestamp())
2658
+ OR (candidate.state = 'publishing'
2659
+ AND candidate.lease_expires_at <= pg_catalog.statement_timestamp()))
2660
+ ORDER BY candidate.next_attempt_at, candidate.created_at, candidate.id
2661
+ FOR UPDATE SKIP LOCKED
2662
+ LIMIT $1
2663
+ ), exhausted AS (
2664
+ UPDATE %1$I.editable_artifact_live_outbox target
2665
+ SET state = 'dead_lettered', lease_owner = NULL, lease_expires_at = NULL,
2666
+ last_error_code = 'attempts_exhausted',
2667
+ dead_lettered_at = pg_catalog.statement_timestamp()
2668
+ FROM exhausted_candidates
2669
+ WHERE target.id = exhausted_candidates.id
2670
+ RETURNING target.id
2671
+ ), candidates AS (
2672
+ SELECT candidate.id
2673
+ FROM %1$I.editable_artifact_live_outbox candidate
2674
+ WHERE candidate.attempt_count < 1000000
2675
+ AND ((candidate.state = 'pending'
2676
+ AND candidate.next_attempt_at <= pg_catalog.statement_timestamp())
2677
+ OR (candidate.state = 'publishing'
2678
+ AND candidate.lease_expires_at <= pg_catalog.statement_timestamp()))
2679
+ ORDER BY candidate.next_attempt_at, candidate.created_at, candidate.id
2680
+ FOR UPDATE SKIP LOCKED
2681
+ LIMIT $1
2682
+ ), claimed AS (
2683
+ UPDATE %1$I.editable_artifact_live_outbox target
2684
+ SET state = 'publishing',
2685
+ attempt_count = target.attempt_count + 1,
2686
+ lease_owner = $2,
2687
+ lease_expires_at = pg_catalog.statement_timestamp()
2688
+ + pg_catalog.make_interval(secs => $3 / 1000.0),
2689
+ last_error_code = NULL
2690
+ FROM candidates
2691
+ WHERE target.id = candidates.id
2692
+ RETURNING target.*
2693
+ )
2694
+ SELECT claimed.id, claimed.account_id, claimed.workspace_id,
2695
+ claimed.artifact_id, claimed.transaction_id, claimed.snapshot_id,
2696
+ claimed.event_kind, claimed.event, claimed.state, claimed.attempt_count,
2697
+ claimed.lease_owner, claimed.lease_expires_at, claimed.next_attempt_at,
2698
+ claimed.last_error_code, claimed.published_at, claimed.dead_lettered_at,
2699
+ claimed.created_at
2700
+ FROM claimed
2701
+ CROSS JOIN (SELECT count(*) FROM exhausted) exhausted_barrier
2702
+ ORDER BY claimed.created_at, claimed.id
2703
+ $query$, data_schema) USING p_limit, p_owner, p_lease_duration_ms;
2704
+ END;
2705
+ $body$;
2706
+
2707
+ CREATE OR REPLACE FUNCTION opengeni_private.renew_editable_artifact_live_outbox(
2708
+ p_outbox_id text,
2709
+ p_owner text,
2710
+ p_attempt_count integer,
2711
+ p_lease_duration_ms integer,
2712
+ p_data_schema name
2713
+ ) RETURNS boolean
2714
+ LANGUAGE plpgsql
2715
+ SECURITY DEFINER
2716
+ SET search_path = pg_catalog, pg_temp
2717
+ AS $body$
2718
+ DECLARE affected integer;
2719
+ DECLARE data_schema name;
2720
+ BEGIN
2721
+ IF p_outbox_id IS NULL OR p_outbox_id !~ '^[0-9a-f]{32}$' OR p_outbox_id ~ '^0+$'
2722
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2723
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000000
2724
+ OR p_lease_duration_ms IS NULL OR p_lease_duration_ms NOT BETWEEN 1 AND 86400000
2725
+ OR p_data_schema IS NULL
2726
+ THEN
2727
+ RAISE EXCEPTION 'invalid editable artifact outbox renewal bounds'
2728
+ USING ERRCODE = '22023';
2729
+ END IF;
2730
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2731
+ EXECUTE pg_catalog.format($query$
2732
+ UPDATE %I.editable_artifact_live_outbox target
2733
+ SET lease_expires_at = pg_catalog.statement_timestamp()
2734
+ + pg_catalog.make_interval(secs => $4 / 1000.0)
2735
+ WHERE target.id = $1
2736
+ AND target.state = 'publishing'
2737
+ AND target.lease_owner = $2
2738
+ AND target.attempt_count = $3
2739
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
2740
+ $query$, data_schema)
2741
+ USING p_outbox_id, p_owner, p_attempt_count, p_lease_duration_ms;
2742
+ GET DIAGNOSTICS affected = ROW_COUNT;
2743
+ RETURN affected = 1;
2744
+ END;
2745
+ $body$;
2746
+
2747
+ CREATE OR REPLACE FUNCTION opengeni_private.retry_editable_artifact_live_outbox(
2748
+ p_outbox_id text,
2749
+ p_owner text,
2750
+ p_attempt_count integer,
2751
+ p_retry_delay_ms integer,
2752
+ p_error_code text,
2753
+ p_data_schema name
2754
+ ) RETURNS boolean
2755
+ LANGUAGE plpgsql
2756
+ SECURITY DEFINER
2757
+ SET search_path = pg_catalog, pg_temp
2758
+ AS $body$
2759
+ DECLARE affected integer;
2760
+ DECLARE data_schema name;
2761
+ BEGIN
2762
+ IF p_outbox_id IS NULL OR p_outbox_id !~ '^[0-9a-f]{32}$' OR p_outbox_id ~ '^0+$'
2763
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2764
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000000
2765
+ OR p_retry_delay_ms IS NULL OR p_retry_delay_ms NOT BETWEEN 1 AND 86400000
2766
+ OR p_error_code NOT IN ('broker_unavailable', 'broker_backpressure', 'publish_timeout')
2767
+ OR p_data_schema IS NULL
2768
+ THEN
2769
+ RAISE EXCEPTION 'invalid editable artifact outbox retry bounds'
2770
+ USING ERRCODE = '22023';
2771
+ END IF;
2772
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2773
+ EXECUTE pg_catalog.format($query$
2774
+ UPDATE %I.editable_artifact_live_outbox target
2775
+ SET state = 'pending', lease_owner = NULL, lease_expires_at = NULL,
2776
+ next_attempt_at = pg_catalog.statement_timestamp()
2777
+ + pg_catalog.make_interval(secs => $4 / 1000.0),
2778
+ last_error_code = $5
2779
+ WHERE target.id = $1
2780
+ AND target.state = 'publishing'
2781
+ AND target.lease_owner = $2
2782
+ AND target.attempt_count = $3
2783
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
2784
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
2785
+ $query$, data_schema)
2786
+ USING p_outbox_id, p_owner, p_attempt_count, p_retry_delay_ms, p_error_code;
2787
+ GET DIAGNOSTICS affected = ROW_COUNT;
2788
+ IF affected = 1 THEN RETURN true; END IF;
2789
+ EXECUTE pg_catalog.format($query$
2790
+ SELECT count(*)::integer
2791
+ FROM %I.editable_artifact_live_outbox target
2792
+ WHERE target.id = $1 AND target.state = 'pending'
2793
+ AND target.attempt_count = $2 AND target.last_error_code = $3
2794
+ $query$, data_schema) INTO affected
2795
+ USING p_outbox_id, p_attempt_count, p_error_code;
2796
+ RETURN affected = 1;
2797
+ END;
2798
+ $body$;
2799
+
2800
+ CREATE OR REPLACE FUNCTION opengeni_private.dead_letter_editable_artifact_live_outbox(
2801
+ p_outbox_id text,
2802
+ p_owner text,
2803
+ p_attempt_count integer,
2804
+ p_error_code text,
2805
+ p_data_schema name
2806
+ ) RETURNS boolean
2807
+ LANGUAGE plpgsql
2808
+ SECURITY DEFINER
2809
+ SET search_path = pg_catalog, pg_temp
2810
+ AS $body$
2811
+ DECLARE affected integer;
2812
+ DECLARE data_schema name;
2813
+ BEGIN
2814
+ IF p_outbox_id IS NULL OR p_outbox_id !~ '^[0-9a-f]{32}$' OR p_outbox_id ~ '^0+$'
2815
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2816
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000000
2817
+ OR p_error_code NOT IN ('invalid_hint', 'oversized_hint')
2818
+ OR p_data_schema IS NULL
2819
+ THEN
2820
+ RAISE EXCEPTION 'invalid editable artifact outbox dead-letter bounds'
2821
+ USING ERRCODE = '22023';
2822
+ END IF;
2823
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2824
+ EXECUTE pg_catalog.format($query$
2825
+ UPDATE %I.editable_artifact_live_outbox target
2826
+ SET state = 'dead_lettered', lease_owner = NULL, lease_expires_at = NULL,
2827
+ last_error_code = $4, dead_lettered_at = pg_catalog.statement_timestamp()
2828
+ WHERE target.id = $1
2829
+ AND target.state = 'publishing'
2830
+ AND target.lease_owner = $2
2831
+ AND target.attempt_count = $3
2832
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
2833
+ $query$, data_schema)
2834
+ USING p_outbox_id, p_owner, p_attempt_count, p_error_code;
2835
+ GET DIAGNOSTICS affected = ROW_COUNT;
2836
+ IF affected = 1 THEN RETURN true; END IF;
2837
+ EXECUTE pg_catalog.format($query$
2838
+ SELECT count(*)::integer
2839
+ FROM %I.editable_artifact_live_outbox target
2840
+ WHERE target.id = $1 AND target.state = 'dead_lettered'
2841
+ AND target.attempt_count = $2 AND target.last_error_code = $3
2842
+ $query$, data_schema) INTO affected
2843
+ USING p_outbox_id, p_attempt_count, p_error_code;
2844
+ RETURN affected = 1;
2845
+ END;
2846
+ $body$;
2847
+
2848
+ CREATE OR REPLACE FUNCTION opengeni_private.mark_editable_artifact_live_outbox_published(
2849
+ p_outbox_id text,
2850
+ p_owner text,
2851
+ p_attempt_count integer,
2852
+ p_data_schema name
2853
+ ) RETURNS boolean
2854
+ LANGUAGE plpgsql
2855
+ SECURITY DEFINER
2856
+ SET search_path = pg_catalog, pg_temp
2857
+ AS $body$
2858
+ DECLARE affected integer;
2859
+ DECLARE data_schema name;
2860
+ BEGIN
2861
+ IF p_outbox_id IS NULL
2862
+ OR p_outbox_id !~ '^[0-9a-f]{32}$' OR p_outbox_id ~ '^0+$'
2863
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2864
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000000
2865
+ OR p_data_schema IS NULL
2866
+ THEN
2867
+ RAISE EXCEPTION 'invalid editable artifact outbox settlement bounds'
2868
+ USING ERRCODE = '22023';
2869
+ END IF;
2870
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2871
+ EXECUTE pg_catalog.format($query$
2872
+ UPDATE %I.editable_artifact_live_outbox target
2873
+ SET state = 'published', published_at = pg_catalog.statement_timestamp(),
2874
+ lease_owner = NULL, lease_expires_at = NULL, last_error_code = NULL
2875
+ WHERE target.id = $1
2876
+ AND target.state = 'publishing'
2877
+ AND target.lease_owner = $2
2878
+ AND target.attempt_count = $3
2879
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
2880
+ $query$, data_schema) USING p_outbox_id, p_owner, p_attempt_count;
2881
+ GET DIAGNOSTICS affected = ROW_COUNT;
2882
+ IF affected = 1 THEN RETURN true; END IF;
2883
+ EXECUTE pg_catalog.format($query$
2884
+ SELECT count(*)::integer
2885
+ FROM %I.editable_artifact_live_outbox target
2886
+ WHERE target.id = $1
2887
+ AND target.attempt_count = $2
2888
+ AND target.state = 'published'
2889
+ $query$, data_schema) INTO affected USING p_outbox_id, p_attempt_count;
2890
+ RETURN affected = 1;
2891
+ END;
2892
+ $body$;
2893
+
2894
+ CREATE OR REPLACE FUNCTION opengeni_private.release_editable_artifact_live_outbox(
2895
+ p_outbox_id text,
2896
+ p_owner text,
2897
+ p_attempt_count integer,
2898
+ p_data_schema name
2899
+ ) RETURNS boolean
2900
+ LANGUAGE plpgsql
2901
+ SECURITY DEFINER
2902
+ SET search_path = pg_catalog, pg_temp
2903
+ AS $body$
2904
+ DECLARE affected integer;
2905
+ DECLARE data_schema name;
2906
+ BEGIN
2907
+ IF p_outbox_id IS NULL
2908
+ OR p_outbox_id !~ '^[0-9a-f]{32}$' OR p_outbox_id ~ '^0+$'
2909
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2910
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000000
2911
+ OR p_data_schema IS NULL
2912
+ THEN
2913
+ RAISE EXCEPTION 'invalid editable artifact outbox release bounds'
2914
+ USING ERRCODE = '22023';
2915
+ END IF;
2916
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2917
+ EXECUTE pg_catalog.format($query$
2918
+ UPDATE %I.editable_artifact_live_outbox target
2919
+ SET state = 'pending', lease_owner = NULL, lease_expires_at = NULL,
2920
+ next_attempt_at = pg_catalog.statement_timestamp()
2921
+ WHERE target.id = $1
2922
+ AND target.state = 'publishing'
2923
+ AND target.lease_owner = $2
2924
+ AND target.attempt_count = $3
2925
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
2926
+ $query$, data_schema) USING p_outbox_id, p_owner, p_attempt_count;
2927
+ GET DIAGNOSTICS affected = ROW_COUNT;
2928
+ IF affected = 1 THEN RETURN true; END IF;
2929
+ EXECUTE pg_catalog.format($query$
2930
+ SELECT count(*)::integer
2931
+ FROM %I.editable_artifact_live_outbox target
2932
+ WHERE target.id = $1
2933
+ AND target.attempt_count = $2
2934
+ AND ((target.state = 'pending' AND target.lease_owner IS NULL)
2935
+ OR target.state = 'published')
2936
+ $query$, data_schema) INTO affected USING p_outbox_id, p_attempt_count;
2937
+ RETURN affected = 1;
2938
+ END;
2939
+ $body$;
2940
+
2941
+ -- Cross-workspace materialization is a dedicated database capability. The
2942
+ -- worker owns no table privileges: these SECURITY DEFINER routines are the only
2943
+ -- path from a global claim to an exact, lease-fenced terminal projection.
2944
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_editable_artifact_materializations(
2945
+ p_owner text,
2946
+ p_lease_duration_ms integer,
2947
+ p_limit integer,
2948
+ p_data_schema name
2949
+ ) RETURNS TABLE (
2950
+ account_id uuid,
2951
+ workspace_id uuid,
2952
+ artifact_id text,
2953
+ job_id text,
2954
+ version_id text,
2955
+ modality text,
2956
+ input_snapshot_id text,
2957
+ target_head_sequence bigint,
2958
+ state_hash text,
2959
+ source_object_reference text,
2960
+ source_byte_size bigint,
2961
+ source_content_hash text,
2962
+ source_mime_type text,
2963
+ model_schema_version integer,
2964
+ operation_protocol_version integer,
2965
+ snapshot_protocol_version integer,
2966
+ format text,
2967
+ codec_id text,
2968
+ normalized_options text,
2969
+ options_hash text,
2970
+ codec_version text,
2971
+ kernel_version text,
2972
+ font_registry_hash text,
2973
+ policy_hash text,
2974
+ attempt_count integer,
2975
+ lease_owner text,
2976
+ lease_expires_at timestamptz
2977
+ )
2978
+ LANGUAGE plpgsql
2979
+ SECURITY DEFINER
2980
+ SET search_path = pg_catalog, pg_temp
2981
+ AS $body$
2982
+ DECLARE data_schema name;
2983
+ BEGIN
2984
+ IF p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
2985
+ OR p_owner <> pg_catalog.btrim(p_owner)
2986
+ OR p_lease_duration_ms IS NULL
2987
+ OR p_lease_duration_ms NOT BETWEEN 1 AND 86400000
2988
+ OR p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 1000
2989
+ OR p_data_schema IS NULL
2990
+ THEN
2991
+ RAISE EXCEPTION 'invalid editable artifact materialization claim bounds'
2992
+ USING ERRCODE = '22023';
2993
+ END IF;
2994
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
2995
+ RETURN QUERY EXECUTE pg_catalog.format($query$
2996
+ WITH exhausted_candidates AS MATERIALIZED (
2997
+ SELECT candidate.account_id, candidate.workspace_id,
2998
+ candidate.artifact_id, candidate.id
2999
+ FROM %1$I.editable_artifact_materialization_jobs candidate
3000
+ WHERE candidate.state = 'running'
3001
+ AND candidate.attempt_count >= 1000
3002
+ AND candidate.lease_expires_at <= pg_catalog.statement_timestamp()
3003
+ ORDER BY candidate.lease_expires_at, candidate.created_at,
3004
+ candidate.account_id, candidate.workspace_id, candidate.artifact_id, candidate.id
3005
+ FOR UPDATE OF candidate SKIP LOCKED
3006
+ LIMIT $1
3007
+ ), exhausted AS (
3008
+ UPDATE %1$I.editable_artifact_materialization_jobs target
3009
+ SET state = 'failed', settled_by_owner = target.lease_owner,
3010
+ lease_owner = NULL, lease_expires_at = NULL,
3011
+ error_code = 'attempts_exhausted',
3012
+ completed_at = pg_catalog.statement_timestamp()
3013
+ FROM exhausted_candidates
3014
+ WHERE target.account_id = exhausted_candidates.account_id
3015
+ AND target.workspace_id = exhausted_candidates.workspace_id
3016
+ AND target.artifact_id = exhausted_candidates.artifact_id
3017
+ AND target.id = exhausted_candidates.id
3018
+ RETURNING target.id
3019
+ ), candidates AS (
3020
+ SELECT candidate.account_id, candidate.workspace_id,
3021
+ candidate.artifact_id, candidate.id
3022
+ FROM %1$I.editable_artifact_materialization_jobs candidate
3023
+ WHERE candidate.attempt_count < 1000
3024
+ AND (candidate.state = 'pending' OR (
3025
+ candidate.state = 'running'
3026
+ AND candidate.lease_expires_at <= pg_catalog.statement_timestamp()
3027
+ ))
3028
+ ORDER BY COALESCE(candidate.lease_expires_at, candidate.created_at),
3029
+ candidate.created_at, candidate.account_id, candidate.workspace_id,
3030
+ candidate.artifact_id, candidate.id
3031
+ FOR UPDATE OF candidate SKIP LOCKED
3032
+ LIMIT $1
3033
+ ), claimed AS (
3034
+ UPDATE %1$I.editable_artifact_materialization_jobs target
3035
+ SET state = 'running', attempt_count = target.attempt_count + 1,
3036
+ lease_owner = $2,
3037
+ lease_expires_at = pg_catalog.statement_timestamp()
3038
+ + pg_catalog.make_interval(secs => $3 / 1000.0),
3039
+ settled_by_owner = NULL, error_code = NULL,
3040
+ started_at = pg_catalog.statement_timestamp(), completed_at = NULL
3041
+ FROM candidates
3042
+ WHERE target.account_id = candidates.account_id
3043
+ AND target.workspace_id = candidates.workspace_id
3044
+ AND target.artifact_id = candidates.artifact_id
3045
+ AND target.id = candidates.id
3046
+ RETURNING target.*
3047
+ )
3048
+ SELECT claimed.account_id, claimed.workspace_id, claimed.artifact_id,
3049
+ claimed.id, claimed.version_id, artifact.modality, claimed.input_snapshot_id,
3050
+ claimed.target_head_sequence, claimed.state_hash,
3051
+ source_blob.object_reference, source_blob.byte_size,
3052
+ source_blob.content_hash, source_blob.mime_type,
3053
+ source_snapshot.model_schema_version, source_snapshot.operation_protocol_version,
3054
+ source_snapshot.crdt_state_version,
3055
+ claimed.format, claimed.codec_id, claimed.normalized_options,
3056
+ claimed.options_hash, claimed.codec_version, claimed.kernel_version,
3057
+ claimed.font_registry_hash, claimed.policy_hash, claimed.attempt_count,
3058
+ claimed.lease_owner, claimed.lease_expires_at
3059
+ FROM claimed
3060
+ CROSS JOIN (SELECT count(*) FROM exhausted) exhausted_barrier
3061
+ JOIN %1$I.editable_artifacts artifact
3062
+ ON artifact.account_id = claimed.account_id
3063
+ AND artifact.workspace_id = claimed.workspace_id
3064
+ AND artifact.id = claimed.artifact_id
3065
+ JOIN %1$I.editable_artifact_snapshots source_snapshot
3066
+ ON source_snapshot.account_id = claimed.account_id
3067
+ AND source_snapshot.workspace_id = claimed.workspace_id
3068
+ AND source_snapshot.artifact_id = claimed.artifact_id
3069
+ AND source_snapshot.id = claimed.input_snapshot_id
3070
+ AND source_snapshot.covered_head_sequence = claimed.target_head_sequence
3071
+ AND source_snapshot.state_hash = claimed.state_hash
3072
+ JOIN %1$I.editable_artifact_blob_refs source_blob
3073
+ ON source_blob.account_id = source_snapshot.account_id
3074
+ AND source_blob.workspace_id = source_snapshot.workspace_id
3075
+ AND source_blob.artifact_id = source_snapshot.artifact_id
3076
+ AND source_blob.id = source_snapshot.blob_ref_id
3077
+ AND source_blob.kind = 'snapshot'
3078
+ AND source_blob.byte_size = source_snapshot.byte_size
3079
+ AND source_blob.content_hash = source_snapshot.content_hash
3080
+ AND source_blob.mime_type = source_snapshot.mime_type
3081
+ ORDER BY claimed.created_at, claimed.account_id, claimed.workspace_id,
3082
+ claimed.artifact_id, claimed.id
3083
+ $query$, data_schema) USING p_limit, p_owner, p_lease_duration_ms;
3084
+ END;
3085
+ $body$;
3086
+
3087
+ CREATE OR REPLACE FUNCTION opengeni_private.renew_editable_artifact_materialization(
3088
+ p_account_id uuid,
3089
+ p_workspace_id uuid,
3090
+ p_artifact_id text,
3091
+ p_job_id text,
3092
+ p_owner text,
3093
+ p_attempt_count integer,
3094
+ p_lease_duration_ms integer,
3095
+ p_data_schema name
3096
+ ) RETURNS TABLE (
3097
+ account_id uuid,
3098
+ workspace_id uuid,
3099
+ artifact_id text,
3100
+ job_id text,
3101
+ lease_owner text,
3102
+ attempt_count integer,
3103
+ lease_expires_at timestamptz
3104
+ )
3105
+ LANGUAGE plpgsql
3106
+ SECURITY DEFINER
3107
+ SET search_path = pg_catalog, pg_temp
3108
+ AS $body$
3109
+ DECLARE data_schema name;
3110
+ BEGIN
3111
+ IF p_account_id IS NULL OR p_workspace_id IS NULL
3112
+ OR p_artifact_id IS NULL OR p_artifact_id !~ '^[0-9a-f]{32}$'
3113
+ OR p_artifact_id ~ '^0+$'
3114
+ OR p_job_id IS NULL OR p_job_id !~ '^[0-9a-f]{32}$' OR p_job_id ~ '^0+$'
3115
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
3116
+ OR p_owner <> pg_catalog.btrim(p_owner)
3117
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000
3118
+ OR p_lease_duration_ms IS NULL
3119
+ OR p_lease_duration_ms NOT BETWEEN 1 AND 86400000
3120
+ OR p_data_schema IS NULL
3121
+ THEN
3122
+ RAISE EXCEPTION 'invalid editable artifact materialization renewal bounds'
3123
+ USING ERRCODE = '22023';
3124
+ END IF;
3125
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
3126
+ RETURN QUERY EXECUTE pg_catalog.format($query$
3127
+ UPDATE %I.editable_artifact_materialization_jobs target
3128
+ SET lease_expires_at = GREATEST(
3129
+ target.lease_expires_at + interval '1 microsecond',
3130
+ pg_catalog.statement_timestamp()
3131
+ + pg_catalog.make_interval(secs => $7 / 1000.0)
3132
+ )
3133
+ WHERE target.account_id = $1 AND target.workspace_id = $2
3134
+ AND target.artifact_id = $3 AND target.id = $4
3135
+ AND target.state = 'running' AND target.lease_owner = $5
3136
+ AND target.attempt_count = $6
3137
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
3138
+ RETURNING target.account_id, target.workspace_id, target.artifact_id,
3139
+ target.id, target.lease_owner, target.attempt_count,
3140
+ target.lease_expires_at
3141
+ $query$, data_schema)
3142
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id, p_owner,
3143
+ p_attempt_count, p_lease_duration_ms;
3144
+ END;
3145
+ $body$;
3146
+
3147
+ CREATE OR REPLACE FUNCTION opengeni_private.succeed_editable_artifact_materialization(
3148
+ p_account_id uuid,
3149
+ p_workspace_id uuid,
3150
+ p_artifact_id text,
3151
+ p_job_id text,
3152
+ p_owner text,
3153
+ p_attempt_count integer,
3154
+ p_result_id text,
3155
+ p_blob_ref_id text,
3156
+ p_object_reference text,
3157
+ p_byte_size bigint,
3158
+ p_content_hash text,
3159
+ p_mime_type text,
3160
+ p_verified_at timestamptz,
3161
+ p_data_schema name
3162
+ ) RETURNS TABLE (
3163
+ outcome text,
3164
+ account_id uuid,
3165
+ workspace_id uuid,
3166
+ artifact_id text,
3167
+ job_id text,
3168
+ result_id text,
3169
+ blob_ref_id text,
3170
+ object_reference text,
3171
+ byte_size bigint,
3172
+ content_hash text,
3173
+ mime_type text,
3174
+ verified_at timestamptz,
3175
+ created_at timestamptz,
3176
+ settled_by_owner text,
3177
+ attempt_count integer
3178
+ )
3179
+ LANGUAGE plpgsql
3180
+ SECURITY DEFINER
3181
+ SET search_path = pg_catalog, pg_temp
3182
+ AS $body$
3183
+ DECLARE data_schema name;
3184
+ DECLARE affected integer := 0;
3185
+ DECLARE job_state text;
3186
+ DECLARE job_attempt integer;
3187
+ DECLARE job_owner text;
3188
+ DECLARE job_lease_expires_at timestamptz;
3189
+ DECLARE job_format text;
3190
+ BEGIN
3191
+ IF p_account_id IS NULL OR p_workspace_id IS NULL
3192
+ OR p_artifact_id IS NULL OR p_artifact_id !~ '^[0-9a-f]{32}$'
3193
+ OR p_artifact_id ~ '^0+$'
3194
+ OR p_job_id IS NULL OR p_job_id !~ '^[0-9a-f]{32}$' OR p_job_id ~ '^0+$'
3195
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
3196
+ OR p_owner <> pg_catalog.btrim(p_owner)
3197
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000
3198
+ OR p_result_id IS NULL OR p_result_id !~ '^[0-9a-f]{32}$'
3199
+ OR p_result_id ~ '^0+$'
3200
+ OR p_blob_ref_id IS NULL OR p_blob_ref_id !~ '^[0-9a-f]{32}$'
3201
+ OR p_blob_ref_id ~ '^0+$'
3202
+ OR p_object_reference IS NULL
3203
+ OR pg_catalog.octet_length(p_object_reference) NOT BETWEEN 1 AND 1024
3204
+ OR p_object_reference <> pg_catalog.btrim(p_object_reference)
3205
+ OR p_byte_size IS NULL OR p_byte_size NOT BETWEEN 1 AND 9007199254740991
3206
+ OR p_content_hash IS NULL OR p_content_hash !~ '^sha256:[0-9a-f]{64}$'
3207
+ OR p_mime_type NOT IN (
3208
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3209
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
3210
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
3211
+ 'application/pdf', 'image/png', 'image/webp'
3212
+ )
3213
+ OR p_verified_at IS NULL
3214
+ OR p_verified_at > pg_catalog.clock_timestamp() + interval '5 minutes'
3215
+ OR p_data_schema IS NULL
3216
+ THEN
3217
+ RAISE EXCEPTION 'invalid editable artifact materialization success bounds'
3218
+ USING ERRCODE = '22023';
3219
+ END IF;
3220
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
3221
+
3222
+ job_state := NULL;
3223
+ EXECUTE pg_catalog.format($query$
3224
+ SELECT target.state, target.attempt_count, target.lease_owner,
3225
+ target.lease_expires_at, target.format
3226
+ FROM %I.editable_artifact_materialization_jobs target
3227
+ WHERE target.account_id = $1 AND target.workspace_id = $2
3228
+ AND target.artifact_id = $3 AND target.id = $4
3229
+ FOR UPDATE
3230
+ $query$, data_schema)
3231
+ INTO job_state, job_attempt, job_owner, job_lease_expires_at, job_format
3232
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id;
3233
+ IF job_state IS NULL THEN RETURN; END IF;
3234
+
3235
+ -- A lost successful response is replayable only when every immutable output
3236
+ -- fact and the exact settled lease fence match. A stale/different caller gets
3237
+ -- no row and is surfaced as lease_fenced by the repository.
3238
+ IF job_state = 'succeeded' THEN
3239
+ RETURN QUERY EXECUTE pg_catalog.format($query$
3240
+ SELECT 'replayed'::text, job.account_id, job.workspace_id,
3241
+ job.artifact_id, job.id, result.id, result.blob_ref_id,
3242
+ blob.object_reference, result.byte_size, result.content_hash,
3243
+ result.mime_type, result.verified_at, result.created_at,
3244
+ job.settled_by_owner, job.attempt_count
3245
+ FROM %1$I.editable_artifact_materialization_jobs job
3246
+ JOIN %1$I.editable_artifact_materialization_results result
3247
+ ON result.account_id = job.account_id
3248
+ AND result.workspace_id = job.workspace_id
3249
+ AND result.artifact_id = job.artifact_id AND result.job_id = job.id
3250
+ JOIN %1$I.editable_artifact_blob_refs blob
3251
+ ON blob.account_id = result.account_id
3252
+ AND blob.workspace_id = result.workspace_id
3253
+ AND blob.artifact_id = result.artifact_id AND blob.id = result.blob_ref_id
3254
+ WHERE job.account_id = $1 AND job.workspace_id = $2
3255
+ AND job.artifact_id = $3 AND job.id = $4
3256
+ AND job.state = 'succeeded' AND job.settled_by_owner = $5
3257
+ AND job.attempt_count = $6 AND result.id = $7
3258
+ AND result.blob_ref_id = $8 AND blob.kind = 'materialization'
3259
+ AND blob.object_reference = $9 AND result.byte_size = $10
3260
+ AND result.content_hash = $11 AND result.mime_type = $12
3261
+ AND result.verified_at = $13
3262
+ AND blob.byte_size = result.byte_size
3263
+ AND blob.content_hash = result.content_hash
3264
+ AND blob.mime_type = result.mime_type
3265
+ $query$, data_schema)
3266
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id, p_owner,
3267
+ p_attempt_count, p_result_id, p_blob_ref_id, p_object_reference,
3268
+ p_byte_size, p_content_hash, p_mime_type, p_verified_at;
3269
+ RETURN;
3270
+ END IF;
3271
+
3272
+ IF job_state <> 'running' OR job_owner IS DISTINCT FROM p_owner
3273
+ OR job_attempt IS DISTINCT FROM p_attempt_count
3274
+ OR job_lease_expires_at <= pg_catalog.statement_timestamp()
3275
+ THEN
3276
+ RETURN;
3277
+ END IF;
3278
+ IF p_mime_type <> (CASE job_format
3279
+ WHEN 'xlsx' THEN 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
3280
+ WHEN 'pptx' THEN 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
3281
+ WHEN 'docx' THEN 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
3282
+ WHEN 'pdf' THEN 'application/pdf'
3283
+ WHEN 'png' THEN 'image/png'
3284
+ WHEN 'webp' THEN 'image/webp'
3285
+ END) THEN
3286
+ RAISE EXCEPTION 'materialization MIME type differs from the claimed format'
3287
+ USING ERRCODE = '22023';
3288
+ END IF;
3289
+
3290
+ EXECUTE pg_catalog.format($query$
3291
+ INSERT INTO %I.editable_artifact_blob_refs (
3292
+ account_id, workspace_id, artifact_id, id, kind, object_reference,
3293
+ byte_size, content_hash, mime_type, created_at
3294
+ ) VALUES ($1, $2, $3, $4, 'materialization', $5, $6, $7, $8,
3295
+ pg_catalog.statement_timestamp())
3296
+ ON CONFLICT DO NOTHING
3297
+ $query$, data_schema)
3298
+ USING p_account_id, p_workspace_id, p_artifact_id, p_blob_ref_id,
3299
+ p_object_reference, p_byte_size, p_content_hash, p_mime_type;
3300
+ EXECUTE pg_catalog.format($query$
3301
+ SELECT pg_catalog.count(*)::integer
3302
+ FROM %I.editable_artifact_blob_refs blob
3303
+ WHERE blob.account_id = $1 AND blob.workspace_id = $2
3304
+ AND blob.artifact_id = $3 AND blob.id = $4
3305
+ AND blob.kind = 'materialization' AND blob.object_reference = $5
3306
+ AND blob.byte_size = $6 AND blob.content_hash = $7 AND blob.mime_type = $8
3307
+ $query$, data_schema) INTO affected
3308
+ USING p_account_id, p_workspace_id, p_artifact_id, p_blob_ref_id,
3309
+ p_object_reference, p_byte_size, p_content_hash, p_mime_type;
3310
+ IF affected <> 1 THEN
3311
+ RAISE EXCEPTION 'materialization blob identity conflicts with immutable facts'
3312
+ USING ERRCODE = '23505';
3313
+ END IF;
3314
+
3315
+ EXECUTE pg_catalog.format($query$
3316
+ UPDATE %I.editable_artifact_materialization_jobs target
3317
+ SET state = 'succeeded', settled_by_owner = target.lease_owner,
3318
+ lease_owner = NULL, lease_expires_at = NULL, error_code = NULL,
3319
+ completed_at = pg_catalog.statement_timestamp()
3320
+ WHERE target.account_id = $1 AND target.workspace_id = $2
3321
+ AND target.artifact_id = $3 AND target.id = $4
3322
+ AND target.state = 'running' AND target.lease_owner = $5
3323
+ AND target.attempt_count = $6
3324
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
3325
+ $query$, data_schema)
3326
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id, p_owner,
3327
+ p_attempt_count;
3328
+ GET DIAGNOSTICS affected = ROW_COUNT;
3329
+ IF affected <> 1 THEN RETURN; END IF;
3330
+
3331
+ EXECUTE pg_catalog.format($query$
3332
+ INSERT INTO %I.editable_artifact_materialization_results (
3333
+ account_id, workspace_id, artifact_id, id, job_id, blob_ref_id,
3334
+ byte_size, content_hash, mime_type, verified_at, created_at
3335
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
3336
+ GREATEST(pg_catalog.statement_timestamp(), $10))
3337
+ $query$, data_schema)
3338
+ USING p_account_id, p_workspace_id, p_artifact_id, p_result_id, p_job_id,
3339
+ p_blob_ref_id, p_byte_size, p_content_hash, p_mime_type, p_verified_at;
3340
+
3341
+ RETURN QUERY EXECUTE pg_catalog.format($query$
3342
+ SELECT 'committed'::text, job.account_id, job.workspace_id,
3343
+ job.artifact_id, job.id, result.id, result.blob_ref_id,
3344
+ blob.object_reference, result.byte_size, result.content_hash,
3345
+ result.mime_type, result.verified_at, result.created_at,
3346
+ job.settled_by_owner, job.attempt_count
3347
+ FROM %1$I.editable_artifact_materialization_jobs job
3348
+ JOIN %1$I.editable_artifact_materialization_results result
3349
+ ON result.account_id = job.account_id AND result.workspace_id = job.workspace_id
3350
+ AND result.artifact_id = job.artifact_id AND result.job_id = job.id
3351
+ JOIN %1$I.editable_artifact_blob_refs blob
3352
+ ON blob.account_id = result.account_id AND blob.workspace_id = result.workspace_id
3353
+ AND blob.artifact_id = result.artifact_id AND blob.id = result.blob_ref_id
3354
+ WHERE job.account_id = $1 AND job.workspace_id = $2
3355
+ AND job.artifact_id = $3 AND job.id = $4 AND result.id = $5
3356
+ $query$, data_schema)
3357
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id, p_result_id;
3358
+ END;
3359
+ $body$;
3360
+
3361
+ CREATE OR REPLACE FUNCTION opengeni_private.fail_editable_artifact_materialization(
3362
+ p_account_id uuid,
3363
+ p_workspace_id uuid,
3364
+ p_artifact_id text,
3365
+ p_job_id text,
3366
+ p_owner text,
3367
+ p_attempt_count integer,
3368
+ p_error_code text,
3369
+ p_data_schema name
3370
+ ) RETURNS TABLE (
3371
+ outcome text,
3372
+ account_id uuid,
3373
+ workspace_id uuid,
3374
+ artifact_id text,
3375
+ job_id text,
3376
+ settled_by_owner text,
3377
+ attempt_count integer,
3378
+ error_code text
3379
+ )
3380
+ LANGUAGE plpgsql
3381
+ SECURITY DEFINER
3382
+ SET search_path = pg_catalog, pg_temp
3383
+ AS $body$
3384
+ DECLARE data_schema name;
3385
+ DECLARE affected integer := 0;
3386
+ DECLARE job_state text;
3387
+ DECLARE job_attempt integer;
3388
+ DECLARE job_owner text;
3389
+ DECLARE job_lease_expires_at timestamptz;
3390
+ DECLARE job_settled_by_owner text;
3391
+ DECLARE job_error_code text;
3392
+ BEGIN
3393
+ IF p_account_id IS NULL OR p_workspace_id IS NULL
3394
+ OR p_artifact_id IS NULL OR p_artifact_id !~ '^[0-9a-f]{32}$'
3395
+ OR p_artifact_id ~ '^0+$'
3396
+ OR p_job_id IS NULL OR p_job_id !~ '^[0-9a-f]{32}$' OR p_job_id ~ '^0+$'
3397
+ OR p_owner IS NULL OR pg_catalog.octet_length(p_owner) NOT BETWEEN 1 AND 256
3398
+ OR p_owner <> pg_catalog.btrim(p_owner)
3399
+ OR p_attempt_count IS NULL OR p_attempt_count NOT BETWEEN 1 AND 1000
3400
+ OR p_error_code IS NULL OR p_error_code !~ '^[a-z][a-z0-9_.-]{0,127}$'
3401
+ OR p_data_schema IS NULL
3402
+ THEN
3403
+ RAISE EXCEPTION 'invalid editable artifact materialization failure bounds'
3404
+ USING ERRCODE = '22023';
3405
+ END IF;
3406
+ data_schema := opengeni_private.resolve_editable_artifact_data_schema(p_data_schema);
3407
+ job_state := NULL;
3408
+ EXECUTE pg_catalog.format($query$
3409
+ SELECT target.state, target.attempt_count, target.lease_owner,
3410
+ target.lease_expires_at, target.settled_by_owner, target.error_code
3411
+ FROM %I.editable_artifact_materialization_jobs target
3412
+ WHERE target.account_id = $1 AND target.workspace_id = $2
3413
+ AND target.artifact_id = $3 AND target.id = $4
3414
+ FOR UPDATE
3415
+ $query$, data_schema)
3416
+ INTO job_state, job_attempt, job_owner, job_lease_expires_at,
3417
+ job_settled_by_owner, job_error_code
3418
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id;
3419
+
3420
+ IF job_state = 'failed' AND job_attempt = p_attempt_count
3421
+ AND job_settled_by_owner = p_owner AND job_error_code = p_error_code
3422
+ THEN
3423
+ RETURN QUERY SELECT 'replayed'::text, p_account_id, p_workspace_id,
3424
+ p_artifact_id, p_job_id, job_settled_by_owner, job_attempt, job_error_code;
3425
+ RETURN;
3426
+ END IF;
3427
+ IF job_state <> 'running' OR job_owner IS DISTINCT FROM p_owner
3428
+ OR job_attempt IS DISTINCT FROM p_attempt_count
3429
+ OR job_lease_expires_at <= pg_catalog.statement_timestamp()
3430
+ THEN
3431
+ RETURN QUERY SELECT 'fenced'::text, p_account_id, p_workspace_id,
3432
+ p_artifact_id, p_job_id, p_owner, p_attempt_count, p_error_code;
3433
+ RETURN;
3434
+ END IF;
3435
+
3436
+ EXECUTE pg_catalog.format($query$
3437
+ UPDATE %I.editable_artifact_materialization_jobs target
3438
+ SET state = 'failed', settled_by_owner = target.lease_owner,
3439
+ lease_owner = NULL, lease_expires_at = NULL, error_code = $7,
3440
+ completed_at = pg_catalog.statement_timestamp()
3441
+ WHERE target.account_id = $1 AND target.workspace_id = $2
3442
+ AND target.artifact_id = $3 AND target.id = $4
3443
+ AND target.state = 'running' AND target.lease_owner = $5
3444
+ AND target.attempt_count = $6
3445
+ AND target.lease_expires_at > pg_catalog.statement_timestamp()
3446
+ $query$, data_schema)
3447
+ USING p_account_id, p_workspace_id, p_artifact_id, p_job_id, p_owner,
3448
+ p_attempt_count, p_error_code;
3449
+ GET DIAGNOSTICS affected = ROW_COUNT;
3450
+ IF affected <> 1 THEN
3451
+ RETURN QUERY SELECT 'fenced'::text, p_account_id, p_workspace_id,
3452
+ p_artifact_id, p_job_id, p_owner, p_attempt_count, p_error_code;
3453
+ RETURN;
3454
+ END IF;
3455
+ RETURN QUERY SELECT 'failed'::text, p_account_id, p_workspace_id,
3456
+ p_artifact_id, p_job_id, p_owner, p_attempt_count, p_error_code;
3457
+ END;
3458
+ $body$;
3459
+
3460
+ REVOKE ALL ON FUNCTION
3461
+ opengeni_private.claim_editable_artifact_materializations(text, integer, integer, name)
3462
+ FROM PUBLIC;
3463
+ REVOKE ALL ON FUNCTION
3464
+ opengeni_private.renew_editable_artifact_materialization(
3465
+ uuid, uuid, text, text, text, integer, integer, name
3466
+ ) FROM PUBLIC;
3467
+ REVOKE ALL ON FUNCTION
3468
+ opengeni_private.succeed_editable_artifact_materialization(
3469
+ uuid, uuid, text, text, text, integer, text, text, text, bigint,
3470
+ text, text, timestamptz, name
3471
+ ) FROM PUBLIC;
3472
+ REVOKE ALL ON FUNCTION
3473
+ opengeni_private.fail_editable_artifact_materialization(
3474
+ uuid, uuid, text, text, text, integer, text, name
3475
+ ) FROM PUBLIC;
3476
+ REVOKE ALL ON FUNCTION
3477
+ opengeni_private.ensure_editable_artifact_scope_authorization_head(uuid, uuid, name)
3478
+ FROM PUBLIC;
3479
+ REVOKE ALL ON FUNCTION
3480
+ opengeni_private.advance_editable_artifact_scope_authorization_revision(
3481
+ uuid, uuid, bigint, bigint, name
3482
+ ) FROM PUBLIC;
3483
+ REVOKE ALL ON FUNCTION
3484
+ opengeni_private.claim_editable_artifact_live_outbox(text, integer, integer, name)
3485
+ FROM PUBLIC;
3486
+ REVOKE ALL ON FUNCTION
3487
+ opengeni_private.mark_editable_artifact_live_outbox_published(text, text, integer, name)
3488
+ FROM PUBLIC;
3489
+ REVOKE ALL ON FUNCTION
3490
+ opengeni_private.renew_editable_artifact_live_outbox(text, text, integer, integer, name)
3491
+ FROM PUBLIC;
3492
+ REVOKE ALL ON FUNCTION
3493
+ opengeni_private.retry_editable_artifact_live_outbox(
3494
+ text, text, integer, integer, text, name
3495
+ ) FROM PUBLIC;
3496
+ REVOKE ALL ON FUNCTION
3497
+ opengeni_private.dead_letter_editable_artifact_live_outbox(
3498
+ text, text, integer, text, name
3499
+ ) FROM PUBLIC;
3500
+ REVOKE ALL ON FUNCTION
3501
+ opengeni_private.release_editable_artifact_live_outbox(text, text, integer, name)
3502
+ FROM PUBLIC;
3503
+ REVOKE ALL ON FUNCTION
3504
+ opengeni_private.advance_editable_artifact_authorization_revision(
3505
+ uuid, uuid, text, bigint, bigint, name
3506
+ ) FROM PUBLIC;
3507
+ REVOKE ALL ON FUNCTION
3508
+ opengeni_private.resolve_editable_artifact_data_schema(name) FROM PUBLIC;
3509
+ REVOKE ALL ON FUNCTION
3510
+ opengeni_private.editable_artifact_frontier_valid(jsonb) FROM PUBLIC;
3511
+ REVOKE ALL ON FUNCTION
3512
+ opengeni_private.editable_artifact_id_array_valid(jsonb) FROM PUBLIC;
3513
+ REVOKE ALL ON FUNCTION
3514
+ opengeni_private.editable_artifact_frontier_dominates(jsonb, jsonb) FROM PUBLIC;
3515
+ REVOKE ALL ON FUNCTION
3516
+ opengeni_private.editable_artifact_frontier_merge_dot_equals(
3517
+ jsonb, jsonb, jsonb, text, bigint
3518
+ ) FROM PUBLIC;
3519
+ REVOKE ALL ON FUNCTION
3520
+ opengeni_private.editable_artifact_object_has_exact_keys(jsonb, text[]) FROM PUBLIC;
3521
+ REVOKE ALL ON FUNCTION
3522
+ opengeni_private.editable_artifact_actor_key_matches(
3523
+ text, text, text, text, text, text, integer, text
3524
+ ) FROM PUBLIC;
3525
+
3526
+ DO $rls$
3527
+ DECLARE table_name text;
3528
+ BEGIN
3529
+ FOREACH table_name IN ARRAY ARRAY[
3530
+ 'editable_artifact_scope_authorization_heads',
3531
+ 'editable_artifacts',
3532
+ 'editable_artifact_transactions',
3533
+ 'editable_artifact_operations',
3534
+ 'editable_artifact_idempotency_receipts',
3535
+ 'editable_artifact_undo_claims',
3536
+ 'editable_artifact_sequence_checkpoints',
3537
+ 'editable_artifact_blob_refs',
3538
+ 'editable_artifact_snapshots',
3539
+ 'editable_artifact_versions',
3540
+ 'editable_artifact_materialization_jobs',
3541
+ 'editable_artifact_materialization_results',
3542
+ 'editable_artifact_live_outbox',
3543
+ 'editable_artifact_replica_leases'
3544
+ ] LOOP
3545
+ EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', table_name);
3546
+ EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', table_name);
3547
+ EXECUTE format(
3548
+ 'CREATE POLICY workspace_isolation ON %I '
3549
+ || 'USING (opengeni_private.workspace_rls_visible(account_id, workspace_id)) '
3550
+ || 'WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id))',
3551
+ table_name
3552
+ );
3553
+ END LOOP;
3554
+ END;
3555
+ $rls$;
3556
+
3557
+ -- FORCE RLS also applies to a table owner. This narrow owner-only policy lets
3558
+ -- the audited SECURITY DEFINER outbox capability claim across workspaces while
3559
+ -- ordinary runtime SQL remains constrained by workspace_isolation.
3560
+ DO $outbox_dispatch_policy$
3561
+ DECLARE data_schema text := current_schema();
3562
+ DECLARE migration_owner text := current_user;
3563
+ BEGIN
3564
+ EXECUTE format(
3565
+ 'CREATE POLICY editable_artifact_outbox_dispatcher ON %I.editable_artifact_live_outbox '
3566
+ || 'FOR ALL USING (current_user = %L) WITH CHECK (current_user = %L)',
3567
+ data_schema, migration_owner, migration_owner
3568
+ );
3569
+ END;
3570
+ $outbox_dispatch_policy$;
3571
+
3572
+ -- The materializer's definer owner needs a deliberately narrow FORCE-RLS path
3573
+ -- for its global claim and atomic projection. The login role itself receives
3574
+ -- no table privilege and therefore cannot exercise these policies directly.
3575
+ DO $artifact_materializer_policy$
3576
+ DECLARE data_schema text := current_schema();
3577
+ DECLARE migration_owner text := current_user;
3578
+ DECLARE table_name text;
3579
+ BEGIN
3580
+ FOREACH table_name IN ARRAY ARRAY[
3581
+ 'editable_artifact_materialization_jobs',
3582
+ 'editable_artifact_materialization_results',
3583
+ 'editable_artifact_blob_refs'
3584
+ ] LOOP
3585
+ EXECUTE format(
3586
+ 'CREATE POLICY editable_artifact_materializer_owner ON %I.%I FOR ALL '
3587
+ || 'USING (current_user = %L) WITH CHECK (current_user = %L)',
3588
+ data_schema, table_name, migration_owner, migration_owner
3589
+ );
3590
+ END LOOP;
3591
+ FOREACH table_name IN ARRAY ARRAY[
3592
+ 'editable_artifact_sequence_checkpoints',
3593
+ 'editable_artifact_versions',
3594
+ 'editable_artifact_idempotency_receipts'
3595
+ ] LOOP
3596
+ EXECUTE format(
3597
+ 'CREATE POLICY editable_artifact_materializer_owner ON %I.%I FOR SELECT '
3598
+ || 'USING (current_user = %L)',
3599
+ data_schema, table_name, migration_owner
3600
+ );
3601
+ END LOOP;
3602
+ END;
3603
+ $artifact_materializer_policy$;
3604
+
3605
+ -- The runtime may read the local create-policy fence, but only the narrow
3606
+ -- owner functions may initialize or advance it across FORCE RLS.
3607
+ DO $artifact_scope_authority_policy$
3608
+ DECLARE data_schema text := current_schema();
3609
+ DECLARE migration_owner text := current_user;
3610
+ BEGIN
3611
+ EXECUTE format(
3612
+ 'CREATE POLICY editable_artifact_scope_authority_owner '
3613
+ || 'ON %I.editable_artifact_scope_authorization_heads FOR ALL '
3614
+ || 'USING (current_user = %L) WITH CHECK (current_user = %L)',
3615
+ data_schema, migration_owner, migration_owner
3616
+ );
3617
+ END;
3618
+ $artifact_scope_authority_policy$;
3619
+
3620
+ -- PostgreSQL grants EXECUTE to PUBLIC on new functions by default. The private
3621
+ -- schema is an explicit capability boundary: callers receive named grants,
3622
+ -- never ambient PUBLIC execution. Preserve that invariant for future helpers
3623
+ -- created by this migration owner as well.
3624
+ REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_private FROM PUBLIC;
3625
+ ALTER DEFAULT PRIVILEGES IN SCHEMA opengeni_private
3626
+ REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
3627
+
3628
+ DO $runtime_grants$
3629
+ DECLARE data_schema text := current_schema();
3630
+ DECLARE table_name text;
3631
+ BEGIN
3632
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
3633
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_private TO opengeni_app;
3634
+ FOREACH table_name IN ARRAY ARRAY[
3635
+ 'editable_artifact_scope_authorization_heads',
3636
+ 'editable_artifacts',
3637
+ 'editable_artifact_transactions',
3638
+ 'editable_artifact_operations',
3639
+ 'editable_artifact_idempotency_receipts',
3640
+ 'editable_artifact_undo_claims',
3641
+ 'editable_artifact_sequence_checkpoints',
3642
+ 'editable_artifact_blob_refs',
3643
+ 'editable_artifact_snapshots',
3644
+ 'editable_artifact_versions',
3645
+ 'editable_artifact_materialization_jobs',
3646
+ 'editable_artifact_materialization_results',
3647
+ 'editable_artifact_live_outbox',
3648
+ 'editable_artifact_replica_leases'
3649
+ ] LOOP
3650
+ EXECUTE format(
3651
+ 'REVOKE ALL PRIVILEGES ON TABLE %I.%I FROM opengeni_app',
3652
+ data_schema, table_name
3653
+ );
3654
+ END LOOP;
3655
+ EXECUTE format(
3656
+ 'GRANT SELECT, INSERT, UPDATE ON TABLE %I.editable_artifacts TO opengeni_app',
3657
+ data_schema
3658
+ );
3659
+ FOREACH table_name IN ARRAY ARRAY[
3660
+ 'editable_artifact_transactions',
3661
+ 'editable_artifact_operations',
3662
+ 'editable_artifact_idempotency_receipts',
3663
+ 'editable_artifact_undo_claims',
3664
+ 'editable_artifact_sequence_checkpoints',
3665
+ 'editable_artifact_blob_refs',
3666
+ 'editable_artifact_snapshots',
3667
+ 'editable_artifact_versions',
3668
+ 'editable_artifact_materialization_results'
3669
+ ] LOOP
3670
+ EXECUTE format(
3671
+ 'GRANT SELECT, INSERT ON TABLE %I.%I TO opengeni_app', data_schema, table_name
3672
+ );
3673
+ END LOOP;
3674
+ EXECUTE format(
3675
+ 'GRANT SELECT, INSERT ON TABLE %I.editable_artifact_materialization_jobs '
3676
+ || 'TO opengeni_app', data_schema
3677
+ );
3678
+ EXECUTE format(
3679
+ 'GRANT SELECT, INSERT ON TABLE %I.editable_artifact_live_outbox TO opengeni_app',
3680
+ data_schema
3681
+ );
3682
+ EXECUTE format(
3683
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.editable_artifact_replica_leases '
3684
+ || 'TO opengeni_app', data_schema
3685
+ );
3686
+ GRANT EXECUTE ON FUNCTION
3687
+ opengeni_private.ensure_editable_artifact_scope_authorization_head(
3688
+ uuid, uuid, name
3689
+ ) TO opengeni_app;
3690
+ GRANT EXECUTE ON FUNCTION
3691
+ opengeni_private.advance_editable_artifact_scope_authorization_revision(
3692
+ uuid, uuid, bigint, bigint, name
3693
+ ) TO opengeni_app;
3694
+ GRANT EXECUTE ON FUNCTION
3695
+ opengeni_private.advance_editable_artifact_authorization_revision(
3696
+ uuid, uuid, text, bigint, bigint, name
3697
+ ) TO opengeni_app;
3698
+ GRANT EXECUTE ON FUNCTION
3699
+ opengeni_private.editable_artifact_frontier_valid(jsonb)
3700
+ TO opengeni_app;
3701
+ GRANT EXECUTE ON FUNCTION
3702
+ opengeni_private.editable_artifact_id_array_valid(jsonb)
3703
+ TO opengeni_app;
3704
+ GRANT EXECUTE ON FUNCTION
3705
+ opengeni_private.editable_artifact_object_has_exact_keys(jsonb, text[])
3706
+ TO opengeni_app;
3707
+ GRANT EXECUTE ON FUNCTION
3708
+ opengeni_private.editable_artifact_actor_key_matches(
3709
+ text, text, text, text, text, text, integer, text
3710
+ ) TO opengeni_app;
3711
+ GRANT EXECUTE ON FUNCTION
3712
+ opengeni_private.editable_artifact_frontier_dominates(jsonb, jsonb)
3713
+ TO opengeni_app;
3714
+ GRANT EXECUTE ON FUNCTION
3715
+ opengeni_private.editable_artifact_frontier_merge_dot_equals(
3716
+ jsonb, jsonb, jsonb, text, bigint
3717
+ ) TO opengeni_app;
3718
+ REVOKE EXECUTE ON FUNCTION
3719
+ opengeni_private.claim_editable_artifact_live_outbox(
3720
+ text, integer, integer, name
3721
+ ) FROM opengeni_app;
3722
+ REVOKE EXECUTE ON FUNCTION
3723
+ opengeni_private.mark_editable_artifact_live_outbox_published(
3724
+ text, text, integer, name
3725
+ ) FROM opengeni_app;
3726
+ REVOKE EXECUTE ON FUNCTION
3727
+ opengeni_private.renew_editable_artifact_live_outbox(
3728
+ text, text, integer, integer, name
3729
+ ) FROM opengeni_app;
3730
+ REVOKE EXECUTE ON FUNCTION
3731
+ opengeni_private.retry_editable_artifact_live_outbox(
3732
+ text, text, integer, integer, text, name
3733
+ ) FROM opengeni_app;
3734
+ REVOKE EXECUTE ON FUNCTION
3735
+ opengeni_private.dead_letter_editable_artifact_live_outbox(
3736
+ text, text, integer, text, name
3737
+ ) FROM opengeni_app;
3738
+ REVOKE EXECUTE ON FUNCTION
3739
+ opengeni_private.release_editable_artifact_live_outbox(
3740
+ text, text, integer, name
3741
+ ) FROM opengeni_app;
3742
+ REVOKE EXECUTE ON FUNCTION
3743
+ opengeni_private.claim_editable_artifact_materializations(
3744
+ text, integer, integer, name
3745
+ ) FROM opengeni_app;
3746
+ REVOKE EXECUTE ON FUNCTION
3747
+ opengeni_private.renew_editable_artifact_materialization(
3748
+ uuid, uuid, text, text, text, integer, integer, name
3749
+ ) FROM opengeni_app;
3750
+ REVOKE EXECUTE ON FUNCTION
3751
+ opengeni_private.succeed_editable_artifact_materialization(
3752
+ uuid, uuid, text, text, text, integer, text, text, text, bigint,
3753
+ text, text, timestamptz, name
3754
+ ) FROM opengeni_app;
3755
+ REVOKE EXECUTE ON FUNCTION
3756
+ opengeni_private.fail_editable_artifact_materialization(
3757
+ uuid, uuid, text, text, text, integer, text, name
3758
+ ) FROM opengeni_app;
3759
+ END IF;
3760
+ END;
3761
+ $runtime_grants$;
3762
+
3763
+ DO $artifact_outbox_dispatcher_grants$
3764
+ DECLARE data_schema text := current_schema();
3765
+ BEGIN
3766
+ IF EXISTS (
3767
+ SELECT 1 FROM pg_catalog.pg_roles
3768
+ WHERE rolname = 'opengeni_artifact_outbox_dispatcher'
3769
+ ) THEN
3770
+ GRANT USAGE ON SCHEMA opengeni_private TO opengeni_artifact_outbox_dispatcher;
3771
+ EXECUTE format(
3772
+ 'GRANT USAGE ON SCHEMA %I TO opengeni_artifact_outbox_dispatcher', data_schema
3773
+ );
3774
+ EXECUTE format(
3775
+ 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %I '
3776
+ || 'FROM opengeni_artifact_outbox_dispatcher', data_schema
3777
+ );
3778
+ EXECUTE format(
3779
+ 'REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %I '
3780
+ || 'FROM opengeni_artifact_outbox_dispatcher', data_schema
3781
+ );
3782
+ REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA opengeni_private
3783
+ FROM opengeni_artifact_outbox_dispatcher;
3784
+ GRANT EXECUTE ON FUNCTION
3785
+ opengeni_private.claim_editable_artifact_live_outbox(text, integer, integer, name)
3786
+ TO opengeni_artifact_outbox_dispatcher;
3787
+ GRANT EXECUTE ON FUNCTION
3788
+ opengeni_private.mark_editable_artifact_live_outbox_published(
3789
+ text, text, integer, name
3790
+ ) TO opengeni_artifact_outbox_dispatcher;
3791
+ GRANT EXECUTE ON FUNCTION
3792
+ opengeni_private.renew_editable_artifact_live_outbox(
3793
+ text, text, integer, integer, name
3794
+ ) TO opengeni_artifact_outbox_dispatcher;
3795
+ GRANT EXECUTE ON FUNCTION
3796
+ opengeni_private.retry_editable_artifact_live_outbox(
3797
+ text, text, integer, integer, text, name
3798
+ ) TO opengeni_artifact_outbox_dispatcher;
3799
+ GRANT EXECUTE ON FUNCTION
3800
+ opengeni_private.dead_letter_editable_artifact_live_outbox(
3801
+ text, text, integer, text, name
3802
+ ) TO opengeni_artifact_outbox_dispatcher;
3803
+ GRANT EXECUTE ON FUNCTION
3804
+ opengeni_private.release_editable_artifact_live_outbox(text, text, integer, name)
3805
+ TO opengeni_artifact_outbox_dispatcher;
3806
+ END IF;
3807
+ END;
3808
+ $artifact_outbox_dispatcher_grants$;
3809
+
3810
+ DO $artifact_materializer_grants$
3811
+ DECLARE data_schema text := current_schema();
3812
+ BEGIN
3813
+ IF EXISTS (
3814
+ SELECT 1 FROM pg_catalog.pg_roles
3815
+ WHERE rolname = 'opengeni_artifact_materializer'
3816
+ ) THEN
3817
+ GRANT USAGE ON SCHEMA opengeni_private TO opengeni_artifact_materializer;
3818
+ EXECUTE format(
3819
+ 'GRANT USAGE ON SCHEMA %I TO opengeni_artifact_materializer', data_schema
3820
+ );
3821
+ EXECUTE format(
3822
+ 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %I '
3823
+ || 'FROM opengeni_artifact_materializer', data_schema
3824
+ );
3825
+ EXECUTE format(
3826
+ 'REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %I '
3827
+ || 'FROM opengeni_artifact_materializer', data_schema
3828
+ );
3829
+ REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA opengeni_private
3830
+ FROM opengeni_artifact_materializer;
3831
+ GRANT EXECUTE ON FUNCTION
3832
+ opengeni_private.claim_editable_artifact_materializations(
3833
+ text, integer, integer, name
3834
+ ) TO opengeni_artifact_materializer;
3835
+ GRANT EXECUTE ON FUNCTION
3836
+ opengeni_private.renew_editable_artifact_materialization(
3837
+ uuid, uuid, text, text, text, integer, integer, name
3838
+ ) TO opengeni_artifact_materializer;
3839
+ GRANT EXECUTE ON FUNCTION
3840
+ opengeni_private.succeed_editable_artifact_materialization(
3841
+ uuid, uuid, text, text, text, integer, text, text, text, bigint,
3842
+ text, text, timestamptz, name
3843
+ ) TO opengeni_artifact_materializer;
3844
+ GRANT EXECUTE ON FUNCTION
3845
+ opengeni_private.fail_editable_artifact_materialization(
3846
+ uuid, uuid, text, text, text, integer, text, name
3847
+ ) TO opengeni_artifact_materializer;
3848
+ END IF;
3849
+ END;
3850
+ $artifact_materializer_grants$;
3851
+
3852
+ RESET statement_timeout;
3853
+ RESET lock_timeout;