@pgflow/core 0.0.0-array-map-steps-cd94242a-20251008042921 → 0.0.0-condition-4354fcb6-20260108134756
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/README.md +24 -7
- package/dist/CHANGELOG.md +187 -13
- package/dist/PgflowSqlClient.js +1 -1
- package/dist/README.md +24 -7
- package/dist/database-types.d.ts +392 -71
- package/dist/database-types.d.ts.map +1 -1
- package/dist/package.json +8 -4
- package/dist/supabase/migrations/20250429164909_pgflow_initial.sql +2 -2
- package/dist/supabase/migrations/20251006073122_pgflow_add_map_step_type.sql +24 -7
- package/dist/supabase/migrations/20251103222045_pgflow_fix_broadcast_order_and_timestamp_handling.sql +622 -0
- package/dist/supabase/migrations/20251104080523_pgflow_upgrade_pgmq_1_5_1.sql +93 -0
- package/dist/supabase/migrations/20251130000000_pgflow_auto_compilation.sql +268 -0
- package/dist/supabase/migrations/20251209074533_pgflow_worker_management.sql +273 -0
- package/dist/supabase/migrations/20251212100113_pgflow_allow_data_loss_parameter.sql +54 -0
- package/dist/supabase/migrations/20251225163110_pgflow_add_flow_input_column.sql +185 -0
- package/dist/supabase/migrations/20260103145141_pgflow_step_output_storage.sql +909 -0
- package/dist/supabase/migrations/20260108131350_pgflow_step_conditions.sql +1515 -0
- package/dist/types.d.ts +7 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -5
- package/dist/ATLAS.md +0 -32
|
@@ -0,0 +1,1515 @@
|
|
|
1
|
+
-- Modify "step_states" table
|
|
2
|
+
ALTER TABLE "pgflow"."step_states" DROP CONSTRAINT "completed_at_or_failed_at", DROP CONSTRAINT "remaining_tasks_state_consistency", ADD CONSTRAINT "remaining_tasks_state_consistency" CHECK ((remaining_tasks IS NULL) OR (status <> ALL (ARRAY['created'::text, 'skipped'::text]))), DROP CONSTRAINT "status_is_valid", ADD CONSTRAINT "status_is_valid" CHECK (status = ANY (ARRAY['created'::text, 'started'::text, 'completed'::text, 'failed'::text, 'skipped'::text])), ADD CONSTRAINT "completed_at_or_failed_at_or_skipped_at" CHECK (((
|
|
3
|
+
CASE
|
|
4
|
+
WHEN (completed_at IS NOT NULL) THEN 1
|
|
5
|
+
ELSE 0
|
|
6
|
+
END +
|
|
7
|
+
CASE
|
|
8
|
+
WHEN (failed_at IS NOT NULL) THEN 1
|
|
9
|
+
ELSE 0
|
|
10
|
+
END) +
|
|
11
|
+
CASE
|
|
12
|
+
WHEN (skipped_at IS NOT NULL) THEN 1
|
|
13
|
+
ELSE 0
|
|
14
|
+
END) <= 1), ADD CONSTRAINT "skip_reason_matches_status" CHECK (((status = 'skipped'::text) AND (skip_reason IS NOT NULL)) OR ((status <> 'skipped'::text) AND (skip_reason IS NULL))), ADD CONSTRAINT "skipped_at_is_after_created_at" CHECK ((skipped_at IS NULL) OR (skipped_at >= created_at)), ADD COLUMN "skip_reason" text NULL, ADD COLUMN "skipped_at" timestamptz NULL;
|
|
15
|
+
-- Create index "idx_step_states_skipped" to table: "step_states"
|
|
16
|
+
CREATE INDEX "idx_step_states_skipped" ON "pgflow"."step_states" ("run_id", "step_slug") WHERE (status = 'skipped'::text);
|
|
17
|
+
-- Modify "steps" table
|
|
18
|
+
ALTER TABLE "pgflow"."steps" ADD CONSTRAINT "when_failed_is_valid" CHECK (when_failed = ANY (ARRAY['fail'::text, 'skip'::text, 'skip-cascade'::text])), ADD CONSTRAINT "when_unmet_is_valid" CHECK (when_unmet = ANY (ARRAY['fail'::text, 'skip'::text, 'skip-cascade'::text])), ADD COLUMN "condition_pattern" jsonb NULL, ADD COLUMN "condition_not_pattern" jsonb NULL, ADD COLUMN "when_unmet" text NOT NULL DEFAULT 'skip', ADD COLUMN "when_failed" text NOT NULL DEFAULT 'fail';
|
|
19
|
+
-- Create "_cascade_force_skip_steps" function
|
|
20
|
+
CREATE FUNCTION "pgflow"."_cascade_force_skip_steps" ("run_id" uuid, "step_slug" text, "skip_reason" text) RETURNS integer LANGUAGE plpgsql AS $$
|
|
21
|
+
DECLARE
|
|
22
|
+
v_flow_slug text;
|
|
23
|
+
v_total_skipped int := 0;
|
|
24
|
+
BEGIN
|
|
25
|
+
-- Get flow_slug for this run
|
|
26
|
+
SELECT r.flow_slug INTO v_flow_slug
|
|
27
|
+
FROM pgflow.runs r
|
|
28
|
+
WHERE r.run_id = _cascade_force_skip_steps.run_id;
|
|
29
|
+
|
|
30
|
+
IF v_flow_slug IS NULL THEN
|
|
31
|
+
RAISE EXCEPTION 'Run not found: %', _cascade_force_skip_steps.run_id;
|
|
32
|
+
END IF;
|
|
33
|
+
|
|
34
|
+
-- ==========================================
|
|
35
|
+
-- SKIP STEPS IN TOPOLOGICAL ORDER
|
|
36
|
+
-- ==========================================
|
|
37
|
+
-- Use recursive CTE to find all downstream dependents,
|
|
38
|
+
-- then skip them in topological order (by step_index)
|
|
39
|
+
WITH RECURSIVE
|
|
40
|
+
-- ---------- Find all downstream steps ----------
|
|
41
|
+
downstream_steps AS (
|
|
42
|
+
-- Base case: the trigger step
|
|
43
|
+
SELECT
|
|
44
|
+
s.flow_slug,
|
|
45
|
+
s.step_slug,
|
|
46
|
+
s.step_index,
|
|
47
|
+
_cascade_force_skip_steps.skip_reason AS reason -- Original reason for trigger step
|
|
48
|
+
FROM pgflow.steps s
|
|
49
|
+
WHERE s.flow_slug = v_flow_slug
|
|
50
|
+
AND s.step_slug = _cascade_force_skip_steps.step_slug
|
|
51
|
+
|
|
52
|
+
UNION ALL
|
|
53
|
+
|
|
54
|
+
-- Recursive case: steps that depend on already-found steps
|
|
55
|
+
SELECT
|
|
56
|
+
s.flow_slug,
|
|
57
|
+
s.step_slug,
|
|
58
|
+
s.step_index,
|
|
59
|
+
'dependency_skipped'::text AS reason -- Downstream steps get this reason
|
|
60
|
+
FROM pgflow.steps s
|
|
61
|
+
JOIN pgflow.deps d ON d.flow_slug = s.flow_slug AND d.step_slug = s.step_slug
|
|
62
|
+
JOIN downstream_steps ds ON ds.flow_slug = d.flow_slug AND ds.step_slug = d.dep_slug
|
|
63
|
+
),
|
|
64
|
+
-- ---------- Deduplicate and order by step_index ----------
|
|
65
|
+
steps_to_skip AS (
|
|
66
|
+
SELECT DISTINCT ON (ds.step_slug)
|
|
67
|
+
ds.flow_slug,
|
|
68
|
+
ds.step_slug,
|
|
69
|
+
ds.step_index,
|
|
70
|
+
ds.reason
|
|
71
|
+
FROM downstream_steps ds
|
|
72
|
+
ORDER BY ds.step_slug, ds.step_index -- Keep first occurrence (trigger step has original reason)
|
|
73
|
+
),
|
|
74
|
+
-- ---------- Skip the steps ----------
|
|
75
|
+
skipped AS (
|
|
76
|
+
UPDATE pgflow.step_states ss
|
|
77
|
+
SET status = 'skipped',
|
|
78
|
+
skip_reason = sts.reason,
|
|
79
|
+
skipped_at = now(),
|
|
80
|
+
remaining_tasks = NULL -- Clear remaining_tasks for skipped steps
|
|
81
|
+
FROM steps_to_skip sts
|
|
82
|
+
WHERE ss.run_id = _cascade_force_skip_steps.run_id
|
|
83
|
+
AND ss.step_slug = sts.step_slug
|
|
84
|
+
AND ss.status IN ('created', 'started') -- Only skip non-terminal steps
|
|
85
|
+
RETURNING
|
|
86
|
+
ss.*,
|
|
87
|
+
-- Broadcast step:skipped event
|
|
88
|
+
realtime.send(
|
|
89
|
+
jsonb_build_object(
|
|
90
|
+
'event_type', 'step:skipped',
|
|
91
|
+
'run_id', ss.run_id,
|
|
92
|
+
'flow_slug', ss.flow_slug,
|
|
93
|
+
'step_slug', ss.step_slug,
|
|
94
|
+
'status', 'skipped',
|
|
95
|
+
'skip_reason', ss.skip_reason,
|
|
96
|
+
'skipped_at', ss.skipped_at
|
|
97
|
+
),
|
|
98
|
+
concat('step:', ss.step_slug, ':skipped'),
|
|
99
|
+
concat('pgflow:run:', ss.run_id),
|
|
100
|
+
false
|
|
101
|
+
) as _broadcast_result
|
|
102
|
+
),
|
|
103
|
+
-- ---------- Update run counters ----------
|
|
104
|
+
run_updates AS (
|
|
105
|
+
UPDATE pgflow.runs r
|
|
106
|
+
SET remaining_steps = r.remaining_steps - skipped_count.count
|
|
107
|
+
FROM (SELECT COUNT(*) AS count FROM skipped) skipped_count
|
|
108
|
+
WHERE r.run_id = _cascade_force_skip_steps.run_id
|
|
109
|
+
AND skipped_count.count > 0
|
|
110
|
+
)
|
|
111
|
+
SELECT COUNT(*) INTO v_total_skipped FROM skipped;
|
|
112
|
+
|
|
113
|
+
RETURN v_total_skipped;
|
|
114
|
+
END;
|
|
115
|
+
$$;
|
|
116
|
+
-- Create "cascade_resolve_conditions" function
|
|
117
|
+
CREATE FUNCTION "pgflow"."cascade_resolve_conditions" ("run_id" uuid) RETURNS boolean LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
118
|
+
DECLARE
|
|
119
|
+
v_run_input jsonb;
|
|
120
|
+
v_run_status text;
|
|
121
|
+
v_first_fail record;
|
|
122
|
+
v_iteration_count int := 0;
|
|
123
|
+
v_max_iterations int := 50;
|
|
124
|
+
v_processed_count int;
|
|
125
|
+
BEGIN
|
|
126
|
+
-- ==========================================
|
|
127
|
+
-- GUARD: Early return if run is already terminal
|
|
128
|
+
-- ==========================================
|
|
129
|
+
SELECT r.status, r.input INTO v_run_status, v_run_input
|
|
130
|
+
FROM pgflow.runs r
|
|
131
|
+
WHERE r.run_id = cascade_resolve_conditions.run_id;
|
|
132
|
+
|
|
133
|
+
IF v_run_status IN ('failed', 'completed') THEN
|
|
134
|
+
RETURN v_run_status != 'failed';
|
|
135
|
+
END IF;
|
|
136
|
+
|
|
137
|
+
-- ==========================================
|
|
138
|
+
-- ITERATE UNTIL CONVERGENCE
|
|
139
|
+
-- ==========================================
|
|
140
|
+
-- After skipping steps, dependents may become ready and need evaluation.
|
|
141
|
+
-- Loop until no more steps are processed.
|
|
142
|
+
LOOP
|
|
143
|
+
v_iteration_count := v_iteration_count + 1;
|
|
144
|
+
IF v_iteration_count > v_max_iterations THEN
|
|
145
|
+
RAISE EXCEPTION 'cascade_resolve_conditions exceeded safety limit of % iterations', v_max_iterations;
|
|
146
|
+
END IF;
|
|
147
|
+
|
|
148
|
+
v_processed_count := 0;
|
|
149
|
+
|
|
150
|
+
-- ==========================================
|
|
151
|
+
-- PHASE 1a: CHECK FOR FAIL CONDITIONS
|
|
152
|
+
-- ==========================================
|
|
153
|
+
-- Find first step (by topological order) with unmet condition and 'fail' mode.
|
|
154
|
+
-- Condition is unmet when:
|
|
155
|
+
-- (condition_pattern is set AND input does NOT contain it) OR
|
|
156
|
+
-- (condition_not_pattern is set AND input DOES contain it)
|
|
157
|
+
WITH steps_with_conditions AS (
|
|
158
|
+
SELECT
|
|
159
|
+
step_state.flow_slug,
|
|
160
|
+
step_state.step_slug,
|
|
161
|
+
step.condition_pattern,
|
|
162
|
+
step.condition_not_pattern,
|
|
163
|
+
step.when_unmet,
|
|
164
|
+
step.deps_count,
|
|
165
|
+
step.step_index
|
|
166
|
+
FROM pgflow.step_states AS step_state
|
|
167
|
+
JOIN pgflow.steps AS step
|
|
168
|
+
ON step.flow_slug = step_state.flow_slug
|
|
169
|
+
AND step.step_slug = step_state.step_slug
|
|
170
|
+
WHERE step_state.run_id = cascade_resolve_conditions.run_id
|
|
171
|
+
AND step_state.status = 'created'
|
|
172
|
+
AND step_state.remaining_deps = 0
|
|
173
|
+
AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL)
|
|
174
|
+
),
|
|
175
|
+
step_deps_output AS (
|
|
176
|
+
SELECT
|
|
177
|
+
swc.step_slug,
|
|
178
|
+
jsonb_object_agg(dep_state.step_slug, dep_state.output) AS deps_output
|
|
179
|
+
FROM steps_with_conditions swc
|
|
180
|
+
JOIN pgflow.deps dep ON dep.flow_slug = swc.flow_slug AND dep.step_slug = swc.step_slug
|
|
181
|
+
JOIN pgflow.step_states dep_state
|
|
182
|
+
ON dep_state.run_id = cascade_resolve_conditions.run_id
|
|
183
|
+
AND dep_state.step_slug = dep.dep_slug
|
|
184
|
+
AND dep_state.status = 'completed' -- Only completed deps (not skipped)
|
|
185
|
+
WHERE swc.deps_count > 0
|
|
186
|
+
GROUP BY swc.step_slug
|
|
187
|
+
),
|
|
188
|
+
condition_evaluations AS (
|
|
189
|
+
SELECT
|
|
190
|
+
swc.*,
|
|
191
|
+
-- condition_met = (if IS NULL OR input @> if) AND (ifNot IS NULL OR NOT(input @> ifNot))
|
|
192
|
+
(swc.condition_pattern IS NULL OR
|
|
193
|
+
CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_pattern)
|
|
194
|
+
AND
|
|
195
|
+
(swc.condition_not_pattern IS NULL OR
|
|
196
|
+
NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_not_pattern))
|
|
197
|
+
AS condition_met
|
|
198
|
+
FROM steps_with_conditions swc
|
|
199
|
+
LEFT JOIN step_deps_output sdo ON sdo.step_slug = swc.step_slug
|
|
200
|
+
)
|
|
201
|
+
SELECT flow_slug, step_slug, condition_pattern, condition_not_pattern
|
|
202
|
+
INTO v_first_fail
|
|
203
|
+
FROM condition_evaluations
|
|
204
|
+
WHERE NOT condition_met AND when_unmet = 'fail'
|
|
205
|
+
ORDER BY step_index
|
|
206
|
+
LIMIT 1;
|
|
207
|
+
|
|
208
|
+
-- Handle fail mode: fail step and run, return false
|
|
209
|
+
-- Note: Cannot use "v_first_fail IS NOT NULL" because records with NULL fields
|
|
210
|
+
-- evaluate to NULL in IS NOT NULL checks. Use FOUND instead.
|
|
211
|
+
IF FOUND THEN
|
|
212
|
+
UPDATE pgflow.step_states
|
|
213
|
+
SET status = 'failed',
|
|
214
|
+
failed_at = now(),
|
|
215
|
+
error_message = 'Condition not met'
|
|
216
|
+
WHERE pgflow.step_states.run_id = cascade_resolve_conditions.run_id
|
|
217
|
+
AND pgflow.step_states.step_slug = v_first_fail.step_slug;
|
|
218
|
+
|
|
219
|
+
UPDATE pgflow.runs
|
|
220
|
+
SET status = 'failed',
|
|
221
|
+
failed_at = now()
|
|
222
|
+
WHERE pgflow.runs.run_id = cascade_resolve_conditions.run_id;
|
|
223
|
+
|
|
224
|
+
RETURN false;
|
|
225
|
+
END IF;
|
|
226
|
+
|
|
227
|
+
-- ==========================================
|
|
228
|
+
-- PHASE 1b: HANDLE SKIP CONDITIONS (with propagation)
|
|
229
|
+
-- ==========================================
|
|
230
|
+
-- Skip steps with unmet conditions and whenUnmet='skip'.
|
|
231
|
+
-- Also decrement remaining_deps on dependents and set initial_tasks=0 for map dependents.
|
|
232
|
+
WITH steps_with_conditions AS (
|
|
233
|
+
SELECT
|
|
234
|
+
step_state.flow_slug,
|
|
235
|
+
step_state.step_slug,
|
|
236
|
+
step.condition_pattern,
|
|
237
|
+
step.condition_not_pattern,
|
|
238
|
+
step.when_unmet,
|
|
239
|
+
step.deps_count,
|
|
240
|
+
step.step_index
|
|
241
|
+
FROM pgflow.step_states AS step_state
|
|
242
|
+
JOIN pgflow.steps AS step
|
|
243
|
+
ON step.flow_slug = step_state.flow_slug
|
|
244
|
+
AND step.step_slug = step_state.step_slug
|
|
245
|
+
WHERE step_state.run_id = cascade_resolve_conditions.run_id
|
|
246
|
+
AND step_state.status = 'created'
|
|
247
|
+
AND step_state.remaining_deps = 0
|
|
248
|
+
AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL)
|
|
249
|
+
),
|
|
250
|
+
step_deps_output AS (
|
|
251
|
+
SELECT
|
|
252
|
+
swc.step_slug,
|
|
253
|
+
jsonb_object_agg(dep_state.step_slug, dep_state.output) AS deps_output
|
|
254
|
+
FROM steps_with_conditions swc
|
|
255
|
+
JOIN pgflow.deps dep ON dep.flow_slug = swc.flow_slug AND dep.step_slug = swc.step_slug
|
|
256
|
+
JOIN pgflow.step_states dep_state
|
|
257
|
+
ON dep_state.run_id = cascade_resolve_conditions.run_id
|
|
258
|
+
AND dep_state.step_slug = dep.dep_slug
|
|
259
|
+
AND dep_state.status = 'completed' -- Only completed deps (not skipped)
|
|
260
|
+
WHERE swc.deps_count > 0
|
|
261
|
+
GROUP BY swc.step_slug
|
|
262
|
+
),
|
|
263
|
+
condition_evaluations AS (
|
|
264
|
+
SELECT
|
|
265
|
+
swc.*,
|
|
266
|
+
-- condition_met = (if IS NULL OR input @> if) AND (ifNot IS NULL OR NOT(input @> ifNot))
|
|
267
|
+
(swc.condition_pattern IS NULL OR
|
|
268
|
+
CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_pattern)
|
|
269
|
+
AND
|
|
270
|
+
(swc.condition_not_pattern IS NULL OR
|
|
271
|
+
NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_not_pattern))
|
|
272
|
+
AS condition_met
|
|
273
|
+
FROM steps_with_conditions swc
|
|
274
|
+
LEFT JOIN step_deps_output sdo ON sdo.step_slug = swc.step_slug
|
|
275
|
+
),
|
|
276
|
+
unmet_skip_steps AS (
|
|
277
|
+
SELECT * FROM condition_evaluations
|
|
278
|
+
WHERE NOT condition_met AND when_unmet = 'skip'
|
|
279
|
+
),
|
|
280
|
+
skipped_steps AS (
|
|
281
|
+
UPDATE pgflow.step_states ss
|
|
282
|
+
SET status = 'skipped',
|
|
283
|
+
skip_reason = 'condition_unmet',
|
|
284
|
+
skipped_at = now()
|
|
285
|
+
FROM unmet_skip_steps uss
|
|
286
|
+
WHERE ss.run_id = cascade_resolve_conditions.run_id
|
|
287
|
+
AND ss.step_slug = uss.step_slug
|
|
288
|
+
RETURNING
|
|
289
|
+
ss.*,
|
|
290
|
+
realtime.send(
|
|
291
|
+
jsonb_build_object(
|
|
292
|
+
'event_type', 'step:skipped',
|
|
293
|
+
'run_id', ss.run_id,
|
|
294
|
+
'flow_slug', ss.flow_slug,
|
|
295
|
+
'step_slug', ss.step_slug,
|
|
296
|
+
'status', 'skipped',
|
|
297
|
+
'skip_reason', 'condition_unmet',
|
|
298
|
+
'skipped_at', ss.skipped_at
|
|
299
|
+
),
|
|
300
|
+
concat('step:', ss.step_slug, ':skipped'),
|
|
301
|
+
concat('pgflow:run:', ss.run_id),
|
|
302
|
+
false
|
|
303
|
+
) AS _broadcast_result
|
|
304
|
+
),
|
|
305
|
+
-- NEW: Update dependent steps (decrement remaining_deps, set initial_tasks=0 for maps)
|
|
306
|
+
dependent_updates AS (
|
|
307
|
+
UPDATE pgflow.step_states child_state
|
|
308
|
+
SET remaining_deps = child_state.remaining_deps - 1,
|
|
309
|
+
-- If child is a map step and this skipped step is its only dependency,
|
|
310
|
+
-- set initial_tasks = 0 (skipped dep = empty array)
|
|
311
|
+
initial_tasks = CASE
|
|
312
|
+
WHEN child_step.step_type = 'map' AND child_step.deps_count = 1 THEN 0
|
|
313
|
+
ELSE child_state.initial_tasks
|
|
314
|
+
END
|
|
315
|
+
FROM skipped_steps parent
|
|
316
|
+
JOIN pgflow.deps dep ON dep.flow_slug = parent.flow_slug AND dep.dep_slug = parent.step_slug
|
|
317
|
+
JOIN pgflow.steps child_step ON child_step.flow_slug = dep.flow_slug AND child_step.step_slug = dep.step_slug
|
|
318
|
+
WHERE child_state.run_id = cascade_resolve_conditions.run_id
|
|
319
|
+
AND child_state.step_slug = dep.step_slug
|
|
320
|
+
),
|
|
321
|
+
run_update AS (
|
|
322
|
+
UPDATE pgflow.runs r
|
|
323
|
+
SET remaining_steps = r.remaining_steps - (SELECT COUNT(*) FROM skipped_steps)
|
|
324
|
+
WHERE r.run_id = cascade_resolve_conditions.run_id
|
|
325
|
+
AND (SELECT COUNT(*) FROM skipped_steps) > 0
|
|
326
|
+
)
|
|
327
|
+
SELECT COUNT(*)::int INTO v_processed_count FROM skipped_steps;
|
|
328
|
+
|
|
329
|
+
-- ==========================================
|
|
330
|
+
-- PHASE 1c: HANDLE SKIP-CASCADE CONDITIONS
|
|
331
|
+
-- ==========================================
|
|
332
|
+
-- Call _cascade_force_skip_steps for each step with unmet condition and whenUnmet='skip-cascade'.
|
|
333
|
+
-- Process in topological order; _cascade_force_skip_steps is idempotent.
|
|
334
|
+
PERFORM pgflow._cascade_force_skip_steps(cascade_resolve_conditions.run_id, ready_step.step_slug, 'condition_unmet')
|
|
335
|
+
FROM pgflow.step_states AS ready_step
|
|
336
|
+
JOIN pgflow.steps AS step
|
|
337
|
+
ON step.flow_slug = ready_step.flow_slug
|
|
338
|
+
AND step.step_slug = ready_step.step_slug
|
|
339
|
+
LEFT JOIN LATERAL (
|
|
340
|
+
SELECT jsonb_object_agg(dep_state.step_slug, dep_state.output) AS deps_output
|
|
341
|
+
FROM pgflow.deps dep
|
|
342
|
+
JOIN pgflow.step_states dep_state
|
|
343
|
+
ON dep_state.run_id = cascade_resolve_conditions.run_id
|
|
344
|
+
AND dep_state.step_slug = dep.dep_slug
|
|
345
|
+
AND dep_state.status = 'completed' -- Only completed deps (not skipped)
|
|
346
|
+
WHERE dep.flow_slug = ready_step.flow_slug
|
|
347
|
+
AND dep.step_slug = ready_step.step_slug
|
|
348
|
+
) AS agg_deps ON step.deps_count > 0
|
|
349
|
+
WHERE ready_step.run_id = cascade_resolve_conditions.run_id
|
|
350
|
+
AND ready_step.status = 'created'
|
|
351
|
+
AND ready_step.remaining_deps = 0
|
|
352
|
+
AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL)
|
|
353
|
+
AND step.when_unmet = 'skip-cascade'
|
|
354
|
+
-- Condition is NOT met when: (if fails) OR (ifNot fails)
|
|
355
|
+
AND NOT (
|
|
356
|
+
(step.condition_pattern IS NULL OR
|
|
357
|
+
CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.condition_pattern)
|
|
358
|
+
AND
|
|
359
|
+
(step.condition_not_pattern IS NULL OR
|
|
360
|
+
NOT (CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.condition_not_pattern))
|
|
361
|
+
)
|
|
362
|
+
ORDER BY step.step_index;
|
|
363
|
+
|
|
364
|
+
-- Check if run was failed during cascade (e.g., if _cascade_force_skip_steps triggers fail)
|
|
365
|
+
SELECT r.status INTO v_run_status
|
|
366
|
+
FROM pgflow.runs r
|
|
367
|
+
WHERE r.run_id = cascade_resolve_conditions.run_id;
|
|
368
|
+
|
|
369
|
+
IF v_run_status IN ('failed', 'completed') THEN
|
|
370
|
+
RETURN v_run_status != 'failed';
|
|
371
|
+
END IF;
|
|
372
|
+
|
|
373
|
+
-- Exit loop if no steps were processed in this iteration
|
|
374
|
+
EXIT WHEN v_processed_count = 0;
|
|
375
|
+
END LOOP;
|
|
376
|
+
|
|
377
|
+
RETURN true;
|
|
378
|
+
END;
|
|
379
|
+
$$;
|
|
380
|
+
-- Modify "start_ready_steps" function
|
|
381
|
+
CREATE OR REPLACE FUNCTION "pgflow"."start_ready_steps" ("run_id" uuid) RETURNS void LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
382
|
+
BEGIN
|
|
383
|
+
-- ==========================================
|
|
384
|
+
-- GUARD: No mutations on terminal runs
|
|
385
|
+
-- ==========================================
|
|
386
|
+
IF EXISTS (
|
|
387
|
+
SELECT 1 FROM pgflow.runs
|
|
388
|
+
WHERE pgflow.runs.run_id = start_ready_steps.run_id
|
|
389
|
+
AND pgflow.runs.status IN ('failed', 'completed')
|
|
390
|
+
) THEN
|
|
391
|
+
RETURN;
|
|
392
|
+
END IF;
|
|
393
|
+
|
|
394
|
+
-- ==========================================
|
|
395
|
+
-- PHASE 1: START READY STEPS
|
|
396
|
+
-- ==========================================
|
|
397
|
+
-- NOTE: Condition evaluation and empty map handling are done by
|
|
398
|
+
-- cascade_resolve_conditions() and cascade_complete_taskless_steps()
|
|
399
|
+
-- which are called before this function.
|
|
400
|
+
WITH
|
|
401
|
+
-- ---------- Find ready steps ----------
|
|
402
|
+
-- Steps with no remaining deps and known task count
|
|
403
|
+
ready_steps AS (
|
|
404
|
+
SELECT *
|
|
405
|
+
FROM pgflow.step_states AS step_state
|
|
406
|
+
WHERE step_state.run_id = start_ready_steps.run_id
|
|
407
|
+
AND step_state.status = 'created'
|
|
408
|
+
AND step_state.remaining_deps = 0
|
|
409
|
+
AND step_state.initial_tasks IS NOT NULL -- Cannot start with unknown count
|
|
410
|
+
AND step_state.initial_tasks > 0 -- Don't start taskless steps (handled by cascade_complete_taskless_steps)
|
|
411
|
+
ORDER BY step_state.step_slug
|
|
412
|
+
FOR UPDATE
|
|
413
|
+
),
|
|
414
|
+
-- ---------- Mark steps as started ----------
|
|
415
|
+
started_step_states AS (
|
|
416
|
+
UPDATE pgflow.step_states
|
|
417
|
+
SET status = 'started',
|
|
418
|
+
started_at = now(),
|
|
419
|
+
remaining_tasks = ready_steps.initial_tasks -- Copy initial_tasks to remaining_tasks when starting
|
|
420
|
+
FROM ready_steps
|
|
421
|
+
WHERE pgflow.step_states.run_id = start_ready_steps.run_id
|
|
422
|
+
AND pgflow.step_states.step_slug = ready_steps.step_slug
|
|
423
|
+
RETURNING pgflow.step_states.*,
|
|
424
|
+
-- Broadcast step:started event atomically with the UPDATE
|
|
425
|
+
-- Using RETURNING ensures this executes during row processing
|
|
426
|
+
-- and cannot be optimized away by the query planner
|
|
427
|
+
realtime.send(
|
|
428
|
+
jsonb_build_object(
|
|
429
|
+
'event_type', 'step:started',
|
|
430
|
+
'run_id', pgflow.step_states.run_id,
|
|
431
|
+
'step_slug', pgflow.step_states.step_slug,
|
|
432
|
+
'status', 'started',
|
|
433
|
+
'started_at', pgflow.step_states.started_at,
|
|
434
|
+
'remaining_tasks', pgflow.step_states.remaining_tasks,
|
|
435
|
+
'remaining_deps', pgflow.step_states.remaining_deps
|
|
436
|
+
),
|
|
437
|
+
concat('step:', pgflow.step_states.step_slug, ':started'),
|
|
438
|
+
concat('pgflow:run:', pgflow.step_states.run_id),
|
|
439
|
+
false
|
|
440
|
+
) as _broadcast_result -- Prefix with _ to indicate internal use only
|
|
441
|
+
),
|
|
442
|
+
|
|
443
|
+
-- ==========================================
|
|
444
|
+
-- PHASE 2: TASK GENERATION AND QUEUE MESSAGES
|
|
445
|
+
-- ==========================================
|
|
446
|
+
-- ---------- Generate tasks and batch messages ----------
|
|
447
|
+
-- Single steps: 1 task (index 0)
|
|
448
|
+
-- Map steps: N tasks (indices 0..N-1)
|
|
449
|
+
message_batches AS (
|
|
450
|
+
SELECT
|
|
451
|
+
started_step.flow_slug,
|
|
452
|
+
started_step.run_id,
|
|
453
|
+
started_step.step_slug,
|
|
454
|
+
COALESCE(step.opt_start_delay, 0) as delay,
|
|
455
|
+
array_agg(
|
|
456
|
+
jsonb_build_object(
|
|
457
|
+
'flow_slug', started_step.flow_slug,
|
|
458
|
+
'run_id', started_step.run_id,
|
|
459
|
+
'step_slug', started_step.step_slug,
|
|
460
|
+
'task_index', task_idx.task_index
|
|
461
|
+
) ORDER BY task_idx.task_index
|
|
462
|
+
) AS messages,
|
|
463
|
+
array_agg(task_idx.task_index ORDER BY task_idx.task_index) AS task_indices
|
|
464
|
+
FROM started_step_states AS started_step
|
|
465
|
+
JOIN pgflow.steps AS step
|
|
466
|
+
ON step.flow_slug = started_step.flow_slug
|
|
467
|
+
AND step.step_slug = started_step.step_slug
|
|
468
|
+
-- Generate task indices from 0 to initial_tasks-1
|
|
469
|
+
CROSS JOIN LATERAL generate_series(0, started_step.initial_tasks - 1) AS task_idx(task_index)
|
|
470
|
+
GROUP BY started_step.flow_slug, started_step.run_id, started_step.step_slug, step.opt_start_delay
|
|
471
|
+
),
|
|
472
|
+
-- ---------- Send messages to queue ----------
|
|
473
|
+
-- Uses batch sending for performance with large arrays
|
|
474
|
+
sent_messages AS (
|
|
475
|
+
SELECT
|
|
476
|
+
mb.flow_slug,
|
|
477
|
+
mb.run_id,
|
|
478
|
+
mb.step_slug,
|
|
479
|
+
task_indices.task_index,
|
|
480
|
+
msg_ids.msg_id
|
|
481
|
+
FROM message_batches mb
|
|
482
|
+
CROSS JOIN LATERAL unnest(mb.task_indices) WITH ORDINALITY AS task_indices(task_index, idx_ord)
|
|
483
|
+
CROSS JOIN LATERAL pgmq.send_batch(mb.flow_slug, mb.messages, mb.delay) WITH ORDINALITY AS msg_ids(msg_id, msg_ord)
|
|
484
|
+
WHERE task_indices.idx_ord = msg_ids.msg_ord
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
-- ==========================================
|
|
488
|
+
-- PHASE 3: RECORD TASKS IN DATABASE
|
|
489
|
+
-- ==========================================
|
|
490
|
+
INSERT INTO pgflow.step_tasks (flow_slug, run_id, step_slug, task_index, message_id)
|
|
491
|
+
SELECT
|
|
492
|
+
sent_messages.flow_slug,
|
|
493
|
+
sent_messages.run_id,
|
|
494
|
+
sent_messages.step_slug,
|
|
495
|
+
sent_messages.task_index,
|
|
496
|
+
sent_messages.msg_id
|
|
497
|
+
FROM sent_messages;
|
|
498
|
+
|
|
499
|
+
END;
|
|
500
|
+
$$;
|
|
501
|
+
-- Modify "complete_task" function
|
|
502
|
+
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 $$
|
|
503
|
+
declare
|
|
504
|
+
v_step_state pgflow.step_states%ROWTYPE;
|
|
505
|
+
v_dependent_map_slug text;
|
|
506
|
+
v_run_record pgflow.runs%ROWTYPE;
|
|
507
|
+
v_step_record pgflow.step_states%ROWTYPE;
|
|
508
|
+
begin
|
|
509
|
+
|
|
510
|
+
-- ==========================================
|
|
511
|
+
-- GUARD: No mutations on failed runs
|
|
512
|
+
-- ==========================================
|
|
513
|
+
IF EXISTS (SELECT 1 FROM pgflow.runs WHERE pgflow.runs.run_id = complete_task.run_id AND pgflow.runs.status = 'failed') THEN
|
|
514
|
+
RETURN QUERY SELECT * FROM pgflow.step_tasks
|
|
515
|
+
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
516
|
+
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
517
|
+
AND pgflow.step_tasks.task_index = complete_task.task_index;
|
|
518
|
+
RETURN;
|
|
519
|
+
END IF;
|
|
520
|
+
|
|
521
|
+
-- ==========================================
|
|
522
|
+
-- LOCK ACQUISITION AND TYPE VALIDATION
|
|
523
|
+
-- ==========================================
|
|
524
|
+
-- Acquire locks first to prevent race conditions
|
|
525
|
+
SELECT * INTO v_run_record FROM pgflow.runs
|
|
526
|
+
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
527
|
+
FOR UPDATE;
|
|
528
|
+
|
|
529
|
+
SELECT * INTO v_step_record FROM pgflow.step_states
|
|
530
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
531
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
532
|
+
FOR UPDATE;
|
|
533
|
+
|
|
534
|
+
-- Check for type violations AFTER acquiring locks
|
|
535
|
+
SELECT child_step.step_slug INTO v_dependent_map_slug
|
|
536
|
+
FROM pgflow.deps dependency
|
|
537
|
+
JOIN pgflow.steps child_step ON child_step.flow_slug = dependency.flow_slug
|
|
538
|
+
AND child_step.step_slug = dependency.step_slug
|
|
539
|
+
JOIN pgflow.steps parent_step ON parent_step.flow_slug = dependency.flow_slug
|
|
540
|
+
AND parent_step.step_slug = dependency.dep_slug
|
|
541
|
+
JOIN pgflow.step_states child_state ON child_state.flow_slug = child_step.flow_slug
|
|
542
|
+
AND child_state.step_slug = child_step.step_slug
|
|
543
|
+
WHERE dependency.dep_slug = complete_task.step_slug -- parent is the completing step
|
|
544
|
+
AND dependency.flow_slug = v_run_record.flow_slug
|
|
545
|
+
AND parent_step.step_type = 'single' -- Only validate single steps
|
|
546
|
+
AND child_step.step_type = 'map'
|
|
547
|
+
AND child_state.run_id = complete_task.run_id
|
|
548
|
+
AND child_state.initial_tasks IS NULL
|
|
549
|
+
AND (complete_task.output IS NULL OR jsonb_typeof(complete_task.output) != 'array')
|
|
550
|
+
LIMIT 1;
|
|
551
|
+
|
|
552
|
+
-- Handle type violation if detected
|
|
553
|
+
IF v_dependent_map_slug IS NOT NULL THEN
|
|
554
|
+
-- Mark run as failed immediately
|
|
555
|
+
UPDATE pgflow.runs
|
|
556
|
+
SET status = 'failed',
|
|
557
|
+
failed_at = now()
|
|
558
|
+
WHERE pgflow.runs.run_id = complete_task.run_id;
|
|
559
|
+
|
|
560
|
+
-- Broadcast run:failed event
|
|
561
|
+
-- Uses PERFORM pattern to ensure execution (proven reliable pattern in this function)
|
|
562
|
+
PERFORM realtime.send(
|
|
563
|
+
jsonb_build_object(
|
|
564
|
+
'event_type', 'run:failed',
|
|
565
|
+
'run_id', complete_task.run_id,
|
|
566
|
+
'flow_slug', v_run_record.flow_slug,
|
|
567
|
+
'status', 'failed',
|
|
568
|
+
'failed_at', now()
|
|
569
|
+
),
|
|
570
|
+
'run:failed',
|
|
571
|
+
concat('pgflow:run:', complete_task.run_id),
|
|
572
|
+
false
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
-- Archive all active messages (both queued and started) to prevent orphaned messages
|
|
576
|
+
PERFORM pgmq.archive(
|
|
577
|
+
v_run_record.flow_slug,
|
|
578
|
+
array_agg(st.message_id)
|
|
579
|
+
)
|
|
580
|
+
FROM pgflow.step_tasks st
|
|
581
|
+
WHERE st.run_id = complete_task.run_id
|
|
582
|
+
AND st.status IN ('queued', 'started')
|
|
583
|
+
AND st.message_id IS NOT NULL
|
|
584
|
+
HAVING count(*) > 0; -- Only call archive if there are messages to archive
|
|
585
|
+
|
|
586
|
+
-- Mark current task as failed and store the output
|
|
587
|
+
UPDATE pgflow.step_tasks
|
|
588
|
+
SET status = 'failed',
|
|
589
|
+
failed_at = now(),
|
|
590
|
+
output = complete_task.output, -- Store the output that caused the violation
|
|
591
|
+
error_message = '[TYPE_VIOLATION] Produced ' ||
|
|
592
|
+
CASE WHEN complete_task.output IS NULL THEN 'null'
|
|
593
|
+
ELSE jsonb_typeof(complete_task.output) END ||
|
|
594
|
+
' instead of array'
|
|
595
|
+
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
596
|
+
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
597
|
+
AND pgflow.step_tasks.task_index = complete_task.task_index;
|
|
598
|
+
|
|
599
|
+
-- Mark step state as failed
|
|
600
|
+
UPDATE pgflow.step_states
|
|
601
|
+
SET status = 'failed',
|
|
602
|
+
failed_at = now(),
|
|
603
|
+
error_message = '[TYPE_VIOLATION] Map step ' || v_dependent_map_slug ||
|
|
604
|
+
' expects array input but dependency ' || complete_task.step_slug ||
|
|
605
|
+
' produced ' || CASE WHEN complete_task.output IS NULL THEN 'null'
|
|
606
|
+
ELSE jsonb_typeof(complete_task.output) END
|
|
607
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
608
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug;
|
|
609
|
+
|
|
610
|
+
-- Broadcast step:failed event
|
|
611
|
+
-- Uses PERFORM pattern to ensure execution (proven reliable pattern in this function)
|
|
612
|
+
PERFORM realtime.send(
|
|
613
|
+
jsonb_build_object(
|
|
614
|
+
'event_type', 'step:failed',
|
|
615
|
+
'run_id', complete_task.run_id,
|
|
616
|
+
'step_slug', complete_task.step_slug,
|
|
617
|
+
'status', 'failed',
|
|
618
|
+
'error_message', '[TYPE_VIOLATION] Map step ' || v_dependent_map_slug ||
|
|
619
|
+
' expects array input but dependency ' || complete_task.step_slug ||
|
|
620
|
+
' produced ' || CASE WHEN complete_task.output IS NULL THEN 'null'
|
|
621
|
+
ELSE jsonb_typeof(complete_task.output) END,
|
|
622
|
+
'failed_at', now()
|
|
623
|
+
),
|
|
624
|
+
concat('step:', complete_task.step_slug, ':failed'),
|
|
625
|
+
concat('pgflow:run:', complete_task.run_id),
|
|
626
|
+
false
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
-- Archive the current task's message (it was started, now failed)
|
|
630
|
+
PERFORM pgmq.archive(
|
|
631
|
+
v_run_record.flow_slug,
|
|
632
|
+
st.message_id -- Single message, use scalar form
|
|
633
|
+
)
|
|
634
|
+
FROM pgflow.step_tasks st
|
|
635
|
+
WHERE st.run_id = complete_task.run_id
|
|
636
|
+
AND st.step_slug = complete_task.step_slug
|
|
637
|
+
AND st.task_index = complete_task.task_index
|
|
638
|
+
AND st.message_id IS NOT NULL;
|
|
639
|
+
|
|
640
|
+
-- Return empty result
|
|
641
|
+
RETURN QUERY SELECT * FROM pgflow.step_tasks WHERE false;
|
|
642
|
+
RETURN;
|
|
643
|
+
END IF;
|
|
644
|
+
|
|
645
|
+
-- ==========================================
|
|
646
|
+
-- MAIN CTE CHAIN: Update task and propagate changes
|
|
647
|
+
-- ==========================================
|
|
648
|
+
WITH
|
|
649
|
+
-- ---------- Task completion ----------
|
|
650
|
+
-- Update the task record with completion status and output
|
|
651
|
+
task AS (
|
|
652
|
+
UPDATE pgflow.step_tasks
|
|
653
|
+
SET
|
|
654
|
+
status = 'completed',
|
|
655
|
+
completed_at = now(),
|
|
656
|
+
output = complete_task.output
|
|
657
|
+
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
658
|
+
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
659
|
+
AND pgflow.step_tasks.task_index = complete_task.task_index
|
|
660
|
+
AND pgflow.step_tasks.status = 'started'
|
|
661
|
+
RETURNING *
|
|
662
|
+
),
|
|
663
|
+
-- ---------- Get step type for output handling ----------
|
|
664
|
+
step_def AS (
|
|
665
|
+
SELECT step.step_type
|
|
666
|
+
FROM pgflow.steps step
|
|
667
|
+
JOIN pgflow.runs run ON run.flow_slug = step.flow_slug
|
|
668
|
+
WHERE run.run_id = complete_task.run_id
|
|
669
|
+
AND step.step_slug = complete_task.step_slug
|
|
670
|
+
),
|
|
671
|
+
-- ---------- Step state update ----------
|
|
672
|
+
-- Decrement remaining_tasks and potentially mark step as completed
|
|
673
|
+
-- Also store output atomically with status transition to completed
|
|
674
|
+
step_state AS (
|
|
675
|
+
UPDATE pgflow.step_states
|
|
676
|
+
SET
|
|
677
|
+
status = CASE
|
|
678
|
+
WHEN pgflow.step_states.remaining_tasks = 1 THEN 'completed' -- Will be 0 after decrement
|
|
679
|
+
ELSE 'started'
|
|
680
|
+
END,
|
|
681
|
+
completed_at = CASE
|
|
682
|
+
WHEN pgflow.step_states.remaining_tasks = 1 THEN now() -- Will be 0 after decrement
|
|
683
|
+
ELSE NULL
|
|
684
|
+
END,
|
|
685
|
+
remaining_tasks = pgflow.step_states.remaining_tasks - 1,
|
|
686
|
+
-- Store output atomically with completion (only when remaining_tasks = 1, meaning step completes)
|
|
687
|
+
output = CASE
|
|
688
|
+
-- Single step: store task output directly when completing
|
|
689
|
+
WHEN (SELECT step_type FROM step_def) = 'single' AND pgflow.step_states.remaining_tasks = 1 THEN
|
|
690
|
+
complete_task.output
|
|
691
|
+
-- Map step: aggregate on completion (ordered by task_index)
|
|
692
|
+
WHEN (SELECT step_type FROM step_def) = 'map' AND pgflow.step_states.remaining_tasks = 1 THEN
|
|
693
|
+
(SELECT COALESCE(jsonb_agg(all_outputs.output ORDER BY all_outputs.task_index), '[]'::jsonb)
|
|
694
|
+
FROM (
|
|
695
|
+
-- All previously completed tasks
|
|
696
|
+
SELECT st.output, st.task_index
|
|
697
|
+
FROM pgflow.step_tasks st
|
|
698
|
+
WHERE st.run_id = complete_task.run_id
|
|
699
|
+
AND st.step_slug = complete_task.step_slug
|
|
700
|
+
AND st.status = 'completed'
|
|
701
|
+
UNION ALL
|
|
702
|
+
-- Current task being completed (not yet visible as completed in snapshot)
|
|
703
|
+
SELECT complete_task.output, complete_task.task_index
|
|
704
|
+
) all_outputs)
|
|
705
|
+
ELSE pgflow.step_states.output
|
|
706
|
+
END
|
|
707
|
+
FROM task
|
|
708
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
709
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
710
|
+
RETURNING pgflow.step_states.*
|
|
711
|
+
),
|
|
712
|
+
-- ---------- Dependency resolution ----------
|
|
713
|
+
-- Find all child steps that depend on the completed parent step (only if parent completed)
|
|
714
|
+
child_steps AS (
|
|
715
|
+
SELECT deps.step_slug AS child_step_slug
|
|
716
|
+
FROM pgflow.deps deps
|
|
717
|
+
JOIN step_state parent_state ON parent_state.status = 'completed' AND deps.flow_slug = parent_state.flow_slug
|
|
718
|
+
WHERE deps.dep_slug = complete_task.step_slug -- dep_slug is the parent, step_slug is the child
|
|
719
|
+
ORDER BY deps.step_slug -- Ensure consistent ordering
|
|
720
|
+
),
|
|
721
|
+
-- ---------- Lock child steps ----------
|
|
722
|
+
-- Acquire locks on all child steps before updating them
|
|
723
|
+
child_steps_lock AS (
|
|
724
|
+
SELECT * FROM pgflow.step_states
|
|
725
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
726
|
+
AND pgflow.step_states.step_slug IN (SELECT child_step_slug FROM child_steps)
|
|
727
|
+
FOR UPDATE
|
|
728
|
+
),
|
|
729
|
+
-- ---------- Update child steps ----------
|
|
730
|
+
-- Decrement remaining_deps and resolve NULL initial_tasks for map steps
|
|
731
|
+
child_steps_update AS (
|
|
732
|
+
UPDATE pgflow.step_states child_state
|
|
733
|
+
SET remaining_deps = child_state.remaining_deps - 1,
|
|
734
|
+
-- Resolve NULL initial_tasks for child map steps
|
|
735
|
+
-- This is where child maps learn their array size from the parent
|
|
736
|
+
-- This CTE only runs when the parent step is complete (see child_steps JOIN)
|
|
737
|
+
initial_tasks = CASE
|
|
738
|
+
WHEN child_step.step_type = 'map' AND child_state.initial_tasks IS NULL THEN
|
|
739
|
+
CASE
|
|
740
|
+
WHEN parent_step.step_type = 'map' THEN
|
|
741
|
+
-- Map->map: Count all completed tasks from parent map
|
|
742
|
+
-- We add 1 because the current task is being completed in this transaction
|
|
743
|
+
-- but isn't yet visible as 'completed' in the step_tasks table
|
|
744
|
+
-- TODO: Refactor to use future column step_states.total_tasks
|
|
745
|
+
-- Would eliminate the COUNT query and just use parent_state.total_tasks
|
|
746
|
+
(SELECT COUNT(*)::int + 1
|
|
747
|
+
FROM pgflow.step_tasks parent_tasks
|
|
748
|
+
WHERE parent_tasks.run_id = complete_task.run_id
|
|
749
|
+
AND parent_tasks.step_slug = complete_task.step_slug
|
|
750
|
+
AND parent_tasks.status = 'completed'
|
|
751
|
+
AND parent_tasks.task_index != complete_task.task_index)
|
|
752
|
+
ELSE
|
|
753
|
+
-- Single->map: Use output array length (single steps complete immediately)
|
|
754
|
+
CASE
|
|
755
|
+
WHEN complete_task.output IS NOT NULL
|
|
756
|
+
AND jsonb_typeof(complete_task.output) = 'array' THEN
|
|
757
|
+
jsonb_array_length(complete_task.output)
|
|
758
|
+
ELSE NULL -- Keep NULL if not an array
|
|
759
|
+
END
|
|
760
|
+
END
|
|
761
|
+
ELSE child_state.initial_tasks -- Keep existing value (including NULL)
|
|
762
|
+
END
|
|
763
|
+
FROM child_steps children
|
|
764
|
+
JOIN pgflow.steps child_step ON child_step.flow_slug = (SELECT r.flow_slug FROM pgflow.runs r WHERE r.run_id = complete_task.run_id)
|
|
765
|
+
AND child_step.step_slug = children.child_step_slug
|
|
766
|
+
JOIN pgflow.steps parent_step ON parent_step.flow_slug = (SELECT r.flow_slug FROM pgflow.runs r WHERE r.run_id = complete_task.run_id)
|
|
767
|
+
AND parent_step.step_slug = complete_task.step_slug
|
|
768
|
+
WHERE child_state.run_id = complete_task.run_id
|
|
769
|
+
AND child_state.step_slug = children.child_step_slug
|
|
770
|
+
)
|
|
771
|
+
-- ---------- Update run remaining_steps ----------
|
|
772
|
+
-- Decrement the run's remaining_steps counter if step completed
|
|
773
|
+
UPDATE pgflow.runs
|
|
774
|
+
SET remaining_steps = pgflow.runs.remaining_steps - 1
|
|
775
|
+
FROM step_state
|
|
776
|
+
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
777
|
+
AND step_state.status = 'completed';
|
|
778
|
+
|
|
779
|
+
-- ==========================================
|
|
780
|
+
-- POST-COMPLETION ACTIONS
|
|
781
|
+
-- ==========================================
|
|
782
|
+
|
|
783
|
+
-- ---------- Get updated state for broadcasting ----------
|
|
784
|
+
SELECT * INTO v_step_state FROM pgflow.step_states
|
|
785
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id AND pgflow.step_states.step_slug = complete_task.step_slug;
|
|
786
|
+
|
|
787
|
+
-- ---------- Handle step completion ----------
|
|
788
|
+
IF v_step_state.status = 'completed' THEN
|
|
789
|
+
-- Broadcast step:completed event FIRST (before cascade)
|
|
790
|
+
-- This ensures parent broadcasts before its dependent children
|
|
791
|
+
-- Use stored output from step_states (set atomically during status transition)
|
|
792
|
+
PERFORM realtime.send(
|
|
793
|
+
jsonb_build_object(
|
|
794
|
+
'event_type', 'step:completed',
|
|
795
|
+
'run_id', complete_task.run_id,
|
|
796
|
+
'step_slug', complete_task.step_slug,
|
|
797
|
+
'status', 'completed',
|
|
798
|
+
'output', v_step_state.output, -- Use stored output instead of re-aggregating
|
|
799
|
+
'completed_at', v_step_state.completed_at
|
|
800
|
+
),
|
|
801
|
+
concat('step:', complete_task.step_slug, ':completed'),
|
|
802
|
+
concat('pgflow:run:', complete_task.run_id),
|
|
803
|
+
false
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
-- THEN evaluate conditions on newly-ready dependent steps
|
|
807
|
+
-- This must happen before cascade_complete_taskless_steps so that
|
|
808
|
+
-- skipped steps can set initial_tasks=0 for their map dependents
|
|
809
|
+
IF NOT pgflow.cascade_resolve_conditions(complete_task.run_id) THEN
|
|
810
|
+
-- Run was failed due to a condition with when_unmet='fail'
|
|
811
|
+
-- Archive the current task's message before returning
|
|
812
|
+
PERFORM pgmq.archive(
|
|
813
|
+
(SELECT r.flow_slug FROM pgflow.runs r WHERE r.run_id = complete_task.run_id),
|
|
814
|
+
(SELECT st.message_id FROM pgflow.step_tasks st
|
|
815
|
+
WHERE st.run_id = complete_task.run_id
|
|
816
|
+
AND st.step_slug = complete_task.step_slug
|
|
817
|
+
AND st.task_index = complete_task.task_index)
|
|
818
|
+
);
|
|
819
|
+
RETURN QUERY SELECT * FROM pgflow.step_tasks
|
|
820
|
+
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
821
|
+
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
822
|
+
AND pgflow.step_tasks.task_index = complete_task.task_index;
|
|
823
|
+
RETURN;
|
|
824
|
+
END IF;
|
|
825
|
+
|
|
826
|
+
-- THEN cascade complete any taskless steps that are now ready
|
|
827
|
+
-- This ensures dependent children broadcast AFTER their parent
|
|
828
|
+
PERFORM pgflow.cascade_complete_taskless_steps(complete_task.run_id);
|
|
829
|
+
END IF;
|
|
830
|
+
|
|
831
|
+
-- ---------- Archive completed task message ----------
|
|
832
|
+
-- Move message from active queue to archive table
|
|
833
|
+
PERFORM (
|
|
834
|
+
WITH completed_tasks AS (
|
|
835
|
+
SELECT r.flow_slug, st.message_id
|
|
836
|
+
FROM pgflow.step_tasks st
|
|
837
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
838
|
+
WHERE st.run_id = complete_task.run_id
|
|
839
|
+
AND st.step_slug = complete_task.step_slug
|
|
840
|
+
AND st.task_index = complete_task.task_index
|
|
841
|
+
AND st.status = 'completed'
|
|
842
|
+
)
|
|
843
|
+
SELECT pgmq.archive(ct.flow_slug, ct.message_id)
|
|
844
|
+
FROM completed_tasks ct
|
|
845
|
+
WHERE EXISTS (SELECT 1 FROM completed_tasks)
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
-- ---------- Trigger next steps ----------
|
|
849
|
+
-- Start any steps that are now ready (deps satisfied)
|
|
850
|
+
PERFORM pgflow.start_ready_steps(complete_task.run_id);
|
|
851
|
+
|
|
852
|
+
-- Check if the entire run is complete
|
|
853
|
+
PERFORM pgflow.maybe_complete_run(complete_task.run_id);
|
|
854
|
+
|
|
855
|
+
-- ---------- Return completed task ----------
|
|
856
|
+
RETURN QUERY SELECT *
|
|
857
|
+
FROM pgflow.step_tasks AS step_task
|
|
858
|
+
WHERE step_task.run_id = complete_task.run_id
|
|
859
|
+
AND step_task.step_slug = complete_task.step_slug
|
|
860
|
+
AND step_task.task_index = complete_task.task_index;
|
|
861
|
+
|
|
862
|
+
end;
|
|
863
|
+
$$;
|
|
864
|
+
-- Modify "fail_task" function
|
|
865
|
+
CREATE OR REPLACE 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 $$
|
|
866
|
+
DECLARE
|
|
867
|
+
v_run_failed boolean;
|
|
868
|
+
v_step_failed boolean;
|
|
869
|
+
v_step_skipped boolean;
|
|
870
|
+
v_when_failed text;
|
|
871
|
+
v_task_exhausted boolean; -- True if task has exhausted retries
|
|
872
|
+
begin
|
|
873
|
+
|
|
874
|
+
-- If run is already failed, no retries allowed
|
|
875
|
+
IF EXISTS (SELECT 1 FROM pgflow.runs WHERE pgflow.runs.run_id = fail_task.run_id AND pgflow.runs.status = 'failed') THEN
|
|
876
|
+
UPDATE pgflow.step_tasks
|
|
877
|
+
SET status = 'failed',
|
|
878
|
+
failed_at = now(),
|
|
879
|
+
error_message = fail_task.error_message
|
|
880
|
+
WHERE pgflow.step_tasks.run_id = fail_task.run_id
|
|
881
|
+
AND pgflow.step_tasks.step_slug = fail_task.step_slug
|
|
882
|
+
AND pgflow.step_tasks.task_index = fail_task.task_index
|
|
883
|
+
AND pgflow.step_tasks.status = 'started';
|
|
884
|
+
|
|
885
|
+
-- Archive the task's message
|
|
886
|
+
PERFORM pgmq.archive(r.flow_slug, ARRAY_AGG(st.message_id))
|
|
887
|
+
FROM pgflow.step_tasks st
|
|
888
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
889
|
+
WHERE st.run_id = fail_task.run_id
|
|
890
|
+
AND st.step_slug = fail_task.step_slug
|
|
891
|
+
AND st.task_index = fail_task.task_index
|
|
892
|
+
AND st.message_id IS NOT NULL
|
|
893
|
+
GROUP BY r.flow_slug
|
|
894
|
+
HAVING COUNT(st.message_id) > 0;
|
|
895
|
+
|
|
896
|
+
RETURN QUERY SELECT * FROM pgflow.step_tasks
|
|
897
|
+
WHERE pgflow.step_tasks.run_id = fail_task.run_id
|
|
898
|
+
AND pgflow.step_tasks.step_slug = fail_task.step_slug
|
|
899
|
+
AND pgflow.step_tasks.task_index = fail_task.task_index;
|
|
900
|
+
RETURN;
|
|
901
|
+
END IF;
|
|
902
|
+
|
|
903
|
+
WITH run_lock AS (
|
|
904
|
+
SELECT * FROM pgflow.runs
|
|
905
|
+
WHERE pgflow.runs.run_id = fail_task.run_id
|
|
906
|
+
FOR UPDATE
|
|
907
|
+
),
|
|
908
|
+
step_lock AS (
|
|
909
|
+
SELECT * FROM pgflow.step_states
|
|
910
|
+
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
911
|
+
AND pgflow.step_states.step_slug = fail_task.step_slug
|
|
912
|
+
FOR UPDATE
|
|
913
|
+
),
|
|
914
|
+
flow_info AS (
|
|
915
|
+
SELECT r.flow_slug
|
|
916
|
+
FROM pgflow.runs r
|
|
917
|
+
WHERE r.run_id = fail_task.run_id
|
|
918
|
+
),
|
|
919
|
+
config AS (
|
|
920
|
+
SELECT
|
|
921
|
+
COALESCE(s.opt_max_attempts, f.opt_max_attempts) AS opt_max_attempts,
|
|
922
|
+
COALESCE(s.opt_base_delay, f.opt_base_delay) AS opt_base_delay,
|
|
923
|
+
s.when_failed
|
|
924
|
+
FROM pgflow.steps s
|
|
925
|
+
JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
|
|
926
|
+
JOIN flow_info fi ON fi.flow_slug = s.flow_slug
|
|
927
|
+
WHERE s.flow_slug = fi.flow_slug AND s.step_slug = fail_task.step_slug
|
|
928
|
+
),
|
|
929
|
+
fail_or_retry_task as (
|
|
930
|
+
UPDATE pgflow.step_tasks as task
|
|
931
|
+
SET
|
|
932
|
+
status = CASE
|
|
933
|
+
WHEN task.attempts_count < (SELECT opt_max_attempts FROM config) THEN 'queued'
|
|
934
|
+
ELSE 'failed'
|
|
935
|
+
END,
|
|
936
|
+
failed_at = CASE
|
|
937
|
+
WHEN task.attempts_count >= (SELECT opt_max_attempts FROM config) THEN now()
|
|
938
|
+
ELSE NULL
|
|
939
|
+
END,
|
|
940
|
+
started_at = CASE
|
|
941
|
+
WHEN task.attempts_count < (SELECT opt_max_attempts FROM config) THEN NULL
|
|
942
|
+
ELSE task.started_at
|
|
943
|
+
END,
|
|
944
|
+
error_message = fail_task.error_message
|
|
945
|
+
WHERE task.run_id = fail_task.run_id
|
|
946
|
+
AND task.step_slug = fail_task.step_slug
|
|
947
|
+
AND task.task_index = fail_task.task_index
|
|
948
|
+
AND task.status = 'started'
|
|
949
|
+
RETURNING *
|
|
950
|
+
),
|
|
951
|
+
-- Determine if task exhausted retries and get when_failed mode
|
|
952
|
+
task_status AS (
|
|
953
|
+
SELECT
|
|
954
|
+
(select status from fail_or_retry_task) AS new_task_status,
|
|
955
|
+
(select when_failed from config) AS when_failed_mode,
|
|
956
|
+
-- Task is exhausted when it's failed (no more retries)
|
|
957
|
+
((select status from fail_or_retry_task) = 'failed') AS is_exhausted
|
|
958
|
+
),
|
|
959
|
+
maybe_fail_step AS (
|
|
960
|
+
UPDATE pgflow.step_states
|
|
961
|
+
SET
|
|
962
|
+
-- Status logic:
|
|
963
|
+
-- - If task not exhausted (retrying): keep current status
|
|
964
|
+
-- - If exhausted AND when_failed='fail': set to 'failed'
|
|
965
|
+
-- - If exhausted AND when_failed IN ('skip', 'skip-cascade'): set to 'skipped'
|
|
966
|
+
status = CASE
|
|
967
|
+
WHEN NOT (select is_exhausted from task_status) THEN pgflow.step_states.status
|
|
968
|
+
WHEN (select when_failed_mode from task_status) = 'fail' THEN 'failed'
|
|
969
|
+
ELSE 'skipped' -- skip or skip-cascade
|
|
970
|
+
END,
|
|
971
|
+
failed_at = CASE
|
|
972
|
+
WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) = 'fail' THEN now()
|
|
973
|
+
ELSE NULL
|
|
974
|
+
END,
|
|
975
|
+
error_message = CASE
|
|
976
|
+
WHEN (select is_exhausted from task_status) THEN fail_task.error_message
|
|
977
|
+
ELSE NULL
|
|
978
|
+
END,
|
|
979
|
+
skip_reason = CASE
|
|
980
|
+
WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN 'handler_failed'
|
|
981
|
+
ELSE pgflow.step_states.skip_reason
|
|
982
|
+
END,
|
|
983
|
+
skipped_at = CASE
|
|
984
|
+
WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN now()
|
|
985
|
+
ELSE pgflow.step_states.skipped_at
|
|
986
|
+
END,
|
|
987
|
+
-- Clear remaining_tasks when skipping (required by remaining_tasks_state_consistency constraint)
|
|
988
|
+
remaining_tasks = CASE
|
|
989
|
+
WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN NULL
|
|
990
|
+
ELSE pgflow.step_states.remaining_tasks
|
|
991
|
+
END
|
|
992
|
+
FROM fail_or_retry_task
|
|
993
|
+
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
994
|
+
AND pgflow.step_states.step_slug = fail_task.step_slug
|
|
995
|
+
RETURNING pgflow.step_states.*
|
|
996
|
+
)
|
|
997
|
+
-- Update run status: only fail when when_failed='fail' and step was failed
|
|
998
|
+
UPDATE pgflow.runs
|
|
999
|
+
SET status = CASE
|
|
1000
|
+
WHEN (select status from maybe_fail_step) = 'failed' THEN 'failed'
|
|
1001
|
+
ELSE status
|
|
1002
|
+
END,
|
|
1003
|
+
failed_at = CASE
|
|
1004
|
+
WHEN (select status from maybe_fail_step) = 'failed' THEN now()
|
|
1005
|
+
ELSE NULL
|
|
1006
|
+
END,
|
|
1007
|
+
-- Decrement remaining_steps when step was skipped (not failed, run continues)
|
|
1008
|
+
remaining_steps = CASE
|
|
1009
|
+
WHEN (select status from maybe_fail_step) = 'skipped' THEN pgflow.runs.remaining_steps - 1
|
|
1010
|
+
ELSE pgflow.runs.remaining_steps
|
|
1011
|
+
END
|
|
1012
|
+
WHERE pgflow.runs.run_id = fail_task.run_id
|
|
1013
|
+
RETURNING (status = 'failed') INTO v_run_failed;
|
|
1014
|
+
|
|
1015
|
+
-- Capture when_failed mode and check if step was skipped for later processing
|
|
1016
|
+
SELECT s.when_failed INTO v_when_failed
|
|
1017
|
+
FROM pgflow.steps s
|
|
1018
|
+
JOIN pgflow.runs r ON r.flow_slug = s.flow_slug
|
|
1019
|
+
WHERE r.run_id = fail_task.run_id
|
|
1020
|
+
AND s.step_slug = fail_task.step_slug;
|
|
1021
|
+
|
|
1022
|
+
SELECT (status = 'skipped') INTO v_step_skipped
|
|
1023
|
+
FROM pgflow.step_states
|
|
1024
|
+
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
1025
|
+
AND pgflow.step_states.step_slug = fail_task.step_slug;
|
|
1026
|
+
|
|
1027
|
+
-- Check if step failed by querying the step_states table
|
|
1028
|
+
SELECT (status = 'failed') INTO v_step_failed
|
|
1029
|
+
FROM pgflow.step_states
|
|
1030
|
+
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
1031
|
+
AND pgflow.step_states.step_slug = fail_task.step_slug;
|
|
1032
|
+
|
|
1033
|
+
-- Send broadcast event for step failure if the step was failed
|
|
1034
|
+
IF v_step_failed THEN
|
|
1035
|
+
PERFORM realtime.send(
|
|
1036
|
+
jsonb_build_object(
|
|
1037
|
+
'event_type', 'step:failed',
|
|
1038
|
+
'run_id', fail_task.run_id,
|
|
1039
|
+
'step_slug', fail_task.step_slug,
|
|
1040
|
+
'status', 'failed',
|
|
1041
|
+
'error_message', fail_task.error_message,
|
|
1042
|
+
'failed_at', now()
|
|
1043
|
+
),
|
|
1044
|
+
concat('step:', fail_task.step_slug, ':failed'),
|
|
1045
|
+
concat('pgflow:run:', fail_task.run_id),
|
|
1046
|
+
false
|
|
1047
|
+
);
|
|
1048
|
+
END IF;
|
|
1049
|
+
|
|
1050
|
+
-- Handle step skipping (when_failed = 'skip' or 'skip-cascade')
|
|
1051
|
+
IF v_step_skipped THEN
|
|
1052
|
+
-- Send broadcast event for step skipped
|
|
1053
|
+
PERFORM realtime.send(
|
|
1054
|
+
jsonb_build_object(
|
|
1055
|
+
'event_type', 'step:skipped',
|
|
1056
|
+
'run_id', fail_task.run_id,
|
|
1057
|
+
'step_slug', fail_task.step_slug,
|
|
1058
|
+
'status', 'skipped',
|
|
1059
|
+
'skip_reason', 'handler_failed',
|
|
1060
|
+
'error_message', fail_task.error_message,
|
|
1061
|
+
'skipped_at', now()
|
|
1062
|
+
),
|
|
1063
|
+
concat('step:', fail_task.step_slug, ':skipped'),
|
|
1064
|
+
concat('pgflow:run:', fail_task.run_id),
|
|
1065
|
+
false
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
-- For skip-cascade: cascade skip to all downstream dependents
|
|
1069
|
+
IF v_when_failed = 'skip-cascade' THEN
|
|
1070
|
+
PERFORM pgflow._cascade_force_skip_steps(fail_task.run_id, fail_task.step_slug, 'handler_failed');
|
|
1071
|
+
END IF;
|
|
1072
|
+
|
|
1073
|
+
-- Try to complete the run (remaining_steps may now be 0)
|
|
1074
|
+
PERFORM pgflow.maybe_complete_run(fail_task.run_id);
|
|
1075
|
+
END IF;
|
|
1076
|
+
|
|
1077
|
+
-- Send broadcast event for run failure if the run was failed
|
|
1078
|
+
IF v_run_failed THEN
|
|
1079
|
+
DECLARE
|
|
1080
|
+
v_flow_slug text;
|
|
1081
|
+
BEGIN
|
|
1082
|
+
SELECT flow_slug INTO v_flow_slug FROM pgflow.runs WHERE pgflow.runs.run_id = fail_task.run_id;
|
|
1083
|
+
|
|
1084
|
+
PERFORM realtime.send(
|
|
1085
|
+
jsonb_build_object(
|
|
1086
|
+
'event_type', 'run:failed',
|
|
1087
|
+
'run_id', fail_task.run_id,
|
|
1088
|
+
'flow_slug', v_flow_slug,
|
|
1089
|
+
'status', 'failed',
|
|
1090
|
+
'error_message', fail_task.error_message,
|
|
1091
|
+
'failed_at', now()
|
|
1092
|
+
),
|
|
1093
|
+
'run:failed',
|
|
1094
|
+
concat('pgflow:run:', fail_task.run_id),
|
|
1095
|
+
false
|
|
1096
|
+
);
|
|
1097
|
+
END;
|
|
1098
|
+
END IF;
|
|
1099
|
+
|
|
1100
|
+
-- Archive all active messages (both queued and started) when run fails
|
|
1101
|
+
IF v_run_failed THEN
|
|
1102
|
+
PERFORM pgmq.archive(r.flow_slug, ARRAY_AGG(st.message_id))
|
|
1103
|
+
FROM pgflow.step_tasks st
|
|
1104
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
1105
|
+
WHERE st.run_id = fail_task.run_id
|
|
1106
|
+
AND st.status IN ('queued', 'started')
|
|
1107
|
+
AND st.message_id IS NOT NULL
|
|
1108
|
+
GROUP BY r.flow_slug
|
|
1109
|
+
HAVING COUNT(st.message_id) > 0;
|
|
1110
|
+
END IF;
|
|
1111
|
+
|
|
1112
|
+
-- For queued tasks: delay the message for retry with exponential backoff
|
|
1113
|
+
PERFORM (
|
|
1114
|
+
WITH retry_config AS (
|
|
1115
|
+
SELECT
|
|
1116
|
+
COALESCE(s.opt_base_delay, f.opt_base_delay) AS base_delay
|
|
1117
|
+
FROM pgflow.steps s
|
|
1118
|
+
JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
|
|
1119
|
+
JOIN pgflow.runs r ON r.flow_slug = f.flow_slug
|
|
1120
|
+
WHERE r.run_id = fail_task.run_id
|
|
1121
|
+
AND s.step_slug = fail_task.step_slug
|
|
1122
|
+
),
|
|
1123
|
+
queued_tasks AS (
|
|
1124
|
+
SELECT
|
|
1125
|
+
r.flow_slug,
|
|
1126
|
+
st.message_id,
|
|
1127
|
+
pgflow.calculate_retry_delay((SELECT base_delay FROM retry_config), st.attempts_count) AS calculated_delay
|
|
1128
|
+
FROM pgflow.step_tasks st
|
|
1129
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
1130
|
+
WHERE st.run_id = fail_task.run_id
|
|
1131
|
+
AND st.step_slug = fail_task.step_slug
|
|
1132
|
+
AND st.task_index = fail_task.task_index
|
|
1133
|
+
AND st.status = 'queued'
|
|
1134
|
+
)
|
|
1135
|
+
SELECT pgmq.set_vt(qt.flow_slug, qt.message_id, qt.calculated_delay)
|
|
1136
|
+
FROM queued_tasks qt
|
|
1137
|
+
WHERE EXISTS (SELECT 1 FROM queued_tasks)
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
-- For failed tasks: archive the message
|
|
1141
|
+
PERFORM pgmq.archive(r.flow_slug, ARRAY_AGG(st.message_id))
|
|
1142
|
+
FROM pgflow.step_tasks st
|
|
1143
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
1144
|
+
WHERE st.run_id = fail_task.run_id
|
|
1145
|
+
AND st.step_slug = fail_task.step_slug
|
|
1146
|
+
AND st.task_index = fail_task.task_index
|
|
1147
|
+
AND st.status = 'failed'
|
|
1148
|
+
AND st.message_id IS NOT NULL
|
|
1149
|
+
GROUP BY r.flow_slug
|
|
1150
|
+
HAVING COUNT(st.message_id) > 0;
|
|
1151
|
+
|
|
1152
|
+
return query select *
|
|
1153
|
+
from pgflow.step_tasks st
|
|
1154
|
+
where st.run_id = fail_task.run_id
|
|
1155
|
+
and st.step_slug = fail_task.step_slug
|
|
1156
|
+
and st.task_index = fail_task.task_index;
|
|
1157
|
+
|
|
1158
|
+
end;
|
|
1159
|
+
$$;
|
|
1160
|
+
-- Modify "start_flow" function
|
|
1161
|
+
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 $$
|
|
1162
|
+
declare
|
|
1163
|
+
v_created_run pgflow.runs%ROWTYPE;
|
|
1164
|
+
v_root_map_count int;
|
|
1165
|
+
begin
|
|
1166
|
+
|
|
1167
|
+
-- ==========================================
|
|
1168
|
+
-- VALIDATION: Root map array input
|
|
1169
|
+
-- ==========================================
|
|
1170
|
+
WITH root_maps AS (
|
|
1171
|
+
SELECT step_slug
|
|
1172
|
+
FROM pgflow.steps
|
|
1173
|
+
WHERE steps.flow_slug = start_flow.flow_slug
|
|
1174
|
+
AND steps.step_type = 'map'
|
|
1175
|
+
AND steps.deps_count = 0
|
|
1176
|
+
)
|
|
1177
|
+
SELECT COUNT(*) INTO v_root_map_count FROM root_maps;
|
|
1178
|
+
|
|
1179
|
+
-- If we have root map steps, validate that input is an array
|
|
1180
|
+
IF v_root_map_count > 0 THEN
|
|
1181
|
+
-- First check for NULL (should be caught by NOT NULL constraint, but be defensive)
|
|
1182
|
+
IF start_flow.input IS NULL THEN
|
|
1183
|
+
RAISE EXCEPTION 'Flow % has root map steps but input is NULL', start_flow.flow_slug;
|
|
1184
|
+
END IF;
|
|
1185
|
+
|
|
1186
|
+
-- Then check if it's not an array
|
|
1187
|
+
IF jsonb_typeof(start_flow.input) != 'array' THEN
|
|
1188
|
+
RAISE EXCEPTION 'Flow % has root map steps but input is not an array (got %)',
|
|
1189
|
+
start_flow.flow_slug, jsonb_typeof(start_flow.input);
|
|
1190
|
+
END IF;
|
|
1191
|
+
END IF;
|
|
1192
|
+
|
|
1193
|
+
-- ==========================================
|
|
1194
|
+
-- MAIN CTE CHAIN: Create run and step states
|
|
1195
|
+
-- ==========================================
|
|
1196
|
+
WITH
|
|
1197
|
+
-- ---------- Gather flow metadata ----------
|
|
1198
|
+
flow_steps AS (
|
|
1199
|
+
SELECT steps.flow_slug, steps.step_slug, steps.step_type, steps.deps_count
|
|
1200
|
+
FROM pgflow.steps
|
|
1201
|
+
WHERE steps.flow_slug = start_flow.flow_slug
|
|
1202
|
+
),
|
|
1203
|
+
-- ---------- Create run record ----------
|
|
1204
|
+
created_run AS (
|
|
1205
|
+
INSERT INTO pgflow.runs (run_id, flow_slug, input, remaining_steps)
|
|
1206
|
+
VALUES (
|
|
1207
|
+
COALESCE(start_flow.run_id, gen_random_uuid()),
|
|
1208
|
+
start_flow.flow_slug,
|
|
1209
|
+
start_flow.input,
|
|
1210
|
+
(SELECT count(*) FROM flow_steps)
|
|
1211
|
+
)
|
|
1212
|
+
RETURNING *
|
|
1213
|
+
),
|
|
1214
|
+
-- ---------- Create step states ----------
|
|
1215
|
+
-- Sets initial_tasks: known for root maps, NULL for dependent maps
|
|
1216
|
+
created_step_states AS (
|
|
1217
|
+
INSERT INTO pgflow.step_states (flow_slug, run_id, step_slug, remaining_deps, initial_tasks)
|
|
1218
|
+
SELECT
|
|
1219
|
+
fs.flow_slug,
|
|
1220
|
+
(SELECT created_run.run_id FROM created_run),
|
|
1221
|
+
fs.step_slug,
|
|
1222
|
+
fs.deps_count,
|
|
1223
|
+
-- Updated logic for initial_tasks:
|
|
1224
|
+
CASE
|
|
1225
|
+
WHEN fs.step_type = 'map' AND fs.deps_count = 0 THEN
|
|
1226
|
+
-- Root map: get array length from input
|
|
1227
|
+
CASE
|
|
1228
|
+
WHEN jsonb_typeof(start_flow.input) = 'array' THEN
|
|
1229
|
+
jsonb_array_length(start_flow.input)
|
|
1230
|
+
ELSE
|
|
1231
|
+
1
|
|
1232
|
+
END
|
|
1233
|
+
WHEN fs.step_type = 'map' AND fs.deps_count > 0 THEN
|
|
1234
|
+
-- Dependent map: unknown until dependencies complete
|
|
1235
|
+
NULL
|
|
1236
|
+
ELSE
|
|
1237
|
+
-- Single steps: always 1 task
|
|
1238
|
+
1
|
|
1239
|
+
END
|
|
1240
|
+
FROM flow_steps fs
|
|
1241
|
+
)
|
|
1242
|
+
SELECT * FROM created_run INTO v_created_run;
|
|
1243
|
+
|
|
1244
|
+
-- ==========================================
|
|
1245
|
+
-- POST-CREATION ACTIONS
|
|
1246
|
+
-- ==========================================
|
|
1247
|
+
|
|
1248
|
+
-- ---------- Broadcast run:started event ----------
|
|
1249
|
+
PERFORM realtime.send(
|
|
1250
|
+
jsonb_build_object(
|
|
1251
|
+
'event_type', 'run:started',
|
|
1252
|
+
'run_id', v_created_run.run_id,
|
|
1253
|
+
'flow_slug', v_created_run.flow_slug,
|
|
1254
|
+
'input', v_created_run.input,
|
|
1255
|
+
'status', 'started',
|
|
1256
|
+
'remaining_steps', v_created_run.remaining_steps,
|
|
1257
|
+
'started_at', v_created_run.started_at
|
|
1258
|
+
),
|
|
1259
|
+
'run:started',
|
|
1260
|
+
concat('pgflow:run:', v_created_run.run_id),
|
|
1261
|
+
false
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
-- ---------- Evaluate conditions on ready steps ----------
|
|
1265
|
+
-- Skip steps with unmet conditions, propagate to dependents
|
|
1266
|
+
IF NOT pgflow.cascade_resolve_conditions(v_created_run.run_id) THEN
|
|
1267
|
+
-- Run was failed due to a condition with when_unmet='fail'
|
|
1268
|
+
RETURN QUERY SELECT * FROM pgflow.runs where pgflow.runs.run_id = v_created_run.run_id;
|
|
1269
|
+
RETURN;
|
|
1270
|
+
END IF;
|
|
1271
|
+
|
|
1272
|
+
-- ---------- Complete taskless steps ----------
|
|
1273
|
+
-- Handle empty array maps that should auto-complete
|
|
1274
|
+
PERFORM pgflow.cascade_complete_taskless_steps(v_created_run.run_id);
|
|
1275
|
+
|
|
1276
|
+
-- ---------- Start initial steps ----------
|
|
1277
|
+
-- Start root steps (those with no dependencies)
|
|
1278
|
+
PERFORM pgflow.start_ready_steps(v_created_run.run_id);
|
|
1279
|
+
|
|
1280
|
+
-- ---------- Check for run completion ----------
|
|
1281
|
+
-- If cascade completed all steps (zero-task flows), finalize the run
|
|
1282
|
+
PERFORM pgflow.maybe_complete_run(v_created_run.run_id);
|
|
1283
|
+
|
|
1284
|
+
RETURN QUERY SELECT * FROM pgflow.runs where pgflow.runs.run_id = v_created_run.run_id;
|
|
1285
|
+
|
|
1286
|
+
end;
|
|
1287
|
+
$$;
|
|
1288
|
+
-- Modify "start_tasks" function
|
|
1289
|
+
CREATE OR REPLACE FUNCTION "pgflow"."start_tasks" ("flow_slug" text, "msg_ids" bigint[], "worker_id" uuid) RETURNS SETOF "pgflow"."step_task_record" LANGUAGE sql SET "search_path" = '' AS $$
|
|
1290
|
+
with tasks as (
|
|
1291
|
+
select
|
|
1292
|
+
task.flow_slug,
|
|
1293
|
+
task.run_id,
|
|
1294
|
+
task.step_slug,
|
|
1295
|
+
task.task_index,
|
|
1296
|
+
task.message_id
|
|
1297
|
+
from pgflow.step_tasks as task
|
|
1298
|
+
join pgflow.runs r on r.run_id = task.run_id
|
|
1299
|
+
where task.flow_slug = start_tasks.flow_slug
|
|
1300
|
+
and task.message_id = any(msg_ids)
|
|
1301
|
+
and task.status = 'queued'
|
|
1302
|
+
-- MVP: Don't start tasks on failed runs
|
|
1303
|
+
and r.status != 'failed'
|
|
1304
|
+
),
|
|
1305
|
+
start_tasks_update as (
|
|
1306
|
+
update pgflow.step_tasks
|
|
1307
|
+
set
|
|
1308
|
+
attempts_count = attempts_count + 1,
|
|
1309
|
+
status = 'started',
|
|
1310
|
+
started_at = now(),
|
|
1311
|
+
last_worker_id = worker_id
|
|
1312
|
+
from tasks
|
|
1313
|
+
where step_tasks.message_id = tasks.message_id
|
|
1314
|
+
and step_tasks.flow_slug = tasks.flow_slug
|
|
1315
|
+
and step_tasks.status = 'queued'
|
|
1316
|
+
),
|
|
1317
|
+
runs as (
|
|
1318
|
+
select
|
|
1319
|
+
r.run_id,
|
|
1320
|
+
r.input
|
|
1321
|
+
from pgflow.runs r
|
|
1322
|
+
where r.run_id in (select run_id from tasks)
|
|
1323
|
+
),
|
|
1324
|
+
deps as (
|
|
1325
|
+
select
|
|
1326
|
+
st.run_id,
|
|
1327
|
+
st.step_slug,
|
|
1328
|
+
dep.dep_slug,
|
|
1329
|
+
-- Read output directly from step_states (already aggregated by writers)
|
|
1330
|
+
dep_state.output as dep_output
|
|
1331
|
+
from tasks st
|
|
1332
|
+
join pgflow.deps dep on dep.flow_slug = st.flow_slug and dep.step_slug = st.step_slug
|
|
1333
|
+
join pgflow.step_states dep_state on
|
|
1334
|
+
dep_state.run_id = st.run_id and
|
|
1335
|
+
dep_state.step_slug = dep.dep_slug and
|
|
1336
|
+
dep_state.status = 'completed' -- Only include completed deps (not skipped)
|
|
1337
|
+
),
|
|
1338
|
+
deps_outputs as (
|
|
1339
|
+
select
|
|
1340
|
+
d.run_id,
|
|
1341
|
+
d.step_slug,
|
|
1342
|
+
jsonb_object_agg(d.dep_slug, d.dep_output) as deps_output,
|
|
1343
|
+
count(*) as dep_count
|
|
1344
|
+
from deps d
|
|
1345
|
+
group by d.run_id, d.step_slug
|
|
1346
|
+
),
|
|
1347
|
+
timeouts as (
|
|
1348
|
+
select
|
|
1349
|
+
task.message_id,
|
|
1350
|
+
task.flow_slug,
|
|
1351
|
+
coalesce(step.opt_timeout, flow.opt_timeout) + 2 as vt_delay
|
|
1352
|
+
from tasks task
|
|
1353
|
+
join pgflow.flows flow on flow.flow_slug = task.flow_slug
|
|
1354
|
+
join pgflow.steps step on step.flow_slug = task.flow_slug and step.step_slug = task.step_slug
|
|
1355
|
+
),
|
|
1356
|
+
-- Batch update visibility timeouts for all messages
|
|
1357
|
+
set_vt_batch as (
|
|
1358
|
+
select pgflow.set_vt_batch(
|
|
1359
|
+
start_tasks.flow_slug,
|
|
1360
|
+
array_agg(t.message_id order by t.message_id),
|
|
1361
|
+
array_agg(t.vt_delay order by t.message_id)
|
|
1362
|
+
)
|
|
1363
|
+
from timeouts t
|
|
1364
|
+
)
|
|
1365
|
+
select
|
|
1366
|
+
st.flow_slug,
|
|
1367
|
+
st.run_id,
|
|
1368
|
+
st.step_slug,
|
|
1369
|
+
-- ==========================================
|
|
1370
|
+
-- INPUT CONSTRUCTION LOGIC
|
|
1371
|
+
-- ==========================================
|
|
1372
|
+
-- This nested CASE statement determines how to construct the input
|
|
1373
|
+
-- for each task based on the step type (map vs non-map).
|
|
1374
|
+
--
|
|
1375
|
+
-- The fundamental difference:
|
|
1376
|
+
-- - Map steps: Receive RAW array elements (e.g., just 42 or "hello")
|
|
1377
|
+
-- - Non-map steps: Receive structured objects with named keys
|
|
1378
|
+
-- (e.g., {"run": {...}, "dependency1": {...}})
|
|
1379
|
+
-- ==========================================
|
|
1380
|
+
CASE
|
|
1381
|
+
-- -------------------- MAP STEPS --------------------
|
|
1382
|
+
-- Map steps process arrays element-by-element.
|
|
1383
|
+
-- Each task receives ONE element from the array at its task_index position.
|
|
1384
|
+
WHEN step.step_type = 'map' THEN
|
|
1385
|
+
-- Map steps get raw array elements without any wrapper object
|
|
1386
|
+
CASE
|
|
1387
|
+
-- ROOT MAP: Gets array from run input
|
|
1388
|
+
-- Example: run input = [1, 2, 3]
|
|
1389
|
+
-- task 0 gets: 1
|
|
1390
|
+
-- task 1 gets: 2
|
|
1391
|
+
-- task 2 gets: 3
|
|
1392
|
+
WHEN step.deps_count = 0 THEN
|
|
1393
|
+
-- Root map (deps_count = 0): no dependencies, reads from run input.
|
|
1394
|
+
-- Extract the element at task_index from the run's input array.
|
|
1395
|
+
-- Note: If run input is not an array, this will return NULL
|
|
1396
|
+
-- and the flow will fail (validated in start_flow).
|
|
1397
|
+
jsonb_array_element(r.input, st.task_index)
|
|
1398
|
+
|
|
1399
|
+
-- DEPENDENT MAP: Gets array from its single dependency
|
|
1400
|
+
-- Example: dependency output = ["a", "b", "c"]
|
|
1401
|
+
-- task 0 gets: "a"
|
|
1402
|
+
-- task 1 gets: "b"
|
|
1403
|
+
-- task 2 gets: "c"
|
|
1404
|
+
ELSE
|
|
1405
|
+
-- Has dependencies (should be exactly 1 for map steps).
|
|
1406
|
+
-- Extract the element at task_index from the dependency's output array.
|
|
1407
|
+
--
|
|
1408
|
+
-- Why the subquery with jsonb_each?
|
|
1409
|
+
-- - The dependency outputs a raw array: [1, 2, 3]
|
|
1410
|
+
-- - deps_outputs aggregates it into: {"dep_name": [1, 2, 3]}
|
|
1411
|
+
-- - We need to unwrap and get just the array value
|
|
1412
|
+
-- - Map steps have exactly 1 dependency (enforced by add_step)
|
|
1413
|
+
-- - So jsonb_each will return exactly 1 row
|
|
1414
|
+
-- - We extract the 'value' which is the raw array [1, 2, 3]
|
|
1415
|
+
-- - Then get the element at task_index from that array
|
|
1416
|
+
(SELECT jsonb_array_element(value, st.task_index)
|
|
1417
|
+
FROM jsonb_each(dep_out.deps_output)
|
|
1418
|
+
LIMIT 1)
|
|
1419
|
+
END
|
|
1420
|
+
|
|
1421
|
+
-- -------------------- NON-MAP STEPS --------------------
|
|
1422
|
+
-- Regular (non-map) steps receive dependency outputs as a structured object.
|
|
1423
|
+
-- Root steps (no dependencies) get empty object - they access flowInput via context.
|
|
1424
|
+
-- Dependent steps get only their dependency outputs.
|
|
1425
|
+
ELSE
|
|
1426
|
+
-- Non-map steps get structured input with dependency keys only
|
|
1427
|
+
-- Example for dependent step: {
|
|
1428
|
+
-- "step1": {"output": "from_step1"},
|
|
1429
|
+
-- "step2": {"output": "from_step2"}
|
|
1430
|
+
-- }
|
|
1431
|
+
-- Example for root step: {}
|
|
1432
|
+
--
|
|
1433
|
+
-- Note: flow_input is available separately in the returned record
|
|
1434
|
+
-- for workers to access via context.flowInput
|
|
1435
|
+
coalesce(dep_out.deps_output, '{}'::jsonb)
|
|
1436
|
+
END as input,
|
|
1437
|
+
st.message_id as msg_id,
|
|
1438
|
+
st.task_index as task_index,
|
|
1439
|
+
-- flow_input: Original run input for worker context
|
|
1440
|
+
-- Only included for root non-map steps to avoid data duplication.
|
|
1441
|
+
-- Root map steps: flowInput IS the array, useless to include
|
|
1442
|
+
-- Dependent steps: lazy load via ctx.flowInput when needed
|
|
1443
|
+
CASE
|
|
1444
|
+
WHEN step.step_type != 'map' AND step.deps_count = 0
|
|
1445
|
+
THEN r.input
|
|
1446
|
+
ELSE NULL
|
|
1447
|
+
END as flow_input
|
|
1448
|
+
from tasks st
|
|
1449
|
+
join runs r on st.run_id = r.run_id
|
|
1450
|
+
join pgflow.steps step on
|
|
1451
|
+
step.flow_slug = st.flow_slug and
|
|
1452
|
+
step.step_slug = st.step_slug
|
|
1453
|
+
left join deps_outputs dep_out on
|
|
1454
|
+
dep_out.run_id = st.run_id and
|
|
1455
|
+
dep_out.step_slug = st.step_slug
|
|
1456
|
+
$$;
|
|
1457
|
+
-- Create "add_step" function
|
|
1458
|
+
CREATE FUNCTION "pgflow"."add_step" ("flow_slug" text, "step_slug" text, "deps_slugs" text[] DEFAULT '{}', "max_attempts" integer DEFAULT NULL::integer, "base_delay" integer DEFAULT NULL::integer, "timeout" integer DEFAULT NULL::integer, "start_delay" integer DEFAULT NULL::integer, "step_type" text DEFAULT 'single', "condition_pattern" jsonb DEFAULT NULL::jsonb, "condition_not_pattern" jsonb DEFAULT NULL::jsonb, "when_unmet" text DEFAULT 'skip', "when_failed" text DEFAULT 'fail') RETURNS "pgflow"."steps" LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
1459
|
+
DECLARE
|
|
1460
|
+
result_step pgflow.steps;
|
|
1461
|
+
next_idx int;
|
|
1462
|
+
BEGIN
|
|
1463
|
+
-- Validate map step constraints
|
|
1464
|
+
-- Map steps can have either:
|
|
1465
|
+
-- 0 dependencies (root map - maps over flow input array)
|
|
1466
|
+
-- 1 dependency (dependent map - maps over dependency output array)
|
|
1467
|
+
IF COALESCE(add_step.step_type, 'single') = 'map' AND COALESCE(array_length(add_step.deps_slugs, 1), 0) > 1 THEN
|
|
1468
|
+
RAISE EXCEPTION 'Map step "%" can have at most one dependency, but % were provided: %',
|
|
1469
|
+
add_step.step_slug,
|
|
1470
|
+
COALESCE(array_length(add_step.deps_slugs, 1), 0),
|
|
1471
|
+
array_to_string(add_step.deps_slugs, ', ');
|
|
1472
|
+
END IF;
|
|
1473
|
+
|
|
1474
|
+
-- Get next step index
|
|
1475
|
+
SELECT COALESCE(MAX(s.step_index) + 1, 0) INTO next_idx
|
|
1476
|
+
FROM pgflow.steps s
|
|
1477
|
+
WHERE s.flow_slug = add_step.flow_slug;
|
|
1478
|
+
|
|
1479
|
+
-- Create the step
|
|
1480
|
+
INSERT INTO pgflow.steps (
|
|
1481
|
+
flow_slug, step_slug, step_type, step_index, deps_count,
|
|
1482
|
+
opt_max_attempts, opt_base_delay, opt_timeout, opt_start_delay,
|
|
1483
|
+
condition_pattern, condition_not_pattern, when_unmet, when_failed
|
|
1484
|
+
)
|
|
1485
|
+
VALUES (
|
|
1486
|
+
add_step.flow_slug,
|
|
1487
|
+
add_step.step_slug,
|
|
1488
|
+
COALESCE(add_step.step_type, 'single'),
|
|
1489
|
+
next_idx,
|
|
1490
|
+
COALESCE(array_length(add_step.deps_slugs, 1), 0),
|
|
1491
|
+
add_step.max_attempts,
|
|
1492
|
+
add_step.base_delay,
|
|
1493
|
+
add_step.timeout,
|
|
1494
|
+
add_step.start_delay,
|
|
1495
|
+
add_step.condition_pattern,
|
|
1496
|
+
add_step.condition_not_pattern,
|
|
1497
|
+
add_step.when_unmet,
|
|
1498
|
+
add_step.when_failed
|
|
1499
|
+
)
|
|
1500
|
+
ON CONFLICT ON CONSTRAINT steps_pkey
|
|
1501
|
+
DO UPDATE SET step_slug = EXCLUDED.step_slug
|
|
1502
|
+
RETURNING * INTO result_step;
|
|
1503
|
+
|
|
1504
|
+
-- Insert dependencies
|
|
1505
|
+
INSERT INTO pgflow.deps (flow_slug, dep_slug, step_slug)
|
|
1506
|
+
SELECT add_step.flow_slug, d.dep_slug, add_step.step_slug
|
|
1507
|
+
FROM unnest(COALESCE(add_step.deps_slugs, '{}')) AS d(dep_slug)
|
|
1508
|
+
WHERE add_step.deps_slugs IS NOT NULL AND array_length(add_step.deps_slugs, 1) > 0
|
|
1509
|
+
ON CONFLICT ON CONSTRAINT deps_pkey DO NOTHING;
|
|
1510
|
+
|
|
1511
|
+
RETURN result_step;
|
|
1512
|
+
END;
|
|
1513
|
+
$$;
|
|
1514
|
+
-- Drop "add_step" function
|
|
1515
|
+
DROP FUNCTION "pgflow"."add_step" (text, text, text[], integer, integer, integer, integer, text);
|