@supacloud/lite 0.37.0 → 0.37.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.37.2](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.37.1...supacloud-lite-v0.37.2) (2026-11-03)
4
+
5
+
6
+ ### Elegance & Refactoring
7
+
8
+ * **commands:** name command and artifact row columns ([#2418](https://github.com/vibeunion/supacloud/issues/2418)) ([7da4967](https://github.com/vibeunion/supacloud/commit/7da496706a152a3c3cbe7529d46ab4ac6c6b2bf0))
9
+ * **workflows:** name workflow row columns ([#2417](https://github.com/vibeunion/supacloud/issues/2417)) ([5e3e9c2](https://github.com/vibeunion/supacloud/commit/5e3e9c20141e0e47ac939888b18cce7f938fabc1))
10
+
11
+ ## [0.37.1](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.37.0...supacloud-lite-v0.37.1) (2026-11-02)
12
+
13
+
14
+ ### Miscellaneous Chores
15
+
16
+ * **deps:** upgrade elysia to 2.0.0-rc.1 ([#2347](https://github.com/vibeunion/supacloud/issues/2347)) ([b5539bc](https://github.com/vibeunion/supacloud/commit/b5539bc4c839fb319f5f073cc1dfbebe3487ff17))
17
+
3
18
  ## [0.37.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.36.0...supacloud-lite-v0.37.0) (2026-11-01)
4
19
 
5
20
 
package/dist/cli.js CHANGED
@@ -12010,7 +12010,7 @@ import { dirname as dirname9, join as join11, resolve as resolve8 } from "path";
12010
12010
  // package.json
12011
12011
  var package_default = {
12012
12012
  name: "@supacloud/lite",
12013
- version: "0.37.0",
12013
+ version: "0.37.2",
12014
12014
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
12015
12015
  type: "module",
12016
12016
  license: "Apache-2.0",
@@ -12070,29 +12070,29 @@ var package_default = {
12070
12070
  "@electric-sql/pglite-pgmq": "0.0.8",
12071
12071
  "@electric-sql/pglite-pgvector": "0.0.11",
12072
12072
  "@electric-sql/pglite-postgis": "0.2.10",
12073
- "@supacloud/db": "0.20.3",
12073
+ "@supacloud/db": "0.20.5",
12074
12074
  "exact-mirror": "1.2.6",
12075
12075
  "fast-xml-parser": "5.11.2",
12076
12076
  tar: "^7.5.22",
12077
12077
  typebox: "1.4.3"
12078
12078
  },
12079
12079
  devDependencies: {
12080
- "@supacloud/app": "0.35.0",
12080
+ "@supacloud/app": "0.36.0",
12081
12081
  "@supacloud/contracts": "0.15.2",
12082
- "@supacloud/compiler": "0.53.0",
12083
- "@supacloud/elysia": "0.37.0",
12082
+ "@supacloud/compiler": "0.55.0",
12083
+ "@supacloud/elysia": "0.38.1",
12084
12084
  "@supabase/supabase-js": "^2.118.0",
12085
12085
  "@types/bun": "^1.4.4",
12086
12086
  effect: "4.0.5",
12087
- elysia: "2.0.0-rc.0",
12087
+ elysia: "2.0.0-rc.1",
12088
12088
  jose: "^6.2.13",
12089
12089
  typescript: "^7.0.2"
12090
12090
  },
12091
12091
  overrides: {
12092
- "@supacloud/db": "0.20.3",
12093
- "@supacloud/app": "0.35.0",
12094
- "@supacloud/compiler": "0.53.0",
12095
- "@supacloud/elysia": "0.37.0",
12092
+ "@supacloud/db": "0.20.5",
12093
+ "@supacloud/app": "0.36.0",
12094
+ "@supacloud/compiler": "0.55.0",
12095
+ "@supacloud/elysia": "0.38.1",
12096
12096
  "@supacloud/contracts": "0.15.2",
12097
12097
  "@supacloud/commands": "0.11.2",
12098
12098
  "@supacloud/delivery": "0.24.0",
@@ -19014,7 +19014,7 @@ BEGIN
19014
19014
  id, run_id, step_key, input, max_attempts, queue_message_id
19015
19015
  ) VALUES (
19016
19016
  step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
19017
- ) RETURNING * INTO created_step;
19017
+ ) RETURNING id, run_id, step_key, status, input, output, error_message, attempts, max_attempts, retry_delay_seconds, queue_message_id, claimed_by, claimed_at, completed_at, next_step_key, created_at, updated_at INTO created_step;
19018
19018
 
19019
19019
  RETURN created_step;
19020
19020
  END;
@@ -19054,11 +19054,11 @@ BEGIN
19054
19054
  END IF;
19055
19055
 
19056
19056
  PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
19057
- SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
19057
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO existing_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id;
19058
19058
  IF FOUND THEN
19059
- SELECT * INTO existing_step
19060
- FROM supacloud_workflows.steps
19061
- WHERE run_id = p_run_id
19059
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO existing_step
19060
+ FROM supacloud_workflows.steps workflow_step
19061
+ WHERE workflow_step.run_id = p_run_id
19062
19062
  ORDER BY created_at, id
19063
19063
  LIMIT 1;
19064
19064
  IF NOT FOUND THEN
@@ -19146,11 +19146,11 @@ BEGIN
19146
19146
  IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
19147
19147
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
19148
19148
  END IF;
19149
- SELECT * INTO claimed_step
19150
- FROM supacloud_workflows.steps
19151
- WHERE id::text = lower(message_step_id)
19152
- AND run_id = candidate_run_id
19153
- AND queue_message_id = queued_message.msg_id
19149
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO claimed_step
19150
+ FROM supacloud_workflows.steps workflow_step
19151
+ WHERE workflow_step.id::text = lower(message_step_id)
19152
+ AND workflow_step.run_id = candidate_run_id
19153
+ AND workflow_step.queue_message_id = queued_message.msg_id
19154
19154
  FOR UPDATE;
19155
19155
  IF NOT FOUND THEN
19156
19156
  PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
@@ -19161,9 +19161,9 @@ BEGIN
19161
19161
  );
19162
19162
  END IF;
19163
19163
 
19164
- SELECT * INTO claimed_run
19165
- FROM supacloud_workflows.runs
19166
- WHERE id = claimed_step.run_id
19164
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO claimed_run
19165
+ FROM supacloud_workflows.runs workflow_run
19166
+ WHERE workflow_run.id = claimed_step.run_id
19167
19167
  FOR UPDATE;
19168
19168
  IF claimed_run.status NOT IN ('queued', 'running')
19169
19169
  OR claimed_step.status NOT IN ('queued', 'running') THEN
@@ -19250,9 +19250,9 @@ BEGIN
19250
19250
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
19251
19251
  END IF;
19252
19252
  PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
19253
- SELECT * INTO active_step
19254
- FROM supacloud_workflows.steps
19255
- WHERE id = p_step_id
19253
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
19254
+ FROM supacloud_workflows.steps workflow_step
19255
+ WHERE workflow_step.id = p_step_id
19256
19256
  FOR UPDATE;
19257
19257
  IF NOT FOUND THEN
19258
19258
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
@@ -19314,9 +19314,9 @@ BEGIN
19314
19314
  );
19315
19315
 
19316
19316
  IF current_step.status = 'completed' THEN
19317
- SELECT * INTO next_step
19318
- FROM supacloud_workflows.steps
19319
- WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
19317
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO next_step
19318
+ FROM supacloud_workflows.steps workflow_step
19319
+ WHERE workflow_step.run_id = current_step.run_id AND workflow_step.step_key = normalized_next_step_key;
19320
19320
  IF NOT FOUND
19321
19321
  OR current_step.output IS DISTINCT FROM p_output
19322
19322
  OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
@@ -19372,9 +19372,9 @@ BEGIN
19372
19372
  current_step := supacloud_workflows.lock_step_attempt(
19373
19373
  p_step_id, p_message_id, p_attempt, p_worker_id
19374
19374
  );
19375
- SELECT * INTO current_run
19376
- FROM supacloud_workflows.runs
19377
- WHERE id = current_step.run_id
19375
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
19376
+ FROM supacloud_workflows.runs workflow_run
19377
+ WHERE workflow_run.id = current_step.run_id
19378
19378
  FOR UPDATE;
19379
19379
 
19380
19380
  IF current_step.status = 'completed' THEN
@@ -19550,9 +19550,9 @@ BEGIN
19550
19550
  current_step := supacloud_workflows.lock_step_attempt(
19551
19551
  p_step_id, p_message_id, p_attempt, p_worker_id
19552
19552
  );
19553
- SELECT * INTO current_run
19554
- FROM supacloud_workflows.runs
19555
- WHERE id = current_step.run_id
19553
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
19554
+ FROM supacloud_workflows.runs workflow_run
19555
+ WHERE workflow_run.id = current_step.run_id
19556
19556
  FOR UPDATE;
19557
19557
 
19558
19558
  IF current_step.status = 'failed' THEN
@@ -19606,7 +19606,7 @@ BEGIN
19606
19606
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
19607
19607
  END IF;
19608
19608
  PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
19609
- SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
19609
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO locked_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id FOR UPDATE;
19610
19610
  IF NOT FOUND THEN
19611
19611
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
19612
19612
  END IF;
@@ -19619,9 +19619,9 @@ BEGIN
19619
19619
  IF locked_run.status NOT IN ('queued', 'running') THEN
19620
19620
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
19621
19621
  END IF;
19622
- SELECT * INTO active_step
19623
- FROM supacloud_workflows.steps
19624
- WHERE run_id = p_run_id AND status IN ('queued', 'running')
19622
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
19623
+ FROM supacloud_workflows.steps workflow_step
19624
+ WHERE workflow_step.run_id = p_run_id AND workflow_step.status IN ('queued', 'running')
19625
19625
  FOR UPDATE;
19626
19626
  IF FOUND THEN
19627
19627
  PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
@@ -20016,7 +20016,7 @@ BEGIN
20016
20016
 
20017
20017
  fingerprint := 'md5:' || md5(payload::text);
20018
20018
  PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));
20019
- SELECT * INTO existing FROM supacloud_commands.receipts WHERE id = command_id;
20019
+ SELECT receipt.id, receipt.command_type, receipt.target_type, receipt.target_id, receipt.actor_id, receipt.payload, receipt.payload_fingerprint, receipt.workflow_run_id, receipt.created_at, receipt.tenant_id INTO existing FROM supacloud_commands.receipts receipt WHERE receipt.id = command_id;
20020
20020
  IF FOUND THEN
20021
20021
  IF existing.command_type <> normalized_command_type
20022
20022
  OR existing.target_type <> normalized_target_type
@@ -20296,7 +20296,7 @@ BEGIN
20296
20296
  END IF;
20297
20297
 
20298
20298
  PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));
20299
- SELECT * INTO existing FROM supacloud_artifacts.artifacts WHERE id = artifact_id;
20299
+ SELECT artifact.id, artifact.storage_object_id, artifact.bucket_id, artifact.object_path, artifact.object_version, artifact.artifact_type, artifact.sha256, artifact.size_bytes, artifact.mime_type, artifact.metadata, artifact.retention_until, artifact.created_by, artifact.created_at INTO existing FROM supacloud_artifacts.artifacts artifact WHERE artifact.id = artifact_id;
20300
20300
  IF FOUND THEN
20301
20301
  IF existing.storage_object_id <> object_row.id
20302
20302
  OR existing.artifact_type <> normalized_type
@@ -33055,7 +33055,8 @@ ${COMMAND_PERSISTENCE_SQL}
33055
33055
  DO $backfill$
33056
33056
  DECLARE r record;
33057
33057
  BEGIN
33058
- FOR r IN SELECT * FROM supacloud_commands.execution_receipts
33058
+ FOR r IN SELECT dispatch_key, tenant_id, actor_id, command, operation_key
33059
+ FROM supacloud_commands.execution_receipts
33059
33060
  WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
33060
33061
  AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
33061
33062
  LOOP
@@ -35607,7 +35608,7 @@ async function readPassword() {
35607
35608
  var SUPABASE_JS_PIN = "2.118.0";
35608
35609
  var SUPABASE_CLI_PIN = "2.122.0";
35609
35610
  var REALTIME_IMAGE_TAG = "v2.150.0";
35610
- var EDGE_RUNTIME_PACKAGE_VERSION = "0.37.2";
35611
+ var EDGE_RUNTIME_PACKAGE_VERSION = "0.37.3";
35611
35612
  async function readInstalledSupabaseJsVersion() {
35612
35613
  try {
35613
35614
  const manifest = await Promise.resolve().then(() => __toESM(require_package(), 1));
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ function randomToken(bytes = 32) {
88
88
  // package.json
89
89
  var package_default = {
90
90
  name: "@supacloud/lite",
91
- version: "0.37.0",
91
+ version: "0.37.2",
92
92
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
93
93
  type: "module",
94
94
  license: "Apache-2.0",
@@ -148,29 +148,29 @@ var package_default = {
148
148
  "@electric-sql/pglite-pgmq": "0.0.8",
149
149
  "@electric-sql/pglite-pgvector": "0.0.11",
150
150
  "@electric-sql/pglite-postgis": "0.2.10",
151
- "@supacloud/db": "0.20.3",
151
+ "@supacloud/db": "0.20.5",
152
152
  "exact-mirror": "1.2.6",
153
153
  "fast-xml-parser": "5.11.2",
154
154
  tar: "^7.5.22",
155
155
  typebox: "1.4.3"
156
156
  },
157
157
  devDependencies: {
158
- "@supacloud/app": "0.35.0",
158
+ "@supacloud/app": "0.36.0",
159
159
  "@supacloud/contracts": "0.15.2",
160
- "@supacloud/compiler": "0.53.0",
161
- "@supacloud/elysia": "0.37.0",
160
+ "@supacloud/compiler": "0.55.0",
161
+ "@supacloud/elysia": "0.38.1",
162
162
  "@supabase/supabase-js": "^2.118.0",
163
163
  "@types/bun": "^1.4.4",
164
164
  effect: "4.0.5",
165
- elysia: "2.0.0-rc.0",
165
+ elysia: "2.0.0-rc.1",
166
166
  jose: "^6.2.13",
167
167
  typescript: "^7.0.2"
168
168
  },
169
169
  overrides: {
170
- "@supacloud/db": "0.20.3",
171
- "@supacloud/app": "0.35.0",
172
- "@supacloud/compiler": "0.53.0",
173
- "@supacloud/elysia": "0.37.0",
170
+ "@supacloud/db": "0.20.5",
171
+ "@supacloud/app": "0.36.0",
172
+ "@supacloud/compiler": "0.55.0",
173
+ "@supacloud/elysia": "0.38.1",
174
174
  "@supacloud/contracts": "0.15.2",
175
175
  "@supacloud/commands": "0.11.2",
176
176
  "@supacloud/delivery": "0.24.0",
@@ -7018,7 +7018,7 @@ BEGIN
7018
7018
  id, run_id, step_key, input, max_attempts, queue_message_id
7019
7019
  ) VALUES (
7020
7020
  step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
7021
- ) RETURNING * INTO created_step;
7021
+ ) RETURNING id, run_id, step_key, status, input, output, error_message, attempts, max_attempts, retry_delay_seconds, queue_message_id, claimed_by, claimed_at, completed_at, next_step_key, created_at, updated_at INTO created_step;
7022
7022
 
7023
7023
  RETURN created_step;
7024
7024
  END;
@@ -7058,11 +7058,11 @@ BEGIN
7058
7058
  END IF;
7059
7059
 
7060
7060
  PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
7061
- SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
7061
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO existing_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id;
7062
7062
  IF FOUND THEN
7063
- SELECT * INTO existing_step
7064
- FROM supacloud_workflows.steps
7065
- WHERE run_id = p_run_id
7063
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO existing_step
7064
+ FROM supacloud_workflows.steps workflow_step
7065
+ WHERE workflow_step.run_id = p_run_id
7066
7066
  ORDER BY created_at, id
7067
7067
  LIMIT 1;
7068
7068
  IF NOT FOUND THEN
@@ -7150,11 +7150,11 @@ BEGIN
7150
7150
  IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
7151
7151
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
7152
7152
  END IF;
7153
- SELECT * INTO claimed_step
7154
- FROM supacloud_workflows.steps
7155
- WHERE id::text = lower(message_step_id)
7156
- AND run_id = candidate_run_id
7157
- AND queue_message_id = queued_message.msg_id
7153
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO claimed_step
7154
+ FROM supacloud_workflows.steps workflow_step
7155
+ WHERE workflow_step.id::text = lower(message_step_id)
7156
+ AND workflow_step.run_id = candidate_run_id
7157
+ AND workflow_step.queue_message_id = queued_message.msg_id
7158
7158
  FOR UPDATE;
7159
7159
  IF NOT FOUND THEN
7160
7160
  PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
@@ -7165,9 +7165,9 @@ BEGIN
7165
7165
  );
7166
7166
  END IF;
7167
7167
 
7168
- SELECT * INTO claimed_run
7169
- FROM supacloud_workflows.runs
7170
- WHERE id = claimed_step.run_id
7168
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO claimed_run
7169
+ FROM supacloud_workflows.runs workflow_run
7170
+ WHERE workflow_run.id = claimed_step.run_id
7171
7171
  FOR UPDATE;
7172
7172
  IF claimed_run.status NOT IN ('queued', 'running')
7173
7173
  OR claimed_step.status NOT IN ('queued', 'running') THEN
@@ -7254,9 +7254,9 @@ BEGIN
7254
7254
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
7255
7255
  END IF;
7256
7256
  PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
7257
- SELECT * INTO active_step
7258
- FROM supacloud_workflows.steps
7259
- WHERE id = p_step_id
7257
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
7258
+ FROM supacloud_workflows.steps workflow_step
7259
+ WHERE workflow_step.id = p_step_id
7260
7260
  FOR UPDATE;
7261
7261
  IF NOT FOUND THEN
7262
7262
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
@@ -7318,9 +7318,9 @@ BEGIN
7318
7318
  );
7319
7319
 
7320
7320
  IF current_step.status = 'completed' THEN
7321
- SELECT * INTO next_step
7322
- FROM supacloud_workflows.steps
7323
- WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
7321
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO next_step
7322
+ FROM supacloud_workflows.steps workflow_step
7323
+ WHERE workflow_step.run_id = current_step.run_id AND workflow_step.step_key = normalized_next_step_key;
7324
7324
  IF NOT FOUND
7325
7325
  OR current_step.output IS DISTINCT FROM p_output
7326
7326
  OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
@@ -7376,9 +7376,9 @@ BEGIN
7376
7376
  current_step := supacloud_workflows.lock_step_attempt(
7377
7377
  p_step_id, p_message_id, p_attempt, p_worker_id
7378
7378
  );
7379
- SELECT * INTO current_run
7380
- FROM supacloud_workflows.runs
7381
- WHERE id = current_step.run_id
7379
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
7380
+ FROM supacloud_workflows.runs workflow_run
7381
+ WHERE workflow_run.id = current_step.run_id
7382
7382
  FOR UPDATE;
7383
7383
 
7384
7384
  IF current_step.status = 'completed' THEN
@@ -7554,9 +7554,9 @@ BEGIN
7554
7554
  current_step := supacloud_workflows.lock_step_attempt(
7555
7555
  p_step_id, p_message_id, p_attempt, p_worker_id
7556
7556
  );
7557
- SELECT * INTO current_run
7558
- FROM supacloud_workflows.runs
7559
- WHERE id = current_step.run_id
7557
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run
7558
+ FROM supacloud_workflows.runs workflow_run
7559
+ WHERE workflow_run.id = current_step.run_id
7560
7560
  FOR UPDATE;
7561
7561
 
7562
7562
  IF current_step.status = 'failed' THEN
@@ -7610,7 +7610,7 @@ BEGIN
7610
7610
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
7611
7611
  END IF;
7612
7612
  PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
7613
- SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
7613
+ SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO locked_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id FOR UPDATE;
7614
7614
  IF NOT FOUND THEN
7615
7615
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
7616
7616
  END IF;
@@ -7623,9 +7623,9 @@ BEGIN
7623
7623
  IF locked_run.status NOT IN ('queued', 'running') THEN
7624
7624
  RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
7625
7625
  END IF;
7626
- SELECT * INTO active_step
7627
- FROM supacloud_workflows.steps
7628
- WHERE run_id = p_run_id AND status IN ('queued', 'running')
7626
+ SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step
7627
+ FROM supacloud_workflows.steps workflow_step
7628
+ WHERE workflow_step.run_id = p_run_id AND workflow_step.status IN ('queued', 'running')
7629
7629
  FOR UPDATE;
7630
7630
  IF FOUND THEN
7631
7631
  PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
@@ -8020,7 +8020,7 @@ BEGIN
8020
8020
 
8021
8021
  fingerprint := 'md5:' || md5(payload::text);
8022
8022
  PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));
8023
- SELECT * INTO existing FROM supacloud_commands.receipts WHERE id = command_id;
8023
+ SELECT receipt.id, receipt.command_type, receipt.target_type, receipt.target_id, receipt.actor_id, receipt.payload, receipt.payload_fingerprint, receipt.workflow_run_id, receipt.created_at, receipt.tenant_id INTO existing FROM supacloud_commands.receipts receipt WHERE receipt.id = command_id;
8024
8024
  IF FOUND THEN
8025
8025
  IF existing.command_type <> normalized_command_type
8026
8026
  OR existing.target_type <> normalized_target_type
@@ -8300,7 +8300,7 @@ BEGIN
8300
8300
  END IF;
8301
8301
 
8302
8302
  PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));
8303
- SELECT * INTO existing FROM supacloud_artifacts.artifacts WHERE id = artifact_id;
8303
+ SELECT artifact.id, artifact.storage_object_id, artifact.bucket_id, artifact.object_path, artifact.object_version, artifact.artifact_type, artifact.sha256, artifact.size_bytes, artifact.mime_type, artifact.metadata, artifact.retention_until, artifact.created_by, artifact.created_at INTO existing FROM supacloud_artifacts.artifacts artifact WHERE artifact.id = artifact_id;
8304
8304
  IF FOUND THEN
8305
8305
  IF existing.storage_object_id <> object_row.id
8306
8306
  OR existing.artifact_type <> normalized_type
@@ -21694,7 +21694,8 @@ ${COMMAND_PERSISTENCE_SQL}
21694
21694
  DO $backfill$
21695
21695
  DECLARE r record;
21696
21696
  BEGIN
21697
- FOR r IN SELECT * FROM supacloud_commands.execution_receipts
21697
+ FOR r IN SELECT dispatch_key, tenant_id, actor_id, command, operation_key
21698
+ FROM supacloud_commands.execution_receipts
21698
21699
  WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
21699
21700
  AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
21700
21701
  LOOP
@@ -19,7 +19,7 @@ export declare const PGMQ_CORE_SQL = "\ncreate schema if not exists pgmq;\nrevok
19
19
  * extension supplies the pgmq schema; this SQL only owns SupaCloud wrappers. */
20
20
  export declare const PGMQ_PUBLIC_SQL = "\n-- supacloud:sql-module:pgmq-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\n\nCREATE SCHEMA IF NOT EXISTS pgmq_public;\nGRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;\nCREATE SCHEMA IF NOT EXISTS supacloud_queue;\nCREATE TABLE IF NOT EXISTS supacloud_queue.job_keys (\n queue_name text NOT NULL,\n job_key text NOT NULL,\n msg_id bigint NOT NULL,\n message jsonb NOT NULL,\n sleep_seconds integer NOT NULL,\n created_at timestamptz NOT NULL DEFAULT clock_timestamp(),\n PRIMARY KEY (queue_name, job_key)\n);\n-- Upgrade early key-only installations without inventing an input identity.\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS message jsonb;\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS sleep_seconds integer;\nREVOKE ALL ON SCHEMA supacloud_queue FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON TABLE supacloud_queue.job_keys FROM PUBLIC, anon, authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)\nRETURNS text\nLANGUAGE plpgsql\nIMMUTABLE\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue_name text := lower(btrim(queue_name));\nBEGIN\n IF left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';\n END IF;\n IF queue_name IS NULL OR queue_name !~ '^[a-z0-9][a-z0-9_-]{0,127}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN queue_name;\nEND;\n$$;\n\n-- Return-type changes require replacement. Do not CASCADE through user dependencies.\nDO $pgmq_receipt_types$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send(text,jsonb,integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send(text,jsonb,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send_batch(text,jsonb[],integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send_batch(text,jsonb[],integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.read(text,integer,integer)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.read(text,integer,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.pop(text)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.pop(text);\n END IF;\nEND\n$pgmq_receipt_types$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_seconds(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_SECONDS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_read_count(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 OR value > 10000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_COUNT_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message_id(value bigint)\nRETURNS bigint LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_ID_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.message_node_count(value jsonb)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n node_count integer;\n maximum_depth integer;\nBEGIN\n IF value IS NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF octet_length(convert_to(value::text, 'UTF8')) > 1048576 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n WITH RECURSIVE tree(item, depth) AS (\n SELECT value, 0\n UNION ALL\n SELECT child.item, tree.depth + 1\n FROM tree\n CROSS JOIN LATERAL (\n SELECT element AS item\n FROM jsonb_array_elements(CASE WHEN jsonb_typeof(tree.item) = 'array' THEN tree.item ELSE '[]'::jsonb END) AS element\n UNION ALL\n SELECT entry.value AS item\n FROM jsonb_each(CASE WHEN jsonb_typeof(tree.item) = 'object' THEN tree.item ELSE '{}'::jsonb END) AS entry\n ) AS child\n WHERE tree.depth < 65\n )\n SELECT count(*)::integer, max(depth) INTO node_count, maximum_depth\n FROM (SELECT depth FROM tree LIMIT 10001) AS bounded;\n IF node_count > 10000 OR maximum_depth > 64 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n RETURN node_count;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message(value jsonb)\nRETURNS jsonb LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n PERFORM pgmq_public.message_node_count(value);\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_messages(values_ jsonb[])\nRETURNS jsonb[] LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n item jsonb;\n total_bytes bigint := 0;\n total_nodes integer := 0;\nBEGIN\n IF values_ IS NULL OR cardinality(values_) < 1 OR cardinality(values_) > 10000\n OR array_ndims(values_) <> 1 OR array_lower(values_, 1) <> 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_BATCH_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH item IN ARRAY values_ LOOP\n total_nodes := total_nodes + pgmq_public.message_node_count(item);\n IF total_nodes > 100000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n total_bytes := total_bytes + octet_length(convert_to(item::text, 'UTF8'));\n IF total_bytes > 8388608 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n RETURN values_;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message(message), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), pgmq_public.require_messages(messages), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\n-- Additive SupaCloud extension; official send/read/archive/delete contracts stay unchanged.\nCREATE OR REPLACE FUNCTION pgmq_public.send_idempotent(\n queue_name text, message jsonb, p_job_key text, sleep_seconds integer DEFAULT 0\n)\nRETURNS TABLE (msg_id text, created boolean)\nLANGUAGE plpgsql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue text := pgmq_public.require_public_queue(queue_name);\n captured_message jsonb := pgmq_public.require_message(message);\n captured_delay integer := pgmq_public.require_seconds(sleep_seconds);\n inserted boolean;\n sent_id bigint;\n existing_id bigint;\n existing_message jsonb;\n existing_delay integer;\nBEGIN\n IF p_job_key IS NULL OR p_job_key !~ '^[A-Za-z0-9_.:@/-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_INVALID' USING ERRCODE = '22023';\n END IF;\n INSERT INTO supacloud_queue.job_keys(queue_name, job_key, msg_id, message, sleep_seconds)\n VALUES (normalized_queue, p_job_key, 0, captured_message, captured_delay)\n ON CONFLICT ON CONSTRAINT job_keys_pkey DO NOTHING;\n inserted := FOUND;\n IF NOT inserted THEN\n SELECT keys.msg_id, keys.message, keys.sleep_seconds\n INTO existing_id, existing_message, existing_delay\n FROM supacloud_queue.job_keys AS keys\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n IF existing_id IS NULL OR existing_id < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_UNAVAILABLE' USING ERRCODE = '55000';\n END IF;\n IF existing_message IS DISTINCT FROM captured_message OR existing_delay IS DISTINCT FROM captured_delay THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_CONFLICT' USING ERRCODE = '22023';\n END IF;\n -- This is a durable enqueue receipt, not evidence that the message is still pending.\n RETURN QUERY SELECT existing_id::text, false;\n RETURN;\n END IF;\n SELECT receipt.msg_id INTO STRICT sent_id\n FROM pgmq.send(normalized_queue, captured_message, captured_delay) AS receipt(msg_id);\n PERFORM pgmq_public.require_message_id(sent_id);\n UPDATE supacloud_queue.job_keys AS keys SET msg_id = sent_id\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n RETURN QUERY SELECT sent_id::text, true;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.read(pgmq_public.require_public_queue(queue_name),\n pgmq_public.require_seconds(sleep_seconds), pgmq_public.require_read_count(n)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.\"delete\"(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;\nGRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;\n\nNOTIFY pgrst, 'reload schema';\n-- supacloud:sql-module:pgmq-public:end\n";
21
21
  export declare const PGMQ_SQL = "\ncreate schema if not exists pgmq;\nrevoke all on schema pgmq from public, anon, authenticated;\ngrant usage on schema pgmq to service_role;\n\ncreate table if not exists pgmq.meta (\n queue_name text primary key,\n physical_name text not null unique,\n is_partitioned boolean not null default false,\n is_unlogged boolean not null default false,\n created_at timestamptz not null default now()\n);\n\n-- Preserve queues created by older Lite versions. Their physical names were\n-- the queue names, so only the already-truncated identifier can be recovered.\ninsert into pgmq.meta (queue_name, physical_name)\nselect substring(tablename from 3), substring(tablename from 3)\nfrom pg_tables\nwhere schemaname = 'pgmq' and left(tablename, 2) = 'q_'\non conflict do nothing;\n\ndo $$ begin\n if not exists (select 1 from pg_type t join pg_namespace n on n.oid = t.typnamespace\n where t.typname = 'message_record' and n.nspname = 'pgmq') then\n create type pgmq.message_record as (\n msg_id bigint, read_ct integer, enqueued_at timestamptz, vt timestamptz, message jsonb\n );\n end if;\nend $$;\n\ncreate or replace function pgmq._normalize_queue_name(queue_name text)\nreturns text language plpgsql immutable set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := btrim(queue_name);\nbegin\n if normalized is null or normalized !~ '^[a-z0-9][a-z0-9_-]{0,127}$' then\n raise exception 'invalid queue name: must match ^[a-z0-9][a-z0-9_-]{0,127}$'\n using errcode = '22023';\n end if;\n return normalized;\nend $pgmq$;\n\n-- PostgreSQL identifiers are limited to 63 bytes. Keep the conventional table\n-- name for short queues and add a deterministic hash for valid 62-128 byte\n-- public names so truncation can never merge two queues.\ncreate or replace function pgmq._physical_queue_name(queue_name text)\nreturns text language sql immutable set search_path = pgmq, pg_catalog, public as $pgmq$\n select case when length(normalized) <= 61 then normalized\n else left(normalized, 28) || '_' || md5(normalized) end\n from (select pgmq._normalize_queue_name(queue_name) as normalized) names;\n$pgmq$;\n\ncreate or replace function pgmq._resolve_queue(queue_name text)\nreturns text language plpgsql stable security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := pgmq._normalize_queue_name(queue_name); physical text;\nbegin\n select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;\n if physical is null then\n raise exception 'queue \"%\" does not exist', normalized using errcode = '42P01';\n end if;\n return physical;\nend $pgmq$;\n\ncreate or replace function pgmq.create(queue_name text) returns void language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := pgmq._normalize_queue_name(queue_name); physical text := pgmq._physical_queue_name(queue_name);\nbegin\n insert into pgmq.meta (queue_name, physical_name) values (normalized, physical)\n on conflict do nothing;\n select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;\n execute format('create table if not exists pgmq.%I (\n msg_id bigint generated always as identity primary key,\n read_ct integer not null default 0,\n enqueued_at timestamptz not null default now(),\n vt timestamptz not null default now(),\n message jsonb)', 'q_' || physical);\n execute format('create table if not exists pgmq.%I (\n msg_id bigint primary key, read_ct integer not null default 0,\n enqueued_at timestamptz not null, archived_at timestamptz not null default now(),\n vt timestamptz, message jsonb)', 'a_' || physical);\nend $pgmq$;\n\ncreate or replace function pgmq.send(queue_name text, msg jsonb, delay integer default 0)\nreturns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare id bigint; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format('insert into pgmq.%I (vt, message) values (now() + make_interval(secs => $1), $2) returning msg_id', 'q_' || physical)\n into id using delay, msg;\n return id;\nend $pgmq$;\n\ncreate or replace function pgmq.send_batch(queue_name text, msgs jsonb[], delay integer default 0)\nreturns setof bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare queue_message jsonb;\nbegin\n perform pgmq._resolve_queue(queue_name);\n foreach queue_message in array msgs loop\n return next pgmq.send(queue_name, queue_message, delay);\n end loop;\nend $pgmq$;\n\ncreate or replace function pgmq.read(queue_name text, vt integer, qty integer)\nreturns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare physical text := pgmq._resolve_queue(queue_name);\nbegin\n return query execute format($fmt$\n with cte as (\n select msg_id from pgmq.%I where vt <= now() order by msg_id limit $1 for update skip locked\n )\n update pgmq.%I m set vt = now() + make_interval(secs => $2), read_ct = read_ct + 1\n from cte where m.msg_id = cte.msg_id\n returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message\n $fmt$, 'q_' || physical, 'q_' || physical) using qty, vt;\nend $pgmq$;\n\ncreate or replace function pgmq.pop(queue_name text)\nreturns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare physical text := pgmq._resolve_queue(queue_name);\nbegin\n return query execute format($fmt$\n with cte as (select msg_id from pgmq.%I where vt <= now() order by msg_id limit 1 for update skip locked)\n delete from pgmq.%I m using cte where m.msg_id = cte.msg_id\n returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message\n $fmt$, 'q_' || physical, 'q_' || physical);\nend $pgmq$;\n\ncreate or replace function pgmq.set_vt(queue_name text, msg_id bigint, vt_offset integer)\nreturns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare physical text := pgmq._resolve_queue(queue_name);\nbegin\n return query execute format($fmt$\n update pgmq.%I m set vt = now() + make_interval(secs => $1)\n where m.msg_id = $2\n returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message\n $fmt$, 'q_' || physical) using vt_offset, msg_id;\nend $pgmq$;\n\ncreate or replace function pgmq.delete(queue_name text, msg_id bigint)\nreturns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare n integer; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format('delete from pgmq.%I where msg_id = $1', 'q_' || physical) using msg_id;\n get diagnostics n = row_count;\n return n > 0;\nend $pgmq$;\n\ncreate or replace function pgmq.archive(queue_name text, msg_id bigint)\nreturns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare n integer; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format($fmt$\n with del as (delete from pgmq.%I where msg_id = $1 returning *)\n insert into pgmq.%I (msg_id, read_ct, enqueued_at, vt, message)\n select msg_id, read_ct, enqueued_at, vt, message from del\n $fmt$, 'q_' || physical, 'a_' || physical) using msg_id;\n get diagnostics n = row_count;\n return n > 0;\nend $pgmq$;\n\ncreate or replace function pgmq.drop_queue(queue_name text) returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare normalized text := pgmq._normalize_queue_name(queue_name); physical text;\nbegin\n select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;\n if physical is null then return false; end if;\n execute format('drop table if exists pgmq.%I', 'q_' || physical);\n execute format('drop table if exists pgmq.%I', 'a_' || physical);\n delete from pgmq.meta where pgmq.meta.queue_name = normalized;\n return true;\nend $pgmq$;\n\ncreate or replace function pgmq.purge_queue(queue_name text) returns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\ndeclare n bigint; physical text := pgmq._resolve_queue(queue_name);\nbegin\n execute format('delete from pgmq.%I', 'q_' || physical);\n get diagnostics n = row_count;\n return n;\nend $pgmq$;\n\ncreate or replace function pgmq.list_queues()\nreturns table(queue_name text, is_partitioned boolean, is_unlogged boolean, created_at timestamptz)\nlanguage sql security definer set search_path = pgmq, pg_catalog, public as $pgmq$\n select queue_name, is_partitioned, is_unlogged, created_at from pgmq.meta;\n$pgmq$;\n\nrevoke all on all functions in schema pgmq from public, anon, authenticated;\ngrant execute on all functions in schema pgmq to service_role;\n\n\n\n-- supacloud:sql-module:pgmq-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\n\nCREATE SCHEMA IF NOT EXISTS pgmq_public;\nGRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;\nCREATE SCHEMA IF NOT EXISTS supacloud_queue;\nCREATE TABLE IF NOT EXISTS supacloud_queue.job_keys (\n queue_name text NOT NULL,\n job_key text NOT NULL,\n msg_id bigint NOT NULL,\n message jsonb NOT NULL,\n sleep_seconds integer NOT NULL,\n created_at timestamptz NOT NULL DEFAULT clock_timestamp(),\n PRIMARY KEY (queue_name, job_key)\n);\n-- Upgrade early key-only installations without inventing an input identity.\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS message jsonb;\nALTER TABLE supacloud_queue.job_keys ADD COLUMN IF NOT EXISTS sleep_seconds integer;\nREVOKE ALL ON SCHEMA supacloud_queue FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON TABLE supacloud_queue.job_keys FROM PUBLIC, anon, authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)\nRETURNS text\nLANGUAGE plpgsql\nIMMUTABLE\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue_name text := lower(btrim(queue_name));\nBEGIN\n IF left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';\n END IF;\n IF queue_name IS NULL OR queue_name !~ '^[a-z0-9][a-z0-9_-]{0,127}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN queue_name;\nEND;\n$$;\n\n-- Return-type changes require replacement. Do not CASCADE through user dependencies.\nDO $pgmq_receipt_types$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send(text,jsonb,integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send(text,jsonb,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.send_batch(text,jsonb[],integer)')\n AND prorettype = 'bigint'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.send_batch(text,jsonb[],integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.read(text,integer,integer)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.read(text,integer,integer);\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_catalog.pg_proc\n WHERE oid = to_regprocedure('pgmq_public.pop(text)')\n AND prorettype = 'pgmq.message_record'::regtype\n ) THEN\n DROP FUNCTION pgmq_public.pop(text);\n END IF;\nEND\n$pgmq_receipt_types$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_seconds(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_SECONDS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_read_count(value integer)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 OR value > 10000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_COUNT_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message_id(value bigint)\nRETURNS bigint LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n IF value IS NULL OR value < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_ID_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.message_node_count(value jsonb)\nRETURNS integer LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n node_count integer;\n maximum_depth integer;\nBEGIN\n IF value IS NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_MESSAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF octet_length(convert_to(value::text, 'UTF8')) > 1048576 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n WITH RECURSIVE tree(item, depth) AS (\n SELECT value, 0\n UNION ALL\n SELECT child.item, tree.depth + 1\n FROM tree\n CROSS JOIN LATERAL (\n SELECT element AS item\n FROM jsonb_array_elements(CASE WHEN jsonb_typeof(tree.item) = 'array' THEN tree.item ELSE '[]'::jsonb END) AS element\n UNION ALL\n SELECT entry.value AS item\n FROM jsonb_each(CASE WHEN jsonb_typeof(tree.item) = 'object' THEN tree.item ELSE '{}'::jsonb END) AS entry\n ) AS child\n WHERE tree.depth < 65\n )\n SELECT count(*)::integer, max(depth) INTO node_count, maximum_depth\n FROM (SELECT depth FROM tree LIMIT 10001) AS bounded;\n IF node_count > 10000 OR maximum_depth > 64 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n RETURN node_count;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_message(value jsonb)\nRETURNS jsonb LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nBEGIN\n PERFORM pgmq_public.message_node_count(value);\n RETURN value;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.require_messages(values_ jsonb[])\nRETURNS jsonb[] LANGUAGE plpgsql IMMUTABLE SET search_path = ''\nAS $$\nDECLARE\n item jsonb;\n total_bytes bigint := 0;\n total_nodes integer := 0;\nBEGIN\n IF values_ IS NULL OR cardinality(values_) < 1 OR cardinality(values_) > 10000\n OR array_ndims(values_) <> 1 OR array_lower(values_, 1) <> 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_BATCH_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH item IN ARRAY values_ LOOP\n total_nodes := total_nodes + pgmq_public.message_node_count(item);\n IF total_nodes > 100000 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_STRUCTURE_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n total_bytes := total_bytes + octet_length(convert_to(item::text, 'UTF8'));\n IF total_bytes > 8388608 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_PAYLOAD_TOO_LARGE' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n RETURN values_;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message(message), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)\nRETURNS SETOF text\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT receipt.msg_id::text FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), pgmq_public.require_messages(messages), pgmq_public.require_seconds(sleep_seconds)) AS receipt(msg_id); $$;\n\n-- Additive SupaCloud extension; official send/read/archive/delete contracts stay unchanged.\nCREATE OR REPLACE FUNCTION pgmq_public.send_idempotent(\n queue_name text, message jsonb, p_job_key text, sleep_seconds integer DEFAULT 0\n)\nRETURNS TABLE (msg_id text, created boolean)\nLANGUAGE plpgsql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nDECLARE\n normalized_queue text := pgmq_public.require_public_queue(queue_name);\n captured_message jsonb := pgmq_public.require_message(message);\n captured_delay integer := pgmq_public.require_seconds(sleep_seconds);\n inserted boolean;\n sent_id bigint;\n existing_id bigint;\n existing_message jsonb;\n existing_delay integer;\nBEGIN\n IF p_job_key IS NULL OR p_job_key !~ '^[A-Za-z0-9_.:@/-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_INVALID' USING ERRCODE = '22023';\n END IF;\n INSERT INTO supacloud_queue.job_keys(queue_name, job_key, msg_id, message, sleep_seconds)\n VALUES (normalized_queue, p_job_key, 0, captured_message, captured_delay)\n ON CONFLICT ON CONSTRAINT job_keys_pkey DO NOTHING;\n inserted := FOUND;\n IF NOT inserted THEN\n SELECT keys.msg_id, keys.message, keys.sleep_seconds\n INTO existing_id, existing_message, existing_delay\n FROM supacloud_queue.job_keys AS keys\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n IF existing_id IS NULL OR existing_id < 1 THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_UNAVAILABLE' USING ERRCODE = '55000';\n END IF;\n IF existing_message IS DISTINCT FROM captured_message OR existing_delay IS DISTINCT FROM captured_delay THEN\n RAISE EXCEPTION 'SUPACLOUD_QUEUE_JOB_KEY_CONFLICT' USING ERRCODE = '22023';\n END IF;\n -- This is a durable enqueue receipt, not evidence that the message is still pending.\n RETURN QUERY SELECT existing_id::text, false;\n RETURN;\n END IF;\n SELECT receipt.msg_id INTO STRICT sent_id\n FROM pgmq.send(normalized_queue, captured_message, captured_delay) AS receipt(msg_id);\n PERFORM pgmq_public.require_message_id(sent_id);\n UPDATE supacloud_queue.job_keys AS keys SET msg_id = sent_id\n WHERE keys.queue_name = normalized_queue AND keys.job_key = p_job_key;\n RETURN QUERY SELECT sent_id::text, true;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.read(pgmq_public.require_public_queue(queue_name),\n pgmq_public.require_seconds(sleep_seconds), pgmq_public.require_read_count(n)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)\nRETURNS TABLE (\n msg_id text, read_ct integer, enqueued_at timestamptz,\n last_read_at timestamptz, vt timestamptz, message jsonb, headers jsonb\n)\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n SELECT receipt.msg_id::text, receipt.read_ct, receipt.enqueued_at,\n (to_jsonb(receipt)->>'last_read_at')::timestamptz, receipt.vt,\n receipt.message, to_jsonb(receipt)->'headers'\n FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)) AS receipt;\n$$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nCREATE OR REPLACE FUNCTION pgmq_public.\"delete\"(queue_name text, message_id bigint)\nRETURNS boolean\nLANGUAGE sql\nVOLATILE\nSECURITY DEFINER\nSET search_path = ''\nAS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), pgmq_public.require_message_id(message_id)); $$;\n\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;\nGRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;\n\nNOTIFY pgrst, 'reload schema';\n-- supacloud:sql-module:pgmq-public:end\n";
22
- export declare const WORKFLOWS_SQL = "\n-- supacloud:sql-module:workflows-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\nCREATE SCHEMA IF NOT EXISTS supacloud_workflows;\nREVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_workflows TO service_role;\n\nSELECT pgmq.create('supacloud_internal_workflows');\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.runs (\n id uuid PRIMARY KEY,\n workflow_name text NOT NULL\n CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n workflow_version text NOT NULL\n CHECK (char_length(workflow_version) BETWEEN 1 AND 80),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),\n created_at timestamptz NOT NULL DEFAULT now(),\n started_at timestamptz,\n completed_at timestamptz,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.steps (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_key text NOT NULL\n CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),\n max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),\n retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400),\n queue_message_id bigint NOT NULL UNIQUE,\n claimed_by text,\n claimed_at timestamptz,\n completed_at timestamptz,\n next_step_key text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (run_id, step_key)\n);\n\nALTER TABLE supacloud_workflows.steps\n ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx\n ON supacloud_workflows.steps (run_id)\n WHERE status IN ('queued', 'running');\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx\n ON supacloud_workflows.runs (status, updated_at DESC, id);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx\n ON supacloud_workflows.steps (run_id, created_at, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.events (\n id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,\n event_type text NOT NULL\n CHECK (event_type IN (\n 'run_started', 'step_claimed', 'step_retried', 'step_completed',\n 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'\n )),\n attempt integer CHECK (attempt IS NULL OR attempt > 0),\n details jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(details) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx\n ON supacloud_workflows.events (run_id, id);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx\n ON supacloud_workflows.events (step_id, attempt)\n WHERE event_type IN ('step_retried', 'step_dead_lettered')\n AND details ->> 'operation' = 'retry';\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(\n p_run_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'runId', run.id,\n 'workflowName', run.workflow_name,\n 'workflowVersion', run.workflow_version,\n 'status', run.status,\n 'input', run.input,\n 'output', run.output,\n 'errorMessage', run.error_message,\n 'rowVersion', run.row_version::text,\n 'createdAt', run.created_at,\n 'startedAt', run.started_at,\n 'completedAt', run.completed_at,\n 'updatedAt', run.updated_at,\n 'idempotent', p_idempotent,\n 'steps', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'stepId', step.id,\n 'stepKey', step.step_key,\n 'status', step.status,\n 'input', step.input,\n 'output', step.output,\n 'errorMessage', step.error_message,\n 'attempts', step.attempts,\n 'maxAttempts', step.max_attempts,\n 'retryDelaySeconds', step.retry_delay_seconds,\n 'queueMessageId', step.queue_message_id::text,\n 'claimedBy', step.claimed_by,\n 'claimedAt', step.claimed_at,\n 'completedAt', step.completed_at,\n 'nextStepKey', step.next_step_key,\n 'createdAt', step.created_at,\n 'updatedAt', step.updated_at\n ) ORDER BY step.created_at, step.id)\n FROM supacloud_workflows.steps step\n WHERE step.run_id = run.id\n ), '[]'::jsonb)\n )\n FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(\n p_run_id uuid,\n p_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_step_key text := nullif(btrim(p_step_key), '');\n step_id uuid := gen_random_uuid();\n message_id bigint;\n created_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF normalized_step_key IS NULL\n OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id AND run.status IN ('queued', 'running')\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT queued_id INTO message_id\n FROM pgmq.send(\n 'supacloud_internal_workflows',\n jsonb_build_object('run_id', p_run_id, 'step_id', step_id),\n 0\n ) AS queued_id;\n\n INSERT INTO supacloud_workflows.steps (\n id, run_id, step_key, input, max_attempts, queue_message_id\n ) VALUES (\n step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id\n ) RETURNING * INTO created_step;\n\n RETURN created_step;\nEND;\n$$;\n\n-- Clean-code exception: private transitions keep typed PostgreSQL arguments and\n-- the complete lock/queue/ledger/event mutation in one transaction. The public\n-- contract already uses one JSON request; revisit if a private routine gains a\n-- second caller or any transition can be decomposed without weakening atomicity.\nCREATE OR REPLACE FUNCTION supacloud_workflows.start_run(\n p_run_id uuid,\n p_workflow_name text,\n p_workflow_version text,\n p_first_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_name text := nullif(btrim(p_workflow_name), '');\n normalized_version text := nullif(btrim(p_workflow_version), '');\n normalized_first_step_key text := nullif(btrim(p_first_step_key), '');\n existing_run supacloud_workflows.runs%ROWTYPE;\n existing_step supacloud_workflows.steps%ROWTYPE;\n first_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL\n OR normalized_name IS NULL\n OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_version IS NULL\n OR char_length(normalized_version) > 80\n OR normalized_first_step_key IS NULL\n OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;\n IF FOUND THEN\n SELECT * INTO existing_step\n FROM supacloud_workflows.steps\n WHERE run_id = p_run_id\n ORDER BY created_at, id\n LIMIT 1;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n IF existing_run.workflow_name <> normalized_name\n OR existing_run.workflow_version <> normalized_version\n OR existing_run.input <> p_input\n OR existing_step.step_key <> normalized_first_step_key\n OR existing_step.input <> p_input\n OR existing_step.max_attempts <> p_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n\n INSERT INTO supacloud_workflows.runs (\n id, workflow_name, workflow_version, input\n ) VALUES (\n p_run_id, normalized_name, normalized_version, p_input\n );\n first_step := supacloud_workflows.enqueue_step(\n p_run_id, normalized_first_step_key, p_input, p_max_attempts\n );\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)\n VALUES (p_run_id, first_step.id, 'run_started');\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(\n p_worker_id text,\n p_visibility_timeout_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n queued_message pgmq.message_record;\n message_run_id text;\n message_step_id text;\n candidate_run_id uuid;\n claimed_step supacloud_workflows.steps%ROWTYPE;\n claimed_run supacloud_workflows.runs%ROWTYPE;\nBEGIN\n IF normalized_worker_id IS NULL\n OR char_length(normalized_worker_id) > 200\n OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO queued_message\n FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)\n LIMIT 1;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n message_run_id := queued_message.message ->> 'run_id';\n message_step_id := queued_message.message ->> 'step_id';\n IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'\n OR message_run_id IS NULL\n OR message_step_id IS NULL\n OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'invalid_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id::text = lower(message_step_id)\n AND step.run_id::text = lower(message_run_id)\n AND step.queue_message_id = queued_message.msg_id;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';\n END IF;\n SELECT * INTO claimed_step\n FROM supacloud_workflows.steps\n WHERE id::text = lower(message_step_id)\n AND run_id = candidate_run_id\n AND queue_message_id = queued_message.msg_id\n FOR UPDATE;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT * INTO claimed_run\n FROM supacloud_workflows.runs\n WHERE id = claimed_step.run_id\n FOR UPDATE;\n IF claimed_run.status NOT IN ('queued', 'running')\n OR claimed_step.status NOT IN ('queued', 'running') THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'step_not_claimable',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF queued_message.read_ct > claimed_step.max_attempts THEN\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', attempts = queued_message.read_ct,\n error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = 'maximum attempts exceeded',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,\n jsonb_build_object('errorMessage', 'maximum attempts exceeded')\n );\n RETURN jsonb_build_object(\n 'status', 'dead_lettered',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts\n );\n END IF;\n\n UPDATE supacloud_workflows.steps\n SET status = 'running', attempts = queued_message.read_ct,\n retry_delay_seconds = 0, claimed_by = normalized_worker_id,\n claimed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'running', started_at = coalesce(started_at, now()),\n updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)\n VALUES (\n claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,\n jsonb_build_object('workerId', normalized_worker_id)\n );\n\n RETURN jsonb_build_object(\n 'status', 'claimed',\n 'runId', claimed_step.run_id,\n 'workflowName', claimed_run.workflow_name,\n 'workflowVersion', claimed_run.workflow_version,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'input', claimed_step.input,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts,\n 'workerId', normalized_worker_id\n );\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(\n p_step_id uuid\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n candidate_run_id uuid;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id = p_step_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));\n SELECT * INTO active_step\n FROM supacloud_workflows.steps\n WHERE id = p_step_id\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n active_step supacloud_workflows.steps%ROWTYPE;\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\nBEGIN\n active_step := supacloud_workflows.lock_step(p_step_id);\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR active_step.queue_message_id IS DISTINCT FROM p_message_id\n OR active_step.attempts IS DISTINCT FROM p_attempt\n OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_output jsonb,\n p_next_step_key text,\n p_next_input jsonb,\n p_next_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n next_step supacloud_workflows.steps%ROWTYPE;\n normalized_next_step_key text := nullif(btrim(p_next_step_key), '');\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'\n OR normalized_next_step_key IS NULL\n OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n\n IF current_step.status = 'completed' THEN\n SELECT * INTO next_step\n FROM supacloud_workflows.steps\n WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;\n IF NOT FOUND\n OR current_step.output IS DISTINCT FROM p_output\n OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key\n OR next_step.input IS DISTINCT FROM p_next_input\n OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_output, error_message = '',\n completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()\n WHERE id = current_step.id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_completed', p_attempt,\n jsonb_build_object('nextStepKey', normalized_next_step_key)\n );\n next_step := supacloud_workflows.enqueue_step(\n current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_step_output jsonb,\n p_run_output jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT * INTO current_run\n FROM supacloud_workflows.runs\n WHERE id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'completed' THEN\n IF current_step.next_step_key IS NOT NULL\n OR current_step.output IS DISTINCT FROM p_step_output\n OR current_run.status IS DISTINCT FROM 'completed'\n OR current_run.output IS DISTINCT FROM p_run_output THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_step_output, error_message = '',\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'completed', output = p_run_output, error_message = '',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text,\n p_delay_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n retry_receipt jsonb;\n queue_message_updated boolean;\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000\n OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step(p_step_id);\n SELECT event.details INTO retry_receipt\n FROM supacloud_workflows.events event\n WHERE event.step_id = current_step.id\n AND event.attempt = p_attempt\n AND event.event_type IN ('step_retried', 'step_dead_lettered')\n AND event.details ->> 'operation' = 'retry'\n ORDER BY event.id DESC\n LIMIT 1;\n IF FOUND THEN\n IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text\n OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id\n OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error\n OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n retry_receipt := retry_receipt || jsonb_build_object(\n 'stepId', current_step.id,\n 'attempt', p_attempt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, true)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR current_step.queue_message_id IS DISTINCT FROM p_message_id\n OR current_step.attempts IS DISTINCT FROM p_attempt\n OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id\n OR current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n IF current_step.attempts >= current_step.max_attempts THEN\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n\n SELECT EXISTS (\n SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)\n ) INTO queue_message_updated;\n IF queue_message_updated IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'queued', error_message = normalized_error,\n retry_delay_seconds = p_delay_seconds, updated_at = now()\n WHERE id = current_step.id;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_retried', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT * INTO current_run\n FROM supacloud_workflows.runs\n WHERE id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'failed' THEN\n IF current_step.error_message IS DISTINCT FROM normalized_error\n OR current_run.status IS DISTINCT FROM 'failed'\n OR current_run.error_message IS DISTINCT FROM normalized_error THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_failed', p_attempt,\n jsonb_build_object('errorMessage', normalized_error)\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(\n p_run_id uuid,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_reason text := nullif(btrim(p_reason), '');\n locked_run supacloud_workflows.runs%ROWTYPE;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n IF locked_run.status = 'cancelled' THEN\n IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n IF locked_run.status NOT IN ('queued', 'running') THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n SELECT * INTO active_step\n FROM supacloud_workflows.steps\n WHERE run_id = p_run_id AND status IN ('queued', 'running')\n FOR UPDATE;\n IF FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);\n UPDATE supacloud_workflows.steps\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now()\n WHERE id = active_step.id;\n END IF;\n UPDATE supacloud_workflows.runs\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = p_run_id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, details\n ) VALUES (\n p_run_id, active_step.id, 'run_cancelled',\n jsonb_build_object('reason', normalized_reason)\n );\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.run_events(\n p_run_id uuid,\n p_after_event_id bigint,\n p_limit integer\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'eventId', page.id::text,\n 'runId', page.run_id,\n 'stepId', page.step_id,\n 'eventType', page.event_type,\n 'attempt', page.attempt,\n 'details', page.details,\n 'createdAt', page.created_at\n ) ORDER BY page.id), '[]'::jsonb)\n FROM (\n SELECT event.*\n FROM supacloud_workflows.events event\n WHERE event.run_id = p_run_id AND event.id > p_after_event_id\n ORDER BY event.id\n LIMIT p_limit\n ) page\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(\n request jsonb,\n key text\n) RETURNS uuid\nLANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$\nDECLARE\n uuid_text text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n uuid_text := request ->> key;\n IF uuid_text IS NULL\n OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN uuid_text::uuid;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n max_attempts integer;\nBEGIN\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n RETURN supacloud_workflows.start_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'workflowName',\n request ->> 'workflowVersion',\n request ->> 'firstStepKey',\n coalesce(request -> 'input', '{}'::jsonb),\n max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n visibility_timeout_seconds integer;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);\n RETURN supacloud_workflows.claim_step(\n request ->> 'workerId', visibility_timeout_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n next_max_attempts integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.advance_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'output', '{}'::jsonb),\n request ->> 'nextStepKey',\n coalesce(request -> 'nextInput', '{}'::jsonb),\n next_max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.complete_run(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'stepOutput', '{}'::jsonb),\n coalesce(request -> 'runOutput', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n delay_seconds integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.retry_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage',\n delay_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.fail_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.cancel_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'reason'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.snapshot(\n supacloud_workflows.request_uuid(request, 'runId'), false\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n after_event_id bigint;\n event_limit integer;\nBEGIN\n after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);\n event_limit := coalesce((request ->> 'limit')::integer, 100);\n IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.run_events(\n supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\n\nREVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;\n\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;\n-- supacloud:sql-module:workflows-public:end\n\n-- supacloud:sql-module:commands-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_commands;\nREVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_commands TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_commands.receipts (\n id uuid PRIMARY KEY,\n command_type text NOT NULL\n CHECK (command_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_type text NOT NULL\n CHECK (target_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_id text NOT NULL CHECK (char_length(target_id) BETWEEN 1 AND 500),\n actor_id uuid,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(payload) = 'object'),\n payload_fingerprint text NOT NULL CHECK (char_length(payload_fingerprint) = 36),\n workflow_run_id uuid NOT NULL UNIQUE\n REFERENCES supacloud_workflows.runs(id) ON DELETE RESTRICT,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_commands_target_idx\n ON supacloud_commands.receipts (target_type, target_id, created_at DESC, id);\n\nALTER TABLE supacloud_commands.receipts ADD COLUMN IF NOT EXISTS tenant_id text;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.snapshot(\n p_command_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'commandId', receipt.id,\n 'commandType', receipt.command_type,\n 'targetType', receipt.target_type,\n 'targetId', receipt.target_id,\n 'actorId', receipt.actor_id,\n 'tenantId', receipt.tenant_id,\n 'payloadFingerprint', receipt.payload_fingerprint,\n 'createdAt', receipt.created_at,\n 'idempotent', p_idempotent,\n 'workflow', supacloud_workflows.snapshot(receipt.workflow_run_id, false)\n )\n FROM supacloud_commands.receipts receipt\n WHERE receipt.id = p_command_id\n$$;\n\n-- Application-owned SECURITY DEFINER RPCs may call this after their domain\n-- mutation. PostgreSQL commits the domain write, receipt, and workflow enqueue\n-- together, so a lost response can be replayed with the same command ID.\nDROP FUNCTION IF EXISTS supacloud_commands.submit(uuid, text, text, text, uuid, jsonb, integer);\nCREATE OR REPLACE FUNCTION supacloud_commands.submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n command_id uuid;\n actor_id uuid;\n max_attempts integer;\n payload jsonb;\n normalized_command_type text;\n normalized_target_type text;\n normalized_target_id text;\n fingerprint text;\n existing supacloud_commands.receipts%ROWTYPE;\n tenant_id text;\n existing_execution jsonb;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id := command_id_text::uuid;\n tenant_id := request->>'tenantId';\n IF request ? 'tenantId' AND (jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR tenant_id !~ '^[A-Za-z0-9._:@|-]{1,200}$') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'actorId' AND request ->> 'actorId' IS NOT NULL THEN\n actor_id := (request ->> 'actorId')::uuid;\n END IF;\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n payload := coalesce(request -> 'payload', '{}'::jsonb);\n normalized_command_type := nullif(btrim(request ->> 'commandType'), '');\n normalized_target_type := nullif(btrim(request ->> 'targetType'), '');\n normalized_target_id := nullif(btrim(request ->> 'targetId'), '');\n IF normalized_command_type IS NULL\n OR normalized_command_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_type IS NULL\n OR normalized_target_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_id IS NULL\n OR char_length(normalized_target_id) > 500\n OR jsonb_typeof(payload) IS DISTINCT FROM 'object'\n OR max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n\n fingerprint := 'md5:' || md5(payload::text);\n PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));\n SELECT * INTO existing FROM supacloud_commands.receipts WHERE id = command_id;\n IF FOUND THEN\n IF existing.command_type <> normalized_command_type\n OR existing.target_type <> normalized_target_type\n OR existing.target_id <> normalized_target_id\n OR existing.actor_id IS DISTINCT FROM actor_id\n OR existing.tenant_id IS DISTINCT FROM tenant_id\n OR existing.payload <> payload\n OR existing.payload_fingerprint <> fingerprint THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_commands.snapshot(command_id, true);\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO existing_execution\n USING jsonb_build_object('commandId',command_id);\n IF existing_execution IS NOT NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n END IF;\n\n PERFORM supacloud_workflows.start_run(\n command_id,\n 'command.' || normalized_command_type,\n '1',\n 'execute',\n jsonb_build_object(\n 'commandId', command_id,\n 'commandType', normalized_command_type,\n 'targetType', normalized_target_type,\n 'targetId', normalized_target_id,\n 'actorId', actor_id,\n 'payload', payload,\n 'payloadFingerprint', fingerprint\n ),\n max_attempts\n );\n\n INSERT INTO supacloud_commands.receipts (\n id, command_type, target_type, target_id, actor_id, payload,\n payload_fingerprint, workflow_run_id, tenant_id\n ) VALUES (\n command_id, normalized_command_type, normalized_target_type,\n normalized_target_id, actor_id, payload, fingerprint, command_id, tenant_id\n );\n RETURN supacloud_commands.snapshot(command_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.status(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n execution jsonb;\n submission jsonb;\n field text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'commandId' THEN\n IF jsonb_typeof(request->'commandId') IS DISTINCT FROM 'string'\n OR request->>'commandId' !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k <> 'commandId') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n request := jsonb_build_object('commandId',(request->>'commandId')::uuid::text);\n ELSE\n IF EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k NOT IN ('tenantId','actorId','command','operationId')) THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH field IN ARRAY ARRAY['tenantId','actorId','command','operationId'] LOOP\n IF jsonb_typeof(request->field) IS DISTINCT FROM 'string' OR request->>field !~ '^[A-Za-z0-9._:@|-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO execution USING request;\n IF execution IS NOT NULL THEN RETURN execution; END IF;\n END IF;\n IF NOT (request ? 'commandId') THEN\n IF jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'actorId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'command') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'operationId') IS DISTINCT FROM 'string' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN NULL;\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n submission := supacloud_commands.snapshot(command_id_text::uuid, false);\n IF submission IS NULL THEN RETURN NULL; END IF;\n RETURN jsonb_build_object('kind','submission','commandId',command_id_text::uuid,\n 'execution',NULL,'workflow',jsonb_build_object(\n 'runId',submission->'workflow'->'runId','status',submission->'workflow'->'status'));\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n PERFORM supacloud_commands.submit(request);\n RETURN supacloud_commands.status(jsonb_build_object('commandId',request->>'commandId'));\nEND\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_get(request jsonb)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_commands.status(request)\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_command_submit(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_command_get(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_submit(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_get(jsonb) TO service_role;\n-- supacloud:sql-module:commands-public:end\n\n-- supacloud:sql-module:artifacts-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_artifacts;\nREVOKE ALL ON SCHEMA supacloud_artifacts FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_artifacts TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.artifacts (\n id uuid PRIMARY KEY,\n storage_object_id uuid NOT NULL UNIQUE REFERENCES storage.objects(id) ON DELETE RESTRICT,\n bucket_id text NOT NULL,\n object_path text NOT NULL,\n object_version text NOT NULL,\n artifact_type text NOT NULL\n CHECK (artifact_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),\n size_bytes bigint NOT NULL CHECK (size_bytes >= 0),\n mime_type text NOT NULL CHECK (char_length(mime_type) BETWEEN 1 AND 255),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n retention_until timestamptz,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n CHECK (retention_until IS NULL OR retention_until >= created_at),\n UNIQUE (bucket_id, object_path, object_version)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lookup_idx\n ON supacloud_artifacts.artifacts (artifact_type, created_at DESC, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.lineage (\n parent_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n child_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n relation_type text NOT NULL\n CHECK (relation_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (parent_artifact_id, child_artifact_id, relation_type),\n CHECK (parent_artifact_id <> child_artifact_id)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lineage_child_idx\n ON supacloud_artifacts.lineage (child_artifact_id, created_at, parent_artifact_id);\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.guard_storage_object()\nRETURNS trigger\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.storage_object_id = OLD.id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IMMUTABLE' USING ERRCODE = '55000';\n END IF;\n RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS supacloud_artifact_storage_fence ON storage.objects;\nCREATE TRIGGER supacloud_artifact_storage_fence\nBEFORE UPDATE OR DELETE ON storage.objects\nFOR EACH ROW EXECUTE FUNCTION supacloud_artifacts.guard_storage_object();\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.snapshot(\n p_artifact_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'artifactId', artifact.id,\n 'bucketId', artifact.bucket_id,\n 'objectPath', artifact.object_path,\n 'objectVersion', artifact.object_version,\n 'artifactType', artifact.artifact_type,\n 'sha256', artifact.sha256,\n 'sizeBytes', artifact.size_bytes::text,\n 'mimeType', artifact.mime_type,\n 'metadata', artifact.metadata,\n 'retentionUntil', artifact.retention_until,\n 'createdBy', artifact.created_by,\n 'createdAt', artifact.created_at,\n 'idempotent', p_idempotent,\n 'parents', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'artifactId', edge.parent_artifact_id,\n 'relationType', edge.relation_type,\n 'metadata', edge.metadata,\n 'createdAt', edge.created_at\n ) ORDER BY edge.created_at, edge.parent_artifact_id)\n FROM supacloud_artifacts.lineage edge\n WHERE edge.child_artifact_id = artifact.id\n ), '[]'::jsonb)\n )\n FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.id = p_artifact_id\n$$;\n\nDROP FUNCTION IF EXISTS supacloud_artifacts.register(uuid, text, text, text, text, bigint, text, jsonb, timestamptz, uuid);\nCREATE OR REPLACE FUNCTION supacloud_artifacts.register(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n artifact_id uuid;\n size_bytes bigint;\n metadata jsonb;\n retention_until timestamptz;\n created_by uuid;\n normalized_bucket text;\n normalized_path text;\n normalized_type text;\n normalized_sha256 text;\n normalized_mime text;\n object_row storage.objects%ROWTYPE;\n existing supacloud_artifacts.artifacts%ROWTYPE;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n artifact_id := (request ->> 'artifactId')::uuid;\n size_bytes := (request ->> 'sizeBytes')::bigint;\n metadata := coalesce(request -> 'metadata', '{}'::jsonb);\n retention_until := (request ->> 'retentionUntil')::timestamptz;\n created_by := (request ->> 'createdBy')::uuid;\n normalized_bucket := nullif(btrim(request ->> 'bucketId'), '');\n normalized_path := nullif(btrim(request ->> 'objectPath'), '');\n normalized_type := nullif(btrim(request ->> 'artifactType'), '');\n normalized_sha256 := lower(nullif(btrim(request ->> 'sha256'), ''));\n normalized_mime := lower(nullif(btrim(request ->> 'mimeType'), ''));\n IF artifact_id IS NULL OR normalized_bucket IS NULL OR normalized_path IS NULL\n OR normalized_path LIKE '/%' OR normalized_path LIKE '%\\%'\n OR normalized_path ~ '(^|/)(.|..)(/|$)'\n OR normalized_type IS NULL\n OR normalized_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_sha256 IS NULL OR normalized_sha256 !~ '^[0-9a-f]{64}$'\n OR size_bytes IS NULL OR size_bytes < 0\n OR normalized_mime IS NULL OR char_length(normalized_mime) > 255\n OR jsonb_typeof(metadata) IS DISTINCT FROM 'object'\n OR (retention_until IS NOT NULL AND retention_until < now()) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO object_row FROM storage.objects\n WHERE bucket_id = normalized_bucket AND name = normalized_path;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_OBJECT_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));\n SELECT * INTO existing FROM supacloud_artifacts.artifacts WHERE id = artifact_id;\n IF FOUND THEN\n IF existing.storage_object_id <> object_row.id\n OR existing.artifact_type <> normalized_type\n OR existing.sha256 <> normalized_sha256\n OR existing.size_bytes <> size_bytes\n OR existing.mime_type <> normalized_mime\n OR existing.metadata <> metadata\n OR existing.retention_until IS DISTINCT FROM retention_until\n OR existing.created_by IS DISTINCT FROM created_by THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(artifact_id, true);\n END IF;\n\n INSERT INTO supacloud_artifacts.artifacts (\n id, storage_object_id, bucket_id, object_path, object_version,\n artifact_type, sha256, size_bytes, mime_type, metadata,\n retention_until, created_by\n ) VALUES (\n artifact_id, object_row.id, normalized_bucket, normalized_path,\n object_row.version::text, normalized_type, normalized_sha256,\n size_bytes, normalized_mime, metadata, retention_until, created_by\n );\n RETURN supacloud_artifacts.snapshot(artifact_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.link(\n p_parent_artifact_id uuid,\n p_child_artifact_id uuid,\n p_relation_type text,\n p_metadata jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_relation text := nullif(btrim(p_relation_type), '');\n inserted_count integer;\nBEGIN\n IF p_parent_artifact_id IS NULL OR p_child_artifact_id IS NULL\n OR p_parent_artifact_id = p_child_artifact_id\n OR normalized_relation IS NULL\n OR normalized_relation !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_metadata) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF EXISTS (\n WITH RECURSIVE descendants(artifact_id) AS (\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n WHERE edge.parent_artifact_id = p_child_artifact_id\n UNION\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n JOIN descendants current ON edge.parent_artifact_id = current.artifact_id\n )\n SELECT 1 FROM descendants WHERE artifact_id = p_parent_artifact_id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_CYCLE' USING ERRCODE = '23514';\n END IF;\n INSERT INTO supacloud_artifacts.lineage (\n parent_artifact_id, child_artifact_id, relation_type, metadata\n ) VALUES (\n p_parent_artifact_id, p_child_artifact_id, normalized_relation, p_metadata\n ) ON CONFLICT DO NOTHING;\n GET DIAGNOSTICS inserted_count = ROW_COUNT;\n IF inserted_count = 0 AND NOT EXISTS (\n SELECT 1 FROM supacloud_artifacts.lineage\n WHERE parent_artifact_id = p_parent_artifact_id\n AND child_artifact_id = p_child_artifact_id\n AND relation_type = normalized_relation\n AND metadata = p_metadata\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(p_child_artifact_id, inserted_count = 0);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_register(request jsonb)\nRETURNS jsonb\nLANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_artifacts.register(request)\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.snapshot((request ->> 'artifactId')::uuid, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_link(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.link(\n (request ->> 'parentArtifactId')::uuid,\n (request ->> 'childArtifactId')::uuid,\n request ->> 'relationType',\n coalesce(request -> 'metadata', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_register(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_link(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_register(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_link(jsonb) TO service_role;\n-- supacloud:sql-module:artifacts-public:end\n";
22
+ export declare const WORKFLOWS_SQL = "\n-- supacloud:sql-module:workflows-public:start\nDO $pgmq_extension$\nBEGIN\n IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN\n EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';\n END IF;\nEND\n$pgmq_extension$;\nCREATE SCHEMA IF NOT EXISTS supacloud_workflows;\nREVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_workflows TO service_role;\n\nSELECT pgmq.create('supacloud_internal_workflows');\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.runs (\n id uuid PRIMARY KEY,\n workflow_name text NOT NULL\n CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n workflow_version text NOT NULL\n CHECK (char_length(workflow_version) BETWEEN 1 AND 80),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),\n created_at timestamptz NOT NULL DEFAULT now(),\n started_at timestamptz,\n completed_at timestamptz,\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.steps (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_key text NOT NULL\n CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),\n input jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(input) = 'object'),\n output jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(output) = 'object'),\n error_message text NOT NULL DEFAULT ''\n CHECK (char_length(error_message) <= 4000),\n attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),\n max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),\n retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400),\n queue_message_id bigint NOT NULL UNIQUE,\n claimed_by text,\n claimed_at timestamptz,\n completed_at timestamptz,\n next_step_key text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (run_id, step_key)\n);\n\nALTER TABLE supacloud_workflows.steps\n ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0\n CHECK (retry_delay_seconds BETWEEN 0 AND 86400);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx\n ON supacloud_workflows.steps (run_id)\n WHERE status IN ('queued', 'running');\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx\n ON supacloud_workflows.runs (status, updated_at DESC, id);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx\n ON supacloud_workflows.steps (run_id, created_at, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_workflows.events (\n id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,\n step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,\n event_type text NOT NULL\n CHECK (event_type IN (\n 'run_started', 'step_claimed', 'step_retried', 'step_completed',\n 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'\n )),\n attempt integer CHECK (attempt IS NULL OR attempt > 0),\n details jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(details) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx\n ON supacloud_workflows.events (run_id, id);\n\nCREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx\n ON supacloud_workflows.events (step_id, attempt)\n WHERE event_type IN ('step_retried', 'step_dead_lettered')\n AND details ->> 'operation' = 'retry';\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(\n p_run_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'runId', run.id,\n 'workflowName', run.workflow_name,\n 'workflowVersion', run.workflow_version,\n 'status', run.status,\n 'input', run.input,\n 'output', run.output,\n 'errorMessage', run.error_message,\n 'rowVersion', run.row_version::text,\n 'createdAt', run.created_at,\n 'startedAt', run.started_at,\n 'completedAt', run.completed_at,\n 'updatedAt', run.updated_at,\n 'idempotent', p_idempotent,\n 'steps', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'stepId', step.id,\n 'stepKey', step.step_key,\n 'status', step.status,\n 'input', step.input,\n 'output', step.output,\n 'errorMessage', step.error_message,\n 'attempts', step.attempts,\n 'maxAttempts', step.max_attempts,\n 'retryDelaySeconds', step.retry_delay_seconds,\n 'queueMessageId', step.queue_message_id::text,\n 'claimedBy', step.claimed_by,\n 'claimedAt', step.claimed_at,\n 'completedAt', step.completed_at,\n 'nextStepKey', step.next_step_key,\n 'createdAt', step.created_at,\n 'updatedAt', step.updated_at\n ) ORDER BY step.created_at, step.id)\n FROM supacloud_workflows.steps step\n WHERE step.run_id = run.id\n ), '[]'::jsonb)\n )\n FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(\n p_run_id uuid,\n p_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_step_key text := nullif(btrim(p_step_key), '');\n step_id uuid := gen_random_uuid();\n message_id bigint;\n created_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF normalized_step_key IS NULL\n OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM supacloud_workflows.runs run\n WHERE run.id = p_run_id AND run.status IN ('queued', 'running')\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT queued_id INTO message_id\n FROM pgmq.send(\n 'supacloud_internal_workflows',\n jsonb_build_object('run_id', p_run_id, 'step_id', step_id),\n 0\n ) AS queued_id;\n\n INSERT INTO supacloud_workflows.steps (\n id, run_id, step_key, input, max_attempts, queue_message_id\n ) VALUES (\n step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id\n ) RETURNING id, run_id, step_key, status, input, output, error_message, attempts, max_attempts, retry_delay_seconds, queue_message_id, claimed_by, claimed_at, completed_at, next_step_key, created_at, updated_at INTO created_step;\n\n RETURN created_step;\nEND;\n$$;\n\n-- Clean-code exception: private transitions keep typed PostgreSQL arguments and\n-- the complete lock/queue/ledger/event mutation in one transaction. The public\n-- contract already uses one JSON request; revisit if a private routine gains a\n-- second caller or any transition can be decomposed without weakening atomicity.\nCREATE OR REPLACE FUNCTION supacloud_workflows.start_run(\n p_run_id uuid,\n p_workflow_name text,\n p_workflow_version text,\n p_first_step_key text,\n p_input jsonb,\n p_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_name text := nullif(btrim(p_workflow_name), '');\n normalized_version text := nullif(btrim(p_workflow_version), '');\n normalized_first_step_key text := nullif(btrim(p_first_step_key), '');\n existing_run supacloud_workflows.runs%ROWTYPE;\n existing_step supacloud_workflows.steps%ROWTYPE;\n first_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL\n OR normalized_name IS NULL\n OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_version IS NULL\n OR char_length(normalized_version) > 80\n OR normalized_first_step_key IS NULL\n OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'\n OR p_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO existing_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id;\n IF FOUND THEN\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO existing_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.run_id = p_run_id\n ORDER BY created_at, id\n LIMIT 1;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n IF existing_run.workflow_name <> normalized_name\n OR existing_run.workflow_version <> normalized_version\n OR existing_run.input <> p_input\n OR existing_step.step_key <> normalized_first_step_key\n OR existing_step.input <> p_input\n OR existing_step.max_attempts <> p_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n\n INSERT INTO supacloud_workflows.runs (\n id, workflow_name, workflow_version, input\n ) VALUES (\n p_run_id, normalized_name, normalized_version, p_input\n );\n first_step := supacloud_workflows.enqueue_step(\n p_run_id, normalized_first_step_key, p_input, p_max_attempts\n );\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)\n VALUES (p_run_id, first_step.id, 'run_started');\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(\n p_worker_id text,\n p_visibility_timeout_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n queued_message pgmq.message_record;\n message_run_id text;\n message_step_id text;\n candidate_run_id uuid;\n claimed_step supacloud_workflows.steps%ROWTYPE;\n claimed_run supacloud_workflows.runs%ROWTYPE;\nBEGIN\n IF normalized_worker_id IS NULL\n OR char_length(normalized_worker_id) > 200\n OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO queued_message\n FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)\n LIMIT 1;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n message_run_id := queued_message.message ->> 'run_id';\n message_step_id := queued_message.message ->> 'step_id';\n IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'\n OR message_run_id IS NULL\n OR message_step_id IS NULL\n OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'invalid_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id::text = lower(message_step_id)\n AND step.run_id::text = lower(message_run_id)\n AND step.queue_message_id = queued_message.msg_id;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';\n END IF;\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO claimed_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.id::text = lower(message_step_id)\n AND workflow_step.run_id = candidate_run_id\n AND workflow_step.queue_message_id = queued_message.msg_id\n FOR UPDATE;\n IF NOT FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'orphaned_message',\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO claimed_run\n FROM supacloud_workflows.runs workflow_run\n WHERE workflow_run.id = claimed_step.run_id\n FOR UPDATE;\n IF claimed_run.status NOT IN ('queued', 'running')\n OR claimed_step.status NOT IN ('queued', 'running') THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n RETURN jsonb_build_object(\n 'status', 'discarded',\n 'reason', 'step_not_claimable',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'messageId', queued_message.msg_id::text\n );\n END IF;\n\n IF queued_message.read_ct > claimed_step.max_attempts THEN\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', attempts = queued_message.read_ct,\n error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = 'maximum attempts exceeded',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,\n jsonb_build_object('errorMessage', 'maximum attempts exceeded')\n );\n RETURN jsonb_build_object(\n 'status', 'dead_lettered',\n 'runId', claimed_step.run_id,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts\n );\n END IF;\n\n UPDATE supacloud_workflows.steps\n SET status = 'running', attempts = queued_message.read_ct,\n retry_delay_seconds = 0, claimed_by = normalized_worker_id,\n claimed_at = now(), updated_at = now()\n WHERE id = claimed_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'running', started_at = coalesce(started_at, now()),\n updated_at = now(), row_version = row_version + 1\n WHERE id = claimed_step.run_id;\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)\n VALUES (\n claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,\n jsonb_build_object('workerId', normalized_worker_id)\n );\n\n RETURN jsonb_build_object(\n 'status', 'claimed',\n 'runId', claimed_step.run_id,\n 'workflowName', claimed_run.workflow_name,\n 'workflowVersion', claimed_run.workflow_version,\n 'stepId', claimed_step.id,\n 'stepKey', claimed_step.step_key,\n 'input', claimed_step.input,\n 'messageId', queued_message.msg_id::text,\n 'attempt', queued_message.read_ct,\n 'maxAttempts', claimed_step.max_attempts,\n 'workerId', normalized_worker_id\n );\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(\n p_step_id uuid\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n candidate_run_id uuid;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n SELECT step.run_id INTO candidate_run_id\n FROM supacloud_workflows.steps step\n WHERE step.id = p_step_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.id = p_step_id\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text\n) RETURNS supacloud_workflows.steps\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n active_step supacloud_workflows.steps%ROWTYPE;\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\nBEGIN\n active_step := supacloud_workflows.lock_step(p_step_id);\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR active_step.queue_message_id IS DISTINCT FROM p_message_id\n OR active_step.attempts IS DISTINCT FROM p_attempt\n OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n RETURN active_step;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_output jsonb,\n p_next_step_key text,\n p_next_input jsonb,\n p_next_max_attempts integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n next_step supacloud_workflows.steps%ROWTYPE;\n normalized_next_step_key text := nullif(btrim(p_next_step_key), '');\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'\n OR normalized_next_step_key IS NULL\n OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n\n IF current_step.status = 'completed' THEN\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO next_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.run_id = current_step.run_id AND workflow_step.step_key = normalized_next_step_key;\n IF NOT FOUND\n OR current_step.output IS DISTINCT FROM p_output\n OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key\n OR next_step.input IS DISTINCT FROM p_next_input\n OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_output, error_message = '',\n completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()\n WHERE id = current_step.id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_completed', p_attempt,\n jsonb_build_object('nextStepKey', normalized_next_step_key)\n );\n next_step := supacloud_workflows.enqueue_step(\n current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_step_output jsonb,\n p_run_output jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n archived boolean;\nBEGIN\n IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'\n OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run\n FROM supacloud_workflows.runs workflow_run\n WHERE workflow_run.id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'completed' THEN\n IF current_step.next_step_key IS NOT NULL\n OR current_step.output IS DISTINCT FROM p_step_output\n OR current_run.status IS DISTINCT FROM 'completed'\n OR current_run.output IS DISTINCT FROM p_run_output THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'completed', output = p_step_output, error_message = '',\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'completed', output = p_run_output, error_message = '',\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);\n INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)\n VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text,\n p_delay_seconds integer\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n normalized_worker_id text := nullif(btrim(p_worker_id), '');\n retry_receipt jsonb;\n queue_message_updated boolean;\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000\n OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step(p_step_id);\n SELECT event.details INTO retry_receipt\n FROM supacloud_workflows.events event\n WHERE event.step_id = current_step.id\n AND event.attempt = p_attempt\n AND event.event_type IN ('step_retried', 'step_dead_lettered')\n AND event.details ->> 'operation' = 'retry'\n ORDER BY event.id DESC\n LIMIT 1;\n IF FOUND THEN\n IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text\n OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id\n OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error\n OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n retry_receipt := retry_receipt || jsonb_build_object(\n 'stepId', current_step.id,\n 'attempt', p_attempt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, true)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n IF normalized_worker_id IS NULL\n OR p_message_id IS NULL\n OR p_attempt IS NULL\n OR current_step.queue_message_id IS DISTINCT FROM p_message_id\n OR current_step.attempts IS DISTINCT FROM p_attempt\n OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id\n OR current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n\n IF current_step.attempts >= current_step.max_attempts THEN\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'dead_lettered', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\n END IF;\n\n SELECT EXISTS (\n SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)\n ) INTO queue_message_updated;\n IF queue_message_updated IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'queued', error_message = normalized_error,\n retry_delay_seconds = p_delay_seconds, updated_at = now()\n WHERE id = current_step.id;\n retry_receipt := jsonb_build_object(\n 'operation', 'retry',\n 'stepId', current_step.id,\n 'messageId', p_message_id::text,\n 'attempt', p_attempt,\n 'workerId', normalized_worker_id,\n 'errorMessage', normalized_error,\n 'delaySeconds', p_delay_seconds\n );\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_retried', p_attempt, retry_receipt\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false)\n || jsonb_build_object('retryReceipt', retry_receipt);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(\n p_step_id uuid,\n p_message_id bigint,\n p_attempt integer,\n p_worker_id text,\n p_error_message text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n current_step supacloud_workflows.steps%ROWTYPE;\n current_run supacloud_workflows.runs%ROWTYPE;\n normalized_error text := nullif(btrim(p_error_message), '');\n archived boolean;\nBEGIN\n IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n current_step := supacloud_workflows.lock_step_attempt(\n p_step_id, p_message_id, p_attempt, p_worker_id\n );\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO current_run\n FROM supacloud_workflows.runs workflow_run\n WHERE workflow_run.id = current_step.run_id\n FOR UPDATE;\n\n IF current_step.status = 'failed' THEN\n IF current_step.error_message IS DISTINCT FROM normalized_error\n OR current_run.status IS DISTINCT FROM 'failed'\n OR current_run.error_message IS DISTINCT FROM normalized_error THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(current_step.run_id, true);\n END IF;\n IF current_step.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';\n END IF;\n IF current_run.status <> 'running' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n\n SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;\n IF archived IS DISTINCT FROM true THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';\n END IF;\n UPDATE supacloud_workflows.steps\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now()\n WHERE id = current_step.id;\n UPDATE supacloud_workflows.runs\n SET status = 'failed', error_message = normalized_error,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = current_step.run_id AND status = 'running';\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, attempt, details\n ) VALUES (\n current_step.run_id, current_step.id, 'step_failed', p_attempt,\n jsonb_build_object('errorMessage', normalized_error)\n );\n RETURN supacloud_workflows.snapshot(current_step.run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(\n p_run_id uuid,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_reason text := nullif(btrim(p_reason), '');\n locked_run supacloud_workflows.runs%ROWTYPE;\n active_step supacloud_workflows.steps%ROWTYPE;\nBEGIN\n IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\n END IF;\n PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));\n SELECT workflow_run.id, workflow_run.workflow_name, workflow_run.workflow_version, workflow_run.status, workflow_run.input, workflow_run.output, workflow_run.error_message, workflow_run.row_version, workflow_run.created_at, workflow_run.started_at, workflow_run.completed_at, workflow_run.updated_at INTO locked_run FROM supacloud_workflows.runs workflow_run WHERE workflow_run.id = p_run_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n IF locked_run.status = 'cancelled' THEN\n IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_workflows.snapshot(p_run_id, true);\n END IF;\n IF locked_run.status NOT IN ('queued', 'running') THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';\n END IF;\n SELECT workflow_step.id, workflow_step.run_id, workflow_step.step_key, workflow_step.status, workflow_step.input, workflow_step.output, workflow_step.error_message, workflow_step.attempts, workflow_step.max_attempts, workflow_step.retry_delay_seconds, workflow_step.queue_message_id, workflow_step.claimed_by, workflow_step.claimed_at, workflow_step.completed_at, workflow_step.next_step_key, workflow_step.created_at, workflow_step.updated_at INTO active_step\n FROM supacloud_workflows.steps workflow_step\n WHERE workflow_step.run_id = p_run_id AND workflow_step.status IN ('queued', 'running')\n FOR UPDATE;\n IF FOUND THEN\n PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);\n UPDATE supacloud_workflows.steps\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now()\n WHERE id = active_step.id;\n END IF;\n UPDATE supacloud_workflows.runs\n SET status = 'cancelled', error_message = normalized_reason,\n completed_at = now(), updated_at = now(), row_version = row_version + 1\n WHERE id = p_run_id;\n INSERT INTO supacloud_workflows.events (\n run_id, step_id, event_type, details\n ) VALUES (\n p_run_id, active_step.id, 'run_cancelled',\n jsonb_build_object('reason', normalized_reason)\n );\n RETURN supacloud_workflows.snapshot(p_run_id, false);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.run_events(\n p_run_id uuid,\n p_after_event_id bigint,\n p_limit integer\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT coalesce(jsonb_agg(jsonb_build_object(\n 'eventId', page.id::text,\n 'runId', page.run_id,\n 'stepId', page.step_id,\n 'eventType', page.event_type,\n 'attempt', page.attempt,\n 'details', page.details,\n 'createdAt', page.created_at\n ) ORDER BY page.id), '[]'::jsonb)\n FROM (\n SELECT event.*\n FROM supacloud_workflows.events event\n WHERE event.run_id = p_run_id AND event.id > p_after_event_id\n ORDER BY event.id\n LIMIT p_limit\n ) page\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(\n request jsonb,\n key text\n) RETURNS uuid\nLANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$\nDECLARE\n uuid_text text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n uuid_text := request ->> key;\n IF uuid_text IS NULL\n OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN uuid_text::uuid;\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n max_attempts integer;\nBEGIN\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n RETURN supacloud_workflows.start_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'workflowName',\n request ->> 'workflowVersion',\n request ->> 'firstStepKey',\n coalesce(request -> 'input', '{}'::jsonb),\n max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n visibility_timeout_seconds integer;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\n END IF;\n visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);\n RETURN supacloud_workflows.claim_step(\n request ->> 'workerId', visibility_timeout_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n next_max_attempts integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.advance_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'output', '{}'::jsonb),\n request ->> 'nextStepKey',\n coalesce(request -> 'nextInput', '{}'::jsonb),\n next_max_attempts\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.complete_run(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n coalesce(request -> 'stepOutput', '{}'::jsonb),\n coalesce(request -> 'runOutput', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\n delay_seconds integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.retry_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage',\n delay_seconds\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n message_id bigint;\n attempt integer;\nBEGIN\n message_id := (request ->> 'messageId')::bigint;\n attempt := (request ->> 'attempt')::integer;\n IF message_id <= 0 OR attempt <= 0 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.fail_step(\n supacloud_workflows.request_uuid(request, 'stepId'),\n message_id,\n attempt,\n request ->> 'workerId',\n request ->> 'errorMessage'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.cancel_run(\n supacloud_workflows.request_uuid(request, 'runId'),\n request ->> 'reason'\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_workflows.snapshot(\n supacloud_workflows.request_uuid(request, 'runId'), false\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n after_event_id bigint;\n event_limit integer;\nBEGIN\n after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);\n event_limit := coalesce((request ->> 'limit')::integer, 100);\n IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN supacloud_workflows.run_events(\n supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows\n FROM PUBLIC, anon, authenticated, service_role;\n\nREVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;\n\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;\n-- supacloud:sql-module:workflows-public:end\n\n-- supacloud:sql-module:commands-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_commands;\nREVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_commands TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_commands.receipts (\n id uuid PRIMARY KEY,\n command_type text NOT NULL\n CHECK (command_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_type text NOT NULL\n CHECK (target_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n target_id text NOT NULL CHECK (char_length(target_id) BETWEEN 1 AND 500),\n actor_id uuid,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb\n CHECK (jsonb_typeof(payload) = 'object'),\n payload_fingerprint text NOT NULL CHECK (char_length(payload_fingerprint) = 36),\n workflow_run_id uuid NOT NULL UNIQUE\n REFERENCES supacloud_workflows.runs(id) ON DELETE RESTRICT,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_commands_target_idx\n ON supacloud_commands.receipts (target_type, target_id, created_at DESC, id);\n\nALTER TABLE supacloud_commands.receipts ADD COLUMN IF NOT EXISTS tenant_id text;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.snapshot(\n p_command_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'commandId', receipt.id,\n 'commandType', receipt.command_type,\n 'targetType', receipt.target_type,\n 'targetId', receipt.target_id,\n 'actorId', receipt.actor_id,\n 'tenantId', receipt.tenant_id,\n 'payloadFingerprint', receipt.payload_fingerprint,\n 'createdAt', receipt.created_at,\n 'idempotent', p_idempotent,\n 'workflow', supacloud_workflows.snapshot(receipt.workflow_run_id, false)\n )\n FROM supacloud_commands.receipts receipt\n WHERE receipt.id = p_command_id\n$$;\n\n-- Application-owned SECURITY DEFINER RPCs may call this after their domain\n-- mutation. PostgreSQL commits the domain write, receipt, and workflow enqueue\n-- together, so a lost response can be replayed with the same command ID.\nDROP FUNCTION IF EXISTS supacloud_commands.submit(uuid, text, text, text, uuid, jsonb, integer);\nCREATE OR REPLACE FUNCTION supacloud_commands.submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n command_id uuid;\n actor_id uuid;\n max_attempts integer;\n payload jsonb;\n normalized_command_type text;\n normalized_target_type text;\n normalized_target_id text;\n fingerprint text;\n existing supacloud_commands.receipts%ROWTYPE;\n tenant_id text;\n existing_execution jsonb;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n command_id := command_id_text::uuid;\n tenant_id := request->>'tenantId';\n IF request ? 'tenantId' AND (jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR tenant_id !~ '^[A-Za-z0-9._:@|-]{1,200}$') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'actorId' AND request ->> 'actorId' IS NOT NULL THEN\n actor_id := (request ->> 'actorId')::uuid;\n END IF;\n max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);\n payload := coalesce(request -> 'payload', '{}'::jsonb);\n normalized_command_type := nullif(btrim(request ->> 'commandType'), '');\n normalized_target_type := nullif(btrim(request ->> 'targetType'), '');\n normalized_target_id := nullif(btrim(request ->> 'targetId'), '');\n IF normalized_command_type IS NULL\n OR normalized_command_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_type IS NULL\n OR normalized_target_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_target_id IS NULL\n OR char_length(normalized_target_id) > 500\n OR jsonb_typeof(payload) IS DISTINCT FROM 'object'\n OR max_attempts NOT BETWEEN 1 AND 100 THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\n END IF;\n\n fingerprint := 'md5:' || md5(payload::text);\n PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));\n SELECT receipt.id, receipt.command_type, receipt.target_type, receipt.target_id, receipt.actor_id, receipt.payload, receipt.payload_fingerprint, receipt.workflow_run_id, receipt.created_at, receipt.tenant_id INTO existing FROM supacloud_commands.receipts receipt WHERE receipt.id = command_id;\n IF FOUND THEN\n IF existing.command_type <> normalized_command_type\n OR existing.target_type <> normalized_target_type\n OR existing.target_id <> normalized_target_id\n OR existing.actor_id IS DISTINCT FROM actor_id\n OR existing.tenant_id IS DISTINCT FROM tenant_id\n OR existing.payload <> payload\n OR existing.payload_fingerprint <> fingerprint THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_commands.snapshot(command_id, true);\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO existing_execution\n USING jsonb_build_object('commandId',command_id);\n IF existing_execution IS NOT NULL THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n END IF;\n\n PERFORM supacloud_workflows.start_run(\n command_id,\n 'command.' || normalized_command_type,\n '1',\n 'execute',\n jsonb_build_object(\n 'commandId', command_id,\n 'commandType', normalized_command_type,\n 'targetType', normalized_target_type,\n 'targetId', normalized_target_id,\n 'actorId', actor_id,\n 'payload', payload,\n 'payloadFingerprint', fingerprint\n ),\n max_attempts\n );\n\n INSERT INTO supacloud_commands.receipts (\n id, command_type, target_type, target_id, actor_id, payload,\n payload_fingerprint, workflow_run_id, tenant_id\n ) VALUES (\n command_id, normalized_command_type, normalized_target_type,\n normalized_target_id, actor_id, payload, fingerprint, command_id, tenant_id\n );\n RETURN supacloud_commands.snapshot(command_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_commands.status(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n command_id_text text;\n execution jsonb;\n submission jsonb;\n field text;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n IF request ? 'commandId' THEN\n IF jsonb_typeof(request->'commandId') IS DISTINCT FROM 'string'\n OR request->>'commandId' !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n OR EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k <> 'commandId') THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n request := jsonb_build_object('commandId',(request->>'commandId')::uuid::text);\n ELSE\n IF EXISTS(SELECT FROM jsonb_object_keys(request) AS k WHERE k NOT IN ('tenantId','actorId','command','operationId')) THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n FOREACH field IN ARRAY ARRAY['tenantId','actorId','command','operationId'] LOOP\n IF jsonb_typeof(request->field) IS DISTINCT FROM 'string' OR request->>field !~ '^[A-Za-z0-9._:@|-]{1,200}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n END LOOP;\n END IF;\n IF to_regprocedure('supacloud_commands.execution_status(jsonb)') IS NOT NULL THEN\n EXECUTE 'SELECT supacloud_commands.execution_status($1)' INTO execution USING request;\n IF execution IS NOT NULL THEN RETURN execution; END IF;\n END IF;\n IF NOT (request ? 'commandId') THEN\n IF jsonb_typeof(request->'tenantId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'actorId') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'command') IS DISTINCT FROM 'string'\n OR jsonb_typeof(request->'operationId') IS DISTINCT FROM 'string' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n RETURN NULL;\n END IF;\n command_id_text := request ->> 'commandId';\n IF command_id_text IS NULL\n OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\n END IF;\n submission := supacloud_commands.snapshot(command_id_text::uuid, false);\n IF submission IS NULL THEN RETURN NULL; END IF;\n RETURN jsonb_build_object('kind','submission','commandId',command_id_text::uuid,\n 'execution',NULL,'workflow',jsonb_build_object(\n 'runId',submission->'workflow'->'runId','status',submission->'workflow'->'status'));\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_submit(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n PERFORM supacloud_commands.submit(request);\n RETURN supacloud_commands.status(jsonb_build_object('commandId',request->>'commandId'));\nEND\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_command_get(request jsonb)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_commands.status(request)\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_commands\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_command_submit(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_command_get(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_submit(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_command_get(jsonb) TO service_role;\n-- supacloud:sql-module:commands-public:end\n\n-- supacloud:sql-module:artifacts-public:start\nCREATE SCHEMA IF NOT EXISTS supacloud_artifacts;\nREVOKE ALL ON SCHEMA supacloud_artifacts FROM PUBLIC, anon, authenticated;\nGRANT USAGE ON SCHEMA supacloud_artifacts TO service_role;\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.artifacts (\n id uuid PRIMARY KEY,\n storage_object_id uuid NOT NULL UNIQUE REFERENCES storage.objects(id) ON DELETE RESTRICT,\n bucket_id text NOT NULL,\n object_path text NOT NULL,\n object_version text NOT NULL,\n artifact_type text NOT NULL\n CHECK (artifact_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),\n size_bytes bigint NOT NULL CHECK (size_bytes >= 0),\n mime_type text NOT NULL CHECK (char_length(mime_type) BETWEEN 1 AND 255),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n retention_until timestamptz,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n CHECK (retention_until IS NULL OR retention_until >= created_at),\n UNIQUE (bucket_id, object_path, object_version)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lookup_idx\n ON supacloud_artifacts.artifacts (artifact_type, created_at DESC, id);\n\nCREATE TABLE IF NOT EXISTS supacloud_artifacts.lineage (\n parent_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n child_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,\n relation_type text NOT NULL\n CHECK (relation_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),\n created_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (parent_artifact_id, child_artifact_id, relation_type),\n CHECK (parent_artifact_id <> child_artifact_id)\n);\n\nCREATE INDEX IF NOT EXISTS supacloud_artifacts_lineage_child_idx\n ON supacloud_artifacts.lineage (child_artifact_id, created_at, parent_artifact_id);\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.guard_storage_object()\nRETURNS trigger\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.storage_object_id = OLD.id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IMMUTABLE' USING ERRCODE = '55000';\n END IF;\n RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS supacloud_artifact_storage_fence ON storage.objects;\nCREATE TRIGGER supacloud_artifact_storage_fence\nBEFORE UPDATE OR DELETE ON storage.objects\nFOR EACH ROW EXECUTE FUNCTION supacloud_artifacts.guard_storage_object();\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.snapshot(\n p_artifact_id uuid,\n p_idempotent boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$\n SELECT jsonb_build_object(\n 'artifactId', artifact.id,\n 'bucketId', artifact.bucket_id,\n 'objectPath', artifact.object_path,\n 'objectVersion', artifact.object_version,\n 'artifactType', artifact.artifact_type,\n 'sha256', artifact.sha256,\n 'sizeBytes', artifact.size_bytes::text,\n 'mimeType', artifact.mime_type,\n 'metadata', artifact.metadata,\n 'retentionUntil', artifact.retention_until,\n 'createdBy', artifact.created_by,\n 'createdAt', artifact.created_at,\n 'idempotent', p_idempotent,\n 'parents', coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'artifactId', edge.parent_artifact_id,\n 'relationType', edge.relation_type,\n 'metadata', edge.metadata,\n 'createdAt', edge.created_at\n ) ORDER BY edge.created_at, edge.parent_artifact_id)\n FROM supacloud_artifacts.lineage edge\n WHERE edge.child_artifact_id = artifact.id\n ), '[]'::jsonb)\n )\n FROM supacloud_artifacts.artifacts artifact\n WHERE artifact.id = p_artifact_id\n$$;\n\nDROP FUNCTION IF EXISTS supacloud_artifacts.register(uuid, text, text, text, text, bigint, text, jsonb, timestamptz, uuid);\nCREATE OR REPLACE FUNCTION supacloud_artifacts.register(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n artifact_id uuid;\n size_bytes bigint;\n metadata jsonb;\n retention_until timestamptz;\n created_by uuid;\n normalized_bucket text;\n normalized_path text;\n normalized_type text;\n normalized_sha256 text;\n normalized_mime text;\n object_row storage.objects%ROWTYPE;\n existing supacloud_artifacts.artifacts%ROWTYPE;\nBEGIN\n IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n artifact_id := (request ->> 'artifactId')::uuid;\n size_bytes := (request ->> 'sizeBytes')::bigint;\n metadata := coalesce(request -> 'metadata', '{}'::jsonb);\n retention_until := (request ->> 'retentionUntil')::timestamptz;\n created_by := (request ->> 'createdBy')::uuid;\n normalized_bucket := nullif(btrim(request ->> 'bucketId'), '');\n normalized_path := nullif(btrim(request ->> 'objectPath'), '');\n normalized_type := nullif(btrim(request ->> 'artifactType'), '');\n normalized_sha256 := lower(nullif(btrim(request ->> 'sha256'), ''));\n normalized_mime := lower(nullif(btrim(request ->> 'mimeType'), ''));\n IF artifact_id IS NULL OR normalized_bucket IS NULL OR normalized_path IS NULL\n OR normalized_path LIKE '/%' OR normalized_path LIKE '%\\%'\n OR normalized_path ~ '(^|/)(.|..)(/|$)'\n OR normalized_type IS NULL\n OR normalized_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR normalized_sha256 IS NULL OR normalized_sha256 !~ '^[0-9a-f]{64}$'\n OR size_bytes IS NULL OR size_bytes < 0\n OR normalized_mime IS NULL OR char_length(normalized_mime) > 255\n OR jsonb_typeof(metadata) IS DISTINCT FROM 'object'\n OR (retention_until IS NOT NULL AND retention_until < now()) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO object_row FROM storage.objects\n WHERE bucket_id = normalized_bucket AND name = normalized_path;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_OBJECT_NOT_FOUND' USING ERRCODE = 'P0002';\n END IF;\n\n PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));\n SELECT artifact.id, artifact.storage_object_id, artifact.bucket_id, artifact.object_path, artifact.object_version, artifact.artifact_type, artifact.sha256, artifact.size_bytes, artifact.mime_type, artifact.metadata, artifact.retention_until, artifact.created_by, artifact.created_at INTO existing FROM supacloud_artifacts.artifacts artifact WHERE artifact.id = artifact_id;\n IF FOUND THEN\n IF existing.storage_object_id <> object_row.id\n OR existing.artifact_type <> normalized_type\n OR existing.sha256 <> normalized_sha256\n OR existing.size_bytes <> size_bytes\n OR existing.mime_type <> normalized_mime\n OR existing.metadata <> metadata\n OR existing.retention_until IS DISTINCT FROM retention_until\n OR existing.created_by IS DISTINCT FROM created_by THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(artifact_id, true);\n END IF;\n\n INSERT INTO supacloud_artifacts.artifacts (\n id, storage_object_id, bucket_id, object_path, object_version,\n artifact_type, sha256, size_bytes, mime_type, metadata,\n retention_until, created_by\n ) VALUES (\n artifact_id, object_row.id, normalized_bucket, normalized_path,\n object_row.version::text, normalized_type, normalized_sha256,\n size_bytes, normalized_mime, metadata, retention_until, created_by\n );\n RETURN supacloud_artifacts.snapshot(artifact_id, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION supacloud_artifacts.link(\n p_parent_artifact_id uuid,\n p_child_artifact_id uuid,\n p_relation_type text,\n p_metadata jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n normalized_relation text := nullif(btrim(p_relation_type), '');\n inserted_count integer;\nBEGIN\n IF p_parent_artifact_id IS NULL OR p_child_artifact_id IS NULL\n OR p_parent_artifact_id = p_child_artifact_id\n OR normalized_relation IS NULL\n OR normalized_relation !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'\n OR jsonb_typeof(p_metadata) IS DISTINCT FROM 'object' THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\n END IF;\n IF EXISTS (\n WITH RECURSIVE descendants(artifact_id) AS (\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n WHERE edge.parent_artifact_id = p_child_artifact_id\n UNION\n SELECT edge.child_artifact_id\n FROM supacloud_artifacts.lineage edge\n JOIN descendants current ON edge.parent_artifact_id = current.artifact_id\n )\n SELECT 1 FROM descendants WHERE artifact_id = p_parent_artifact_id\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_CYCLE' USING ERRCODE = '23514';\n END IF;\n INSERT INTO supacloud_artifacts.lineage (\n parent_artifact_id, child_artifact_id, relation_type, metadata\n ) VALUES (\n p_parent_artifact_id, p_child_artifact_id, normalized_relation, p_metadata\n ) ON CONFLICT DO NOTHING;\n GET DIAGNOSTICS inserted_count = ROW_COUNT;\n IF inserted_count = 0 AND NOT EXISTS (\n SELECT 1 FROM supacloud_artifacts.lineage\n WHERE parent_artifact_id = p_parent_artifact_id\n AND child_artifact_id = p_child_artifact_id\n AND relation_type = normalized_relation\n AND metadata = p_metadata\n ) THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';\n END IF;\n RETURN supacloud_artifacts.snapshot(p_child_artifact_id, inserted_count = 0);\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_register(request jsonb)\nRETURNS jsonb\nLANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n SELECT supacloud_artifacts.register(request)\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_get(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.snapshot((request ->> 'artifactId')::uuid, false);\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_GET_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.supacloud_artifact_link(request jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN supacloud_artifacts.link(\n (request ->> 'parentArtifactId')::uuid,\n (request ->> 'childArtifactId')::uuid,\n request ->> 'relationType',\n coalesce(request -> 'metadata', '{}'::jsonb)\n );\nEXCEPTION\n WHEN invalid_parameter_value OR invalid_text_representation THEN\n RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';\nEND;\n$$;\n\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_artifacts\n FROM PUBLIC, anon, authenticated, service_role;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_register(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_get(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.supacloud_artifact_link(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_register(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_get(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.supacloud_artifact_link(jsonb) TO service_role;\n-- supacloud:sql-module:artifacts-public:end\n";
23
23
  /**
24
24
  * pg_cron emulation: the `cron.job` / `cron.job_run_details` tables and
25
25
  * schedule/unschedule functions. This only records jobs; the in-process
@@ -1 +1 @@
1
- {"version":3,"file":"emulated.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/emulated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;GAIG;AACH,eAAO,MAAM,aAAa,uwRAyLzB,CAAA;AAED;gFACgF;AAChF,eAAO,MAAM,eAAe,uuWAkT3B,CAAA;AAED,eAAO,MAAM,QAAQ,2+nBACF,CAAA;AAEnB,eAAO,MAAM,aAAa,4mgEAwjDzB,CAAA;AAID;;;;GAIG;AACH,eAAO,MAAM,QAAQ,uyEA6DpB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,82GA0EnB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,ulBAqB1B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,m/FAoErB,CAAA"}
1
+ {"version":3,"file":"emulated.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/emulated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;GAIG;AACH,eAAO,MAAM,aAAa,uwRAyLzB,CAAA;AAED;gFACgF;AAChF,eAAO,MAAM,eAAe,uuWAkT3B,CAAA;AAED,eAAO,MAAM,QAAQ,2+nBACF,CAAA;AAEnB,eAAO,MAAM,aAAa,gqpEAwjDzB,CAAA;AAID;;;;GAIG;AACH,eAAO,MAAM,QAAQ,uyEA6DpB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,82GA0EnB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,ulBAqB1B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,m/FAoErB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/lite",
3
- "version": "0.37.0",
3
+ "version": "0.37.2",
4
4
  "description": "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -60,29 +60,29 @@
60
60
  "@electric-sql/pglite-pgmq": "0.0.8",
61
61
  "@electric-sql/pglite-pgvector": "0.0.11",
62
62
  "@electric-sql/pglite-postgis": "0.2.10",
63
- "@supacloud/db": "0.20.3",
63
+ "@supacloud/db": "0.20.5",
64
64
  "exact-mirror": "1.2.6",
65
65
  "fast-xml-parser": "5.11.2",
66
66
  "tar": "^7.5.22",
67
67
  "typebox": "1.4.3"
68
68
  },
69
69
  "devDependencies": {
70
- "@supacloud/app": "0.35.0",
70
+ "@supacloud/app": "0.36.0",
71
71
  "@supacloud/contracts": "0.15.2",
72
- "@supacloud/compiler": "0.53.0",
73
- "@supacloud/elysia": "0.37.0",
72
+ "@supacloud/compiler": "0.55.0",
73
+ "@supacloud/elysia": "0.38.1",
74
74
  "@supabase/supabase-js": "^2.118.0",
75
75
  "@types/bun": "^1.4.4",
76
76
  "effect": "4.0.5",
77
- "elysia": "2.0.0-rc.0",
77
+ "elysia": "2.0.0-rc.1",
78
78
  "jose": "^6.2.13",
79
79
  "typescript": "^7.0.2"
80
80
  },
81
81
  "overrides": {
82
- "@supacloud/db": "0.20.3",
83
- "@supacloud/app": "0.35.0",
84
- "@supacloud/compiler": "0.53.0",
85
- "@supacloud/elysia": "0.37.0",
82
+ "@supacloud/db": "0.20.5",
83
+ "@supacloud/app": "0.36.0",
84
+ "@supacloud/compiler": "0.55.0",
85
+ "@supacloud/elysia": "0.38.1",
86
86
  "@supacloud/contracts": "0.15.2",
87
87
  "@supacloud/commands": "0.11.2",
88
88
  "@supacloud/delivery": "0.24.0",