@pgflow/core 0.0.0-logger-improvements-0b909bce-20251209215048 → 0.0.0-pgflow-installer-45a8ec76-20251211182851

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/CHANGELOG.md +8 -2
  2. package/dist/migrations/_generated/m_20250429164909.d.ts +3 -0
  3. package/dist/migrations/_generated/m_20250429164909.d.ts.map +1 -0
  4. package/dist/migrations/_generated/m_20250429164909.js +584 -0
  5. package/dist/migrations/_generated/m_20250517072017.d.ts +3 -0
  6. package/dist/migrations/_generated/m_20250517072017.d.ts.map +1 -0
  7. package/dist/migrations/_generated/m_20250517072017.js +106 -0
  8. package/dist/migrations/_generated/m_20250609105135.d.ts +3 -0
  9. package/dist/migrations/_generated/m_20250609105135.d.ts.map +1 -0
  10. package/dist/migrations/_generated/m_20250609105135.js +376 -0
  11. package/dist/migrations/_generated/m_20250610180554.d.ts +3 -0
  12. package/dist/migrations/_generated/m_20250610180554.d.ts.map +1 -0
  13. package/dist/migrations/_generated/m_20250610180554.js +132 -0
  14. package/dist/migrations/_generated/m_20250614124241.d.ts +3 -0
  15. package/dist/migrations/_generated/m_20250614124241.d.ts.map +1 -0
  16. package/dist/migrations/_generated/m_20250614124241.js +506 -0
  17. package/dist/migrations/_generated/m_20250619195327.d.ts +3 -0
  18. package/dist/migrations/_generated/m_20250619195327.d.ts.map +1 -0
  19. package/dist/migrations/_generated/m_20250619195327.js +190 -0
  20. package/dist/migrations/_generated/m_20250627090700.d.ts +3 -0
  21. package/dist/migrations/_generated/m_20250627090700.d.ts.map +1 -0
  22. package/dist/migrations/_generated/m_20250627090700.js +11 -0
  23. package/dist/migrations/_generated/m_20250707210212.d.ts +3 -0
  24. package/dist/migrations/_generated/m_20250707210212.d.ts.map +1 -0
  25. package/dist/migrations/_generated/m_20250707210212.js +108 -0
  26. package/dist/migrations/_generated/m_20250719205006.d.ts +3 -0
  27. package/dist/migrations/_generated/m_20250719205006.d.ts.map +1 -0
  28. package/dist/migrations/_generated/m_20250719205006.js +7 -0
  29. package/dist/migrations/_generated/m_20251006073122.d.ts +3 -0
  30. package/dist/migrations/_generated/m_20251006073122.d.ts.map +1 -0
  31. package/dist/migrations/_generated/m_20251006073122.js +1249 -0
  32. package/dist/migrations/_generated/m_20251103222045.d.ts +3 -0
  33. package/dist/migrations/_generated/m_20251103222045.d.ts.map +1 -0
  34. package/dist/migrations/_generated/m_20251103222045.js +627 -0
  35. package/dist/migrations/_generated/m_20251104080523.d.ts +3 -0
  36. package/dist/migrations/_generated/m_20251104080523.d.ts.map +1 -0
  37. package/dist/migrations/_generated/m_20251104080523.js +98 -0
  38. package/dist/migrations/_generated/m_20251130000000.d.ts +3 -0
  39. package/dist/migrations/_generated/m_20251130000000.d.ts.map +1 -0
  40. package/dist/migrations/_generated/m_20251130000000.js +273 -0
  41. package/dist/migrations/_generated/m_20251209074533.d.ts +3 -0
  42. package/dist/migrations/_generated/m_20251209074533.d.ts.map +1 -0
  43. package/dist/migrations/_generated/m_20251209074533.js +278 -0
  44. package/dist/migrations/index.d.ts +11 -0
  45. package/dist/migrations/index.d.ts.map +1 -0
  46. package/dist/migrations/index.js +39 -0
  47. package/dist/migrations/types.d.ts +12 -0
  48. package/dist/migrations/types.d.ts.map +1 -0
  49. package/dist/migrations/types.js +1 -0
  50. package/dist/package.json +10 -1
  51. package/package.json +11 -2
package/dist/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @pgflow/core
2
2
 
3
- ## 0.0.0-logger-improvements-0b909bce-20251209215048
3
+ ## 0.0.0-pgflow-installer-45a8ec76-20251211182851
4
+
5
+ ### Patch Changes
6
+
7
+ - @pgflow/dsl@0.0.0-pgflow-installer-45a8ec76-20251211182851
8
+
9
+ ## 0.10.0
4
10
 
5
11
  ### Minor Changes
6
12
 
@@ -10,7 +16,7 @@
10
16
 
11
17
  - 0b84bb0: Add automatic flow compilation at worker startup. Workers now call ensure_flow_compiled to verify flows are up-to-date. In development, mismatched flows are recompiled automatically. In production, mismatches cause errors. Use ensureCompiledOnStartup: false to opt-out.
12
18
  - Updated dependencies [0b84bb0]
13
- - @pgflow/dsl@0.0.0-logger-improvements-0b909bce-20251209215048
19
+ - @pgflow/dsl@0.10.0
14
20
 
15
21
  ## 0.9.1
16
22
 
@@ -0,0 +1,3 @@
1
+ import type { Migration } from '../types.js';
2
+ export declare const migration: Migration;
3
+ //# sourceMappingURL=m_20250429164909.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"m_20250429164909.d.ts","sourceRoot":"","sources":["../../../src/migrations/_generated/m_20250429164909.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,eAAO,MAAM,SAAS,EAAE,SAukBvB,CAAC"}
@@ -0,0 +1,584 @@
1
+ export const migration = {
2
+ timestamp: '20250429164909',
3
+ filename: '20250429164909_pgflow_initial.sql',
4
+ content: `-- Add new schema named "pgflow"
5
+ CREATE SCHEMA IF NOT EXISTS "pgflow";
6
+ -- Add new schema named "pgmq"
7
+ CREATE SCHEMA IF NOT EXISTS "pgmq";
8
+ -- Create extension "pgmq"
9
+ CREATE EXTENSION IF NOT EXISTS "pgmq" WITH SCHEMA "pgmq";
10
+ -- Create "read_with_poll" function
11
+ CREATE FUNCTION "pgflow"."read_with_poll" ("queue_name" text, "vt" integer, "qty" integer, "max_poll_seconds" integer DEFAULT 5, "poll_interval_ms" integer DEFAULT 100, "conditional" jsonb DEFAULT '{}') RETURNS SETOF pgmq.message_record LANGUAGE plpgsql AS $$
12
+ DECLARE
13
+ r pgmq.message_record;
14
+ stop_at TIMESTAMP;
15
+ sql TEXT;
16
+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');
17
+ BEGIN
18
+ stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);
19
+ LOOP
20
+ IF (SELECT clock_timestamp() >= stop_at) THEN
21
+ RETURN;
22
+ END IF;
23
+
24
+ sql := FORMAT(
25
+ $QUERY$
26
+ WITH cte AS
27
+ (
28
+ SELECT msg_id
29
+ FROM pgmq.%I
30
+ WHERE vt <= clock_timestamp() AND CASE
31
+ WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer
32
+ ELSE 1
33
+ END = 1
34
+ ORDER BY msg_id ASC
35
+ LIMIT $1
36
+ FOR UPDATE SKIP LOCKED
37
+ )
38
+ UPDATE pgmq.%I m
39
+ SET
40
+ vt = clock_timestamp() + %L,
41
+ read_ct = read_ct + 1
42
+ FROM cte
43
+ WHERE m.msg_id = cte.msg_id
44
+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message;
45
+ $QUERY$,
46
+ qtable, conditional, qtable, make_interval(secs => vt)
47
+ );
48
+
49
+ FOR r IN
50
+ EXECUTE sql USING qty
51
+ LOOP
52
+ RETURN NEXT r;
53
+ END LOOP;
54
+ IF FOUND THEN
55
+ RETURN;
56
+ ELSE
57
+ PERFORM pg_sleep(poll_interval_ms::numeric / 1000);
58
+ END IF;
59
+ END LOOP;
60
+ END;
61
+ $$;
62
+ -- Create composite type "step_task_record"
63
+ CREATE TYPE "pgflow"."step_task_record" AS ("flow_slug" text, "run_id" uuid, "step_slug" text, "input" jsonb, "msg_id" bigint);
64
+ -- Create "is_valid_slug" function
65
+ CREATE FUNCTION "pgflow"."is_valid_slug" ("slug" text) RETURNS boolean LANGUAGE plpgsql IMMUTABLE AS $$
66
+ begin
67
+ return
68
+ slug is not null
69
+ and slug <> ''
70
+ and length(slug) <= 128
71
+ and slug ~ '^[a-zA-Z_][a-zA-Z0-9_]*$'
72
+ and slug NOT IN ('run'); -- reserved words
73
+ end;
74
+ $$;
75
+ -- Create "flows" table
76
+ CREATE TABLE "pgflow"."flows" ("flow_slug" text NOT NULL, "opt_max_attempts" integer NOT NULL DEFAULT 3, "opt_base_delay" integer NOT NULL DEFAULT 1, "opt_timeout" integer NOT NULL DEFAULT 60, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("flow_slug"), CONSTRAINT "opt_base_delay_is_nonnegative" CHECK (opt_base_delay >= 0), CONSTRAINT "opt_max_attempts_is_nonnegative" CHECK (opt_max_attempts >= 0), CONSTRAINT "opt_timeout_is_positive" CHECK (opt_timeout > 0), CONSTRAINT "slug_is_valid" CHECK (pgflow.is_valid_slug(flow_slug)));
77
+ -- Create "steps" table
78
+ CREATE TABLE "pgflow"."steps" ("flow_slug" text NOT NULL, "step_slug" text NOT NULL, "step_type" text NOT NULL DEFAULT 'single', "step_index" integer NOT NULL DEFAULT 0, "deps_count" integer NOT NULL DEFAULT 0, "opt_max_attempts" integer NULL, "opt_base_delay" integer NULL, "opt_timeout" integer NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("flow_slug", "step_slug"), CONSTRAINT "steps_flow_slug_step_index_key" UNIQUE ("flow_slug", "step_index"), CONSTRAINT "steps_flow_slug_fkey" FOREIGN KEY ("flow_slug") REFERENCES "pgflow"."flows" ("flow_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "opt_base_delay_is_nonnegative" CHECK ((opt_base_delay IS NULL) OR (opt_base_delay >= 0)), CONSTRAINT "opt_max_attempts_is_nonnegative" CHECK ((opt_max_attempts IS NULL) OR (opt_max_attempts >= 0)), CONSTRAINT "opt_timeout_is_positive" CHECK ((opt_timeout IS NULL) OR (opt_timeout > 0)), CONSTRAINT "steps_deps_count_check" CHECK (deps_count >= 0), CONSTRAINT "steps_step_slug_check" CHECK (pgflow.is_valid_slug(step_slug)), CONSTRAINT "steps_step_type_check" CHECK (step_type = 'single'::text));
79
+ -- Create "deps" table
80
+ CREATE TABLE "pgflow"."deps" ("flow_slug" text NOT NULL, "dep_slug" text NOT NULL, "step_slug" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("flow_slug", "dep_slug", "step_slug"), CONSTRAINT "deps_flow_slug_dep_slug_fkey" FOREIGN KEY ("flow_slug", "dep_slug") REFERENCES "pgflow"."steps" ("flow_slug", "step_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "deps_flow_slug_fkey" FOREIGN KEY ("flow_slug") REFERENCES "pgflow"."flows" ("flow_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "deps_flow_slug_step_slug_fkey" FOREIGN KEY ("flow_slug", "step_slug") REFERENCES "pgflow"."steps" ("flow_slug", "step_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "deps_check" CHECK (dep_slug <> step_slug));
81
+ -- Create index "idx_deps_by_flow_dep" to table: "deps"
82
+ CREATE INDEX "idx_deps_by_flow_dep" ON "pgflow"."deps" ("flow_slug", "dep_slug");
83
+ -- Create index "idx_deps_by_flow_step" to table: "deps"
84
+ CREATE INDEX "idx_deps_by_flow_step" ON "pgflow"."deps" ("flow_slug", "step_slug");
85
+ -- Create "runs" table
86
+ CREATE TABLE "pgflow"."runs" ("run_id" uuid NOT NULL DEFAULT gen_random_uuid(), "flow_slug" text NOT NULL, "status" text NOT NULL DEFAULT 'started', "input" jsonb NOT NULL, "output" jsonb NULL, "remaining_steps" integer NOT NULL DEFAULT 0, "started_at" timestamptz NOT NULL DEFAULT now(), "completed_at" timestamptz NULL, "failed_at" timestamptz NULL, PRIMARY KEY ("run_id"), CONSTRAINT "runs_flow_slug_fkey" FOREIGN KEY ("flow_slug") REFERENCES "pgflow"."flows" ("flow_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "completed_at_is_after_started_at" CHECK ((completed_at IS NULL) OR (completed_at >= started_at)), CONSTRAINT "completed_at_or_failed_at" CHECK (NOT ((completed_at IS NOT NULL) AND (failed_at IS NOT NULL))), CONSTRAINT "failed_at_is_after_started_at" CHECK ((failed_at IS NULL) OR (failed_at >= started_at)), CONSTRAINT "runs_remaining_steps_check" CHECK (remaining_steps >= 0), CONSTRAINT "status_is_valid" CHECK (status = ANY (ARRAY['started'::text, 'failed'::text, 'completed'::text])));
87
+ -- Create index "idx_runs_flow_slug" to table: "runs"
88
+ CREATE INDEX "idx_runs_flow_slug" ON "pgflow"."runs" ("flow_slug");
89
+ -- Create index "idx_runs_status" to table: "runs"
90
+ CREATE INDEX "idx_runs_status" ON "pgflow"."runs" ("status");
91
+ -- Create "step_states" table
92
+ CREATE TABLE "pgflow"."step_states" ("flow_slug" text NOT NULL, "run_id" uuid NOT NULL, "step_slug" text NOT NULL, "status" text NOT NULL DEFAULT 'created', "remaining_tasks" integer NOT NULL DEFAULT 1, "remaining_deps" integer NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now(), "started_at" timestamptz NULL, "completed_at" timestamptz NULL, "failed_at" timestamptz NULL, PRIMARY KEY ("run_id", "step_slug"), CONSTRAINT "step_states_flow_slug_fkey" FOREIGN KEY ("flow_slug") REFERENCES "pgflow"."flows" ("flow_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "step_states_flow_slug_step_slug_fkey" FOREIGN KEY ("flow_slug", "step_slug") REFERENCES "pgflow"."steps" ("flow_slug", "step_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "step_states_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "pgflow"."runs" ("run_id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "completed_at_is_after_started_at" CHECK ((completed_at IS NULL) OR (completed_at >= started_at)), CONSTRAINT "completed_at_or_failed_at" CHECK (NOT ((completed_at IS NOT NULL) AND (failed_at IS NOT NULL))), CONSTRAINT "failed_at_is_after_started_at" CHECK ((failed_at IS NULL) OR (failed_at >= started_at)), CONSTRAINT "started_at_is_after_created_at" CHECK ((started_at IS NULL) OR (started_at >= created_at)), CONSTRAINT "status_and_remaining_tasks_match" CHECK ((status <> 'completed'::text) OR (remaining_tasks = 0)), CONSTRAINT "status_is_valid" CHECK (status = ANY (ARRAY['created'::text, 'started'::text, 'completed'::text, 'failed'::text])), CONSTRAINT "step_states_remaining_deps_check" CHECK (remaining_deps >= 0), CONSTRAINT "step_states_remaining_tasks_check" CHECK (remaining_tasks >= 0));
93
+ -- Create index "idx_step_states_failed" to table: "step_states"
94
+ CREATE INDEX "idx_step_states_failed" ON "pgflow"."step_states" ("run_id", "step_slug") WHERE (status = 'failed'::text);
95
+ -- Create index "idx_step_states_flow_slug" to table: "step_states"
96
+ CREATE INDEX "idx_step_states_flow_slug" ON "pgflow"."step_states" ("flow_slug");
97
+ -- Create index "idx_step_states_ready" to table: "step_states"
98
+ CREATE INDEX "idx_step_states_ready" ON "pgflow"."step_states" ("run_id", "status", "remaining_deps") WHERE ((status = 'created'::text) AND (remaining_deps = 0));
99
+ -- Create "step_tasks" table
100
+ CREATE TABLE "pgflow"."step_tasks" ("flow_slug" text NOT NULL, "run_id" uuid NOT NULL, "step_slug" text NOT NULL, "message_id" bigint NULL, "task_index" integer NOT NULL DEFAULT 0, "status" text NOT NULL DEFAULT 'queued', "attempts_count" integer NOT NULL DEFAULT 0, "error_message" text NULL, "output" jsonb NULL, "queued_at" timestamptz NOT NULL DEFAULT now(), "completed_at" timestamptz NULL, "failed_at" timestamptz NULL, PRIMARY KEY ("run_id", "step_slug", "task_index"), CONSTRAINT "step_tasks_flow_slug_fkey" FOREIGN KEY ("flow_slug") REFERENCES "pgflow"."flows" ("flow_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "step_tasks_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "pgflow"."runs" ("run_id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "step_tasks_run_id_step_slug_fkey" FOREIGN KEY ("run_id", "step_slug") REFERENCES "pgflow"."step_states" ("run_id", "step_slug") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "attempts_count_nonnegative" CHECK (attempts_count >= 0), CONSTRAINT "completed_at_is_after_queued_at" CHECK ((completed_at IS NULL) OR (completed_at >= queued_at)), CONSTRAINT "completed_at_or_failed_at" CHECK (NOT ((completed_at IS NOT NULL) AND (failed_at IS NOT NULL))), CONSTRAINT "failed_at_is_after_queued_at" CHECK ((failed_at IS NULL) OR (failed_at >= queued_at)), CONSTRAINT "only_single_task_per_step" CHECK (task_index = 0), CONSTRAINT "output_valid_only_for_completed" CHECK ((output IS NULL) OR (status = 'completed'::text)), CONSTRAINT "valid_status" CHECK (status = ANY (ARRAY['queued'::text, 'completed'::text, 'failed'::text])));
101
+ -- Create index "idx_step_tasks_completed" to table: "step_tasks"
102
+ CREATE INDEX "idx_step_tasks_completed" ON "pgflow"."step_tasks" ("run_id", "step_slug") WHERE (status = 'completed'::text);
103
+ -- Create index "idx_step_tasks_failed" to table: "step_tasks"
104
+ CREATE INDEX "idx_step_tasks_failed" ON "pgflow"."step_tasks" ("run_id", "step_slug") WHERE (status = 'failed'::text);
105
+ -- Create index "idx_step_tasks_flow_run_step" to table: "step_tasks"
106
+ CREATE INDEX "idx_step_tasks_flow_run_step" ON "pgflow"."step_tasks" ("flow_slug", "run_id", "step_slug");
107
+ -- Create index "idx_step_tasks_message_id" to table: "step_tasks"
108
+ CREATE INDEX "idx_step_tasks_message_id" ON "pgflow"."step_tasks" ("message_id");
109
+ -- Create index "idx_step_tasks_queued" to table: "step_tasks"
110
+ CREATE INDEX "idx_step_tasks_queued" ON "pgflow"."step_tasks" ("run_id", "step_slug") WHERE (status = 'queued'::text);
111
+ -- Create "poll_for_tasks" function
112
+ CREATE FUNCTION "pgflow"."poll_for_tasks" ("queue_name" text, "vt" integer, "qty" integer, "max_poll_seconds" integer DEFAULT 5, "poll_interval_ms" integer DEFAULT 100) RETURNS SETOF "pgflow"."step_task_record" LANGUAGE sql SET "search_path" = '' AS $$
113
+ with read_messages as (
114
+ select *
115
+ from pgflow.read_with_poll(
116
+ queue_name,
117
+ vt,
118
+ qty,
119
+ max_poll_seconds,
120
+ poll_interval_ms
121
+ )
122
+ ),
123
+ tasks as (
124
+ select
125
+ task.flow_slug,
126
+ task.run_id,
127
+ task.step_slug,
128
+ task.task_index,
129
+ task.message_id
130
+ from pgflow.step_tasks as task
131
+ join read_messages as message on message.msg_id = task.message_id
132
+ where task.message_id = message.msg_id
133
+ and task.status = 'queued'
134
+ ),
135
+ increment_attempts as (
136
+ update pgflow.step_tasks
137
+ set attempts_count = attempts_count + 1
138
+ from tasks
139
+ where step_tasks.message_id = tasks.message_id
140
+ and status = 'queued'
141
+ ),
142
+ runs as (
143
+ select
144
+ r.run_id,
145
+ r.input
146
+ from pgflow.runs r
147
+ where r.run_id in (select run_id from tasks)
148
+ ),
149
+ deps as (
150
+ select
151
+ st.run_id,
152
+ st.step_slug,
153
+ dep.dep_slug,
154
+ dep_task.output as dep_output
155
+ from tasks st
156
+ join pgflow.deps dep on dep.flow_slug = st.flow_slug and dep.step_slug = st.step_slug
157
+ join pgflow.step_tasks dep_task on
158
+ dep_task.run_id = st.run_id and
159
+ dep_task.step_slug = dep.dep_slug and
160
+ dep_task.status = 'completed'
161
+ ),
162
+ deps_outputs as (
163
+ select
164
+ d.run_id,
165
+ d.step_slug,
166
+ jsonb_object_agg(d.dep_slug, d.dep_output) as deps_output
167
+ from deps d
168
+ group by d.run_id, d.step_slug
169
+ ),
170
+ timeouts as (
171
+ select
172
+ task.message_id,
173
+ coalesce(step.opt_timeout, flow.opt_timeout) + 2 as vt_delay
174
+ from tasks task
175
+ join pgflow.flows flow on flow.flow_slug = task.flow_slug
176
+ join pgflow.steps step on step.flow_slug = task.flow_slug and step.step_slug = task.step_slug
177
+ )
178
+
179
+ select
180
+ st.flow_slug,
181
+ st.run_id,
182
+ st.step_slug,
183
+ jsonb_build_object('run', r.input) ||
184
+ coalesce(dep_out.deps_output, '{}'::jsonb) as input,
185
+ st.message_id as msg_id
186
+ from tasks st
187
+ join runs r on st.run_id = r.run_id
188
+ left join deps_outputs dep_out on
189
+ dep_out.run_id = st.run_id and
190
+ dep_out.step_slug = st.step_slug
191
+ cross join lateral (
192
+ -- TODO: this is slow because it calls set_vt for each row, and set_vt
193
+ -- builds dynamic query from string every time it is called
194
+ -- implement set_vt_batch(msgs_ids bigint[], vt_delays int[])
195
+ select pgmq.set_vt(queue_name, st.message_id,
196
+ (select t.vt_delay from timeouts t where t.message_id = st.message_id)
197
+ )
198
+ ) set_vt;
199
+ $$;
200
+ -- Create "add_step" function
201
+ CREATE FUNCTION "pgflow"."add_step" ("flow_slug" text, "step_slug" text, "deps_slugs" text[], "max_attempts" integer DEFAULT NULL::integer, "base_delay" integer DEFAULT NULL::integer, "timeout" integer DEFAULT NULL::integer) RETURNS "pgflow"."steps" LANGUAGE sql SET "search_path" = '' AS $$
202
+ WITH
203
+ next_index AS (
204
+ SELECT COALESCE(MAX(step_index) + 1, 0) as idx
205
+ FROM pgflow.steps
206
+ WHERE flow_slug = add_step.flow_slug
207
+ ),
208
+ create_step AS (
209
+ INSERT INTO pgflow.steps (flow_slug, step_slug, step_index, deps_count, opt_max_attempts, opt_base_delay, opt_timeout)
210
+ SELECT add_step.flow_slug, add_step.step_slug, idx, COALESCE(array_length(deps_slugs, 1), 0), max_attempts, base_delay, timeout
211
+ FROM next_index
212
+ ON CONFLICT (flow_slug, step_slug)
213
+ DO UPDATE SET step_slug = pgflow.steps.step_slug
214
+ RETURNING *
215
+ ),
216
+ insert_deps AS (
217
+ INSERT INTO pgflow.deps (flow_slug, dep_slug, step_slug)
218
+ SELECT add_step.flow_slug, d.dep_slug, add_step.step_slug
219
+ FROM unnest(deps_slugs) AS d(dep_slug)
220
+ ON CONFLICT (flow_slug, dep_slug, step_slug) DO NOTHING
221
+ RETURNING 1
222
+ )
223
+ -- Return the created step
224
+ SELECT * FROM create_step;
225
+ $$;
226
+ -- Create "add_step" function
227
+ CREATE FUNCTION "pgflow"."add_step" ("flow_slug" text, "step_slug" text, "max_attempts" integer DEFAULT NULL::integer, "base_delay" integer DEFAULT NULL::integer, "timeout" integer DEFAULT NULL::integer) RETURNS "pgflow"."steps" LANGUAGE sql SET "search_path" = '' AS $$
228
+ -- Call the original function with an empty array
229
+ SELECT * FROM pgflow.add_step(flow_slug, step_slug, ARRAY[]::text[], max_attempts, base_delay, timeout);
230
+ $$;
231
+ -- Create "calculate_retry_delay" function
232
+ CREATE FUNCTION "pgflow"."calculate_retry_delay" ("base_delay" numeric, "attempts_count" integer) RETURNS integer LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$ select floor(base_delay * power(2, attempts_count))::int $$;
233
+ -- Create "maybe_complete_run" function
234
+ CREATE FUNCTION "pgflow"."maybe_complete_run" ("run_id" uuid) RETURNS void LANGUAGE sql SET "search_path" = '' AS $$
235
+ -- Update run status to completed and set output when there are no remaining steps
236
+ -- All done in a single declarative SQL statement
237
+ UPDATE pgflow.runs
238
+ SET
239
+ status = 'completed',
240
+ completed_at = now(),
241
+ output = (
242
+ -- Get outputs from final steps (steps that are not dependencies for other steps)
243
+ SELECT jsonb_object_agg(st.step_slug, st.output)
244
+ FROM pgflow.step_tasks st
245
+ JOIN pgflow.step_states ss ON ss.run_id = st.run_id AND ss.step_slug = st.step_slug
246
+ JOIN pgflow.runs r ON r.run_id = ss.run_id AND r.flow_slug = ss.flow_slug
247
+ WHERE st.run_id = maybe_complete_run.run_id
248
+ AND st.status = 'completed'
249
+ AND NOT EXISTS (
250
+ SELECT 1
251
+ FROM pgflow.deps d
252
+ WHERE d.flow_slug = ss.flow_slug
253
+ AND d.dep_slug = ss.step_slug
254
+ )
255
+ )
256
+ WHERE pgflow.runs.run_id = maybe_complete_run.run_id
257
+ AND pgflow.runs.remaining_steps = 0
258
+ AND pgflow.runs.status != 'completed';
259
+ $$;
260
+ -- Create "start_ready_steps" function
261
+ CREATE FUNCTION "pgflow"."start_ready_steps" ("run_id" uuid) RETURNS void LANGUAGE sql SET "search_path" = '' AS $$
262
+ WITH ready_steps AS (
263
+ SELECT *
264
+ FROM pgflow.step_states AS step_state
265
+ WHERE step_state.run_id = start_ready_steps.run_id
266
+ AND step_state.status = 'created'
267
+ AND step_state.remaining_deps = 0
268
+ ORDER BY step_state.step_slug
269
+ FOR UPDATE
270
+ ),
271
+ started_step_states AS (
272
+ UPDATE pgflow.step_states
273
+ SET status = 'started',
274
+ started_at = now()
275
+ FROM ready_steps
276
+ WHERE pgflow.step_states.run_id = start_ready_steps.run_id
277
+ AND pgflow.step_states.step_slug = ready_steps.step_slug
278
+ RETURNING pgflow.step_states.*
279
+ ),
280
+ sent_messages AS (
281
+ SELECT
282
+ started_step.flow_slug,
283
+ started_step.run_id,
284
+ started_step.step_slug,
285
+ pgmq.send(started_step.flow_slug, jsonb_build_object(
286
+ 'flow_slug', started_step.flow_slug,
287
+ 'run_id', started_step.run_id,
288
+ 'step_slug', started_step.step_slug,
289
+ 'task_index', 0
290
+ )) AS msg_id
291
+ FROM started_step_states AS started_step
292
+ )
293
+ INSERT INTO pgflow.step_tasks (flow_slug, run_id, step_slug, message_id)
294
+ SELECT
295
+ sent_messages.flow_slug,
296
+ sent_messages.run_id,
297
+ sent_messages.step_slug,
298
+ sent_messages.msg_id
299
+ FROM sent_messages;
300
+ $$;
301
+ -- Create "complete_task" function
302
+ CREATE FUNCTION "pgflow"."complete_task" ("run_id" uuid, "step_slug" text, "task_index" integer, "output" jsonb) RETURNS SETOF "pgflow"."step_tasks" LANGUAGE plpgsql SET "search_path" = '' AS $$
303
+ begin
304
+
305
+ WITH run_lock AS (
306
+ SELECT * FROM pgflow.runs
307
+ WHERE pgflow.runs.run_id = complete_task.run_id
308
+ FOR UPDATE
309
+ ),
310
+ step_lock AS (
311
+ SELECT * FROM pgflow.step_states
312
+ WHERE pgflow.step_states.run_id = complete_task.run_id
313
+ AND pgflow.step_states.step_slug = complete_task.step_slug
314
+ FOR UPDATE
315
+ ),
316
+ task AS (
317
+ UPDATE pgflow.step_tasks
318
+ SET
319
+ status = 'completed',
320
+ completed_at = now(),
321
+ output = complete_task.output
322
+ WHERE pgflow.step_tasks.run_id = complete_task.run_id
323
+ AND pgflow.step_tasks.step_slug = complete_task.step_slug
324
+ AND pgflow.step_tasks.task_index = complete_task.task_index
325
+ RETURNING *
326
+ ),
327
+ step_state AS (
328
+ UPDATE pgflow.step_states
329
+ SET
330
+ status = CASE
331
+ WHEN pgflow.step_states.remaining_tasks = 1 THEN 'completed' -- Will be 0 after decrement
332
+ ELSE 'started'
333
+ END,
334
+ completed_at = CASE
335
+ WHEN pgflow.step_states.remaining_tasks = 1 THEN now() -- Will be 0 after decrement
336
+ ELSE NULL
337
+ END,
338
+ remaining_tasks = pgflow.step_states.remaining_tasks - 1
339
+ FROM task
340
+ WHERE pgflow.step_states.run_id = complete_task.run_id
341
+ AND pgflow.step_states.step_slug = complete_task.step_slug
342
+ RETURNING pgflow.step_states.*
343
+ ),
344
+ -- Find all dependent steps if the current step was completed
345
+ dependent_steps AS (
346
+ SELECT d.step_slug AS dependent_step_slug
347
+ FROM pgflow.deps d
348
+ JOIN step_state s ON s.status = 'completed' AND d.flow_slug = s.flow_slug
349
+ WHERE d.dep_slug = complete_task.step_slug
350
+ ORDER BY d.step_slug -- Ensure consistent ordering
351
+ ),
352
+ -- Lock dependent steps before updating
353
+ dependent_steps_lock AS (
354
+ SELECT * FROM pgflow.step_states
355
+ WHERE pgflow.step_states.run_id = complete_task.run_id
356
+ AND pgflow.step_states.step_slug IN (SELECT dependent_step_slug FROM dependent_steps)
357
+ FOR UPDATE
358
+ ),
359
+ -- Update all dependent steps
360
+ dependent_steps_update AS (
361
+ UPDATE pgflow.step_states
362
+ SET remaining_deps = pgflow.step_states.remaining_deps - 1
363
+ FROM dependent_steps
364
+ WHERE pgflow.step_states.run_id = complete_task.run_id
365
+ AND pgflow.step_states.step_slug = dependent_steps.dependent_step_slug
366
+ )
367
+ -- Only decrement remaining_steps, don't update status
368
+ UPDATE pgflow.runs
369
+ SET remaining_steps = pgflow.runs.remaining_steps - 1
370
+ FROM step_state
371
+ WHERE pgflow.runs.run_id = complete_task.run_id
372
+ AND step_state.status = 'completed';
373
+
374
+ PERFORM pgmq.archive(
375
+ queue_name => (SELECT run.flow_slug FROM pgflow.runs AS run WHERE run.run_id = complete_task.run_id),
376
+ msg_id => (SELECT message_id FROM pgflow.step_tasks AS step_task
377
+ WHERE step_task.run_id = complete_task.run_id
378
+ AND step_task.step_slug = complete_task.step_slug
379
+ AND step_task.task_index = complete_task.task_index)
380
+ );
381
+
382
+ PERFORM pgflow.start_ready_steps(complete_task.run_id);
383
+
384
+ PERFORM pgflow.maybe_complete_run(complete_task.run_id);
385
+
386
+ RETURN QUERY SELECT *
387
+ FROM pgflow.step_tasks AS step_task
388
+ WHERE step_task.run_id = complete_task.run_id
389
+ AND step_task.step_slug = complete_task.step_slug
390
+ AND step_task.task_index = complete_task.task_index;
391
+
392
+ end;
393
+ $$;
394
+ -- Create "create_flow" function
395
+ CREATE FUNCTION "pgflow"."create_flow" ("flow_slug" text, "max_attempts" integer DEFAULT 3, "base_delay" integer DEFAULT 5, "timeout" integer DEFAULT 60) RETURNS "pgflow"."flows" LANGUAGE sql SET "search_path" = '' AS $$
396
+ WITH
397
+ flow_upsert AS (
398
+ INSERT INTO pgflow.flows (flow_slug, opt_max_attempts, opt_base_delay, opt_timeout)
399
+ VALUES (flow_slug, max_attempts, base_delay, timeout)
400
+ ON CONFLICT (flow_slug) DO UPDATE
401
+ SET flow_slug = pgflow.flows.flow_slug -- Dummy update
402
+ RETURNING *
403
+ ),
404
+ ensure_queue AS (
405
+ SELECT pgmq.create(flow_slug)
406
+ WHERE NOT EXISTS (
407
+ SELECT 1 FROM pgmq.list_queues() WHERE queue_name = flow_slug
408
+ )
409
+ )
410
+ SELECT f.*
411
+ FROM flow_upsert f
412
+ LEFT JOIN (SELECT 1 FROM ensure_queue) _dummy ON true; -- Left join ensures flow is returned
413
+ $$;
414
+ -- Create "fail_task" function
415
+ CREATE FUNCTION "pgflow"."fail_task" ("run_id" uuid, "step_slug" text, "task_index" integer, "error_message" text) RETURNS SETOF "pgflow"."step_tasks" LANGUAGE plpgsql SET "search_path" = '' AS $$
416
+ begin
417
+
418
+ WITH run_lock AS (
419
+ SELECT * FROM pgflow.runs
420
+ WHERE pgflow.runs.run_id = fail_task.run_id
421
+ FOR UPDATE
422
+ ),
423
+ step_lock AS (
424
+ SELECT * FROM pgflow.step_states
425
+ WHERE pgflow.step_states.run_id = fail_task.run_id
426
+ AND pgflow.step_states.step_slug = fail_task.step_slug
427
+ FOR UPDATE
428
+ ),
429
+ flow_info AS (
430
+ SELECT r.flow_slug
431
+ FROM pgflow.runs r
432
+ WHERE r.run_id = fail_task.run_id
433
+ ),
434
+ config AS (
435
+ SELECT
436
+ COALESCE(s.opt_max_attempts, f.opt_max_attempts) AS opt_max_attempts,
437
+ COALESCE(s.opt_base_delay, f.opt_base_delay) AS opt_base_delay
438
+ FROM pgflow.steps s
439
+ JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
440
+ JOIN flow_info fi ON fi.flow_slug = s.flow_slug
441
+ WHERE s.flow_slug = fi.flow_slug AND s.step_slug = fail_task.step_slug
442
+ ),
443
+
444
+ fail_or_retry_task as (
445
+ UPDATE pgflow.step_tasks as task
446
+ SET
447
+ status = CASE
448
+ WHEN task.attempts_count < (SELECT opt_max_attempts FROM config) THEN 'queued'
449
+ ELSE 'failed'
450
+ END,
451
+ failed_at = CASE
452
+ WHEN task.attempts_count >= (SELECT opt_max_attempts FROM config) THEN now()
453
+ ELSE NULL
454
+ END,
455
+ error_message = fail_task.error_message
456
+ WHERE task.run_id = fail_task.run_id
457
+ AND task.step_slug = fail_task.step_slug
458
+ AND task.task_index = fail_task.task_index
459
+ AND task.status = 'queued'
460
+ RETURNING *
461
+ ),
462
+ maybe_fail_step AS (
463
+ UPDATE pgflow.step_states
464
+ SET
465
+ status = CASE
466
+ WHEN (select fail_or_retry_task.status from fail_or_retry_task) = 'failed' THEN 'failed'
467
+ ELSE pgflow.step_states.status
468
+ END,
469
+ failed_at = CASE
470
+ WHEN (select fail_or_retry_task.status from fail_or_retry_task) = 'failed' THEN now()
471
+ ELSE NULL
472
+ END
473
+ FROM fail_or_retry_task
474
+ WHERE pgflow.step_states.run_id = fail_task.run_id
475
+ AND pgflow.step_states.step_slug = fail_task.step_slug
476
+ RETURNING pgflow.step_states.*
477
+ )
478
+ UPDATE pgflow.runs
479
+ SET status = CASE
480
+ WHEN (select status from maybe_fail_step) = 'failed' THEN 'failed'
481
+ ELSE status
482
+ END,
483
+ failed_at = CASE
484
+ WHEN (select status from maybe_fail_step) = 'failed' THEN now()
485
+ ELSE NULL
486
+ END
487
+ WHERE pgflow.runs.run_id = fail_task.run_id;
488
+
489
+ -- For queued tasks: delay the message for retry with exponential backoff
490
+ PERFORM (
491
+ WITH retry_config AS (
492
+ SELECT
493
+ COALESCE(s.opt_base_delay, f.opt_base_delay) AS base_delay
494
+ FROM pgflow.steps s
495
+ JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
496
+ JOIN pgflow.runs r ON r.flow_slug = f.flow_slug
497
+ WHERE r.run_id = fail_task.run_id
498
+ AND s.step_slug = fail_task.step_slug
499
+ ),
500
+ queued_tasks AS (
501
+ SELECT
502
+ r.flow_slug,
503
+ st.message_id,
504
+ pgflow.calculate_retry_delay((SELECT base_delay FROM retry_config), st.attempts_count) AS calculated_delay
505
+ FROM pgflow.step_tasks st
506
+ JOIN pgflow.runs r ON st.run_id = r.run_id
507
+ WHERE st.run_id = fail_task.run_id
508
+ AND st.step_slug = fail_task.step_slug
509
+ AND st.task_index = fail_task.task_index
510
+ AND st.status = 'queued'
511
+ )
512
+ SELECT pgmq.set_vt(qt.flow_slug, qt.message_id, qt.calculated_delay)
513
+ FROM queued_tasks qt
514
+ WHERE EXISTS (SELECT 1 FROM queued_tasks)
515
+ );
516
+
517
+ -- For failed tasks: archive the message
518
+ PERFORM (
519
+ WITH failed_tasks AS (
520
+ SELECT r.flow_slug, st.message_id
521
+ FROM pgflow.step_tasks st
522
+ JOIN pgflow.runs r ON st.run_id = r.run_id
523
+ WHERE st.run_id = fail_task.run_id
524
+ AND st.step_slug = fail_task.step_slug
525
+ AND st.task_index = fail_task.task_index
526
+ AND st.status = 'failed'
527
+ )
528
+ SELECT pgmq.archive(ft.flow_slug, ft.message_id)
529
+ FROM failed_tasks ft
530
+ WHERE EXISTS (SELECT 1 FROM failed_tasks)
531
+ );
532
+
533
+ return query select *
534
+ from pgflow.step_tasks st
535
+ where st.run_id = fail_task.run_id
536
+ and st.step_slug = fail_task.step_slug
537
+ and st.task_index = fail_task.task_index;
538
+
539
+ end;
540
+ $$;
541
+ -- Create "start_flow" function
542
+ CREATE FUNCTION "pgflow"."start_flow" ("flow_slug" text, "input" jsonb) RETURNS SETOF "pgflow"."runs" LANGUAGE plpgsql SET "search_path" = '' AS $$
543
+ declare
544
+ v_created_run pgflow.runs%ROWTYPE;
545
+ begin
546
+
547
+ WITH
548
+ flow_steps AS (
549
+ SELECT steps.flow_slug, steps.step_slug, steps.deps_count
550
+ FROM pgflow.steps
551
+ WHERE steps.flow_slug = start_flow.flow_slug
552
+ ),
553
+ created_run AS (
554
+ INSERT INTO pgflow.runs (flow_slug, input, remaining_steps)
555
+ VALUES (
556
+ start_flow.flow_slug,
557
+ start_flow.input,
558
+ (SELECT count(*) FROM flow_steps)
559
+ )
560
+ RETURNING *
561
+ ),
562
+ created_step_states AS (
563
+ INSERT INTO pgflow.step_states (flow_slug, run_id, step_slug, remaining_deps)
564
+ SELECT
565
+ fs.flow_slug,
566
+ (SELECT run_id FROM created_run),
567
+ fs.step_slug,
568
+ fs.deps_count
569
+ FROM flow_steps fs
570
+ )
571
+ SELECT * FROM created_run INTO v_created_run;
572
+
573
+ PERFORM pgflow.start_ready_steps(v_created_run.run_id);
574
+
575
+ RETURN QUERY SELECT * FROM pgflow.runs where run_id = v_created_run.run_id;
576
+
577
+ end;
578
+ $$;
579
+ -- Create "workers" table
580
+ CREATE TABLE "pgflow"."workers" ("worker_id" uuid NOT NULL, "queue_name" text NOT NULL, "function_name" text NOT NULL, "started_at" timestamptz NOT NULL DEFAULT now(), "stopped_at" timestamptz NULL, "last_heartbeat_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("worker_id"));
581
+ -- Create index "idx_workers_queue_name" to table: "workers"
582
+ CREATE INDEX "idx_workers_queue_name" ON "pgflow"."workers" ("queue_name");
583
+ `,
584
+ };
@@ -0,0 +1,3 @@
1
+ import type { Migration } from '../types.js';
2
+ export declare const migration: Migration;
3
+ //# sourceMappingURL=m_20250517072017.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"m_20250517072017.d.ts","sourceRoot":"","sources":["../../../src/migrations/_generated/m_20250517072017.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,eAAO,MAAM,SAAS,EAAE,SAyGvB,CAAC"}