-- Copyright (c) Microsoft Corporation. -- Licensed under the PostgreSQL License. -- Regression tests for: df.loop continue_as_new restarts from root when loop is not root -- (https://github.com/microsoft/pg_durable/issues/227) -- -- When a loop is not the root node of a function graph (i.e., there are prefix or suffix -- nodes), continue_as_new must NOT re-execute the prefix or skip the suffix. Loops are -- now executed as a scoped sub-orchestration so continue_as_new is scoped to the loop -- child, leaving the parent graph orchestration parked. SET SESSION AUTHORIZATION df_e2e_user; -- === Test 1: Non-root loop — prefix runs once, body runs N times === -- -- Graph: INSERT into prefix_table ~> df.loop(INSERT into body_table ~> break after 3) -- Expected: prefix_table has exactly 1 row after completion, body_table has exactly 3 rows. DROP TABLE IF EXISTS test_nonroot_prefix; DROP TABLE IF EXISTS test_nonroot_body; CREATE TABLE test_nonroot_prefix (id SERIAL, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TABLE test_nonroot_body (id SERIAL, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TEMP TABLE _t1 AS SELECT df.start( df.seq( 'INSERT INTO test_nonroot_prefix DEFAULT VALUES', df.loop( 'INSERT INTO test_nonroot_body DEFAULT VALUES' ~> ( 'SELECT COUNT(*) >= 3 FROM test_nonroot_body' ?> df.break() !> df.sleep(1) ) ) ), 'test-nonroot-loop-prefix' ) AS instance_id; DO $$ DECLARE v_id TEXT; v_status TEXT; v_prefix INT; v_body INT; BEGIN SELECT instance_id INTO v_id FROM _t1; RAISE NOTICE 'Test 1 - non-root loop prefix: instance %', v_id; SELECT df.await_instance(v_id, 90) INTO v_status; IF v_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED [nonroot-prefix]: expected completed, got %', v_status; END IF; SELECT COUNT(*) INTO v_prefix FROM test_nonroot_prefix; SELECT COUNT(*) INTO v_body FROM test_nonroot_body; IF v_prefix != 1 THEN RAISE EXCEPTION 'TEST FAILED [nonroot-prefix]: prefix ran % time(s) (expected 1); ' 'prefix nodes must not be re-executed on each loop iteration', v_prefix; END IF; IF v_body != 3 THEN RAISE EXCEPTION 'TEST FAILED [nonroot-prefix]: body ran % time(s) (expected 3)', v_body; END IF; RAISE NOTICE 'PASSED: non-root loop prefix — prefix ran once, body ran 3 times'; END $$; DROP TABLE _t1; DROP TABLE test_nonroot_prefix; DROP TABLE test_nonroot_body; -- === Test 2: Non-root loop — prefix once, body N times, suffix once === -- -- Graph: INSERT prefix ~> df.loop(body, break after 2) ~> INSERT suffix -- Expected: prefix_table = 1 row, body_table = 2 rows, suffix_table = 1 row. DROP TABLE IF EXISTS test_nonroot2_prefix; DROP TABLE IF EXISTS test_nonroot2_body; DROP TABLE IF EXISTS test_nonroot2_suffix; CREATE TABLE test_nonroot2_prefix (id SERIAL, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TABLE test_nonroot2_body (id SERIAL, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TABLE test_nonroot2_suffix (id SERIAL, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TEMP TABLE _t2 AS SELECT df.start( df.seq( 'INSERT INTO test_nonroot2_prefix DEFAULT VALUES', df.seq( df.loop( 'INSERT INTO test_nonroot2_body DEFAULT VALUES' ~> ( 'SELECT COUNT(*) >= 2 FROM test_nonroot2_body' ?> df.break() !> df.sleep(1) ) ), 'INSERT INTO test_nonroot2_suffix DEFAULT VALUES' ) ), 'test-nonroot-loop-prefix-suffix' ) AS instance_id; DO $$ DECLARE v_id TEXT; v_status TEXT; v_prefix INT; v_body INT; v_suffix INT; BEGIN SELECT instance_id INTO v_id FROM _t2; RAISE NOTICE 'Test 2 - non-root loop prefix+suffix: instance %', v_id; SELECT df.await_instance(v_id, 90) INTO v_status; IF v_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED [nonroot-suffix]: expected completed, got %', v_status; END IF; SELECT COUNT(*) INTO v_prefix FROM test_nonroot2_prefix; SELECT COUNT(*) INTO v_body FROM test_nonroot2_body; SELECT COUNT(*) INTO v_suffix FROM test_nonroot2_suffix; IF v_prefix != 1 THEN RAISE EXCEPTION 'TEST FAILED [nonroot-suffix]: prefix ran % time(s) (expected 1)', v_prefix; END IF; IF v_body != 2 THEN RAISE EXCEPTION 'TEST FAILED [nonroot-suffix]: body ran % time(s) (expected 2)', v_body; END IF; IF v_suffix != 1 THEN RAISE EXCEPTION 'TEST FAILED [nonroot-suffix]: suffix ran % time(s) (expected 1)', v_suffix; END IF; RAISE NOTICE 'PASSED: non-root loop prefix+suffix — prefix once, body twice, suffix once'; END $$; DROP TABLE _t2; DROP TABLE test_nonroot2_prefix; DROP TABLE test_nonroot2_body; DROP TABLE test_nonroot2_suffix; -- === Test 3: Non-root loop — named result from prefix available inside loop body === -- -- Verify that named results accumulated before the loop are still accessible -- inside the loop body after continue_as_new (they are preserved in the loop -- sub-orchestration's input). DROP TABLE IF EXISTS test_nonroot3_log; DROP TABLE IF EXISTS test_nonroot3_suffix; CREATE TABLE test_nonroot3_log (id SERIAL, val TEXT, ts TIMESTAMPTZ DEFAULT clock_timestamp()); CREATE TABLE test_nonroot3_suffix (val TEXT); CREATE TEMP TABLE _t3 AS SELECT df.start( df.seq( ('SELECT ''hello'' AS greeting' |=> 'prefix_result'), df.seq( df.loop( ($$INSERT INTO test_nonroot3_log (val) VALUES ($prefix_result) RETURNING val$$ |=> 'last_val') ~> ( 'SELECT COUNT(*) >= 2 FROM test_nonroot3_log' ?> df.break() !> df.sleep(1) ) ), $$INSERT INTO test_nonroot3_suffix (val) VALUES ($last_val.val)$$ ) ), 'test-nonroot-loop-named-result' ) AS instance_id; DO $$ DECLARE v_id TEXT; v_status TEXT; v_cnt INT; v_val TEXT; v_suffix TEXT; BEGIN SELECT instance_id INTO v_id FROM _t3; RAISE NOTICE 'Test 3 - non-root loop uses prefix named result: instance %', v_id; SELECT df.await_instance(v_id, 90) INTO v_status; IF v_status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED [nonroot-named]: expected completed, got %', v_status; END IF; SELECT COUNT(*) INTO v_cnt FROM test_nonroot3_log; IF v_cnt != 2 THEN RAISE EXCEPTION 'TEST FAILED [nonroot-named]: expected 2 rows, got %', v_cnt; END IF; SELECT val INTO v_val FROM test_nonroot3_log ORDER BY id LIMIT 1; IF v_val != 'hello' THEN RAISE EXCEPTION 'TEST FAILED [nonroot-named]: expected ''hello'', got ''%''', v_val; END IF; SELECT val INTO v_suffix FROM test_nonroot3_suffix; IF v_suffix IS DISTINCT FROM 'hello' THEN RAISE EXCEPTION 'TEST FAILED [nonroot-named]: loop child named result did not reach suffix, got ''%''', v_suffix; END IF; RAISE NOTICE 'PASSED: non-root loop uses prefix named result across iterations'; END $$; DROP TABLE _t3; DROP TABLE test_nonroot3_log; DROP TABLE test_nonroot3_suffix; RESET SESSION AUTHORIZATION; SELECT 'TEST PASSED' AS result;