diff --git a/pkgs/core/schemas/0050_tables_definitions.sql b/pkgs/core/schemas/0050_tables_definitions.sql index 5b69de9b9..1ea94b079 100644 --- a/pkgs/core/schemas/0050_tables_definitions.sql +++ b/pkgs/core/schemas/0050_tables_definitions.sql @@ -24,8 +24,8 @@ create table pgflow.steps ( opt_base_delay int, opt_timeout int, opt_start_delay int, - condition_pattern jsonb, -- JSON pattern for @> containment check (if) - condition_not_pattern jsonb, -- JSON pattern for NOT @> containment check (ifNot) + required_input_pattern jsonb, -- JSON pattern for @> containment check (if) + forbidden_input_pattern jsonb, -- JSON pattern for NOT @> containment check (ifNot) when_unmet text not null default 'skip', -- What to do when condition not met (skip is natural default) when_failed text not null default 'fail', -- What to do when handler fails after retries created_at timestamptz not null default now(), diff --git a/pkgs/core/schemas/0100_function_add_step.sql b/pkgs/core/schemas/0100_function_add_step.sql index 06a9dabd9..0ed71bf69 100644 --- a/pkgs/core/schemas/0100_function_add_step.sql +++ b/pkgs/core/schemas/0100_function_add_step.sql @@ -7,8 +7,8 @@ create or replace function pgflow.add_step( timeout int default null, start_delay int default null, step_type text default 'single', - condition_pattern jsonb default null, - condition_not_pattern jsonb default null, + required_input_pattern jsonb default null, + forbidden_input_pattern jsonb default null, when_unmet text default 'skip', when_failed text default 'fail' ) @@ -41,7 +41,7 @@ BEGIN INSERT INTO pgflow.steps ( flow_slug, step_slug, step_type, step_index, deps_count, opt_max_attempts, opt_base_delay, opt_timeout, opt_start_delay, - condition_pattern, condition_not_pattern, when_unmet, when_failed + required_input_pattern, forbidden_input_pattern, when_unmet, when_failed ) VALUES ( add_step.flow_slug, @@ -53,8 +53,8 @@ BEGIN add_step.base_delay, add_step.timeout, add_step.start_delay, - add_step.condition_pattern, - add_step.condition_not_pattern, + add_step.required_input_pattern, + add_step.forbidden_input_pattern, add_step.when_unmet, add_step.when_failed ) diff --git a/pkgs/core/schemas/0100_function_cascade_resolve_conditions.sql b/pkgs/core/schemas/0100_function_cascade_resolve_conditions.sql index 36ce12d6f..927e4564b 100644 --- a/pkgs/core/schemas/0100_function_cascade_resolve_conditions.sql +++ b/pkgs/core/schemas/0100_function_cascade_resolve_conditions.sql @@ -47,14 +47,14 @@ BEGIN -- ========================================== -- Find first step (by topological order) with unmet condition and 'fail' mode. -- Condition is unmet when: - -- (condition_pattern is set AND input does NOT contain it) OR - -- (condition_not_pattern is set AND input DOES contain it) + -- (required_input_pattern is set AND input does NOT contain it) OR + -- (forbidden_input_pattern is set AND input DOES contain it) WITH steps_with_conditions AS ( SELECT step_state.flow_slug, step_state.step_slug, - step.condition_pattern, - step.condition_not_pattern, + step.required_input_pattern, + step.forbidden_input_pattern, step.when_unmet, step.deps_count, step.step_index @@ -65,7 +65,7 @@ BEGIN WHERE step_state.run_id = cascade_resolve_conditions.run_id AND step_state.status = 'created' AND step_state.remaining_deps = 0 - AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL) + AND (step.required_input_pattern IS NOT NULL OR step.forbidden_input_pattern IS NOT NULL) ), step_deps_output AS ( SELECT @@ -84,16 +84,16 @@ BEGIN SELECT swc.*, -- condition_met = (if IS NULL OR input @> if) AND (ifNot IS NULL OR NOT(input @> ifNot)) - (swc.condition_pattern IS NULL OR - CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_pattern) + (swc.required_input_pattern IS NULL OR + CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.required_input_pattern) AND - (swc.condition_not_pattern IS NULL OR - NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_not_pattern)) + (swc.forbidden_input_pattern IS NULL OR + NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.forbidden_input_pattern)) AS condition_met FROM steps_with_conditions swc LEFT JOIN step_deps_output sdo ON sdo.step_slug = swc.step_slug ) - SELECT flow_slug, step_slug, condition_pattern, condition_not_pattern + SELECT flow_slug, step_slug, required_input_pattern, forbidden_input_pattern INTO v_first_fail FROM condition_evaluations WHERE NOT condition_met AND when_unmet = 'fail' @@ -128,8 +128,8 @@ BEGIN SELECT step_state.flow_slug, step_state.step_slug, - step.condition_pattern, - step.condition_not_pattern, + step.required_input_pattern, + step.forbidden_input_pattern, step.when_unmet, step.deps_count, step.step_index @@ -140,7 +140,7 @@ BEGIN WHERE step_state.run_id = cascade_resolve_conditions.run_id AND step_state.status = 'created' AND step_state.remaining_deps = 0 - AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL) + AND (step.required_input_pattern IS NOT NULL OR step.forbidden_input_pattern IS NOT NULL) ), step_deps_output AS ( SELECT @@ -159,11 +159,11 @@ BEGIN SELECT swc.*, -- condition_met = (if IS NULL OR input @> if) AND (ifNot IS NULL OR NOT(input @> ifNot)) - (swc.condition_pattern IS NULL OR - CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_pattern) + (swc.required_input_pattern IS NULL OR + CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.required_input_pattern) AND - (swc.condition_not_pattern IS NULL OR - NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_not_pattern)) + (swc.forbidden_input_pattern IS NULL OR + NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.forbidden_input_pattern)) AS condition_met FROM steps_with_conditions swc LEFT JOIN step_deps_output sdo ON sdo.step_slug = swc.step_slug @@ -244,15 +244,15 @@ BEGIN WHERE ready_step.run_id = cascade_resolve_conditions.run_id AND ready_step.status = 'created' AND ready_step.remaining_deps = 0 - AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL) + AND (step.required_input_pattern IS NOT NULL OR step.forbidden_input_pattern IS NOT NULL) AND step.when_unmet = 'skip-cascade' -- Condition is NOT met when: (if fails) OR (ifNot fails) AND NOT ( - (step.condition_pattern IS NULL OR - CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.condition_pattern) + (step.required_input_pattern IS NULL OR + CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.required_input_pattern) AND - (step.condition_not_pattern IS NULL OR - NOT (CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.condition_not_pattern)) + (step.forbidden_input_pattern IS NULL OR + NOT (CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.forbidden_input_pattern)) ) ORDER BY step.step_index; diff --git a/pkgs/core/schemas/0100_function_compare_flow_shapes.sql b/pkgs/core/schemas/0100_function_compare_flow_shapes.sql index ae543c138..b91839faf 100644 --- a/pkgs/core/schemas/0100_function_compare_flow_shapes.sql +++ b/pkgs/core/schemas/0100_function_compare_flow_shapes.sql @@ -133,6 +133,34 @@ BEGIN ) ); END IF; + + -- Compare requiredInputPattern (structural - affects DAG execution semantics) + -- Uses -> (jsonb) not ->> (text) to properly compare wrapper objects + IF v_local_step->'requiredInputPattern' IS DISTINCT FROM v_db_step->'requiredInputPattern' THEN + v_differences := array_append( + v_differences, + format( + $$Step at index %s: requiredInputPattern differs '%s' vs '%s'$$, + v_idx, + v_local_step->'requiredInputPattern', + v_db_step->'requiredInputPattern' + ) + ); + END IF; + + -- Compare forbiddenInputPattern (structural - affects DAG execution semantics) + -- Uses -> (jsonb) not ->> (text) to properly compare wrapper objects + IF v_local_step->'forbiddenInputPattern' IS DISTINCT FROM v_db_step->'forbiddenInputPattern' THEN + v_differences := array_append( + v_differences, + format( + $$Step at index %s: forbiddenInputPattern differs '%s' vs '%s'$$, + v_idx, + v_local_step->'forbiddenInputPattern', + v_db_step->'forbiddenInputPattern' + ) + ); + END IF; END IF; END LOOP; diff --git a/pkgs/core/schemas/0100_function_create_flow_from_shape.sql b/pkgs/core/schemas/0100_function_create_flow_from_shape.sql index 7c9e445c2..51b055482 100644 --- a/pkgs/core/schemas/0100_function_create_flow_from_shape.sql +++ b/pkgs/core/schemas/0100_function_create_flow_from_shape.sql @@ -49,7 +49,17 @@ BEGIN start_delay => (v_step_options->>'startDelay')::int, step_type => v_step->>'stepType', when_unmet => v_step->>'whenUnmet', - when_failed => v_step->>'whenFailed' + when_failed => v_step->>'whenFailed', + required_input_pattern => CASE + WHEN (v_step->'requiredInputPattern'->>'defined')::boolean + THEN v_step->'requiredInputPattern'->'value' + ELSE NULL + END, + forbidden_input_pattern => CASE + WHEN (v_step->'forbiddenInputPattern'->>'defined')::boolean + THEN v_step->'forbiddenInputPattern'->'value' + ELSE NULL + END ); END LOOP; END; diff --git a/pkgs/core/schemas/0100_function_get_flow_shape.sql b/pkgs/core/schemas/0100_function_get_flow_shape.sql index 725388ebc..985f0d4d1 100644 --- a/pkgs/core/schemas/0100_function_get_flow_shape.sql +++ b/pkgs/core/schemas/0100_function_get_flow_shape.sql @@ -24,7 +24,17 @@ as $$ '[]'::jsonb ), 'whenUnmet', step.when_unmet, - 'whenFailed', step.when_failed + 'whenFailed', step.when_failed, + 'requiredInputPattern', CASE + WHEN step.required_input_pattern IS NULL + THEN '{"defined": false}'::jsonb + ELSE jsonb_build_object('defined', true, 'value', step.required_input_pattern) + END, + 'forbiddenInputPattern', CASE + WHEN step.forbidden_input_pattern IS NULL + THEN '{"defined": false}'::jsonb + ELSE jsonb_build_object('defined', true, 'value', step.forbidden_input_pattern) + END ) ORDER BY step.step_index ), diff --git a/pkgs/core/src/database-types.ts b/pkgs/core/src/database-types.ts index 1d2966461..739a8bd3c 100644 --- a/pkgs/core/src/database-types.ts +++ b/pkgs/core/src/database-types.ts @@ -278,15 +278,15 @@ export type Database = { } steps: { Row: { - condition_not_pattern: Json | null - condition_pattern: Json | null created_at: string deps_count: number flow_slug: string + forbidden_input_pattern: Json | null opt_base_delay: number | null opt_max_attempts: number | null opt_start_delay: number | null opt_timeout: number | null + required_input_pattern: Json | null step_index: number step_slug: string step_type: string @@ -294,15 +294,15 @@ export type Database = { when_unmet: string } Insert: { - condition_not_pattern?: Json | null - condition_pattern?: Json | null created_at?: string deps_count?: number flow_slug: string + forbidden_input_pattern?: Json | null opt_base_delay?: number | null opt_max_attempts?: number | null opt_start_delay?: number | null opt_timeout?: number | null + required_input_pattern?: Json | null step_index?: number step_slug: string step_type?: string @@ -310,15 +310,15 @@ export type Database = { when_unmet?: string } Update: { - condition_not_pattern?: Json | null - condition_pattern?: Json | null created_at?: string deps_count?: number flow_slug?: string + forbidden_input_pattern?: Json | null opt_base_delay?: number | null opt_max_attempts?: number | null opt_start_delay?: number | null opt_timeout?: number | null + required_input_pattern?: Json | null step_index?: number step_slug?: string step_type?: string @@ -413,11 +413,11 @@ export type Database = { add_step: { Args: { base_delay?: number - condition_not_pattern?: Json - condition_pattern?: Json deps_slugs?: string[] flow_slug: string + forbidden_input_pattern?: Json max_attempts?: number + required_input_pattern?: Json start_delay?: number step_slug: string step_type?: string @@ -426,15 +426,15 @@ export type Database = { when_unmet?: string } Returns: { - condition_not_pattern: Json | null - condition_pattern: Json | null created_at: string deps_count: number flow_slug: string + forbidden_input_pattern: Json | null opt_base_delay: number | null opt_max_attempts: number | null opt_start_delay: number | null opt_timeout: number | null + required_input_pattern: Json | null step_index: number step_slug: string step_type: string diff --git a/pkgs/core/supabase/migrations/20260108131350_pgflow_step_conditions.sql b/pkgs/core/supabase/migrations/20260108131350_pgflow_step_conditions.sql index 529436eaf..1317a1989 100644 --- a/pkgs/core/supabase/migrations/20260108131350_pgflow_step_conditions.sql +++ b/pkgs/core/supabase/migrations/20260108131350_pgflow_step_conditions.sql @@ -15,7 +15,7 @@ END) <= 1), ADD CONSTRAINT "skip_reason_matches_status" CHECK (((status = 'skipp -- Create index "idx_step_states_skipped" to table: "step_states" CREATE INDEX "idx_step_states_skipped" ON "pgflow"."step_states" ("run_id", "step_slug") WHERE (status = 'skipped'::text); -- Modify "steps" table -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'; +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 "required_input_pattern" jsonb NULL, ADD COLUMN "forbidden_input_pattern" jsonb NULL, ADD COLUMN "when_unmet" text NOT NULL DEFAULT 'skip', ADD COLUMN "when_failed" text NOT NULL DEFAULT 'fail'; -- Modify "_compare_flow_shapes" function CREATE OR REPLACE FUNCTION "pgflow"."_compare_flow_shapes" ("p_local" jsonb, "p_db" jsonb) RETURNS text[] LANGUAGE plpgsql STABLE SET "search_path" = '' AS $BODY$ DECLARE @@ -142,6 +142,34 @@ BEGIN ) ); END IF; + + -- Compare requiredInputPattern (structural - affects DAG execution semantics) + -- Uses -> (jsonb) not ->> (text) to properly compare wrapper objects + IF v_local_step->'requiredInputPattern' IS DISTINCT FROM v_db_step->'requiredInputPattern' THEN + v_differences := array_append( + v_differences, + format( + $$Step at index %s: requiredInputPattern differs '%s' vs '%s'$$, + v_idx, + v_local_step->'requiredInputPattern', + v_db_step->'requiredInputPattern' + ) + ); + END IF; + + -- Compare forbiddenInputPattern (structural - affects DAG execution semantics) + -- Uses -> (jsonb) not ->> (text) to properly compare wrapper objects + IF v_local_step->'forbiddenInputPattern' IS DISTINCT FROM v_db_step->'forbiddenInputPattern' THEN + v_differences := array_append( + v_differences, + format( + $$Step at index %s: forbiddenInputPattern differs '%s' vs '%s'$$, + v_idx, + v_local_step->'forbiddenInputPattern', + v_db_step->'forbiddenInputPattern' + ) + ); + END IF; END IF; END LOOP; @@ -149,7 +177,7 @@ BEGIN END; $BODY$; -- Create "add_step" function -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 $$ +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', "required_input_pattern" jsonb DEFAULT NULL::jsonb, "forbidden_input_pattern" jsonb DEFAULT NULL::jsonb, "when_unmet" text DEFAULT 'skip', "when_failed" text DEFAULT 'fail') RETURNS "pgflow"."steps" LANGUAGE plpgsql SET "search_path" = '' AS $$ DECLARE result_step pgflow.steps; next_idx int; @@ -174,7 +202,7 @@ BEGIN INSERT INTO pgflow.steps ( flow_slug, step_slug, step_type, step_index, deps_count, opt_max_attempts, opt_base_delay, opt_timeout, opt_start_delay, - condition_pattern, condition_not_pattern, when_unmet, when_failed + required_input_pattern, forbidden_input_pattern, when_unmet, when_failed ) VALUES ( add_step.flow_slug, @@ -186,8 +214,8 @@ BEGIN add_step.base_delay, add_step.timeout, add_step.start_delay, - add_step.condition_pattern, - add_step.condition_not_pattern, + add_step.required_input_pattern, + add_step.forbidden_input_pattern, add_step.when_unmet, add_step.when_failed ) @@ -246,7 +274,17 @@ BEGIN start_delay => (v_step_options->>'startDelay')::int, step_type => v_step->>'stepType', when_unmet => v_step->>'whenUnmet', - when_failed => v_step->>'whenFailed' + when_failed => v_step->>'whenFailed', + required_input_pattern => CASE + WHEN (v_step->'requiredInputPattern'->>'defined')::boolean + THEN v_step->'requiredInputPattern'->'value' + ELSE NULL + END, + forbidden_input_pattern => CASE + WHEN (v_step->'forbiddenInputPattern'->>'defined')::boolean + THEN v_step->'forbiddenInputPattern'->'value' + ELSE NULL + END ); END LOOP; END; @@ -270,7 +308,17 @@ SELECT jsonb_build_object( '[]'::jsonb ), 'whenUnmet', step.when_unmet, - 'whenFailed', step.when_failed + 'whenFailed', step.when_failed, + 'requiredInputPattern', CASE + WHEN step.required_input_pattern IS NULL + THEN '{"defined": false}'::jsonb + ELSE jsonb_build_object('defined', true, 'value', step.required_input_pattern) + END, + 'forbiddenInputPattern', CASE + WHEN step.forbidden_input_pattern IS NULL + THEN '{"defined": false}'::jsonb + ELSE jsonb_build_object('defined', true, 'value', step.forbidden_input_pattern) + END ) ORDER BY step.step_index ), @@ -416,14 +464,14 @@ BEGIN -- ========================================== -- Find first step (by topological order) with unmet condition and 'fail' mode. -- Condition is unmet when: - -- (condition_pattern is set AND input does NOT contain it) OR - -- (condition_not_pattern is set AND input DOES contain it) + -- (required_input_pattern is set AND input does NOT contain it) OR + -- (forbidden_input_pattern is set AND input DOES contain it) WITH steps_with_conditions AS ( SELECT step_state.flow_slug, step_state.step_slug, - step.condition_pattern, - step.condition_not_pattern, + step.required_input_pattern, + step.forbidden_input_pattern, step.when_unmet, step.deps_count, step.step_index @@ -434,7 +482,7 @@ BEGIN WHERE step_state.run_id = cascade_resolve_conditions.run_id AND step_state.status = 'created' AND step_state.remaining_deps = 0 - AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL) + AND (step.required_input_pattern IS NOT NULL OR step.forbidden_input_pattern IS NOT NULL) ), step_deps_output AS ( SELECT @@ -453,16 +501,16 @@ BEGIN SELECT swc.*, -- condition_met = (if IS NULL OR input @> if) AND (ifNot IS NULL OR NOT(input @> ifNot)) - (swc.condition_pattern IS NULL OR - CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_pattern) + (swc.required_input_pattern IS NULL OR + CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.required_input_pattern) AND - (swc.condition_not_pattern IS NULL OR - NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_not_pattern)) + (swc.forbidden_input_pattern IS NULL OR + NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.forbidden_input_pattern)) AS condition_met FROM steps_with_conditions swc LEFT JOIN step_deps_output sdo ON sdo.step_slug = swc.step_slug ) - SELECT flow_slug, step_slug, condition_pattern, condition_not_pattern + SELECT flow_slug, step_slug, required_input_pattern, forbidden_input_pattern INTO v_first_fail FROM condition_evaluations WHERE NOT condition_met AND when_unmet = 'fail' @@ -497,8 +545,8 @@ BEGIN SELECT step_state.flow_slug, step_state.step_slug, - step.condition_pattern, - step.condition_not_pattern, + step.required_input_pattern, + step.forbidden_input_pattern, step.when_unmet, step.deps_count, step.step_index @@ -509,7 +557,7 @@ BEGIN WHERE step_state.run_id = cascade_resolve_conditions.run_id AND step_state.status = 'created' AND step_state.remaining_deps = 0 - AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL) + AND (step.required_input_pattern IS NOT NULL OR step.forbidden_input_pattern IS NOT NULL) ), step_deps_output AS ( SELECT @@ -528,11 +576,11 @@ BEGIN SELECT swc.*, -- condition_met = (if IS NULL OR input @> if) AND (ifNot IS NULL OR NOT(input @> ifNot)) - (swc.condition_pattern IS NULL OR - CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_pattern) + (swc.required_input_pattern IS NULL OR + CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.required_input_pattern) AND - (swc.condition_not_pattern IS NULL OR - NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.condition_not_pattern)) + (swc.forbidden_input_pattern IS NULL OR + NOT (CASE WHEN swc.deps_count = 0 THEN v_run_input ELSE COALESCE(sdo.deps_output, '{}'::jsonb) END @> swc.forbidden_input_pattern)) AS condition_met FROM steps_with_conditions swc LEFT JOIN step_deps_output sdo ON sdo.step_slug = swc.step_slug @@ -613,15 +661,15 @@ BEGIN WHERE ready_step.run_id = cascade_resolve_conditions.run_id AND ready_step.status = 'created' AND ready_step.remaining_deps = 0 - AND (step.condition_pattern IS NOT NULL OR step.condition_not_pattern IS NOT NULL) + AND (step.required_input_pattern IS NOT NULL OR step.forbidden_input_pattern IS NOT NULL) AND step.when_unmet = 'skip-cascade' -- Condition is NOT met when: (if fails) OR (ifNot fails) AND NOT ( - (step.condition_pattern IS NULL OR - CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.condition_pattern) + (step.required_input_pattern IS NULL OR + CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.required_input_pattern) AND - (step.condition_not_pattern IS NULL OR - NOT (CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.condition_not_pattern)) + (step.forbidden_input_pattern IS NULL OR + NOT (CASE WHEN step.deps_count = 0 THEN v_run_input ELSE COALESCE(agg_deps.deps_output, '{}'::jsonb) END @> step.forbidden_input_pattern)) ) ORDER BY step.step_index; diff --git a/pkgs/core/supabase/migrations/atlas.sum b/pkgs/core/supabase/migrations/atlas.sum index acade6fd5..b49ccc368 100644 --- a/pkgs/core/supabase/migrations/atlas.sum +++ b/pkgs/core/supabase/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:VPsRoEfaQqAEkEJMJL839iHbH9F6ZaPPa6kOgbWRoI4= +h1:MljSQIIDQAOfrt257piE1p2uMUWZiMfjIQO+We1t78I= 20250429164909_pgflow_initial.sql h1:I3n/tQIg5Q5nLg7RDoU3BzqHvFVjmumQxVNbXTPG15s= 20250517072017_pgflow_fix_poll_for_tasks_to_use_separate_statement_for_polling.sql h1:wTuXuwMxVniCr3ONCpodpVWJcHktoQZIbqMZ3sUHKMY= 20250609105135_pgflow_add_start_tasks_and_started_status.sql h1:ggGanW4Wyt8Kv6TWjnZ00/qVb3sm+/eFVDjGfT8qyPg= @@ -16,4 +16,4 @@ h1:VPsRoEfaQqAEkEJMJL839iHbH9F6ZaPPa6kOgbWRoI4= 20251212100113_pgflow_allow_data_loss_parameter.sql h1:Fg3RHj51STNHS4epQ2J4AFMj7NwG0XfyDTSA/9dcBIQ= 20251225163110_pgflow_add_flow_input_column.sql h1:734uCbTgKmPhTK3TY56uNYZ31T8u59yll9ea7nwtEoc= 20260103145141_pgflow_step_output_storage.sql h1:mgVHSFDLdtYy//SZ6C03j9Str1iS9xCM8Rz/wyFwn3o= -20260108131350_pgflow_step_conditions.sql h1:lSVGOZvKeW8Jw0j95es8CB87nJxZyog9TLQs0L9PNm8= +20260108131350_pgflow_step_conditions.sql h1:Kz/c/H9C0BXxQ8QCF+hwI+zx19RPic1PBijrkeuVpqI= diff --git a/pkgs/core/supabase/tests/add_step/condition_not_pattern.test.sql b/pkgs/core/supabase/tests/add_step/condition_not_pattern.test.sql index a1ae628a4..5451537fe 100644 --- a/pkgs/core/supabase/tests/add_step/condition_not_pattern.test.sql +++ b/pkgs/core/supabase/tests/add_step/condition_not_pattern.test.sql @@ -1,59 +1,59 @@ --- Test: add_step - condition_not_pattern parameter --- Verifies the ifNot pattern (condition_not_pattern) is stored correctly +-- Test: add_step - forbidden_input_pattern parameter +-- Verifies the ifNot pattern (forbidden_input_pattern) is stored correctly begin; select plan(6); select pgflow_tests.reset_db(); select pgflow.create_flow('ifnot_test'); --- Test 1: Add step with condition_not_pattern only +-- Test 1: Add step with forbidden_input_pattern only select pgflow.add_step( 'ifnot_test', 'step_with_ifnot', - condition_not_pattern => '{"role": "admin"}'::jsonb + forbidden_input_pattern => '{"role": "admin"}'::jsonb ); select is( - (select condition_not_pattern from pgflow.steps + (select forbidden_input_pattern from pgflow.steps where flow_slug = 'ifnot_test' and step_slug = 'step_with_ifnot'), '{"role": "admin"}'::jsonb, - 'condition_not_pattern should be stored correctly' + 'forbidden_input_pattern should be stored correctly' ); --- Test 2: Default condition_not_pattern should be NULL +-- Test 2: Default forbidden_input_pattern should be NULL select pgflow.add_step('ifnot_test', 'step_default_not'); select is( - (select condition_not_pattern from pgflow.steps + (select forbidden_input_pattern from pgflow.steps where flow_slug = 'ifnot_test' and step_slug = 'step_default_not'), NULL::jsonb, - 'Default condition_not_pattern should be NULL' + 'Default forbidden_input_pattern should be NULL' ); --- Test 3: Both condition_pattern and condition_not_pattern together +-- Test 3: Both required_input_pattern and forbidden_input_pattern together select pgflow.add_step( 'ifnot_test', 'step_with_both', - condition_pattern => '{"active": true}'::jsonb, - condition_not_pattern => '{"suspended": true}'::jsonb + required_input_pattern => '{"active": true}'::jsonb, + forbidden_input_pattern => '{"suspended": true}'::jsonb ); select ok( (select - condition_pattern = '{"active": true}'::jsonb - AND condition_not_pattern = '{"suspended": true}'::jsonb + required_input_pattern = '{"active": true}'::jsonb + AND forbidden_input_pattern = '{"suspended": true}'::jsonb from pgflow.steps where flow_slug = 'ifnot_test' and step_slug = 'step_with_both'), - 'Both condition_pattern and condition_not_pattern should be stored together' + 'Both required_input_pattern and forbidden_input_pattern should be stored together' ); --- Test 4: condition_not_pattern with all other options +-- Test 4: forbidden_input_pattern with all other options select pgflow.add_step( 'ifnot_test', 'step_all_options', max_attempts => 5, timeout => 30, - condition_not_pattern => '{"status": "disabled"}'::jsonb, + forbidden_input_pattern => '{"status": "disabled"}'::jsonb, when_unmet => 'skip' ); @@ -61,41 +61,41 @@ select ok( (select opt_max_attempts = 5 AND opt_timeout = 30 - AND condition_not_pattern = '{"status": "disabled"}'::jsonb + AND forbidden_input_pattern = '{"status": "disabled"}'::jsonb AND when_unmet = 'skip' from pgflow.steps where flow_slug = 'ifnot_test' and step_slug = 'step_all_options'), - 'condition_not_pattern should work with all other step options' + 'forbidden_input_pattern should work with all other step options' ); --- Test 5: Complex nested condition_not_pattern +-- Test 5: Complex nested forbidden_input_pattern select pgflow.add_step( 'ifnot_test', 'step_nested_not', - condition_not_pattern => '{"user": {"role": "admin", "department": "IT"}}'::jsonb + forbidden_input_pattern => '{"user": {"role": "admin", "department": "IT"}}'::jsonb ); select is( - (select condition_not_pattern from pgflow.steps + (select forbidden_input_pattern from pgflow.steps where flow_slug = 'ifnot_test' and step_slug = 'step_nested_not'), '{"user": {"role": "admin", "department": "IT"}}'::jsonb, - 'Nested condition_not_pattern should be stored correctly' + 'Nested forbidden_input_pattern should be stored correctly' ); --- Test 6: condition_not_pattern on dependent step +-- Test 6: forbidden_input_pattern on dependent step select pgflow.add_step('ifnot_test', 'first_step'); select pgflow.add_step( 'ifnot_test', 'dependent_step', deps_slugs => ARRAY['first_step'], - condition_not_pattern => '{"first_step": {"error": true}}'::jsonb + forbidden_input_pattern => '{"first_step": {"error": true}}'::jsonb ); select is( - (select condition_not_pattern from pgflow.steps + (select forbidden_input_pattern from pgflow.steps where flow_slug = 'ifnot_test' and step_slug = 'dependent_step'), '{"first_step": {"error": true}}'::jsonb, - 'condition_not_pattern should be stored for dependent step' + 'forbidden_input_pattern should be stored for dependent step' ); select finish(); diff --git a/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql b/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql index 324aae2bf..04344ca0a 100644 --- a/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql +++ b/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql @@ -1,23 +1,23 @@ -- Test: add_step - New condition parameters --- Verifies condition_pattern, when_unmet, when_failed parameters work correctly +-- Verifies required_input_pattern, when_unmet, when_failed parameters work correctly begin; select plan(9); select pgflow_tests.reset_db(); select pgflow.create_flow('condition_test'); --- Test 1: Add step with condition_pattern +-- Test 1: Add step with required_input_pattern select pgflow.add_step( 'condition_test', 'step_with_condition', - condition_pattern => '{"type": "premium"}'::jsonb + required_input_pattern => '{"type": "premium"}'::jsonb ); select is( - (select condition_pattern from pgflow.steps + (select required_input_pattern from pgflow.steps where flow_slug = 'condition_test' and step_slug = 'step_with_condition'), '{"type": "premium"}'::jsonb, - 'condition_pattern should be stored correctly' + 'required_input_pattern should be stored correctly' ); -- Test 2: Add step with when_unmet = skip @@ -94,26 +94,26 @@ select is( 'Default when_failed should be fail' ); --- Test 8: Default condition_pattern should be NULL +-- Test 8: Default required_input_pattern should be NULL select is( - (select condition_pattern from pgflow.steps + (select required_input_pattern from pgflow.steps where flow_slug = 'condition_test' and step_slug = 'step_default_unmet'), NULL::jsonb, - 'Default condition_pattern should be NULL' + 'Default required_input_pattern should be NULL' ); -- Test 9: Add step with all condition parameters select pgflow.add_step( 'condition_test', 'step_all_params', - condition_pattern => '{"status": "active"}'::jsonb, + required_input_pattern => '{"status": "active"}'::jsonb, when_unmet => 'skip', when_failed => 'skip-cascade' ); select ok( (select - condition_pattern = '{"status": "active"}'::jsonb + required_input_pattern = '{"status": "active"}'::jsonb AND when_unmet = 'skip' AND when_failed = 'skip-cascade' from pgflow.steps diff --git a/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql b/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql index d280cc91d..328c29cce 100644 --- a/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql +++ b/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql @@ -16,7 +16,7 @@ select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip-cascade", "whenFailed": "fail"} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip-cascade", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, pgflow._get_flow_shape('drift_test') @@ -30,7 +30,7 @@ select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "skip-cascade"} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, pgflow._get_flow_shape('drift_test') @@ -44,7 +44,7 @@ select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "fail", "whenFailed": "skip"} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "fail", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, pgflow._get_flow_shape('drift_test') diff --git a/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql b/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql new file mode 100644 index 000000000..fea92b812 --- /dev/null +++ b/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql @@ -0,0 +1,42 @@ +begin; +select plan(2); +select pgflow_tests.reset_db(); + +-- Test: Patterns with same value should match +select is( + pgflow._compare_flow_shapes( + '{ + "steps": [ + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} + ] + }'::jsonb, + '{ + "steps": [ + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} + ] + }'::jsonb + ), + '{}'::text[], + 'Shapes with identical patterns should have no differences' +); + +-- Test: Different requiredInputPattern should be detected +select is( + pgflow._compare_flow_shapes( + '{ + "steps": [ + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} + ] + }'::jsonb, + '{ + "steps": [ + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "pending"}}, "forbiddenInputPattern": {"defined": false}} + ] + }'::jsonb + ), + ARRAY['Step at index 0: requiredInputPattern differs ''{"value": {"status": "active"}, "defined": true}'' vs ''{"value": {"status": "pending"}, "defined": true}''']::text[], + 'Different requiredInputPattern values should be detected' +); + +select finish(); +rollback; diff --git a/pkgs/core/supabase/tests/condition_evaluation/branching_opposite_conditions.test.sql b/pkgs/core/supabase/tests/condition_evaluation/branching_opposite_conditions.test.sql index 77b65b36d..c24beb5b2 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/branching_opposite_conditions.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/branching_opposite_conditions.test.sql @@ -13,14 +13,14 @@ select pgflow.create_flow('branch_flow'); select pgflow.add_step( flow_slug => 'branch_flow', step_slug => 'admin_branch', - condition_pattern => '{"role": "admin"}'::jsonb, -- if: role=admin + required_input_pattern => '{"role": "admin"}'::jsonb, -- if: role=admin when_unmet => 'skip' ); -- Regular branch: only runs when role!=admin select pgflow.add_step( flow_slug => 'branch_flow', step_slug => 'regular_branch', - condition_not_pattern => '{"role": "admin"}'::jsonb, -- ifNot: role=admin + forbidden_input_pattern => '{"role": "admin"}'::jsonb, -- ifNot: role=admin when_unmet => 'skip' ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/combined_if_and_ifnot.test.sql b/pkgs/core/supabase/tests/condition_evaluation/combined_if_and_ifnot.test.sql index fe659bd1d..03cf35f68 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/combined_if_and_ifnot.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/combined_if_and_ifnot.test.sql @@ -12,8 +12,8 @@ select pgflow.create_flow('combined_flow'); select pgflow.add_step( flow_slug => 'combined_flow', step_slug => 'admin_action', - condition_pattern => '{"role": "admin", "active": true}'::jsonb, -- if - condition_not_pattern => '{"suspended": true}'::jsonb, -- ifNot + required_input_pattern => '{"role": "admin", "active": true}'::jsonb, -- if + forbidden_input_pattern => '{"suspended": true}'::jsonb, -- ifNot when_unmet => 'skip' ); -- Add another step without conditions diff --git a/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_met.test.sql b/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_met.test.sql index 3cd064e52..8a60c18da 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_met.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_met.test.sql @@ -14,7 +14,7 @@ select pgflow.add_step( flow_slug => 'conditional_flow', step_slug => 'checked_step', deps_slugs => ARRAY['first'], - condition_pattern => '{"first": {"success": true}}'::jsonb, -- first.success must be true + required_input_pattern => '{"first": {"success": true}}'::jsonb, -- first.success must be true when_unmet => 'skip' ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_unmet_skip.test.sql b/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_unmet_skip.test.sql index 93ca1f3ce..0e0d7d81f 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_unmet_skip.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/dependent_step_condition_unmet_skip.test.sql @@ -14,7 +14,7 @@ select pgflow.add_step( flow_slug => 'conditional_flow', step_slug => 'checked_step', deps_slugs => ARRAY['first'], - condition_pattern => '{"first": {"success": true}}'::jsonb, -- first.success must be true + required_input_pattern => '{"first": {"success": true}}'::jsonb, -- first.success must be true when_unmet => 'skip' ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_matches_fail.test.sql b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_matches_fail.test.sql index 1efbd33d6..fe7a7111b 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_matches_fail.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_matches_fail.test.sql @@ -11,7 +11,7 @@ select pgflow.create_flow('ifnot_fail_flow'); select pgflow.add_step( flow_slug => 'ifnot_fail_flow', step_slug => 'no_admin_step', - condition_not_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin + forbidden_input_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin when_unmet => 'fail' ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_not_matches.test.sql b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_not_matches.test.sql index 64dcd5695..cc9dcbc81 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_not_matches.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_pattern_not_matches.test.sql @@ -11,7 +11,7 @@ select pgflow.create_flow('ifnot_pass_flow'); select pgflow.add_step( flow_slug => 'ifnot_pass_flow', step_slug => 'no_admin_step', - condition_not_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin + forbidden_input_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin when_unmet => 'fail' -- (doesn't matter for this test since condition is met) ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip.test.sql b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip.test.sql index e7a3dc7b7..d340f822f 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip.test.sql @@ -10,7 +10,7 @@ select pgflow.create_flow('ifnot_skip_flow'); select pgflow.add_step( flow_slug => 'ifnot_skip_flow', step_slug => 'no_admin_step', - condition_not_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin + forbidden_input_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin when_unmet => 'skip' ); -- Add another root step without condition diff --git a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip_cascade.test.sql b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip_cascade.test.sql index cb51fa66a..6d79fa2cb 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip_cascade.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/ifnot_root_step_skip_cascade.test.sql @@ -10,7 +10,7 @@ select pgflow.create_flow('ifnot_cascade_flow'); select pgflow.add_step( flow_slug => 'ifnot_cascade_flow', step_slug => 'no_admin_step', - condition_not_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin + forbidden_input_pattern => '{"role": "admin"}'::jsonb, -- must NOT contain role=admin when_unmet => 'skip-cascade' ); -- Add a dependent step diff --git a/pkgs/core/supabase/tests/condition_evaluation/no_condition_always_executes.test.sql b/pkgs/core/supabase/tests/condition_evaluation/no_condition_always_executes.test.sql index 46f172760..78d0de5a2 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/no_condition_always_executes.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/no_condition_always_executes.test.sql @@ -1,5 +1,5 @@ -- Test: Step with no condition (NULL pattern) always executes --- Verifies that steps without condition_pattern execute normally +-- Verifies that steps without required_input_pattern execute normally -- regardless of input content begin; select plan(2); diff --git a/pkgs/core/supabase/tests/condition_evaluation/plain_skip_iterates_until_convergence.test.sql b/pkgs/core/supabase/tests/condition_evaluation/plain_skip_iterates_until_convergence.test.sql index 2681373e6..a35ce708d 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/plain_skip_iterates_until_convergence.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/plain_skip_iterates_until_convergence.test.sql @@ -22,14 +22,14 @@ select pgflow.create_flow('chain_skip'); select pgflow.add_step( flow_slug => 'chain_skip', step_slug => 'step_a', - condition_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true + required_input_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true when_unmet => 'skip' -- plain skip ); select pgflow.add_step( flow_slug => 'chain_skip', step_slug => 'step_b', deps_slugs => ARRAY['step_a'], - condition_pattern => '{"step_a": {"success": true}}'::jsonb, -- a.success must be true + required_input_pattern => '{"step_a": {"success": true}}'::jsonb, -- a.success must be true when_unmet => 'skip' -- plain skip (won't be met since a was skipped) ); select pgflow.add_step( diff --git a/pkgs/core/supabase/tests/condition_evaluation/plain_skip_propagates_to_map.test.sql b/pkgs/core/supabase/tests/condition_evaluation/plain_skip_propagates_to_map.test.sql index 241090b51..fa2c88c8f 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/plain_skip_propagates_to_map.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/plain_skip_propagates_to_map.test.sql @@ -18,7 +18,7 @@ select pgflow.create_flow('skip_to_map'); select pgflow.add_step( flow_slug => 'skip_to_map', step_slug => 'producer', - condition_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true + required_input_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true when_unmet => 'skip' -- plain skip (not skip-cascade) ); -- Map consumer: no condition, just depends on producer diff --git a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_met.test.sql b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_met.test.sql index b86a91f24..03063a4a2 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_met.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_met.test.sql @@ -12,7 +12,7 @@ select pgflow.create_flow('conditional_flow'); select pgflow.add_step( flow_slug => 'conditional_flow', step_slug => 'checked_step', - condition_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true + required_input_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true when_unmet => 'skip' ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_fail.test.sql b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_fail.test.sql index dca13a3e4..97ae0039a 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_fail.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_fail.test.sql @@ -12,7 +12,7 @@ select pgflow.create_flow('conditional_flow'); select pgflow.add_step( flow_slug => 'conditional_flow', step_slug => 'checked_step', - condition_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true + required_input_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true when_unmet => 'fail' -- causes run to fail ); diff --git a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip.test.sql b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip.test.sql index b11364d35..67970f326 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip.test.sql @@ -12,7 +12,7 @@ select pgflow.create_flow('conditional_flow'); select pgflow.add_step( flow_slug => 'conditional_flow', step_slug => 'checked_step', - condition_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true + required_input_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true when_unmet => 'skip' ); -- Add another root step without condition diff --git a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip_cascade.test.sql b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip_cascade.test.sql index 59201f043..c9104f5e2 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip_cascade.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/root_step_condition_unmet_skip_cascade.test.sql @@ -12,7 +12,7 @@ select pgflow.create_flow('conditional_flow'); select pgflow.add_step( flow_slug => 'conditional_flow', step_slug => 'checked_step', - condition_pattern => '{"enabled": true}'::jsonb, + required_input_pattern => '{"enabled": true}'::jsonb, when_unmet => 'skip-cascade' -- skip this AND dependents ); select pgflow.add_step( diff --git a/pkgs/core/supabase/tests/condition_evaluation/skipped_deps_excluded_from_input.test.sql b/pkgs/core/supabase/tests/condition_evaluation/skipped_deps_excluded_from_input.test.sql index d88eacfc1..52b342d4c 100644 --- a/pkgs/core/supabase/tests/condition_evaluation/skipped_deps_excluded_from_input.test.sql +++ b/pkgs/core/supabase/tests/condition_evaluation/skipped_deps_excluded_from_input.test.sql @@ -24,7 +24,7 @@ select pgflow.create_flow('skip_diamond'); select pgflow.add_step( flow_slug => 'skip_diamond', step_slug => 'step_a', - condition_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true + required_input_pattern => '{"enabled": true}'::jsonb, -- requires enabled=true when_unmet => 'skip' -- plain skip ); select pgflow.add_step( diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql index 6d9b08a9d..e74c19675 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql @@ -7,9 +7,9 @@ select pgflow._create_flow_from_shape( 'test_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ); @@ -47,9 +47,9 @@ select is( pgflow._get_flow_shape('test_flow'), '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Shape should round-trip correctly' diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql index 40350a315..99ff0942f 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql @@ -7,9 +7,9 @@ select pgflow._create_flow_from_shape( 'condition_flow', '{ "steps": [ - {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip"}, - {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade"} + {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ); @@ -33,9 +33,9 @@ select is( pgflow._get_flow_shape('condition_flow'), '{ "steps": [ - {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip"}, - {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade"} + {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Shape with condition modes should round-trip correctly' @@ -47,9 +47,9 @@ select is( pgflow._get_flow_shape('condition_flow'), '{ "steps": [ - {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip"}, - {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade"} + {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ), diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql index c2396efd5..0c71d3a6d 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql @@ -7,8 +7,8 @@ select pgflow._create_flow_from_shape( 'map_flow', '{ "steps": [ - {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ); @@ -25,8 +25,8 @@ select is( pgflow._get_flow_shape('map_flow'), '{ "steps": [ - {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Shape should round-trips correctly' diff --git a/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql b/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql index 80ca90ab6..34738ac94 100644 --- a/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql +++ b/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql @@ -15,8 +15,8 @@ select is( 'existing_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": []}, - {"slug": "second", "stepType": "single", "dependencies": ["first"]} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ) as result @@ -33,8 +33,8 @@ select is( 'existing_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": []}, - {"slug": "second", "stepType": "single", "dependencies": ["first"]} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ) as result diff --git a/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql b/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql index 41792e00a..840af07c0 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql @@ -13,9 +13,9 @@ select is( pgflow._get_flow_shape('test_flow'), '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Should return correct shape for simple sequential flow' diff --git a/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql b/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql index efcdad088..67c26e206 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql @@ -20,8 +20,8 @@ select is( pgflow._get_flow_shape('map_flow'), '{ "steps": [ - {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Should correctly identify map step type' diff --git a/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql b/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql index 0a838016d..099275036 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql @@ -16,10 +16,10 @@ select is( pgflow._get_flow_shape('multi_deps'), '{ "steps": [ - {"slug": "alpha", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "beta", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "gamma", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"}, - {"slug": "final", "stepType": "single", "dependencies": ["alpha", "beta", "gamma"], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "alpha", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "beta", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "gamma", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "final", "stepType": "single", "dependencies": ["alpha", "beta", "gamma"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Dependencies should be sorted alphabetically' diff --git a/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql b/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql new file mode 100644 index 000000000..481d15a1e --- /dev/null +++ b/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql @@ -0,0 +1,44 @@ +begin; +select plan(2); +select pgflow_tests.reset_db(); + +-- Setup: Create a flow with pattern conditions +select pgflow.create_flow('test_flow'); +select pgflow.add_step('test_flow', 'step_with_if', max_attempts := 1, required_input_pattern := '{"status": "active"}'::jsonb); +select pgflow.add_step('test_flow', 'step_with_ifnot', max_attempts := 1, forbidden_input_pattern := '{"type": "deleted"}'::jsonb); +select pgflow.add_step('test_flow', 'step_with_both', max_attempts := 1, required_input_pattern := '{"status": "active"}'::jsonb, forbidden_input_pattern := '{"type": "archived"}'::jsonb); + +-- Test: Get flow shape with patterns (order matches insertion order: if, ifnot, both) +select is( + pgflow._get_flow_shape('test_flow'), + '{ + "steps": [ + {"slug": "step_with_if", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "step_with_ifnot", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": true, "value": {"type": "deleted"}}}, + {"slug": "step_with_both", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": true, "value": {"type": "archived"}}} + ] + }'::jsonb, + 'Should return correct shape with pattern conditions' +); + +-- Test: Verify patterns are stored in steps table +select results_eq( + $$ + SELECT step_slug, required_input_pattern, forbidden_input_pattern + FROM pgflow.steps + WHERE flow_slug = 'test_flow' + ORDER BY step_slug + $$, + $$ + SELECT * + FROM (VALUES + ('step_with_both', '{"status": "active"}'::jsonb, '{"type": "archived"}'::jsonb), + ('step_with_if', '{"status": "active"}'::jsonb, NULL::jsonb), + ('step_with_ifnot', NULL::jsonb, '{"type": "deleted"}'::jsonb) + ) AS t(step_slug, required_input_pattern, forbidden_input_pattern) + $$, + 'Pattern columns should be correctly stored in steps table' +); + +select finish(); +rollback; diff --git a/pkgs/dsl/__tests__/runtime/condition-options.test.ts b/pkgs/dsl/__tests__/runtime/condition-options.test.ts index 445f36d43..1dcd89ea7 100644 --- a/pkgs/dsl/__tests__/runtime/condition-options.test.ts +++ b/pkgs/dsl/__tests__/runtime/condition-options.test.ts @@ -59,7 +59,7 @@ describe('Condition Options', () => { }); describe('compileFlow includes condition parameters', () => { - it('should compile condition_pattern for root step', () => { + it('should compile required_input_pattern for root step', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1', if: { enabled: true } }, () => 'result' @@ -69,7 +69,7 @@ describe('Condition Options', () => { expect(statements).toHaveLength(2); expect(statements[1]).toContain( - 'condition_pattern => \'{"enabled":true}\'' + 'required_input_pattern => \'{"enabled":true}\'' ); }); @@ -85,7 +85,7 @@ describe('Condition Options', () => { expect(statements[1]).toContain("when_unmet => 'fail'"); }); - it('should compile both condition_pattern and when_unmet together', () => { + it('should compile both required_input_pattern and when_unmet together', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1', @@ -99,7 +99,7 @@ describe('Condition Options', () => { expect(statements).toHaveLength(2); expect(statements[1]).toContain( - 'condition_pattern => \'{"active":true,"type":"premium"}\'' + 'required_input_pattern => \'{"active":true,"type":"premium"}\'' ); expect(statements[1]).toContain("when_unmet => 'skip-cascade'"); }); @@ -122,7 +122,7 @@ describe('Condition Options', () => { expect(statements[1]).toContain('max_attempts => 3'); expect(statements[1]).toContain('timeout => 60'); expect(statements[1]).toContain( - 'condition_pattern => \'{"enabled":true}\'' + 'required_input_pattern => \'{"enabled":true}\'' ); expect(statements[1]).toContain("when_unmet => 'skip'"); }); @@ -145,7 +145,7 @@ describe('Condition Options', () => { expect(statements).toHaveLength(3); expect(statements[2]).toContain("ARRAY['first']"); expect(statements[2]).toContain( - 'condition_pattern => \'{"first":{"success":true}}\'' + 'required_input_pattern => \'{"first":{"success":true}}\'' ); expect(statements[2]).toContain("when_unmet => 'skip'"); }); @@ -225,7 +225,7 @@ describe('Condition Options', () => { }); describe('compileFlow includes ifNot parameters', () => { - it('should compile condition_not_pattern for root step', () => { + it('should compile forbidden_input_pattern for root step', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1', ifNot: { role: 'admin' } }, () => 'result' @@ -235,7 +235,7 @@ describe('Condition Options', () => { expect(statements).toHaveLength(2); expect(statements[1]).toContain( - 'condition_not_pattern => \'{"role":"admin"}\'' + 'forbidden_input_pattern => \'{"role":"admin"}\'' ); }); @@ -254,10 +254,10 @@ describe('Condition Options', () => { expect(statements).toHaveLength(2); expect(statements[1]).toContain( - 'condition_pattern => \'{"active":true}\'' + 'required_input_pattern => \'{"active":true}\'' ); expect(statements[1]).toContain( - 'condition_not_pattern => \'{"suspended":true}\'' + 'forbidden_input_pattern => \'{"suspended":true}\'' ); expect(statements[1]).toContain("when_unmet => 'skip'"); }); @@ -280,7 +280,7 @@ describe('Condition Options', () => { expect(statements).toHaveLength(3); expect(statements[2]).toContain("ARRAY['first']"); expect(statements[2]).toContain( - 'condition_not_pattern => \'{"first":{"error":true}}\'' + 'forbidden_input_pattern => \'{"first":{"error":true}}\'' ); expect(statements[2]).toContain("when_unmet => 'skip'"); }); diff --git a/pkgs/dsl/__tests__/runtime/flow-shape.test.ts b/pkgs/dsl/__tests__/runtime/flow-shape.test.ts index 50e8781cf..50691a4b2 100644 --- a/pkgs/dsl/__tests__/runtime/flow-shape.test.ts +++ b/pkgs/dsl/__tests__/runtime/flow-shape.test.ts @@ -63,6 +63,8 @@ describe('extractFlowShape', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }); }); @@ -112,6 +114,8 @@ describe('extractFlowShape', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, options: { maxAttempts: 3, baseDelay: 5, @@ -135,6 +139,8 @@ describe('extractFlowShape', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }); expect('options' in shape.steps[0]).toBe(false); }); @@ -168,6 +174,8 @@ describe('extractFlowShape', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }); }); @@ -185,6 +193,8 @@ describe('extractFlowShape', () => { dependencies: ['get_items'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }); }); }); @@ -226,6 +236,8 @@ describe('extractFlowShape', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'sentiment', @@ -233,6 +245,8 @@ describe('extractFlowShape', () => { dependencies: ['website'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, options: { maxAttempts: 5, timeout: 30, @@ -244,6 +258,8 @@ describe('extractFlowShape', () => { dependencies: ['website'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'save_to_db', @@ -251,6 +267,8 @@ describe('extractFlowShape', () => { dependencies: ['sentiment', 'summary'], // sorted alphabetically whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], options: { @@ -274,6 +292,89 @@ describe('extractFlowShape', () => { 'third', ]); }); + + describe('pattern extraction', () => { + it('should extract requiredInputPattern from step with if option', () => { + const flow = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', if: { status: 'active' } }, + (flowInput) => flowInput + ); + const shape = extractFlowShape(flow); + + expect(shape.steps[0]).toEqual({ + slug: 'step1', + stepType: 'single', + dependencies: [], + whenUnmet: 'skip', + whenFailed: 'fail', + requiredInputPattern: { defined: true, value: { status: 'active' } }, + forbiddenInputPattern: { defined: false }, + }); + }); + + it('should extract forbiddenInputPattern from step with ifNot option', () => { + const flow = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', ifNot: { status: 'deleted' } }, + (flowInput) => flowInput + ); + const shape = extractFlowShape(flow); + + expect(shape.steps[0]).toEqual({ + slug: 'step1', + stepType: 'single', + dependencies: [], + whenUnmet: 'skip', + whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: true, value: { status: 'deleted' } }, + }); + }); + + it('should extract both pattern fields when both if and ifNot are set', () => { + const flow = new Flow<{ status: string; type: string }>({ + slug: 'test_flow', + }).step( + { + slug: 'step1', + if: { status: 'active' }, + ifNot: { type: 'archived' }, + }, + (flowInput) => flowInput + ); + const shape = extractFlowShape(flow); + + expect(shape.steps[0]).toEqual({ + slug: 'step1', + stepType: 'single', + dependencies: [], + whenUnmet: 'skip', + whenFailed: 'fail', + requiredInputPattern: { defined: true, value: { status: 'active' } }, + forbiddenInputPattern: { defined: true, value: { type: 'archived' } }, + }); + }); + + it('should include pattern keys with defined:false when no patterns are set', () => { + const flow = new Flow({ slug: 'test_flow' }).step( + { slug: 'step1' }, + (flowInput) => flowInput + ); + const shape = extractFlowShape(flow); + + expect(shape.steps[0]).toEqual({ + slug: 'step1', + stepType: 'single', + dependencies: [], + whenUnmet: 'skip', + whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, + }); + // Pattern keys are now always present with the wrapper format + expect('requiredInputPattern' in shape.steps[0]).toBe(true); + expect('forbiddenInputPattern' in shape.steps[0]).toBe(true); + }); + }); }); }); @@ -288,6 +389,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -321,6 +424,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -342,6 +447,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -364,6 +471,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'step_b', @@ -371,6 +480,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -382,6 +493,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'step_d', @@ -389,6 +502,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -414,6 +529,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'step_b', @@ -421,6 +538,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -432,6 +551,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'step_a', @@ -439,6 +560,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -464,6 +587,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -475,6 +600,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -497,6 +624,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -508,6 +637,8 @@ describe('compareFlowShapes', () => { dependencies: ['step0'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -528,6 +659,8 @@ describe('compareFlowShapes', () => { dependencies: ['dep1', 'dep2'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -539,6 +672,8 @@ describe('compareFlowShapes', () => { dependencies: ['dep1'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -559,6 +694,8 @@ describe('compareFlowShapes', () => { dependencies: ['old_dep'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -570,6 +707,8 @@ describe('compareFlowShapes', () => { dependencies: ['new_dep'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -628,6 +767,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -639,6 +780,8 @@ describe('compareFlowShapes', () => { dependencies: ['dep1'], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, { slug: 'step2', @@ -646,6 +789,8 @@ describe('compareFlowShapes', () => { dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], }; @@ -740,5 +885,89 @@ describe('compareFlowShapes', () => { 'Step at index 1: dependencies differ [] vs [step1]' ); }); + + describe('pattern comparison', () => { + it('should detect requiredInputPattern difference', () => { + const flowA = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', if: { status: 'active' } }, + (flowInput) => flowInput + ); + + const flowB = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', if: { status: 'pending' } }, + (flowInput) => flowInput + ); + + const shapeA = extractFlowShape(flowA); + const shapeB = extractFlowShape(flowB); + + const result = compareFlowShapes(shapeA, shapeB); + expect(result.match).toBe(false); + expect(result.differences).toContain( + 'Step at index 0: requiredInputPattern differs \'{"defined":true,"value":{"status":"active"}}\' vs \'{"defined":true,"value":{"status":"pending"}}\'' + ); + }); + + it('should detect forbiddenInputPattern difference', () => { + const flowA = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', ifNot: { status: 'deleted' } }, + (flowInput) => flowInput + ); + + const flowB = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', ifNot: { status: 'archived' } }, + (flowInput) => flowInput + ); + + const shapeA = extractFlowShape(flowA); + const shapeB = extractFlowShape(flowB); + + const result = compareFlowShapes(shapeA, shapeB); + expect(result.match).toBe(false); + expect(result.differences).toContain( + 'Step at index 0: forbiddenInputPattern differs \'{"defined":true,"value":{"status":"deleted"}}\' vs \'{"defined":true,"value":{"status":"archived"}}\'' + ); + }); + + it('should match flows with identical patterns', () => { + const createFlow = () => + new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { + slug: 'step1', + if: { status: 'active' }, + ifNot: { status: 'deleted' }, + }, + (flowInput) => flowInput + ); + + const shapeA = extractFlowShape(createFlow()); + const shapeB = extractFlowShape(createFlow()); + + const result = compareFlowShapes(shapeA, shapeB); + expect(result.match).toBe(true); + expect(result.differences).toEqual([]); + }); + + it('should detect missing requiredInputPattern', () => { + const flowA = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1' }, + (flowInput) => flowInput + ); + + const flowB = new Flow<{ status: string }>({ slug: 'test_flow' }).step( + { slug: 'step1', if: { status: 'active' } }, + (flowInput) => flowInput + ); + + const shapeA = extractFlowShape(flowA); + const shapeB = extractFlowShape(flowB); + + const result = compareFlowShapes(shapeA, shapeB); + expect(result.match).toBe(false); + expect(result.differences).toContain( + 'Step at index 0: requiredInputPattern differs \'{"defined":false}\' vs \'{"defined":true,"value":{"status":"active"}}\'' + ); + }); + }); }); }); diff --git a/pkgs/dsl/__tests__/runtime/when-failed-options.test.ts b/pkgs/dsl/__tests__/runtime/when-failed-options.test.ts index db831fe94..527bfb7bb 100644 --- a/pkgs/dsl/__tests__/runtime/when-failed-options.test.ts +++ b/pkgs/dsl/__tests__/runtime/when-failed-options.test.ts @@ -141,7 +141,7 @@ describe('retriesExhausted Options', () => { expect(statements[1]).toContain('max_attempts => 3'); expect(statements[1]).toContain('timeout => 60'); expect(statements[1]).toContain( - 'condition_pattern => \'{"enabled":true}\'' + 'required_input_pattern => \'{"enabled":true}\'' ); expect(statements[1]).toContain("when_unmet => 'skip'"); expect(statements[1]).toContain("when_failed => 'skip-cascade'"); diff --git a/pkgs/dsl/src/compile-flow.ts b/pkgs/dsl/src/compile-flow.ts index 9f197b186..1286e2203 100644 --- a/pkgs/dsl/src/compile-flow.ts +++ b/pkgs/dsl/src/compile-flow.ts @@ -67,13 +67,13 @@ function formatRuntimeOptions( if ('if' in options && options.if !== undefined) { // Serialize JSON pattern and escape for SQL const jsonStr = JSON.stringify(options.if); - parts.push(`condition_pattern => '${jsonStr}'`); + parts.push(`required_input_pattern => '${jsonStr}'`); } if ('ifNot' in options && options.ifNot !== undefined) { // Serialize JSON pattern and escape for SQL const jsonStr = JSON.stringify(options.ifNot); - parts.push(`condition_not_pattern => '${jsonStr}'`); + parts.push(`forbidden_input_pattern => '${jsonStr}'`); } if ('whenUnmet' in options && options.whenUnmet !== undefined) { diff --git a/pkgs/dsl/src/flow-shape.ts b/pkgs/dsl/src/flow-shape.ts index d0fa85911..108fc3ab6 100644 --- a/pkgs/dsl/src/flow-shape.ts +++ b/pkgs/dsl/src/flow-shape.ts @@ -1,9 +1,18 @@ -import { AnyFlow, WhenUnmetMode, RetriesExhaustedMode } from './dsl.js'; +import { AnyFlow, WhenUnmetMode, RetriesExhaustedMode, Json } from './dsl.js'; // ======================== // SHAPE TYPE DEFINITIONS // ======================== +/** + * Input pattern wrapper - explicit representation to avoid null vs JSON-null ambiguity. + * - { defined: false } means no pattern (don't check) + * - { defined: true, value: Json } means pattern is set (check against value) + */ +export type InputPattern = + | { defined: false } + | { defined: true; value: Json }; + /** * Step-level options that can be included in the shape for creation, * but are NOT compared during shape comparison (runtime tunable). @@ -32,8 +41,8 @@ export interface FlowShapeOptions { * shape comparison. Options can be tuned at runtime via SQL without * requiring recompilation. See: /deploy/tune-flow-config/ * - * `whenUnmet` and `whenFailed` ARE structural - they affect DAG execution - * semantics and must match between worker and database. + * `whenUnmet`, `whenFailed`, and pattern fields ARE structural - they affect + * DAG execution semantics and must match between worker and database. */ export interface StepShape { slug: string; @@ -41,6 +50,8 @@ export interface StepShape { dependencies: string[]; // sorted alphabetically for deterministic comparison whenUnmet: WhenUnmetMode; whenFailed: RetriesExhaustedMode; + requiredInputPattern: InputPattern; + forbiddenInputPattern: InputPattern; options?: StepShapeOptions; } @@ -115,6 +126,15 @@ export function extractFlowShape(flow: AnyFlow): FlowShape { // Condition modes are structural - they affect DAG execution semantics whenUnmet: stepDef.options.whenUnmet ?? 'skip', whenFailed: stepDef.options.retriesExhausted ?? 'fail', + // Input patterns use explicit wrapper to avoid null vs JSON-null ambiguity + requiredInputPattern: + stepDef.options.if !== undefined + ? { defined: true, value: stepDef.options.if } + : { defined: false }, + forbiddenInputPattern: + stepDef.options.ifNot !== undefined + ? { defined: true, value: stepDef.options.ifNot } + : { defined: false }, }; // Only include options if at least one is defined @@ -247,4 +267,22 @@ function compareSteps( `Step at index ${index}: whenFailed differs '${a.whenFailed}' vs '${b.whenFailed}'` ); } + + // Compare pattern fields (structural - affects DAG execution semantics) + // Uses wrapper objects: { defined: false } or { defined: true, value: Json } + const aReqPattern = JSON.stringify(a.requiredInputPattern); + const bReqPattern = JSON.stringify(b.requiredInputPattern); + if (aReqPattern !== bReqPattern) { + differences.push( + `Step at index ${index}: requiredInputPattern differs '${aReqPattern}' vs '${bReqPattern}'` + ); + } + + const aForbPattern = JSON.stringify(a.forbiddenInputPattern); + const bForbPattern = JSON.stringify(b.forbiddenInputPattern); + if (aForbPattern !== bForbPattern) { + differences.push( + `Step at index ${index}: forbiddenInputPattern differs '${aForbPattern}' vs '${bForbPattern}'` + ); + } } diff --git a/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts b/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts index ebd35bdd0..2a61052d3 100644 --- a/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts +++ b/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts @@ -307,6 +307,8 @@ Deno.test( dependencies: [], whenUnmet: 'skip', whenFailed: 'fail', + requiredInputPattern: { defined: false }, + forbiddenInputPattern: { defined: false }, }, ], };