-- Copyright (c) Microsoft Corporation. -- Licensed under the PostgreSQL License. /* */ /* This file is auto generated by pgrx. The ordering of items is not stable, it is driven by a dependency graph. */ /* */ /* */ -- src/lib.rs:146 CREATE SCHEMA IF NOT EXISTS df; /* pg_durable::df */ /* */ /* */ -- src/lib.rs:153 -- requires: -- df -- Table to store function nodes (SQL steps, THEN chains, etc.) CREATE TABLE IF NOT EXISTS df.nodes ( id VARCHAR(8) PRIMARY KEY, instance_id VARCHAR(8), node_type TEXT NOT NULL, query TEXT, result_name TEXT, left_node VARCHAR(8), right_node VARCHAR(8), status TEXT DEFAULT 'pending', result JSONB, error TEXT, submitted_by REGROLE, database TEXT, created_at TIMESTAMPTZ DEFAULT pg_catalog.now(), updated_at TIMESTAMPTZ DEFAULT pg_catalog.now() ); COMMENT ON COLUMN df.nodes.submitted_by IS 'Effective role (current_user) at df.start() time - used for connection authentication and SQL execution'; -- Table to store function instances CREATE TABLE IF NOT EXISTS df.instances ( id VARCHAR(8) PRIMARY KEY, label TEXT, root_node VARCHAR(8) NOT NULL, status TEXT DEFAULT 'pending', submitted_by REGROLE NOT NULL, database TEXT, created_at TIMESTAMPTZ DEFAULT pg_catalog.now(), updated_at TIMESTAMPTZ DEFAULT pg_catalog.now(), completed_at TIMESTAMPTZ ); COMMENT ON COLUMN df.instances.submitted_by IS 'Effective role (current_user) at df.start() time - used for connection authentication and SQL execution'; -- Index for finding pending instances CREATE INDEX IF NOT EXISTS idx_instances_status ON df.instances(status); -- Index for finding nodes by instance CREATE INDEX IF NOT EXISTS idx_nodes_instance ON df.nodes(instance_id); -- Table to store workflow variables (captured at df.start()) -- Per-user scoping: each user has their own variable namespace. CREATE TABLE IF NOT EXISTS df.vars ( name TEXT NOT NULL, value TEXT, owner REGROLE NOT NULL DEFAULT pg_catalog.quote_ident(current_user)::pg_catalog.regrole, PRIMARY KEY (owner, name) ); -- Sentinel table: the background worker writes its epoch_id here after -- initialising. If the extension is DROP-ed and re-CREATEd between -- two poll ticks the epoch row disappears, so the worker detects the -- recreation even though the extension is always "present" in pg_extension. CREATE TABLE IF NOT EXISTS df._worker_epoch ( epoch_id UUID PRIMARY KEY, started_at TIMESTAMPTZ DEFAULT pg_catalog.now(), last_seen_at TIMESTAMPTZ DEFAULT pg_catalog.now() ); ALTER TABLE df.instances ADD CONSTRAINT instances_id_format_chk -- Operators (OPERATOR(pg_catalog.)) and functions (e.g. pg_catalog.now) -- are schema-qualified throughout this install DDL so name resolution never -- depends on the session search_path -- closing the CVE-2018-1058 vector -- (a malicious schema shadowing `=`, `~`, etc.). Enforced by the pgspot CI -- gate (scripts/pgspot-gate.sh). CHECK (id OPERATOR(pg_catalog.~) '^[0-9a-f]{8}$') NOT VALID, ADD CONSTRAINT instances_root_node_format_chk CHECK (root_node OPERATOR(pg_catalog.~) '^[0-9a-f]{8}$') NOT VALID, ADD CONSTRAINT instances_status_chk CHECK (status OPERATOR(pg_catalog.=) ANY (ARRAY['pending', 'running', 'completed', 'failed', 'cancelled'])) NOT VALID, -- Supports the composite FK from df.nodes that ties node identity to the instance row. ADD CONSTRAINT instances_identity_key UNIQUE (id, submitted_by); ALTER TABLE df.nodes ADD CONSTRAINT nodes_instance_id_present_chk CHECK (instance_id IS NOT NULL) NOT VALID, ADD CONSTRAINT nodes_submitted_by_present_chk CHECK (submitted_by IS NOT NULL) NOT VALID, ADD CONSTRAINT nodes_id_format_chk CHECK (id OPERATOR(pg_catalog.~) '^[0-9a-f]{8}$') NOT VALID, ADD CONSTRAINT nodes_instance_id_format_chk CHECK (instance_id OPERATOR(pg_catalog.~) '^[0-9a-f]{8}$') NOT VALID, ADD CONSTRAINT nodes_left_node_format_chk CHECK (left_node IS NULL OR left_node OPERATOR(pg_catalog.~) '^[0-9a-f]{8}$') NOT VALID, ADD CONSTRAINT nodes_right_node_format_chk CHECK (right_node IS NULL OR right_node OPERATOR(pg_catalog.~) '^[0-9a-f]{8}$') NOT VALID, ADD CONSTRAINT nodes_node_type_chk CHECK (node_type OPERATOR(pg_catalog.=) ANY (ARRAY['SQL', 'THEN', 'IF', 'JOIN', 'LOOP', 'BREAK', 'RACE', 'SLEEP', 'WAIT_SCHEDULE', 'HTTP', 'SIGNAL'])) NOT VALID, ADD CONSTRAINT nodes_result_name_chk CHECK (result_name IS NULL OR result_name OPERATOR(pg_catalog.~) '^[A-Za-z_][A-Za-z0-9_]*$') NOT VALID, ADD CONSTRAINT nodes_status_chk CHECK (status OPERATOR(pg_catalog.=) ANY (ARRAY['pending', 'running', 'completed', 'failed'])) NOT VALID, ADD CONSTRAINT nodes_result_status_chk CHECK (result IS NULL OR status OPERATOR(pg_catalog.=) ANY (ARRAY['completed', 'failed'])) NOT VALID, ADD CONSTRAINT nodes_structure_chk CHECK ( CASE WHEN node_type OPERATOR(pg_catalog.=) ANY (ARRAY['SQL', 'SLEEP', 'WAIT_SCHEDULE', 'BREAK', 'HTTP', 'SIGNAL']) THEN left_node IS NULL AND right_node IS NULL AND query IS NOT NULL WHEN node_type OPERATOR(pg_catalog.=) 'THEN' THEN left_node IS NOT NULL AND right_node IS NOT NULL AND query IS NULL WHEN node_type OPERATOR(pg_catalog.=) 'IF' THEN left_node IS NOT NULL AND right_node IS NOT NULL AND query IS NOT NULL WHEN node_type OPERATOR(pg_catalog.=) 'LOOP' THEN left_node IS NOT NULL AND right_node IS NULL WHEN node_type OPERATOR(pg_catalog.=) 'JOIN' THEN left_node IS NOT NULL AND right_node IS NOT NULL WHEN node_type OPERATOR(pg_catalog.=) 'RACE' THEN left_node IS NOT NULL AND right_node IS NOT NULL AND query IS NULL ELSE FALSE END ) NOT VALID, ADD CONSTRAINT nodes_instance_node_key UNIQUE (instance_id, id); ALTER TABLE df.nodes ADD CONSTRAINT nodes_instance_identity_fkey FOREIGN KEY (instance_id, submitted_by) REFERENCES df.instances (id, submitted_by) DEFERRABLE INITIALLY DEFERRED NOT VALID, ADD CONSTRAINT nodes_left_node_same_instance_fkey FOREIGN KEY (instance_id, left_node) REFERENCES df.nodes (instance_id, id) DEFERRABLE INITIALLY DEFERRED NOT VALID, ADD CONSTRAINT nodes_right_node_same_instance_fkey FOREIGN KEY (instance_id, right_node) REFERENCES df.nodes (instance_id, id) DEFERRABLE INITIALLY DEFERRED NOT VALID; ALTER TABLE df.instances ADD CONSTRAINT instances_root_node_same_instance_fkey FOREIGN KEY (id, root_node) REFERENCES df.nodes (instance_id, id) DEFERRABLE INITIALLY DEFERRED NOT VALID; /* */ /* */ -- src/dsl.rs:924 -- pg_durable::dsl::cancel CREATE FUNCTION df."cancel"( "instance_id" TEXT, /* &str */ "reason" TEXT DEFAULT 'Cancelled by user' /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'cancel_wrapper'; /* */ /* */ -- src/explain.rs:36 -- pg_durable::explain::explain CREATE FUNCTION df."explain"( "input" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'explain_wrapper'; /* */ /* */ -- src/dsl.rs:48 -- pg_durable::dsl::debug_connection CREATE FUNCTION df."debug_connection"() RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'debug_connection_wrapper'; /* */ /* */ -- src/dsl.rs:973 -- pg_durable::dsl::run CREATE FUNCTION df."run"( "instance_id" TEXT DEFAULT NULL /* core::option::Option<&str> */ ) RETURNS TEXT /* alloc::string::String */ LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'run_wrapper'; /* */ /* */ -- src/dsl.rs:38 -- pg_durable::dsl::version CREATE FUNCTION df."version"() RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'version_wrapper'; /* */ /* */ -- src/dsl.rs:587 -- pg_durable::dsl::signal CREATE FUNCTION df."signal"( "instance_id" TEXT, /* &str */ "signal_name" TEXT, /* &str */ "signal_data" TEXT DEFAULT '{}' /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'signal_wrapper'; /* */ /* */ -- src/dsl.rs:369 -- pg_durable::dsl::if CREATE FUNCTION df."if"( "condition" TEXT, /* &str */ "then_branch" TEXT, /* &str */ "else_branch" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'if_fn_wrapper'; /* */ /* */ -- src/dsl.rs:414 -- pg_durable::dsl::join CREATE FUNCTION df."join"( "a" TEXT, /* &str */ "b" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'join_wrapper'; /* */ /* */ -- src/dsl.rs:1011 -- pg_durable::dsl::wait_for_completion CREATE FUNCTION df."wait_for_completion"( "instance_id" TEXT, /* &str */ "timeout_seconds" INT DEFAULT 30 /* i32 */ ) RETURNS TEXT /* core::result::Result> */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'wait_for_completion_wrapper'; /* */ /* */ -- src/monitoring.rs:275 -- pg_durable::monitoring::metrics CREATE FUNCTION df."metrics"() RETURNS TABLE ( "total_instances" bigint, /* i64 */ "running_instances" bigint, /* i64 */ "completed_instances" bigint, /* i64 */ "failed_instances" bigint, /* i64 */ "total_executions" bigint, /* i64 */ "total_events" bigint /* i64 */ ) STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'metrics_wrapper'; /* */ /* */ -- src/dsl.rs:147 -- pg_durable::dsl::getvar CREATE FUNCTION df."getvar"( "name" TEXT /* &str */ ) RETURNS TEXT /* core::option::Option */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'getvar_wrapper'; /* */ /* */ -- src/dsl.rs:124 -- pg_durable::dsl::setvar CREATE FUNCTION df."setvar"( "name" TEXT, /* &str */ "value" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'setvar_wrapper'; /* */ /* */ -- src/monitoring.rs:323 -- pg_durable::monitoring::instance_nodes CREATE FUNCTION df."instance_nodes"( "instance_id_param" TEXT, /* &str */ "last_n_executions" INT DEFAULT 5 /* i32 */ ) RETURNS TABLE ( "execution_id" bigint, /* i64 */ "node_id" TEXT, /* alloc::string::String */ "node_type" TEXT, /* alloc::string::String */ "query" TEXT, /* core::option::Option */ "result_name" TEXT, /* core::option::Option */ "left_node" TEXT, /* core::option::Option */ "right_node" TEXT, /* core::option::Option */ "status" TEXT, /* core::option::Option */ "result" TEXT, /* core::option::Option */ "updated_at" timestamp with time zone /* core::option::Option */ ) STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'instance_nodes_wrapper'; /* */ /* */ -- src/types.rs:129 -- pg_durable::types::target_database CREATE FUNCTION df."target_database"() RETURNS TEXT /* alloc::string::String */ IMMUTABLE STRICT PARALLEL SAFE LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'target_database_wrapper'; /* */ /* */ -- src/lib.rs:574 -- requires: -- df -- target_database -- Validate that CREATE EXTENSION is run in the correct database -- The background worker connects to one specific database (determined by -- the pg_durable.database GUC, defaults to "postgres"). -- The extension must be created in that database for workflows to execute. DO $$ DECLARE current_db TEXT; target_db TEXT; BEGIN -- Get the current database SELECT pg_catalog.current_database() INTO current_db; -- Get the target database that the background worker will connect to SELECT df.target_database() INTO target_db; IF current_db OPERATOR(pg_catalog.<>) target_db THEN RAISE EXCEPTION 'pg_durable extension must be created in database "%" (currently in "%"). The background worker only processes functions in the database specified by the pg_durable.database GUC (defaults to "postgres").', target_db, current_db USING HINT = 'Connect to the correct database and run: CREATE EXTENSION pg_durable;'; END IF; END $$; /* */ /* */ -- src/lib.rs:621 -- requires: -- validate_database -- The duroxide provider schema is created here so the extension owns it. -- No IF NOT EXISTS: fails loudly if a duroxide schema already exists, -- preventing adoption of a potentially attacker-crafted schema. -- The background worker populates this schema at startup via ApplyAll. CREATE SCHEMA duroxide; /* */ /* */ -- src/dsl.rs:181 -- pg_durable::dsl::clearvars CREATE FUNCTION df."clearvars"() RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'clearvars_wrapper'; /* */ /* */ -- src/dsl.rs:452 -- pg_durable::dsl::race CREATE FUNCTION df."race"( "a" TEXT, /* &str */ "b" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'race_wrapper'; /* */ /* */ -- src/dsl.rs:249 -- pg_durable::dsl::sleep CREATE FUNCTION df."sleep"( "seconds" bigint /* i64 */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'sleep_wrapper'; /* */ /* */ -- src/dsl.rs:237 -- pg_durable::dsl::as CREATE FUNCTION df."as"( "fut" TEXT, /* &str */ "name" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'as_named_wrapper'; /* */ /* */ -- src/monitoring.rs:116 -- pg_durable::monitoring::instance_info CREATE FUNCTION df."instance_info"( "instance_id" TEXT /* &str */ ) RETURNS TABLE ( "instance_id" TEXT, /* alloc::string::String */ "label" TEXT, /* core::option::Option */ "function_name" TEXT, /* alloc::string::String */ "function_version" TEXT, /* alloc::string::String */ "current_execution_id" bigint, /* i64 */ "status" TEXT, /* alloc::string::String */ "output" TEXT /* core::option::Option */ ) STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'instance_info_wrapper'; /* */ /* */ -- src/dsl.rs:161 -- pg_durable::dsl::unsetvar CREATE FUNCTION df."unsetvar"( "name" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'unsetvar_wrapper'; /* */ /* */ -- src/dsl.rs:206 -- pg_durable::dsl::sql CREATE FUNCTION df."sql"( "query" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'sql_wrapper'; /* */ /* */ -- src/dsl.rs:219 -- pg_durable::dsl::seq CREATE FUNCTION df."seq"( "a" TEXT, /* &str */ "b" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'then_fn_wrapper'; /* */ /* */ -- src/dsl.rs:430 -- pg_durable::dsl::join3 CREATE FUNCTION df."join3"( "a" TEXT, /* &str */ "b" TEXT, /* &str */ "c" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'join3_wrapper'; /* */ /* */ -- src/dsl.rs:392 -- pg_durable::dsl::if_rows CREATE FUNCTION df."if_rows"( "result_name" TEXT, /* &str */ "then_branch" TEXT, /* &str */ "else_branch" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'if_rows_fn_wrapper'; /* */ /* */ -- src/dsl.rs:265 -- pg_durable::dsl::wait_for_schedule CREATE FUNCTION df."wait_for_schedule"( "cron_expr" TEXT /* &str */ ) RETURNS TEXT /* alloc::string::String */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'wait_for_schedule_wrapper'; /* */ /* */ -- src/dsl.rs:983 -- pg_durable::dsl::result CREATE FUNCTION df."result"( "instance_id" TEXT /* &str */ ) RETURNS TEXT /* core::option::Option */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'result_wrapper'; /* */ /* */ -- src/dsl.rs:353 -- pg_durable::dsl::break CREATE FUNCTION df."break"( "value" TEXT DEFAULT NULL /* core::option::Option<&str> */ ) RETURNS TEXT /* alloc::string::String */ LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'break_fn_wrapper'; /* */ /* */ -- src/dsl.rs:553 -- pg_durable::dsl::wait_for_signal CREATE FUNCTION df."wait_for_signal"( "name" TEXT, /* &str */ "timeout_seconds" INT DEFAULT NULL /* core::option::Option */ ) RETURNS TEXT /* alloc::string::String */ LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'wait_for_signal_wrapper'; /* */ /* */ -- src/dsl.rs:312 -- pg_durable::dsl::loop CREATE FUNCTION df."loop"( "body" TEXT, /* &str */ "condition" TEXT DEFAULT NULL /* core::option::Option<&str> */ ) RETURNS TEXT /* alloc::string::String */ LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'loop_fn_wrapper'; /* */ /* */ -- src/lib.rs:652 -- requires: -- dsl::then_fn -- dsl::as_named -- dsl::join -- dsl::race -- dsl::if_fn -- dsl::loop_fn -- Operator ~> for sequencing: a ~> b means "run a, then run b" CREATE OPERATOR ~> ( FUNCTION = df.seq, LEFTARG = text, RIGHTARG = text ); -- Operator |=> for naming: fut |=> 'name' means "name this result as $name" CREATE OR REPLACE FUNCTION df.as_op(fut text, name text) RETURNS text AS $$ SELECT df.as(fut, name); $$ LANGUAGE SQL IMMUTABLE SET search_path = pg_catalog, df, pg_temp; CREATE OPERATOR |=> ( FUNCTION = df.as_op, LEFTARG = text, RIGHTARG = text ); -- Operator & for parallel join: a & b means "run a and b in parallel, wait for both" CREATE OPERATOR & ( FUNCTION = df.join, LEFTARG = text, RIGHTARG = text ); -- Operator | for race: a | b means "run a and b in parallel, first wins" CREATE OPERATOR | ( FUNCTION = df.race, LEFTARG = text, RIGHTARG = text ); -- Operators ?> and !> for if-then-else: cond ?> then_branch !> else_branch -- We need helper functions to build the if node incrementally -- Helper: cond ?> then creates a partial if (stores condition and then branch) CREATE OR REPLACE FUNCTION df.if_then_op(condition text, then_branch text) RETURNS text AS $$ DECLARE cond_fut jsonb; then_fut jsonb; result_obj jsonb; BEGIN -- Ensure both are durofuts cond_fut := df.ensure_durofut(condition)::jsonb; then_fut := df.ensure_durofut(then_branch)::jsonb; -- Return a special marker object for the partial if result_obj := jsonb_build_object( '_partial_if', true, 'condition', cond_fut, 'then_branch', then_fut ); RETURN result_obj::text; END; $$ LANGUAGE plpgsql IMMUTABLE SET search_path = pg_catalog, df, pg_temp; -- Helper: partial_if !> else completes the if node CREATE OR REPLACE FUNCTION df.if_else_op(partial_if text, else_branch text) RETURNS text AS $$ DECLARE partial jsonb; else_fut text; cond_text text; then_text text; BEGIN partial := partial_if::jsonb; -- Check if it's a partial if IF partial->>'_partial_if' IS NULL THEN RAISE EXCEPTION 'Invalid if-then-else: left side of !> must be a ?> expression'; END IF; cond_text := partial->'condition'::text; then_text := partial->'then_branch'::text; else_fut := df.ensure_durofut(else_branch); -- Now call the real df.if function RETURN df.if(cond_text, then_text, else_fut); END; $$ LANGUAGE plpgsql IMMUTABLE SET search_path = pg_catalog, df, pg_temp; -- Helper to ensure a value is a durofut (returns JSON string) -- Rejects JSON with unknown node_type values. -- NOTE: The valid node type list here must be kept in sync with -- VALID_NODE_TYPES in src/types.rs (the Rust constant is the canonical source). CREATE OR REPLACE FUNCTION df.ensure_durofut(val text) RETURNS text AS $$ DECLARE node_type_val text; BEGIN -- Try to parse as JSON to check if it's already a durofut BEGIN node_type_val := (val::jsonb)->>'node_type'; IF node_type_val IS NOT NULL THEN -- Has a node_type - validate it IF node_type_val NOT IN ('SQL', 'THEN', 'IF', 'JOIN', 'LOOP', 'BREAK', 'RACE', 'SLEEP', 'WAIT_SCHEDULE', 'HTTP', 'SIGNAL') THEN RAISE EXCEPTION 'Unknown node_type ''%''. Valid types: SQL, THEN, IF, JOIN, LOOP, BREAK, RACE, SLEEP, WAIT_SCHEDULE, HTTP, SIGNAL', node_type_val; END IF; RETURN val; END IF; EXCEPTION WHEN invalid_text_representation THEN -- Not valid JSON, treat as SQL NULL; WHEN raise_exception THEN -- Re-raise our validation error RAISE; WHEN OTHERS THEN -- Not valid JSON, treat as SQL NULL; END; -- It's plain SQL, wrap it RETURN df.sql(val); END; $$ LANGUAGE plpgsql IMMUTABLE SET search_path = pg_catalog, df, pg_temp; CREATE OPERATOR ?> ( FUNCTION = df.if_then_op, LEFTARG = text, RIGHTARG = text ); CREATE OPERATOR !> ( FUNCTION = df.if_else_op, LEFTARG = text, RIGHTARG = text ); -- Operator @> for loop: @> body means "repeat body forever" -- This is a PREFIX operator with lowest precedence CREATE OR REPLACE FUNCTION df.loop_prefix_op(body text) RETURNS text AS $$ SELECT df.loop(body); $$ LANGUAGE SQL IMMUTABLE SET search_path = pg_catalog, df, pg_temp; CREATE OPERATOR @> ( FUNCTION = df.loop_prefix_op, RIGHTARG = text ); /* */ /* */ -- src/monitoring.rs:195 -- pg_durable::monitoring::instance_executions CREATE FUNCTION df."instance_executions"( "instance_id" TEXT, /* &str */ "limit_count" INT DEFAULT 5 /* i32 */ ) RETURNS TABLE ( "execution_id" bigint, /* i64 */ "status" TEXT, /* alloc::string::String */ "event_count" bigint, /* i64 */ "duration_ms" bigint, /* i64 */ "output" TEXT /* core::option::Option */ ) STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'instance_executions_wrapper'; /* */ /* */ -- src/monitoring.rs:18 -- pg_durable::monitoring::list_instances CREATE FUNCTION df."list_instances"( "status_filter" TEXT DEFAULT NULL, /* core::option::Option<&str> */ "limit_count" INT DEFAULT 100 /* i32 */ ) RETURNS TABLE ( "instance_id" TEXT, /* alloc::string::String */ "label" TEXT, /* core::option::Option */ "function_name" TEXT, /* alloc::string::String */ "status" TEXT, /* alloc::string::String */ "execution_count" bigint, /* i64 */ "output" TEXT /* core::option::Option */ ) LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'list_instances_wrapper'; /* */ /* */ -- src/dsl.rs:962 -- pg_durable::dsl::status CREATE FUNCTION df."status"( "instance_id" TEXT /* &str */ ) RETURNS TEXT /* core::option::Option */ STRICT LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'status_wrapper'; /* */ /* */ -- src/dsl.rs:478 -- pg_durable::dsl::http CREATE FUNCTION df."http"( "url" TEXT, /* &str */ "method" TEXT DEFAULT 'POST', /* &str */ "body" TEXT DEFAULT NULL, /* core::option::Option<&str> */ "headers" jsonb DEFAULT NULL, /* core::option::Option */ "timeout_seconds" INT DEFAULT 30 /* i32 */ ) RETURNS TEXT /* alloc::string::String */ LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'http_wrapper'; /* */ /* */ -- src/lib.rs:303 -- requires: -- create_tables -- dsl::http -- Enable RLS on df.instances (no FORCE — superuser/table-owner bypasses RLS) ALTER TABLE df.instances ENABLE ROW LEVEL SECURITY; CREATE POLICY instances_user_isolation ON df.instances FOR ALL USING (submitted_by OPERATOR(pg_catalog.=) pg_catalog.quote_ident(current_user)::pg_catalog.regrole) WITH CHECK (submitted_by OPERATOR(pg_catalog.=) pg_catalog.quote_ident(current_user)::pg_catalog.regrole); -- Enable RLS on df.nodes ALTER TABLE df.nodes ENABLE ROW LEVEL SECURITY; CREATE POLICY nodes_user_isolation ON df.nodes FOR ALL USING (submitted_by OPERATOR(pg_catalog.=) pg_catalog.quote_ident(current_user)::pg_catalog.regrole) WITH CHECK (submitted_by OPERATOR(pg_catalog.=) pg_catalog.quote_ident(current_user)::pg_catalog.regrole); -- Enable RLS on df.vars (per-user variable isolation) ALTER TABLE df.vars ENABLE ROW LEVEL SECURITY; CREATE POLICY vars_user_isolation ON df.vars FOR ALL USING (owner OPERATOR(pg_catalog.=) pg_catalog.quote_ident(current_user)::pg_catalog.regrole) WITH CHECK (owner OPERATOR(pg_catalog.=) pg_catalog.quote_ident(current_user)::pg_catalog.regrole); -- No automatic PUBLIC grants. Admins must explicitly grant privileges -- to application roles after CREATE EXTENSION. -- Use df.grant_usage('role_name') (recommended) or see USER_GUIDE.md -- "Privilege Grants" for the equivalent manual GRANT statements. -- Helper: grant all required df privileges to a role in one call. -- Authorization model: This function is SECURITY INVOKER and EXECUTE is -- revoked from PUBLIC, so only roles explicitly granted EXECUTE (via -- with_grant => true or a direct superuser GRANT) can call it. The inner -- GRANT statements run as the caller, so the caller must also hold the -- underlying privileges WITH GRANT OPTION (automatically true for -- superusers; for delegated admins, df.grant_usage(..., with_grant => true) -- grants all privileges WITH GRANT OPTION). -- -- This function is purely additive — it never issues REVOKE. To downgrade -- a role's privileges, call df.revoke_usage() first, then df.grant_usage() -- with the desired options. -- -- include_http controls whether the role is granted EXECUTE on df.http(). -- Default is false: HTTP access is opt-in because df.http() makes outbound -- network requests and requires explicit administrator approval. -- -- with_grant controls whether the target role receives privileges WITH GRANT -- OPTION and can itself call df.grant_usage() / df.revoke_usage() to manage -- other roles' access. Authorization is enforced by PostgreSQL's native -- WITH GRANT OPTION mechanism: the caller must hold each underlying -- privilege WITH GRANT OPTION, which is automatically true for superusers -- and for delegated admins granted via with_grant => true. -- -- MAINTENANCE: when adding a new df.* function, add it to the func_sigs -- array below. Functions NOT in this list are deny-by-default. CREATE OR REPLACE FUNCTION df.grant_usage( p_role TEXT, include_http boolean DEFAULT false, with_grant boolean DEFAULT false ) RETURNS VOID LANGUAGE plpgsql SET search_path = pg_catalog, df, pg_temp AS $fn$ DECLARE grant_opt TEXT := ''; func_sig TEXT; -- Explicit list of df.* functions to grant. Sensitive functions -- (df.http, df.grant_usage, df.revoke_usage) are excluded from this -- list and granted conditionally below. func_sigs TEXT[] := ARRAY[ -- DSL functions 'df.sql(text)', 'df.seq(text, text)', 'df.as(text, text)', 'df.sleep(bigint)', 'df.wait_for_schedule(text)', 'df.loop(text, text)', 'df.break(text)', 'df.if(text, text, text)', 'df.if_rows(text, text, text)', 'df.join(text, text)', 'df.join3(text, text, text)', 'df.race(text, text)', 'df.wait_for_signal(text, integer)', 'df.signal(text, text, text)', 'df.start(text, text, text)', 'df.setvar(text, text)', 'df.getvar(text)', 'df.unsetvar(text)', 'df.clearvars()', -- Monitoring functions 'df.status(text)', 'df.result(text)', 'df.cancel(text, text)', 'df.wait_for_completion(text, integer)', 'df.run(text)', 'df.list_instances(text, integer)', 'df.instance_info(text)', 'df.instance_nodes(text, integer)', 'df.instance_executions(text, integer)', 'df.metrics()', -- Internal helpers (operators, version, etc.) 'df.as_op(text, text)', 'df.if_then_op(text, text)', 'df.if_else_op(text, text)', 'df.ensure_durofut(text)', 'df.loop_prefix_op(text)', 'df.version()', 'df.debug_connection()', 'df.explain(text)', 'df.target_database()' ]; BEGIN -- Validate the role exists IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = p_role) THEN RAISE EXCEPTION 'role "%" does not exist', p_role; END IF; IF with_grant THEN grant_opt := ' WITH GRANT OPTION'; END IF; -- Schema access EXECUTE format('GRANT USAGE ON SCHEMA df TO %I', p_role) || grant_opt; -- Grant EXECUTE on each standard function explicitly. FOREACH func_sig IN ARRAY func_sigs LOOP EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO %I', func_sig, p_role) || grant_opt; END LOOP; -- df.http() — opt-in because it makes outbound network requests. IF include_http THEN EXECUTE format('GRANT EXECUTE ON FUNCTION df.http(text, text, text, jsonb, integer) TO %I', p_role) || grant_opt; END IF; -- Admin helpers — only for delegated administrators. IF with_grant THEN EXECUTE format('GRANT EXECUTE ON FUNCTION df.grant_usage(text, boolean, boolean) TO %I', p_role) || grant_opt; EXECUTE format('GRANT EXECUTE ON FUNCTION df.revoke_usage(text) TO %I', p_role) || grant_opt; END IF; -- Table privileges EXECUTE format('GRANT SELECT ON df.instances TO %I', p_role) || grant_opt; EXECUTE format('GRANT UPDATE (status, updated_at) ON df.instances TO %I', p_role) || grant_opt; EXECUTE format('GRANT SELECT ON df.nodes TO %I', p_role) || grant_opt; EXECUTE format('GRANT INSERT (id, label, root_node, submitted_by, database) ON df.instances TO %I', p_role) || grant_opt; EXECUTE format('GRANT INSERT (id, instance_id, node_type, query, result_name, left_node, right_node, submitted_by, database) ON df.nodes TO %I', p_role) || grant_opt; EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON df.vars TO %I', p_role) || grant_opt; RAISE NOTICE 'pg_durable: granted df usage privileges to "%"', p_role; END; $fn$; -- Revoke all df privileges previously granted by df.grant_usage(). -- Authorization: same model as df.grant_usage() — EXECUTE is revoked from -- PUBLIC, caller must hold the underlying privileges to revoke them. -- Safety: format(%I) quotes identifiers to prevent SQL injection. Additionally, -- this is SECURITY INVOKER so it cannot escalate beyond the caller's privileges. CREATE OR REPLACE FUNCTION df.revoke_usage(p_role TEXT) RETURNS VOID LANGUAGE plpgsql SET search_path = pg_catalog, df, pg_temp AS $fn$ DECLARE func_oid oid; BEGIN -- Validate the role exists IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = p_role) THEN RAISE EXCEPTION 'role "%" does not exist', p_role; END IF; -- Prevent accidentally revoking your own access. pg_has_role checks -- both direct identity (current_user = p_role) and inherited membership -- (current_user is a member of p_role), so revoking a parent role that -- the caller depends on is also caught. -- Superusers are exempt: pg_has_role returns true for all roles when the -- caller is a superuser, and superusers can always re-grant themselves. IF NOT EXISTS ( SELECT 1 FROM pg_roles WHERE rolname = current_user AND rolsuper ) AND pg_has_role(current_user, p_role, 'MEMBER') THEN RAISE EXCEPTION 'cannot revoke df privileges from "%" because the current role ("%") is a member of it — use a different administrator', p_role, current_user; END IF; -- CASCADE: if the target role granted sub-grants (via WITH GRANT OPTION), -- CASCADE ensures those dependent privileges are also revoked. -- Column-level revokes must match the column-level grants from grant_usage(). EXECUTE format('REVOKE SELECT, INSERT, UPDATE, DELETE ON df.vars FROM %I CASCADE', p_role); EXECUTE format('REVOKE INSERT (id, instance_id, node_type, query, result_name, left_node, right_node, submitted_by, database) ON df.nodes FROM %I CASCADE', p_role); EXECUTE format('REVOKE SELECT ON df.nodes FROM %I CASCADE', p_role); EXECUTE format('REVOKE INSERT (id, label, root_node, submitted_by, database) ON df.instances FROM %I CASCADE', p_role); EXECUTE format('REVOKE UPDATE (status, updated_at) ON df.instances FROM %I CASCADE', p_role); EXECUTE format('REVOKE SELECT ON df.instances FROM %I CASCADE', p_role); -- Revoke EXECUTE per-function rather than using the blanket -- REVOKE ON ALL FUNCTIONS. A delegated admin may lack privilege on -- some functions (e.g. df.http); per-function revokes let us skip those. FOR func_oid IN SELECT p.oid FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = 'df' LOOP BEGIN EXECUTE format('REVOKE EXECUTE ON FUNCTION %s FROM %I CASCADE', func_oid::regprocedure, p_role); EXCEPTION WHEN insufficient_privilege THEN NULL; END; END LOOP; EXECUTE format('REVOKE USAGE ON SCHEMA df FROM %I CASCADE', p_role); RAISE NOTICE 'pg_durable: revoked df usage privileges granted by "%" from "%"', current_user, p_role; END; $fn$; -- Validate that the worker role is a superuser. -- The background worker must bypass RLS to manage all users' instances/nodes. -- If the worker role is not a superuser, workflows will silently fail because -- RLS will filter out rows the worker needs to read/update. DO $$ DECLARE wrole TEXT; is_super BOOLEAN; BEGIN wrole := pg_catalog.current_setting('pg_durable.worker_role', true); IF wrole IS NULL OR wrole OPERATOR(pg_catalog.=) '' THEN wrole := 'azuresu'; END IF; SELECT rolsuper INTO is_super FROM pg_catalog.pg_roles WHERE rolname OPERATOR(pg_catalog.=) wrole; IF is_super IS NULL THEN RAISE WARNING 'pg_durable: worker role "%" does not exist. The background worker will not be able to process workflows. Create the role as a superuser before using pg_durable.', wrole; ELSIF NOT is_super THEN RAISE WARNING 'pg_durable: worker role "%" is not a superuser. The background worker must be a superuser to bypass RLS and manage all users'' instances. Grant superuser or BYPASSRLS to this role.', wrole; END IF; END $$; -- df.http() carries network-access implications and must be opt-in. -- PostgreSQL grants EXECUTE to PUBLIC by default when functions are created; -- revoke that so only roles explicitly granted access (via -- df.grant_usage(role, include_http => true) or a direct GRANT) can make -- HTTP requests. REVOKE EXECUTE ON FUNCTION df.http(text, text, text, jsonb, integer) FROM PUBLIC; -- df.grant_usage() and df.revoke_usage() are admin-only helpers. -- Revoke PUBLIC's default EXECUTE privilege so that only roles explicitly -- granted access (via with_grant => true or a direct superuser GRANT) can -- manage other roles' df privileges. REVOKE EXECUTE ON FUNCTION df.grant_usage(text, boolean, boolean) FROM PUBLIC; REVOKE EXECUTE ON FUNCTION df.revoke_usage(text) FROM PUBLIC; /* */ /* */ -- src/dsl.rs:630 -- pg_durable::dsl::start CREATE FUNCTION df."start"( "fut" TEXT, /* &str */ "label" TEXT DEFAULT NULL, /* core::option::Option<&str> */ "database" TEXT DEFAULT NULL /* core::option::Option<&str> */ ) RETURNS TEXT /* alloc::string::String */ LANGUAGE c /* Rust */ AS 'MODULE_PATHNAME', 'start_wrapper'; /* */