@@ -204,5 +204,206 @@ END $$;
204204DROP TABLE _t3;
205205DROP TABLE test_nonroot3_log;
206206
207+ -- === Test 4: Nested loop — a loop body that itself contains a loop ===
208+ --
209+ -- Each df.loop() spawns an execute_loop sub-orchestration, so a nested loop spawns a
210+ -- child execute_loop from *within* another execute_loop generation. This verifies that
211+ -- continue_as_new in the outer loop does not disturb the inner loop and vice versa.
212+ --
213+ -- Outer loop runs 2 iterations (break when outer_marker has 2 rows); each outer iteration
214+ -- runs an inner loop that inserts exactly one row and breaks immediately.
215+ -- Expected: outer_marker = 2 rows, inner_table = 2 rows.
216+
217+ DROP TABLE IF EXISTS test_nested_outer;
218+ DROP TABLE IF EXISTS test_nested_inner;
219+ CREATE TABLE test_nested_outer (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
220+ CREATE TABLE test_nested_inner (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
221+
222+ CREATE TEMP TABLE _t4 AS
223+ SELECT df .start (
224+ df .loop (
225+ ' INSERT INTO test_nested_outer DEFAULT VALUES'
226+ ~> df .loop (
227+ ' INSERT INTO test_nested_inner DEFAULT VALUES'
228+ ~> df .break ()
229+ )
230+ ~> (
231+ ' SELECT COUNT(*) >= 2 FROM test_nested_outer'
232+ ?> df .break ()
233+ !> df .sleep (1 )
234+ )
235+ ),
236+ ' test-nested-loop'
237+ ) AS instance_id;
238+
239+ DO $$
240+ DECLARE
241+ v_id TEXT ;
242+ v_status TEXT ;
243+ v_outer INT ;
244+ v_inner INT ;
245+ BEGIN
246+ SELECT instance_id INTO v_id FROM _t4;
247+ RAISE NOTICE ' Test 4 - nested loop: instance %' , v_id;
248+
249+ SELECT df .wait_for_completion (v_id, 90 ) INTO v_status;
250+
251+ IF v_status != ' completed' THEN
252+ RAISE EXCEPTION ' TEST FAILED [nested]: expected completed, got %' , v_status;
253+ END IF;
254+
255+ SELECT COUNT (* ) INTO v_outer FROM test_nested_outer;
256+ SELECT COUNT (* ) INTO v_inner FROM test_nested_inner;
257+
258+ IF v_outer != 2 THEN
259+ RAISE EXCEPTION ' TEST FAILED [nested]: outer ran % time(s) (expected 2)' , v_outer;
260+ END IF;
261+
262+ IF v_inner != 2 THEN
263+ RAISE EXCEPTION ' TEST FAILED [nested]: inner ran % time(s) (expected 2)' , v_inner;
264+ END IF;
265+
266+ RAISE NOTICE ' PASSED: nested loop — outer ran twice, inner ran once per outer iteration' ;
267+ END $$;
268+
269+ DROP TABLE _t4;
270+ DROP TABLE test_nested_outer;
271+ DROP TABLE test_nested_inner;
272+
273+ -- === Test 5: Loop inside a JOIN branch ===
274+ --
275+ -- JOIN branches execute as execute_subtree sub-orchestrations, so a loop in a branch
276+ -- spawns an execute_loop child from *within* execute_subtree. This verifies the loop
277+ -- sub-orchestration nests correctly under a parallel branch and that the JOIN still
278+ -- completes once both branches finish.
279+ --
280+ -- Left branch inserts one row; right branch loops until join_loop has 2 rows.
281+ -- Expected: join_left = 1 row, join_loop = 2 rows.
282+
283+ DROP TABLE IF EXISTS test_join_left;
284+ DROP TABLE IF EXISTS test_join_loop;
285+ CREATE TABLE test_join_left (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
286+ CREATE TABLE test_join_loop (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
287+
288+ CREATE TEMP TABLE _t5 AS
289+ SELECT df .start (
290+ ' INSERT INTO test_join_left DEFAULT VALUES'
291+ & df .loop (
292+ ' INSERT INTO test_join_loop DEFAULT VALUES'
293+ ~> (
294+ ' SELECT COUNT(*) >= 2 FROM test_join_loop'
295+ ?> df .break ()
296+ !> df .sleep (1 )
297+ )
298+ ),
299+ ' test-loop-in-join-branch'
300+ ) AS instance_id;
301+
302+ DO $$
303+ DECLARE
304+ v_id TEXT ;
305+ v_status TEXT ;
306+ v_left INT ;
307+ v_loop INT ;
308+ BEGIN
309+ SELECT instance_id INTO v_id FROM _t5;
310+ RAISE NOTICE ' Test 5 - loop in JOIN branch: instance %' , v_id;
311+
312+ SELECT df .wait_for_completion (v_id, 90 ) INTO v_status;
313+
314+ IF v_status != ' completed' THEN
315+ RAISE EXCEPTION ' TEST FAILED [loop-in-join]: expected completed, got %' , v_status;
316+ END IF;
317+
318+ SELECT COUNT (* ) INTO v_left FROM test_join_left;
319+ SELECT COUNT (* ) INTO v_loop FROM test_join_loop;
320+
321+ IF v_left != 1 THEN
322+ RAISE EXCEPTION ' TEST FAILED [loop-in-join]: left branch ran % time(s) (expected 1)' , v_left;
323+ END IF;
324+
325+ IF v_loop != 2 THEN
326+ RAISE EXCEPTION ' TEST FAILED [loop-in-join]: loop branch body ran % time(s) (expected 2)' , v_loop;
327+ END IF;
328+
329+ RAISE NOTICE ' PASSED: loop in JOIN branch — left ran once, loop body ran twice' ;
330+ END $$;
331+
332+ DROP TABLE _t5;
333+ DROP TABLE test_join_left;
334+ DROP TABLE test_join_loop;
335+
336+ -- === Test 6: Non-root while-loop — prefix once, while-condition exit, suffix once ===
337+ --
338+ -- The earlier tests exit via df.break(); this one exits via a false while-condition
339+ -- (df.loop(body, condition)). The condition node also runs inside the loop
340+ -- sub-orchestration, so this exercises the while-false exit path across generations.
341+ --
342+ -- Graph: INSERT prefix ~> df.loop(body, 'COUNT < 3') ~> INSERT suffix
343+ -- Expected: prefix = 1 row, body = 3 rows (loop stops when count reaches 3), suffix = 1 row.
344+
345+ DROP TABLE IF EXISTS test_while_prefix;
346+ DROP TABLE IF EXISTS test_while_body;
347+ DROP TABLE IF EXISTS test_while_suffix;
348+ CREATE TABLE test_while_prefix (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
349+ CREATE TABLE test_while_body (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
350+ CREATE TABLE test_while_suffix (id SERIAL , ts TIMESTAMPTZ DEFAULT clock_timestamp());
351+
352+ CREATE TEMP TABLE _t6 AS
353+ SELECT df .start (
354+ df .seq (
355+ ' INSERT INTO test_while_prefix DEFAULT VALUES' ,
356+ df .seq (
357+ df .loop (
358+ ' INSERT INTO test_while_body DEFAULT VALUES' ~> df .sleep (1 ),
359+ ' SELECT COUNT(*) < 3 FROM test_while_body'
360+ ),
361+ ' INSERT INTO test_while_suffix DEFAULT VALUES'
362+ )
363+ ),
364+ ' test-nonroot-while-loop'
365+ ) AS instance_id;
366+
367+ DO $$
368+ DECLARE
369+ v_id TEXT ;
370+ v_status TEXT ;
371+ v_prefix INT ;
372+ v_body INT ;
373+ v_suffix INT ;
374+ BEGIN
375+ SELECT instance_id INTO v_id FROM _t6;
376+ RAISE NOTICE ' Test 6 - non-root while loop: instance %' , v_id;
377+
378+ SELECT df .wait_for_completion (v_id, 90 ) INTO v_status;
379+
380+ IF v_status != ' completed' THEN
381+ RAISE EXCEPTION ' TEST FAILED [nonroot-while]: expected completed, got %' , v_status;
382+ END IF;
383+
384+ SELECT COUNT (* ) INTO v_prefix FROM test_while_prefix;
385+ SELECT COUNT (* ) INTO v_body FROM test_while_body;
386+ SELECT COUNT (* ) INTO v_suffix FROM test_while_suffix;
387+
388+ IF v_prefix != 1 THEN
389+ RAISE EXCEPTION ' TEST FAILED [nonroot-while]: prefix ran % time(s) (expected 1)' , v_prefix;
390+ END IF;
391+
392+ IF v_body != 3 THEN
393+ RAISE EXCEPTION ' TEST FAILED [nonroot-while]: body ran % time(s) (expected 3)' , v_body;
394+ END IF;
395+
396+ IF v_suffix != 1 THEN
397+ RAISE EXCEPTION ' TEST FAILED [nonroot-while]: suffix ran % time(s) (expected 1)' , v_suffix;
398+ END IF;
399+
400+ RAISE NOTICE ' PASSED: non-root while loop — prefix once, body 3x via while-condition, suffix once' ;
401+ END $$;
402+
403+ DROP TABLE _t6;
404+ DROP TABLE test_while_prefix;
405+ DROP TABLE test_while_body;
406+ DROP TABLE test_while_suffix;
407+
207408RESET SESSION AUTHORIZATION;
208409SELECT ' TEST PASSED' AS result;
0 commit comments