@pgflow/core 0.0.0-array-map-steps-302d00a8-20250922101336 → 0.0.0-array-map-steps-b956f8f9-20251006084236

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.
@@ -1,321 +0,0 @@
1
- -- Create "cascade_complete_taskless_steps" function
2
- CREATE FUNCTION "pgflow"."cascade_complete_taskless_steps" ("run_id" uuid) RETURNS integer LANGUAGE plpgsql AS $$
3
- DECLARE
4
- v_total_completed int := 0;
5
- v_iteration_completed int;
6
- v_iterations int := 0;
7
- v_max_iterations int := 50;
8
- BEGIN
9
- LOOP
10
- -- Safety counter to prevent infinite loops
11
- v_iterations := v_iterations + 1;
12
- IF v_iterations > v_max_iterations THEN
13
- RAISE EXCEPTION 'Cascade loop exceeded safety limit of % iterations', v_max_iterations;
14
- END IF;
15
-
16
- WITH completed AS (
17
- -- Complete all ready taskless steps in topological order
18
- UPDATE pgflow.step_states ss
19
- SET status = 'completed',
20
- started_at = now(),
21
- completed_at = now(),
22
- remaining_tasks = 0
23
- FROM pgflow.steps s
24
- WHERE ss.run_id = cascade_complete_taskless_steps.run_id
25
- AND ss.flow_slug = s.flow_slug
26
- AND ss.step_slug = s.step_slug
27
- AND ss.status = 'created'
28
- AND ss.remaining_deps = 0
29
- AND ss.initial_tasks = 0
30
- -- Process in topological order to ensure proper cascade
31
- RETURNING ss.*
32
- ),
33
- dep_updates AS (
34
- -- Update remaining_deps and initial_tasks for dependents of completed steps
35
- UPDATE pgflow.step_states ss
36
- SET remaining_deps = ss.remaining_deps - dep_count.count,
37
- -- If the dependent is a map step and its dependency completed with 0 tasks,
38
- -- set its initial_tasks to 0 as well
39
- initial_tasks = CASE
40
- WHEN s.step_type = 'map' AND dep_count.has_zero_tasks
41
- THEN 0
42
- ELSE ss.initial_tasks
43
- END
44
- FROM (
45
- -- Count how many completed steps are dependencies of each dependent
46
- SELECT
47
- d.flow_slug,
48
- d.step_slug as dependent_slug,
49
- COUNT(*) as count,
50
- BOOL_OR(c.initial_tasks = 0) as has_zero_tasks
51
- FROM completed c
52
- JOIN pgflow.deps d ON d.flow_slug = c.flow_slug
53
- AND d.dep_slug = c.step_slug
54
- GROUP BY d.flow_slug, d.step_slug
55
- ) dep_count,
56
- pgflow.steps s
57
- WHERE ss.run_id = cascade_complete_taskless_steps.run_id
58
- AND ss.flow_slug = dep_count.flow_slug
59
- AND ss.step_slug = dep_count.dependent_slug
60
- AND s.flow_slug = ss.flow_slug
61
- AND s.step_slug = ss.step_slug
62
- ),
63
- run_updates AS (
64
- -- Update run's remaining_steps count
65
- UPDATE pgflow.runs r
66
- SET remaining_steps = r.remaining_steps - c.completed_count,
67
- status = CASE
68
- WHEN r.remaining_steps - c.completed_count = 0
69
- THEN 'completed'
70
- ELSE r.status
71
- END,
72
- completed_at = CASE
73
- WHEN r.remaining_steps - c.completed_count = 0
74
- THEN now()
75
- ELSE r.completed_at
76
- END
77
- FROM (SELECT COUNT(*) AS completed_count FROM completed) c
78
- WHERE r.run_id = cascade_complete_taskless_steps.run_id
79
- AND c.completed_count > 0
80
- )
81
- SELECT COUNT(*) INTO v_iteration_completed FROM completed;
82
-
83
- EXIT WHEN v_iteration_completed = 0;
84
- v_total_completed := v_total_completed + v_iteration_completed;
85
- END LOOP;
86
-
87
- RETURN v_total_completed;
88
- END;
89
- $$;
90
- -- Modify "complete_task" function
91
- CREATE OR REPLACE 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 $$
92
- declare
93
- v_step_state pgflow.step_states%ROWTYPE;
94
- begin
95
-
96
- WITH run_lock AS (
97
- SELECT * FROM pgflow.runs
98
- WHERE pgflow.runs.run_id = complete_task.run_id
99
- FOR UPDATE
100
- ),
101
- step_lock AS (
102
- SELECT * FROM pgflow.step_states
103
- WHERE pgflow.step_states.run_id = complete_task.run_id
104
- AND pgflow.step_states.step_slug = complete_task.step_slug
105
- FOR UPDATE
106
- ),
107
- task AS (
108
- UPDATE pgflow.step_tasks
109
- SET
110
- status = 'completed',
111
- completed_at = now(),
112
- output = complete_task.output
113
- WHERE pgflow.step_tasks.run_id = complete_task.run_id
114
- AND pgflow.step_tasks.step_slug = complete_task.step_slug
115
- AND pgflow.step_tasks.task_index = complete_task.task_index
116
- AND pgflow.step_tasks.status = 'started'
117
- RETURNING *
118
- ),
119
- step_state AS (
120
- UPDATE pgflow.step_states
121
- SET
122
- status = CASE
123
- WHEN pgflow.step_states.remaining_tasks = 1 THEN 'completed' -- Will be 0 after decrement
124
- ELSE 'started'
125
- END,
126
- completed_at = CASE
127
- WHEN pgflow.step_states.remaining_tasks = 1 THEN now() -- Will be 0 after decrement
128
- ELSE NULL
129
- END,
130
- remaining_tasks = pgflow.step_states.remaining_tasks - 1
131
- FROM task
132
- WHERE pgflow.step_states.run_id = complete_task.run_id
133
- AND pgflow.step_states.step_slug = complete_task.step_slug
134
- RETURNING pgflow.step_states.*
135
- ),
136
- -- Find all dependent steps if the current step was completed
137
- dependent_steps AS (
138
- SELECT d.step_slug AS dependent_step_slug
139
- FROM pgflow.deps d
140
- JOIN step_state s ON s.status = 'completed' AND d.flow_slug = s.flow_slug
141
- WHERE d.dep_slug = complete_task.step_slug
142
- ORDER BY d.step_slug -- Ensure consistent ordering
143
- ),
144
- -- Lock dependent steps before updating
145
- dependent_steps_lock AS (
146
- SELECT * FROM pgflow.step_states
147
- WHERE pgflow.step_states.run_id = complete_task.run_id
148
- AND pgflow.step_states.step_slug IN (SELECT dependent_step_slug FROM dependent_steps)
149
- FOR UPDATE
150
- ),
151
- -- Update all dependent steps
152
- dependent_steps_update AS (
153
- UPDATE pgflow.step_states ss
154
- SET remaining_deps = ss.remaining_deps - 1,
155
- -- For map dependents of single steps producing arrays, set initial_tasks
156
- initial_tasks = CASE
157
- WHEN s.step_type = 'map' AND jsonb_typeof(complete_task.output) = 'array'
158
- THEN jsonb_array_length(complete_task.output)
159
- ELSE ss.initial_tasks
160
- END
161
- FROM dependent_steps ds, pgflow.steps s
162
- WHERE ss.run_id = complete_task.run_id
163
- AND ss.step_slug = ds.dependent_step_slug
164
- AND s.flow_slug = ss.flow_slug
165
- AND s.step_slug = ss.step_slug
166
- )
167
- -- Only decrement remaining_steps, don't update status
168
- UPDATE pgflow.runs
169
- SET remaining_steps = pgflow.runs.remaining_steps - 1
170
- FROM step_state
171
- WHERE pgflow.runs.run_id = complete_task.run_id
172
- AND step_state.status = 'completed';
173
-
174
- -- Get the updated step state for broadcasting
175
- SELECT * INTO v_step_state FROM pgflow.step_states
176
- WHERE pgflow.step_states.run_id = complete_task.run_id AND pgflow.step_states.step_slug = complete_task.step_slug;
177
-
178
- -- Send broadcast event for step completed if the step is completed
179
- IF v_step_state.status = 'completed' THEN
180
- -- Step just completed, cascade any ready taskless steps
181
- PERFORM pgflow.cascade_complete_taskless_steps(complete_task.run_id);
182
-
183
- PERFORM realtime.send(
184
- jsonb_build_object(
185
- 'event_type', 'step:completed',
186
- 'run_id', complete_task.run_id,
187
- 'step_slug', complete_task.step_slug,
188
- 'status', 'completed',
189
- 'output', complete_task.output,
190
- 'completed_at', v_step_state.completed_at
191
- ),
192
- concat('step:', complete_task.step_slug, ':completed'),
193
- concat('pgflow:run:', complete_task.run_id),
194
- false
195
- );
196
- END IF;
197
-
198
- -- For completed tasks: archive the message
199
- PERFORM (
200
- WITH completed_tasks AS (
201
- SELECT r.flow_slug, st.message_id
202
- FROM pgflow.step_tasks st
203
- JOIN pgflow.runs r ON st.run_id = r.run_id
204
- WHERE st.run_id = complete_task.run_id
205
- AND st.step_slug = complete_task.step_slug
206
- AND st.task_index = complete_task.task_index
207
- AND st.status = 'completed'
208
- )
209
- SELECT pgmq.archive(ct.flow_slug, ct.message_id)
210
- FROM completed_tasks ct
211
- WHERE EXISTS (SELECT 1 FROM completed_tasks)
212
- );
213
-
214
- PERFORM pgflow.start_ready_steps(complete_task.run_id);
215
-
216
- PERFORM pgflow.maybe_complete_run(complete_task.run_id);
217
-
218
- RETURN QUERY SELECT *
219
- FROM pgflow.step_tasks AS step_task
220
- WHERE step_task.run_id = complete_task.run_id
221
- AND step_task.step_slug = complete_task.step_slug
222
- AND step_task.task_index = complete_task.task_index;
223
-
224
- end;
225
- $$;
226
- -- Modify "start_flow" function
227
- CREATE OR REPLACE FUNCTION "pgflow"."start_flow" ("flow_slug" text, "input" jsonb, "run_id" uuid DEFAULT NULL::uuid) RETURNS SETOF "pgflow"."runs" LANGUAGE plpgsql SET "search_path" = '' AS $$
228
- declare
229
- v_created_run pgflow.runs%ROWTYPE;
230
- v_root_map_count int;
231
- begin
232
-
233
- -- Check for root map steps and validate input
234
- WITH root_maps AS (
235
- SELECT step_slug
236
- FROM pgflow.steps
237
- WHERE steps.flow_slug = start_flow.flow_slug
238
- AND steps.step_type = 'map'
239
- AND steps.deps_count = 0
240
- )
241
- SELECT COUNT(*) INTO v_root_map_count FROM root_maps;
242
-
243
- -- If we have root map steps, validate that input is an array
244
- IF v_root_map_count > 0 THEN
245
- -- First check for NULL (should be caught by NOT NULL constraint, but be defensive)
246
- IF start_flow.input IS NULL THEN
247
- RAISE EXCEPTION 'Flow % has root map steps but input is NULL', start_flow.flow_slug;
248
- END IF;
249
-
250
- -- Then check if it's not an array
251
- IF jsonb_typeof(start_flow.input) != 'array' THEN
252
- RAISE EXCEPTION 'Flow % has root map steps but input is not an array (got %)',
253
- start_flow.flow_slug, jsonb_typeof(start_flow.input);
254
- END IF;
255
- END IF;
256
-
257
- WITH
258
- flow_steps AS (
259
- SELECT steps.flow_slug, steps.step_slug, steps.step_type, steps.deps_count
260
- FROM pgflow.steps
261
- WHERE steps.flow_slug = start_flow.flow_slug
262
- ),
263
- created_run AS (
264
- INSERT INTO pgflow.runs (run_id, flow_slug, input, remaining_steps)
265
- VALUES (
266
- COALESCE(start_flow.run_id, gen_random_uuid()),
267
- start_flow.flow_slug,
268
- start_flow.input,
269
- (SELECT count(*) FROM flow_steps)
270
- )
271
- RETURNING *
272
- ),
273
- created_step_states AS (
274
- INSERT INTO pgflow.step_states (flow_slug, run_id, step_slug, remaining_deps, initial_tasks)
275
- SELECT
276
- fs.flow_slug,
277
- (SELECT created_run.run_id FROM created_run),
278
- fs.step_slug,
279
- fs.deps_count,
280
- -- For root map steps (map with no deps), set initial_tasks to array length
281
- -- For all other steps, set initial_tasks to 1
282
- CASE
283
- WHEN fs.step_type = 'map' AND fs.deps_count = 0 THEN
284
- CASE
285
- WHEN jsonb_typeof(start_flow.input) = 'array' THEN
286
- jsonb_array_length(start_flow.input)
287
- ELSE
288
- 1
289
- END
290
- ELSE
291
- 1
292
- END
293
- FROM flow_steps fs
294
- )
295
- SELECT * FROM created_run INTO v_created_run;
296
-
297
- -- Send broadcast event for run started
298
- PERFORM realtime.send(
299
- jsonb_build_object(
300
- 'event_type', 'run:started',
301
- 'run_id', v_created_run.run_id,
302
- 'flow_slug', v_created_run.flow_slug,
303
- 'input', v_created_run.input,
304
- 'status', 'started',
305
- 'remaining_steps', v_created_run.remaining_steps,
306
- 'started_at', v_created_run.started_at
307
- ),
308
- 'run:started',
309
- concat('pgflow:run:', v_created_run.run_id),
310
- false
311
- );
312
-
313
- -- Complete any taskless steps that are ready (e.g., empty array maps)
314
- PERFORM pgflow.cascade_complete_taskless_steps(v_created_run.run_id);
315
-
316
- PERFORM pgflow.start_ready_steps(v_created_run.run_id);
317
-
318
- RETURN QUERY SELECT * FROM pgflow.runs where pgflow.runs.run_id = v_created_run.run_id;
319
-
320
- end;
321
- $$;