-- Copyright (c) Microsoft Corporation. -- Licensed under the PostgreSQL License. -- Regression tests for: df.loop nested INSIDE a JOIN or RACE branch -- (https://github.com/microsoft/pg_durable/issues/233) -- -- A loop nested inside a parallel branch is a non-root loop, so it runs as its own child -- sub-orchestration spawned by the branch's subtree orchestration. Its instance id embeds -- the branch lineage, so it gets a distinct durable instance and continue_as_new restarts -- only the loop body (not the branch). Before the fix this raised -- "Missing graph in ExecuteSubtree input" / failed to iterate. SET SESSION AUTHORIZATION df_e2e_user; -- === Test 1: loop inside a JOIN branch, loop iterates >= 2 === -- -- Graph: df.loop(body, break after 2) & 'SELECT sibling' -- Expected: completes, loop body ran exactly 2 times, sibling branch ran once. DROP TABLE IF EXISTS test_joinloop_body; DROP TABLE IF EXISTS test_joinloop_sibling; CREATE TABLE test_joinloop_body (id SERIAL, iteration INT, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TABLE test_joinloop_sibling (id SERIAL, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TEMP TABLE _t1 AS SELECT df.start( ( df.loop( 'INSERT INTO test_joinloop_body (iteration) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_joinloop_body))' ~> ( 'SELECT COUNT(*) >= 2 FROM test_joinloop_body' ?> df.break() !> df.sleep(1) ) ) & 'INSERT INTO test_joinloop_sibling DEFAULT VALUES' ), 'test-loop-in-join-branch' ) AS instance_id; DO $$ DECLARE v_id TEXT; v_status TEXT; v_body INT; v_sibling INT; BEGIN SELECT instance_id INTO v_id FROM _t1; RAISE NOTICE 'Test 1 - loop inside JOIN branch: instance %', v_id; SELECT df.await_instance(v_id, 90) INTO v_status; IF v_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED [join-loop]: expected completed, got %', v_status; END IF; SELECT COUNT(*) INTO v_body FROM test_joinloop_body; SELECT COUNT(*) INTO v_sibling FROM test_joinloop_sibling; IF v_body != 2 THEN RAISE EXCEPTION 'TEST FAILED [join-loop]: loop body ran % time(s) (expected 2)', v_body; END IF; IF v_sibling != 1 THEN RAISE EXCEPTION 'TEST FAILED [join-loop]: sibling branch ran % time(s) (expected 1)', v_sibling; END IF; RAISE NOTICE 'PASSED: loop inside JOIN branch — loop iterated twice, sibling ran once'; END $$; DROP TABLE _t1; DROP TABLE test_joinloop_body; DROP TABLE test_joinloop_sibling; -- === Test 2: loop inside a RACE branch, loop iterates >= 2 and wins === -- -- Graph: df.race( df.loop(body, break after 2), 'SELECT pg_sleep(30)' ) -- The loop completes after 2 iterations (a few seconds) and wins the race against the slow -- branch. Expected: completes, loop body ran exactly 2 times. DROP TABLE IF EXISTS test_raceloop_body; CREATE TABLE test_raceloop_body (id SERIAL, iteration INT, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TEMP TABLE _t2 AS SELECT df.start( df.race( df.loop( 'INSERT INTO test_raceloop_body (iteration) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_raceloop_body))' ~> ( 'SELECT COUNT(*) >= 2 FROM test_raceloop_body' ?> df.break() !> df.sleep(1) ) ), 'SELECT pg_sleep(30)' ), 'test-loop-in-race-branch' ) AS instance_id; DO $$ DECLARE v_id TEXT; v_status TEXT; v_body INT; BEGIN SELECT instance_id INTO v_id FROM _t2; RAISE NOTICE 'Test 2 - loop inside RACE branch: instance %', v_id; SELECT df.await_instance(v_id, 90) INTO v_status; IF v_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED [race-loop]: expected completed, got %', v_status; END IF; SELECT COUNT(*) INTO v_body FROM test_raceloop_body; IF v_body != 2 THEN RAISE EXCEPTION 'TEST FAILED [race-loop]: loop body ran % time(s) (expected 2)', v_body; END IF; RAISE NOTICE 'PASSED: loop inside RACE branch — loop iterated twice and won the race'; END $$; DROP TABLE _t2; DROP TABLE test_raceloop_body; -- === Test 3: topology — race(loop, sleep) spawns 1 parent + 2 subs (loop not double-wrapped) === -- -- Same shape as Test 2 (a RACE at the root; left = df.loop(sleep), right = a sleep) but this -- time we assert the *orchestration topology* rather than the behaviour: -- * parent : the function-graph orchestration (RACE is its root node) -- * loop sub : an execute-subtree child rooted AT the loop node, which runs the -- loop inline and drives its own continue_as_new -- * right-branch sub : an execute-subtree wrapping the sleep -- -- The loop branch must NOT be double-wrapped (an outer subtree that itself spawns a second -- child for the loop): doing so would create a third sub-orchestration for the single loop -- branch. We verify the loop's own node is stamped by an orchestration instance that is a -- *direct* child of the parent, i.e. `{parent}::{parent_generation}::{loop_node_id}`, with no -- extra wrapper in between. -- -- The stamp recorded in df.nodes.status_details->>'execution_id' has the shape -- `{orchestration_instance_id}::{execution_id}`, and an orchestration instance id is itself -- `{parent}::{parent_execution_id}::{branch_root_node_id}` for a spawned child. DROP TABLE IF EXISTS test_topo_log; CREATE TABLE test_topo_log (id SERIAL, iteration INT, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TEMP TABLE _t3 AS SELECT df.start( df.race( df.loop( 'INSERT INTO test_topo_log (iteration) VALUES ((SELECT COALESCE(MAX(iteration), 0) + 1 FROM test_topo_log))' ~> ( 'SELECT COUNT(*) >= 2 FROM test_topo_log' ?> df.break() !> df.sleep(1) ) ), 'SELECT pg_sleep(30)' ), 'test-loop-in-race-topology' ) AS instance_id; DO $$ DECLARE v_id TEXT; v_status TEXT; v_loop_node TEXT; v_loop_scope TEXT; v_body_scope TEXT; v_loop_tokens TEXT[]; v_loop_parent TEXT; v_distinct_orch INT; BEGIN SELECT instance_id INTO v_id FROM _t3; RAISE NOTICE 'Test 3 - race(loop, sleep) topology: instance %', v_id; SELECT df.await_instance(v_id, 90) INTO v_status; IF v_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED [topo]: expected completed, got %', v_status; END IF; -- The loop node id is the root of the loop sub-orchestration. SELECT id INTO v_loop_node FROM df.nodes WHERE instance_id = v_id AND node_type = 'LOOP'; -- Strip the trailing :: token from the loop node's stamp to recover the -- loop sub-orchestration's instance id. SELECT array_to_string( (string_to_array(status_details->>'execution_id', '::'))[1:array_length(string_to_array(status_details->>'execution_id', '::'), 1) - 1], '::' ) INTO v_loop_scope FROM df.nodes WHERE instance_id = v_id AND id = v_loop_node; v_loop_tokens := string_to_array(v_loop_scope, '::'); v_loop_parent := array_to_string(v_loop_tokens[1:array_length(v_loop_tokens, 1) - 2], '::'); IF v_loop_parent IS DISTINCT FROM v_id OR v_loop_tokens[array_length(v_loop_tokens, 1)] IS DISTINCT FROM v_loop_node THEN RAISE EXCEPTION 'TEST FAILED [topo]: loop scope % is not a direct child of parent % rooted at node %', v_loop_scope, v_id, v_loop_node; END IF; -- The loop body (the INSERT SQL node) must run inside the SAME loop sub-orchestration. SELECT array_to_string( (string_to_array(status_details->>'execution_id', '::'))[1:array_length(string_to_array(status_details->>'execution_id', '::'), 1) - 1], '::' ) INTO v_body_scope FROM df.nodes WHERE instance_id = v_id AND node_type = 'SQL' AND query LIKE 'INSERT INTO test_topo_log%'; IF v_body_scope IS DISTINCT FROM v_loop_scope THEN RAISE EXCEPTION 'TEST FAILED [topo]: loop body scope = % (expected loop scope %)', v_body_scope, v_loop_scope; END IF; -- Count distinct orchestration instances that stamped any node of this instance. -- Correct topology is exactly three scopes: parent RACE, loop child, and right-branch -- subtree. A double-wrapped loop adds a fourth. The abandoned right branch reliably stamps -- its node running before the race is decided, so all three expected scopes are observable. SELECT count(DISTINCT array_to_string( (string_to_array(status_details->>'execution_id', '::'))[1:array_length(string_to_array(status_details->>'execution_id', '::'), 1) - 1], '::' )) INTO v_distinct_orch FROM df.nodes WHERE instance_id = v_id AND status_details->>'execution_id' IS NOT NULL; IF v_distinct_orch != 3 THEN RAISE EXCEPTION 'TEST FAILED [topo]: % distinct orchestration scopes (expected exactly parent + loop child + sibling child)', v_distinct_orch; END IF; RAISE NOTICE 'PASSED: race(loop, sleep) = 1 parent + 2 subs; loop spawned directly (% distinct scopes)', v_distinct_orch; END $$; DROP TABLE _t3; DROP TABLE test_topo_log; SELECT 'TEST PASSED' AS result;