-- Copyright (c) Microsoft Corporation. -- Licensed under the PostgreSQL License. -- Merged from: 30_graph_reuse, 32_invalid_node_type -- Tests: graph storage and reuse, identical DSL produces identical JSON, -- invalid node_type rejection by df.start() and df.explain() SET SESSION AUTHORIZATION df_e2e_user; -- === Test: 30_graph_reuse === DROP TABLE IF EXISTS test_graph_reuse; CREATE TABLE test_graph_reuse ( id SERIAL PRIMARY KEY, value INT ); -- Test 1: Store a graph and start it later CREATE TEMP TABLE _stored_graph AS SELECT 'SELECT 1' ~> 'INSERT INTO test_graph_reuse (value) VALUES (1)' AS graph_json; CREATE TEMP TABLE _test1_instance1 AS SELECT df.start((SELECT graph_json FROM _stored_graph), 'test-reuse-1') AS instance_id; DO $$ DECLARE inst_id TEXT; status TEXT; attempts INT := 0; BEGIN SELECT instance_id INTO inst_id FROM _test1_instance1; LOOP SELECT s INTO status FROM df.status(inst_id) s; EXIT WHEN lower(status) IN ('completed', 'failed', 'cancelled') OR attempts > 300; PERFORM pg_sleep(0.1); attempts := attempts + 1; END LOOP; IF lower(status) != 'completed' THEN RAISE EXCEPTION 'TEST FAILED: First execution status = %', status; END IF; RAISE NOTICE 'First execution completed'; END $$; CREATE TEMP TABLE _test1_instance2 AS SELECT df.start((SELECT graph_json FROM _stored_graph), 'test-reuse-2') AS instance_id; DO $$ DECLARE inst_id TEXT; status TEXT; attempts INT := 0; BEGIN SELECT instance_id INTO inst_id FROM _test1_instance2; LOOP SELECT s INTO status FROM df.status(inst_id) s; EXIT WHEN lower(status) IN ('completed', 'failed', 'cancelled') OR attempts > 300; PERFORM pg_sleep(0.1); attempts := attempts + 1; END LOOP; IF lower(status) != 'completed' THEN RAISE EXCEPTION 'TEST FAILED: Second execution status = %', status; END IF; RAISE NOTICE 'Second execution completed'; END $$; DO $$ BEGIN IF (SELECT COUNT(*) FROM test_graph_reuse) != 2 THEN RAISE EXCEPTION 'TEST FAILED: Expected 2 rows, got %', (SELECT COUNT(*) FROM test_graph_reuse); END IF; RAISE NOTICE 'Test 1 PASSED: Graph stored and reused successfully'; END $$; DROP TABLE _stored_graph; DROP TABLE _test1_instance1; DROP TABLE _test1_instance2; -- Test 2: Verify identical DSL expressions produce identical JSON DO $$ DECLARE graph1 TEXT; graph2 TEXT; BEGIN SELECT 'SELECT 2' ~> 'SELECT 3' INTO graph1; SELECT 'SELECT 2' ~> 'SELECT 3' INTO graph2; IF graph1 IS DISTINCT FROM graph2 THEN RAISE EXCEPTION 'TEST FAILED: Identical DSL expressions produced different JSON. Graph 1: %, Graph 2: %', graph1, graph2; END IF; RAISE NOTICE 'Test 2 PASSED: Identical DSL expressions produce identical Durofut JSON'; END $$; -- Test 3: Different graphs should have different JSON DO $$ DECLARE graph1 TEXT; graph2 TEXT; BEGIN SELECT 'SELECT 4' ~> 'SELECT 5' INTO graph1; SELECT 'SELECT 6' ~> 'SELECT 7' INTO graph2; IF graph1 = graph2 THEN RAISE EXCEPTION 'TEST FAILED: Different graphs produced identical JSON: %', graph1; END IF; RAISE NOTICE 'Test 3 PASSED: Different graphs produce different Durofut JSON'; END $$; DROP TABLE test_graph_reuse; -- === Test: 32_invalid_node_type === DO $body$ DECLARE explanation TEXT; BEGIN BEGIN PERFORM df.start('{"node_type":"NOT_A_NODE"}'); RAISE EXCEPTION 'TEST FAILED: df.start should have rejected invalid node_type'; EXCEPTION WHEN OTHERS THEN IF SQLERRM LIKE 'TEST FAILED:%' THEN RAISE; END IF; IF SQLERRM NOT LIKE '%Unknown node_type ''NOT_A_NODE''%' THEN RAISE EXCEPTION 'TEST FAILED: df.start returned the wrong error: %', SQLERRM; END IF; RAISE NOTICE 'Caught expected error: %', SQLERRM; END; explanation := df.explain('{"node_type":"NOT_A_NODE"}'); IF explanation NOT LIKE 'Invalid durable function graph: root: Unknown node_type ''NOT_A_NODE''%' THEN RAISE EXCEPTION 'TEST FAILED: df.explain returned the wrong error: %', explanation; END IF; RAISE NOTICE 'TEST PASSED: invalid node_type handling'; END $body$; -- === Test: await_instance blocked inside workflow === -- df.await_instance blocks the calling backend on a polling loop. -- Inside a workflow that backend is a BGW thread, so calling it would -- pin a worker slot for up to `timeout_seconds` and (if waiting on the -- current instance) deadlock the workflow on itself. The function must -- refuse to run when df.in_workflow='true'. CREATE TEMP TABLE _test_wait_blocked (instance_id TEXT); INSERT INTO _test_wait_blocked SELECT df.start( 'SELECT df.await_instance(''nonexistent-instance'')', 'test-wait-blocked' ); DO $$ DECLARE inst_id TEXT; status TEXT; node_error TEXT; BEGIN SELECT instance_id INTO inst_id FROM _test_wait_blocked; RAISE NOTICE 'Testing await_instance blocked in workflow: %', inst_id; SELECT df.await_instance(inst_id) INTO status; IF status != 'failed' THEN RAISE EXCEPTION 'TEST FAILED: expected workflow to fail but status = %', status; END IF; SELECT n.result::text INTO node_error FROM df.nodes n WHERE n.instance_id = inst_id AND n.status = 'failed' LIMIT 1; IF node_error NOT LIKE '%cannot be called inside a workflow%' THEN RAISE EXCEPTION 'TEST FAILED: expected "cannot be called inside a workflow" error, got: %', node_error; END IF; RAISE NOTICE 'TEST PASSED: await_instance_blocked_in_workflow'; END $$; DROP TABLE _test_wait_blocked; -- === Test: deep graph composition (#327) === DO $$ DECLARE graph TEXT := df.sql('SELECT 1'); explanation TEXT; BEGIN FOR i IN 1..129 LOOP graph := df.seq(graph, 'SELECT 1'); END LOOP; explanation := df.explain(graph); IF explanation LIKE 'Invalid durable function graph:%' OR pg_catalog.regexp_count(explanation, '→') != 129 THEN RAISE EXCEPTION 'TEST FAILED: 129-level df.seq graph was corrupted: %', explanation; END IF; RAISE NOTICE 'TEST PASSED: df.seq composes beyond serde recursion limit'; END $$; DO $$ DECLARE graph TEXT := df.sql('SELECT 1'); explanation TEXT; BEGIN FOR i IN 1..129 LOOP graph := ('SELECT true' ?> graph) !> 'SELECT 0'; END LOOP; explanation := df.explain(graph); IF explanation LIKE 'Invalid durable function graph:%' OR pg_catalog.regexp_count(explanation, 'IF') != 129 THEN RAISE EXCEPTION 'TEST FAILED: 129-level ?>/!> graph was corrupted: %', explanation; END IF; RAISE NOTICE 'TEST PASSED: ?>/!> composes beyond parser recursion limit'; END $$; DO $$ DECLARE condition_graph TEXT := df.sql('SELECT true'); graph TEXT; explanation TEXT; BEGIN -- 129 levels intentionally exceed serde_json's default 128-level recursion limit. FOR i IN 1..129 LOOP condition_graph := df.seq(condition_graph, 'SELECT true'); END LOOP; graph := df.if(condition_graph, 'SELECT 1', 'SELECT 0'); explanation := df.explain(graph); IF explanation LIKE 'Invalid durable function graph:%' OR pg_catalog.regexp_count(explanation, '→') != 129 THEN RAISE EXCEPTION 'TEST FAILED: deep condition graph was corrupted: %', explanation; END IF; RAISE NOTICE 'TEST PASSED: config children compose beyond serde recursion limit'; END $$; DO $$ DECLARE graph TEXT := df.sql('SELECT 1'); explanation TEXT; BEGIN FOR i IN 1..256 LOOP graph := df.seq(graph, 'SELECT 1'); END LOOP; explanation := df.explain(graph); IF explanation LIKE 'Invalid durable function graph:%' THEN RAISE EXCEPTION 'TEST FAILED: graph at maximum depth was rejected: %', explanation; END IF; graph := df.seq(graph, 'SELECT 1'); explanation := df.explain(graph); IF explanation NOT LIKE '%maximum nesting depth of 256%' THEN RAISE EXCEPTION 'TEST FAILED: graph beyond maximum depth was not rejected cleanly: %', explanation; END IF; RAISE NOTICE 'TEST PASSED: df.explain enforces the configured depth boundary'; END $$; CREATE TEMP TABLE _deep_graph_execution (instance_id TEXT); DO $$ DECLARE graph TEXT := df.sql('SELECT 1'); BEGIN FOR i IN 2..200 LOOP graph := df.seq(graph, 'SELECT 1'); END LOOP; INSERT INTO _deep_graph_execution SELECT df.start(graph, 'test-deep-graph-200'); END $$; DO $$ DECLARE inst_id TEXT; status TEXT; BEGIN SELECT instance_id INTO inst_id FROM _deep_graph_execution; SELECT df.await_instance(inst_id, 300) INTO status; IF status != 'completed' THEN RAISE EXCEPTION 'TEST FAILED: 200-step graph status = %', status; END IF; IF (SELECT count(*) FROM df.nodes WHERE instance_id = inst_id) != 399 THEN RAISE EXCEPTION 'TEST FAILED: 200-step graph did not materialize 399 nodes'; END IF; RAISE NOTICE 'TEST PASSED: 200-step graph executes end to end'; END $$; DROP TABLE _deep_graph_execution; -- Exercise the node INSERT chunk boundary without committing a workflow that -- would schedule 1,001 parallel SQL activities. BEGIN; CREATE TEMP TABLE _batched_graph_execution (instance_id TEXT); INSERT INTO _batched_graph_execution SELECT df.start( pg_catalog.jsonb_build_object( 'node_type', 'JOIN', 'left_node', pg_catalog.jsonb_build_object('node_type', 'SQL', 'query', 'SELECT 1'), 'right_node', pg_catalog.jsonb_build_object('node_type', 'SQL', 'query', 'SELECT 1'), 'extra_nodes', ( SELECT pg_catalog.jsonb_agg( pg_catalog.jsonb_build_object('node_type', 'SQL', 'query', 'SELECT 1') ) FROM pg_catalog.generate_series(1, 999) ) )::TEXT, 'test-batched-node-insert' ); DO $$ DECLARE inst_id TEXT; node_total BIGINT; BEGIN SELECT instance_id INTO inst_id FROM _batched_graph_execution; SELECT count(*) INTO node_total FROM df.nodes WHERE instance_id = inst_id; IF node_total != 1002 THEN RAISE EXCEPTION 'TEST FAILED: batched graph materialized % nodes instead of 1002', node_total; END IF; RAISE NOTICE 'TEST PASSED: df.start inserts nodes across the batch boundary'; END $$; ROLLBACK; RESET SESSION AUTHORIZATION; SELECT 'TEST PASSED' AS result;