/**
* @file
* @brief ProvSQL PL/pgSQL extension code
*
* This file contains the PL/pgSQL code of the ProvSQL extension. This
* extension requires the standard uuid-ossp extension.
*/
/**
* @brief provsql schema
*
* All types and functions introduced by ProvSQL are defined in the
* provsql schema, requiring prefixing them by provsql. or
* using PostgreSQL's search_path variable with a command such
* as \code{.sql}SET search_path TO public, provsql;\endcode
*/
CREATE SCHEMA provsql;
SET search_path TO provsql;
/**
* @brief Provenance circuit gate types
*
* Each gate in the provenance circuit has a type that determines
* its semantics during semiring evaluation.
*/
CREATE TYPE provenance_gate AS
ENUM(
'input', -- Input (variable) gate of the circuit
'plus', -- Semiring plus
'times', -- Semiring times
'monus', -- M-Semiring monus
'project', -- Project gate (for where provenance)
'zero', -- Semiring zero
'one', -- Semiring one
'eq', -- Equijoin gate (for where provenance)
'agg', -- Aggregation operator (for aggregate provenance)
'semimod', -- Semimodule scalar multiplication (for aggregate provenance)
'cmp', -- Comparison of aggregate values (HAVING-clause provenance)
'delta', -- δ-semiring operator (see Amsterdamer, Deutch, Tannen, PODS 2011)
'value', -- Scalar value (for aggregate provenance)
'mulinput',-- Multivalued input (for Boolean provenance)
'update', -- Update operation
'rv', -- Continuous random-variable leaf
'arith', -- n-ary arithmetic gate over scalar-valued children
'mixture', -- Probabilistic mixture of two scalar RV roots with a Bernoulli weight
'assumed', -- Structural assumption marker over a single child: the
-- wrapped sub-circuit was computed under the
-- assumption named by the gate's extra label --
-- 'boolean' (e.g. the safe-query rewrite; the
-- default when the label is absent) or
-- 'absorptive' (the bounded-treewidth
-- reachability route). Transparent for
-- evaluation semirings satisfying the assumption,
-- fatal error for the rest, rendered as an
-- explicit element in PROV-XML export.
'annotation', -- Transparent single-child wrapper carrying a
-- query-level annotation string in @c extra
-- (e.g. the inversion-free tractability
-- certificate / per-input order key). Identity
-- for EVERY evaluator; its UUID folds in @c extra
-- so distinct annotations over the same child are
-- distinct gates.
'conditioned', -- Conditioning marker: two children
-- [target, evidence]. Evaluated only in the
-- measure interpretation: probability_evaluate
-- returns P(target ∧ evidence) / P(evidence); the
-- RV / agg_token evaluators return the restricted
-- distribution. For the uuid carrier it is a
-- TERMINAL gate (never a child of a semiring gate);
-- nested conditioning folds into a conjunction of
-- evidence. Refused by every general sr_* semiring
-- (normalization is not a semiring operation).
'mobius', -- Signed Möbius combination over child islands: one
-- integer coefficient per child in @c extra (the
-- gate_arith precedent), probability_evaluate returns
-- Σ_i coeff_i · P(child_i). The one new primitive of
-- the safe-UCQ Möbius-inversion route, evaluated only
-- in the measure interpretation; refused by every
-- general sr_* semiring (a signed combination is not a
-- semiring operation).
'case', -- N-ary guarded selection over scalar (RV) children:
-- wires [guard_1, value_1, ..., guard_k, value_k,
-- default], first-match semantics (the value of the
-- first guard event that holds, else the default).
-- Backs a CASE expression over random variables (and
-- abs / clamp / ReLU as sugar). RV/measure-carrier;
-- refused by every general sr_* semiring.
'observe', -- Latent-variable observation (likelihood-weighting
-- evidence): one wire -> an observed bare gate_rv
-- leaf, the datum in extra. Contributes a
-- continuous density factor (the leaf's pdf at the
-- datum) rather than a Boolean truth value,
-- composing into an evidence circuit by gate_times
-- exactly like a conditioning event. Evaluated only
-- by the importance-sampling weight walk; refused by
-- every Boolean / semiring evaluator.
'fixvar', -- Unknown of an equation system: a leaf standing for
-- the solution of one equation, meaningful only
-- inside the 'fixsystem' gate that lists it.
'fixsystem', -- Linear equation system x_i = f_i(x) of a recursive
-- query, one equation per derived tuple: wires
-- [x_1, ..., x_n, f_1, ..., f_n]. Read through
-- 'fixpoint'.
'fixpoint' -- Component of the least solution of an equation
-- system: one wire to a 'fixsystem', the 1-based
-- index in info1; solved per semiring at evaluation.
);
/** @defgroup gate_manipulation Circuit gate manipulation
* Low-level functions for creating and querying provenance circuit gates.
* @{
*/
/**
* @brief Create a new gate in the provenance circuit
*
* @param token UUID identifying the new gate
* @param type gate type (see provenance_gate)
* @param children optional array of child gate UUIDs
*/
CREATE OR REPLACE FUNCTION create_gate(
token UUID,
type provenance_gate,
children uuid[] DEFAULT NULL)
RETURNS void AS
'provsql','create_gate' LANGUAGE C PARALLEL SAFE;
/**
* @brief Create a gate together with what it records (internal)
*
* One unanswered message to the worker, where create_gate() followed by
* set_infos() or set_extra() would wait for the worker's verdict on each.
* For the callers that know what the gate records when they create it: a
* gate whose address hashes its infos and text, or a fresh token nobody else
* can have written to. Infos and text are still written once: should
* something else be recorded at that address, the first value stays and the
* worker logs it.
*
* @param token UUID identifying the new gate
* @param type gate type (see provenance_gate)
* @param children optional array of child gate UUIDs
* The infos are used in different ways by different gate types: for
* mulinput, info1 is the value of the multivalued variable; for eq, info1
* and info2 are the attribute positions equated; for agg, info1 is the OID
* of the aggregate function and info2 that of its result type; for cmp,
* info1 is the OID of the comparison operator; for arith, info1 is the
* operator. The text is used in different ways too: for project,
* a text-encoded array of two-element arrays mapping input attributes to
* output attributes; for value and agg, the text-encoded scalar value; for
* rv, the distribution family and its parameters; for assumed, the
* assumption kind; for annotation, inert metadata read only by the code
* that placed it.
*
* @param info1 first info, or NULL
* @param info2 second info, or NULL
* @param extra text of the gate, or NULL
*/
CREATE OR REPLACE FUNCTION create_gate(
token UUID,
type provenance_gate,
children uuid[],
info1 INT,
info2 INT,
extra TEXT)
RETURNS void AS
'provsql','create_gate' LANGUAGE C PARALLEL SAFE;
/**
* @brief Return the gate type of a provenance token
*
* Returns @c 'input' for any token not yet materialized in the circuit,
* since input is the default semantics of an unmaterialized provenance token.
*/
CREATE OR REPLACE FUNCTION get_gate_type(
token UUID)
RETURNS provenance_gate AS
'provsql','get_gate_type' LANGUAGE C IMMUTABLE PARALLEL SAFE;
/** @brief Return the children of a provenance gate */
CREATE OR REPLACE FUNCTION get_children(
token UUID)
RETURNS uuid[] AS
'provsql','get_children' LANGUAGE C IMMUTABLE PARALLEL SAFE;
/**
* @brief Write the probability of an input gate, once
*
* A probability is a fact appended to the circuit, like the gate it
* belongs to: it can be written on a gate that has none, written again
* with the identical value (so setup scripts and notebook cells stay
* re-runnable), and is otherwise refused. A write made by a
* transaction that rolls back is cleared, leaving the circuit as the
* transaction found it.
*
* To give a tuple a *different* probability, mint a fresh input gate
* with @c provsql.replace_input and store it in the row's @c provsql
* column. The base table's token column is the place of truth, the
* same rule the data-modification triggers already follow, and the
* circuit keeps the old gate and everything derived from it.
*
* @param token UUID of the input gate
* @param p probability value in [0,1]
*/
CREATE OR REPLACE FUNCTION set_prob(
token UUID, p DOUBLE PRECISION)
RETURNS void AS
'provsql','set_prob' LANGUAGE C PARALLEL RESTRICTED;
/**
* @brief Report whether a probability has been written on a gate
*
* @c get_prob returns the value an evaluation would use, so it answers
* 1 both for a gate written as certain and for one nobody has given a
* probability. This distinguishes them, which is what a client needs
* in order to offer "set" on the one and "replace" on the other.
*/
CREATE OR REPLACE FUNCTION probability_is_set(token UUID)
RETURNS BOOLEAN AS
'provsql','probability_is_set' LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief Declare a just-minted leaf gate a replacement for a tracked row
*
* Internal. @c provsql.replace_input and @c provsql.replace_block call
* this so the @c UPDATE that stores the new token does not look, to
* @c provenance_guard, like a user pasting in an arbitrary UUID -- which
* would flip the table to @c OPAQUE. The declaration lasts for the
* transaction.
*/
CREATE OR REPLACE FUNCTION note_fresh_leaf(token UUID)
RETURNS void AS
'provsql','note_fresh_leaf' LANGUAGE C;
/** @brief Whether @c token was minted as a replacement leaf by this
* transaction (see @c note_fresh_leaf). Internal. */
CREATE OR REPLACE FUNCTION is_fresh_leaf(token UUID)
RETURNS BOOLEAN AS
'provsql','is_fresh_leaf' LANGUAGE C VOLATILE;
/**
* @brief Mint a replacement input gate carrying a different probability
*
* Probabilities are written once, so a tuple's probability is changed
* the way its data is: by rewriting the row. This mints a fresh input
* gate with probability @c p and returns it, for the row's @c provsql
* column to carry:
*
* @code
* UPDATE s SET provsql = provsql.replace_input(provsql, 0.3) WHERE id = 42;
* @endcode
*
* The update is an ordinary heap write, so it has MVCC isolation, WAL,
* replication and @c pg_dump behind it, and the new gate and its
* probability roll back with it. The old gate and everything derived
* from it stay as they were: re-running a query over the base table
* builds new derived gates over the new token and so sees the new
* probability, while a table materialised earlier keeps the old tokens
* and the old probability -- a derived table reflects the base tables
* as they were when it was built, the same rule a @c DELETE under
* @c provsql.update_provenance already follows.
*
* @param old the token being replaced; must name an input gate
* @param p the new probability, in [0,1]
*/
CREATE OR REPLACE FUNCTION replace_input(old UUID, p DOUBLE PRECISION)
RETURNS UUID AS
$$
DECLARE
t UUID;
tp provsql.provenance_gate;
BEGIN
IF old IS NULL OR p IS NULL THEN
RAISE EXCEPTION 'replace_input: neither argument may be NULL';
END IF;
tp := provsql.get_gate_type(old);
IF tp = 'mulinput' THEN
RAISE EXCEPTION 'replace_input: % belongs to a repair_key block', old
USING HINT = 'A block''s values share one key gate and their masses '
'are meaningful together, so they are replaced together: '
'use provsql.replace_block().';
ELSIF tp = 'update' THEN
RAISE EXCEPTION 'replace_input: % is an update gate', old
USING HINT = 'Use provsql.replace_update() to give a recorded data '
'modification a different probability.';
ELSIF tp <> 'input' THEN
RAISE EXCEPTION 'replace_input: % is a gate of type %, not an input', old, tp
USING HINT = 'Only a leaf carries a probability of its own; a derived '
'gate''s is computed from its leaves.';
END IF;
t := public.uuid_generate_v4();
PERFORM provsql.create_gate(t, 'input');
PERFORM provsql.set_prob(t, p);
PERFORM provsql.note_fresh_leaf(t);
RETURN t;
END
$$ LANGUAGE plpgsql;
/**
* @brief Replace a tracked row's input gate, rewriting the row
*
* The procedural form of @c replace_input: it mints the new gate and
* issues the @c UPDATE itself, which is what a client with only the
* token in hand (the Studio inspector, say) needs.
*
* @param _tbl the tracked table holding the row
* @param old the token the row carries
* @param p the new probability, in [0,1]
* @return the token the row now carries
*/
CREATE OR REPLACE FUNCTION replace_input(
_tbl regclass, old UUID, p DOUBLE PRECISION)
RETURNS UUID AS
$$
DECLARE
t UUID;
n INT;
BEGIN
t := provsql.replace_input(old, p);
EXECUTE format('UPDATE %s SET provsql = $1 WHERE provsql = $2', _tbl)
USING t, old;
GET DIAGNOSTICS n = ROW_COUNT;
IF n = 0 THEN
RAISE EXCEPTION 'replace_input: no row of % carries the token %', _tbl, old;
END IF;
RETURN t;
END
$$ LANGUAGE plpgsql;
/**
* @brief Give a @c repair_key block a new set of probabilities
*
* The block-level counterpart of @c replace_input. A block's values
* share one key gate and their masses are meaningful together, so they
* are replaced together: this mints a new key gate and one new
* @c mulinput per row of the block, writes @c probs to them in the
* block's own order (the @c within_group index @c repair_key recorded),
* and rewrites the block's rows in @p _tbl to carry the new tokens.
*
* @param _tbl the tracked table holding the block's rows
* @param old_key the block's key gate -- the shared child of its rows'
* @c mulinput tokens, as reported by
* @c "get_children(provsql)[1]"
* @param probs one probability per row of the block, in block order;
* @c NULL leaves them unwritten, so the block evaluates
* at the uniform weight again
*/
CREATE OR REPLACE FUNCTION replace_block(
_tbl regclass, old_key UUID, probs DOUBLE PRECISION[] DEFAULT NULL)
RETURNS void AS
$$
DECLARE
r RECORD;
n INT;
i INT := 0;
new_key UUID;
new_tok UUID;
was_active TEXT;
BEGIN
IF provsql.get_gate_type(old_key) <> 'input' THEN
RAISE EXCEPTION 'replace_block: % is not a block key gate', old_key;
END IF;
-- The rewriter has no business in the bookkeeping below: the tokens of
-- _tbl are what this function is here to rewrite, not provenance to
-- carry into a temporary table. Restored before returning; a failure
-- aborts the transaction, which restores it too.
was_active := coalesce(current_setting('provsql.active', true), 'on');
PERFORM set_config('provsql.active', 'off', true);
EXECUTE format(
'CREATE TEMP TABLE provsql_replace_block_tmp ON COMMIT DROP AS
SELECT t.provsql AS old_token,
NULL::uuid AS new_token,
(provsql.get_infos(t.provsql)).info1 AS ord
FROM %s t
WHERE provsql.get_gate_type(t.provsql) = ''mulinput''
AND (provsql.get_children(t.provsql))[1] = %L', _tbl, old_key);
SELECT count(*) INTO n FROM provsql_replace_block_tmp;
IF n = 0 THEN
RAISE EXCEPTION 'replace_block: no row of % belongs to block %', _tbl, old_key;
END IF;
IF probs IS NOT NULL AND array_length(probs, 1) <> n THEN
RAISE EXCEPTION 'replace_block: block % has % rows but % probabilities were given',
old_key, n, array_length(probs, 1);
END IF;
new_key := public.uuid_generate_v4();
PERFORM provsql.create_gate(new_key, 'input');
FOR r IN SELECT old_token, ord FROM provsql_replace_block_tmp ORDER BY ord LOOP
i := i + 1;
new_tok := public.uuid_generate_v4();
PERFORM provsql.create_gate(new_tok, 'mulinput', ARRAY[new_key], r.ord, n, NULL);
IF probs IS NOT NULL THEN
PERFORM provsql.set_prob(new_tok, probs[i]);
END IF;
PERFORM provsql.note_fresh_leaf(new_tok);
UPDATE provsql_replace_block_tmp SET new_token = new_tok
WHERE old_token = r.old_token;
END LOOP;
EXECUTE format(
'UPDATE %s t SET provsql = b.new_token
FROM provsql_replace_block_tmp b WHERE t.provsql = b.old_token', _tbl);
DROP TABLE provsql_replace_block_tmp;
PERFORM set_config('provsql.active', was_active, true);
END
$$ LANGUAGE plpgsql;
/** @brief Get the probability associated with an input gate */
CREATE OR REPLACE FUNCTION get_prob(
token UUID)
RETURNS DOUBLE PRECISION AS
'provsql','get_prob' LANGUAGE C STABLE PARALLEL SAFE;
/** @brief Get the integer info values associated with a circuit gate */
CREATE OR REPLACE FUNCTION get_infos(
token UUID, OUT info1 INT, OUT info2 INT)
RETURNS record AS
'provsql','get_infos' LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief Wrap @p token in a fresh @c gate_assumed carrying @p assumption
* as its label, and return the wrapper's UUID.
*
* Public primitive callable from any rewrite or driver that needs to
* flag a sub-circuit as sound only under an evaluation assumption:
*
* - @c 'boolean' -- the sub-circuit only preserves the Boolean function
* of the lineage (e.g. the safe-query rewrite collapses derivation
* multiplicities); transparent for semirings admitting a homomorphism
* from Boolean functions.
* - @c 'absorptive' -- the sub-circuit was truncated at the absorptive
* value fixpoint (cyclic recursive query); transparent for absorptive
* semirings (probability, boolean, min-plus over nonnegative
* costs...), fatal for the rest (counting, why-provenance).
*
* Incompatible evaluators raise a @c CircuitException. Always kept as
* an explicit node in PROV-XML export.
*
* The wrapper UUID is content-derived via @c uuid_generate_v5 on the
* assumption and the child, so identical children always wrap to the
* same outer UUID per assumption. No-op (returns NULL) on a NULL
* input.
*
* Implemented in C (gate_builders.c): the gate is created together
* with what it records, in one unanswered message.
*/
CREATE OR REPLACE FUNCTION provenance_assume(token UUID, assumption TEXT)
RETURNS UUID AS
'provsql','provenance_assume' LANGUAGE C COST 100 PARALLEL SAFE;
/**
* @brief Wrap @p token in a Boolean-assumption marker (compatibility
* name; see @c provenance_assume).
*
* This is the entry point the safe-query (read-once) rewriter calls on
* every per-row root it produces, so the wrapper additionally carries
* @c PROVSQL_ROUTE_SQ_REWRITE in @c info1: the assumption kind alone does
* not identify the route (@c provenance_assume(t, 'boolean') is public),
* and the probability dispatcher reads the tag back to report
* @c sq-rewrite rather than the generic @c independent. Build an untagged
* Boolean-assumption wrapper with @c provenance_assume directly.
*
* Implemented in C (gate_builders.c): the gate is created together
* with what it records, in one unanswered message.
*/
CREATE OR REPLACE FUNCTION assume_boolean(token UUID) RETURNS UUID AS
'provsql','assume_boolean' LANGUAGE C COST 100 PARALLEL SAFE;
/**
* @brief Wrap @p token in a fresh transparent @c gate_annotation carrying
* @p extra, and return the wrapper's UUID.
*
* Unlike every other gate, the annotation wrapper's UUID folds in @p extra
* (not just the child): @c uuid_generate_v5 over @c concat('annotation',
* token, extra). This is deliberate -- two annotations over the same child
* with different @p extra must be distinct gates (e.g. the same input tuple
* carrying different per-occurrence order keys, or two queries attaching
* different certificates to a shared root). The wrapper is transparent
* (identity) for EVERY evaluator; @p extra is inert metadata read only by the
* code that placed it. No-op (returns NULL) on a NULL input.
*
* Implemented in C (gate_builders.c), with the declared cost of the
* function it replaces, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION annotate(token UUID, extra TEXT) RETURNS UUID AS
'provsql','annotate' LANGUAGE C COST 100 PARALLEL SAFE;
/**
* @brief Peel every transparent @c gate_annotation wrapper off @p token,
* returning the first non-annotation gate underneath.
*
* The dual of @c annotate, for consumers keyed to gate *identity* rather
* than gate value: a provenance mapping matches input-gate UUIDs, and the
* reachability edge classifier matches token shapes, so both must see the
* wrapped gate, not the wrapper (e.g. the inversion-free certificate /
* order marker the planner attaches to a certified query's row roots).
* Identity on a token with no annotation wrapper; NULL on NULL.
*/
CREATE OR REPLACE FUNCTION strip_annotations(token UUID) RETURNS UUID AS
$$
WITH RECURSIVE peel(g) AS (
SELECT token
UNION ALL
SELECT (provsql.get_children(p.g))[1] FROM peel p
WHERE provsql.get_gate_type(p.g) = 'annotation'
)
SELECT g FROM peel WHERE provsql.get_gate_type(g) <> 'annotation' LIMIT 1;
$$ LANGUAGE sql STABLE PARALLEL SAFE;
/**
* @brief Condition a provenance token (a Boolean event) on another.
*
* Builds the terminal @c gate_conditioned that the measure evaluators read
* as @c "P(target ∧ evidence) / P(evidence)". This is the backing function
* of the binary @c | operator (@c "target | evidence", value-level
* conditioning of the uuid carrier).
*
* The gate stores three children @c [target, evidence, joint] with
* @c joint @c = @c times(target, @c evidence); evaluation is then the plain
* ratio @c P(joint)/P(evidence), and content-addressing makes a base tuple
* shared by @p target and @p evidence the same input gate in both circuits,
* so the conditional is exact and correlation-aware.
*
* Conventions:
* - Conditioning on a certain or absent event is a no-op: @c evidence NULL
* or @c gate_one() returns @p target unchanged (@c "P(X|true)=P(X)").
* - A @p target with no provenance defaults to the certain event 1, so
* @c "1 | c" is the well-defined certain-row posterior.
* - Nested conditioning folds (sequential Bayesian update):
* @c "(X | A) | B = X | (A ∧ B)" -- the gate never nests, it stays one
* level deep with the evidence accumulated by @c times.
*
* The result is TERMINAL: a conditioned token may not become a child of a
* @c plus / @c times / @c monus / @c agg gate (those constructors refuse
* it); the only operation it admits is more conditioning.
*/
CREATE OR REPLACE FUNCTION cond(target UUID, evidence UUID) RETURNS UUID AS
$$
DECLARE
tgt uuid;
ev uuid;
jnt uuid;
result uuid;
ch uuid[];
BEGIN
-- P(X | true) = P(X): conditioning on a certain / absent event is inert.
IF evidence IS NULL OR evidence = gate_one() THEN
RETURN target;
END IF;
-- A row with no provenance defaults to the certain event 1.
tgt := coalesce(target, gate_one());
IF get_gate_type(tgt) = 'conditioned' THEN
-- Sequential update (X | A) | B = X | (A ∧ B): fold B into both the
-- evidence and the joint of the inner gate so the result stays a single
-- gate_conditioned over the ORIGINAL target.
ch := get_children(tgt);
tgt := ch[1]; -- original target X
ev := provenance_times(ch[2], evidence); -- A ∧ B
jnt := provenance_times(ch[3], evidence); -- (X ∧ A) ∧ B
ELSE
ev := evidence;
jnt := provenance_times(tgt, evidence); -- X ∧ C
END IF;
result := public.uuid_generate_v5(uuid_ns_provsql(),
concat('conditioned', tgt, ev, jnt));
PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev, jnt]);
RETURN result;
END
$$ LANGUAGE plpgsql IMMUTABLE SET search_path=provsql,pg_temp,public
SECURITY DEFINER PARALLEL SAFE;
/**
* @brief Binary @c | : value-level conditioning, @c "target | evidence".
*
* Carrier-parametric in its left operand; the uuid form builds the terminal
* @c gate_conditioned via @c cond. Does not collide with core PostgreSQL's
* integer bitwise @c | (different argument types).
*/
CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=UUID, PROCEDURE=cond);
/**
* @brief Placeholder for @c "X | (predicate)" on a uuid event.
*
* Lets the conditioning event be written as a natural Boolean combination of
* random_variable / aggregate comparisons (e.g. @c "event | (sensor > 3)")
* instead of a hand-built gate. Never executes: the ProvSQL planner hook
* converts the Boolean operand into a condition gate and emits @c cond.
*/
CREATE OR REPLACE FUNCTION cond_predicate(target UUID, predicate boolean)
RETURNS UUID AS
$$
BEGIN
RAISE EXCEPTION 'uuid | (predicate) must be rewritten by the ProvSQL '
'planner hook: the right operand must be a Boolean combination of '
'random_variable / aggregate comparisons (is provsql.active off?)'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=boolean, PROCEDURE=cond_predicate);
/**
* @brief Placeholder for @c "(predicate) | (predicate)" on two events.
*
* Conditions one comparison event on another when both operands are written
* as comparisons rather than pre-built tokens (e.g.
* @c "probability((x >= 2000) | (x >= 1000))"): an @c random_variable /
* @c agg_token comparison is statically @c boolean-typed, so neither the
* @c "uuid | uuid" (@c cond) nor the @c "uuid | boolean" (@c cond_predicate)
* operator resolves. Never executes: the ProvSQL planner hook lowers each
* Boolean operand to its event gate and emits @c cond(target, evidence), so
* the result carries the correlation-aware @c Pr(A ∧ B) / Pr(B). Returns
* @c uuid, so @c "A | B" is a first-class event token in every position
* (a @c probability(uuid) argument, a projected column, a further @c "|").
*/
CREATE OR REPLACE FUNCTION predicate_cond_predicate(target boolean, evidence boolean)
RETURNS UUID AS
$$
BEGIN
RAISE EXCEPTION '(predicate) | (predicate) must be rewritten by the ProvSQL '
'planner hook: both operands must be Boolean combinations of '
'random_variable / aggregate comparisons (is provsql.active off?)'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR | (LEFTARG=boolean, RIGHTARG=boolean, PROCEDURE=predicate_cond_predicate);
/**
* @brief Deterministic indicator gate for an ordinary (regular) comparison.
*
* The predicate-provenance of an ordinary comparison (both sides of regular
* type, e.g. @c "region = 'north'") is the deterministic indicator
* @c "χ(cond)": @c gate_one() when the comparison holds on the row,
* @c gate_zero() otherwise (Definition in the HAVING-provenance semantics).
* The planner emits this for a regular comparison appearing inside a MIXED
* conditioning predicate (one that also has a random_variable / aggregate
* comparison); @c cond is evaluated per row, so the indicator is the row's
* own truth value, combined by @c ⊗ / @c ⊕ with the probabilistic gates.
*/
CREATE OR REPLACE FUNCTION regular_indicator(cond boolean) RETURNS UUID AS
$$
SELECT CASE WHEN cond THEN provsql.gate_one() ELSE provsql.gate_zero() END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief Whole-tuple output conditioning directive: @c "given(evidence)".
*
* Written as a term in the select list, @c given(c) conditions the OUTPUT
* provenance of the current query's rows on @p c:
*
* @code
* SELECT a, b, given((SELECT provenance() FROM tests
* WHERE patient_id = s.id AND result = 'positive'))
* FROM source s;
* -- visible columns: a, b (the given(...) term is stripped)
* -- per-row output provenance: provenance() |
* @endcode
*
* The query rewriter recognises the marker, STRIPS it from the visible
* projection, and wraps each output row's provenance expression in
* @c cond(row_provenance, c) -- deriving a new conditioned relation, never
* mutating any stored provenance. @p c is evaluated per output row and may
* correlate with the row's columns, so each tuple is conditioned on its own
* evidence. When the rewriter is inactive the call is a harmless identity
* (it returns @p evidence as an ordinary column).
*
* When @b executed rather than stripped -- i.e. nested inside an expression,
* the idiom @c "and_agg(given(Y = d))" that folds one observation per row
* into a latent-variable evidence circuit -- a point-equality @c "Y = d" on
* a bare random-variable leaf is turned into likelihood-weighting evidence
* (@c evidence_as_observation); any other evidence passes through unchanged.
*/
CREATE OR REPLACE FUNCTION given(evidence UUID) RETURNS UUID AS
$$
BEGIN
RETURN provsql.evidence_as_observation(evidence);
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/**
* @brief Prefix unary @c | : alias for @c given, @c "| evidence".
*
* Disambiguated from the binary @c | by the absence of a left operand
* (@c "a, | c" parses @c "| c" as the prefix form). PostgreSQL keeps
* prefix operators on every supported version (postfix operators were
* removed in PG14), so @c "| c" is safe across the CI matrix.
*/
CREATE OPERATOR | (RIGHTARG=UUID, PROCEDURE=given);
/**
* @brief Conditioning-evidence from a predicate: @c "given(predicate)"
* (also the prefix @c "| (predicate)").
*
* Two uses of the same marker:
* - whole-tuple output conditioning written as a select-list term, the
* natural-predicate spelling of @c given (@c "SELECT a, given(sensor > 3)");
* - per-row evidence for a latent-variable posterior, folded with
* @c and_agg -- @c "and_agg(given(normal(mu,1) = x))" turns each row's
* observation into likelihood-weighting evidence.
*
* Never executes: the planner converts the Boolean operand into a condition
* gate and emits @c given(gate); a point-equality @c "Y = d" on a bare
* random-variable leaf then becomes an observation (see @c given(uuid) /
* @c evidence_as_observation).
*/
CREATE OR REPLACE FUNCTION given(predicate boolean) RETURNS UUID AS
$$
BEGIN
RAISE EXCEPTION 'given(predicate) / prefix | (predicate) must be rewritten '
'by the ProvSQL planner hook: the operand must be a Boolean combination '
'of random_variable / aggregate comparisons (is provsql.active off?)'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR | (RIGHTARG=boolean, PROCEDURE=given);
/**
* @brief Event negation: @c "! event" / @c "provenance_not(event)".
*
* The complement of a Boolean provenance event: @c "!x" holds in exactly the
* worlds where @p x does not. It is sugar for @c "monus(one, x)" -- an
* ordinary m-semiring expression (Boolean @c NOT, probability @c "1 - P(x)"),
* NOT a measure-only marker -- so it composes like any @c monus, and a
* conditioned / terminal token is refused as its child (so @c "!(x | c)"
* errors, as conditioning cannot be buried under further algebra).
*
* The motivating use is conditioning on the NON-occurrence of an arbitrary
* violation query @p W (a denial constraint), where @p W itself is built with
* ordinary idioms and needs no hand-rolled gates:
*
* @code
* -- W = "some pair of overlapping same-room bookings is present"
* WITH w AS (SELECT provenance() AS tok
* FROM bookings a JOIN bookings b
* ON a.id < b.id AND a.room = b.room
* AND a.lo < b.hi AND b.lo < a.hi
* GROUP BY ())
* SELECT probability_evaluate((SELECT provenance() FROM bookings WHERE id=1)
* | !w.tok) -- P(booking 1 | no overlap)
* FROM w;
* @endcode
*
* Named @c provenance_not, after the @c "provenance_times / _plus / _monus"
* family; the prefix @c ! operator is the ergonomic form (SQL's reserved
* @c NOT keyword cannot serve as a function name).
*/
CREATE OR REPLACE FUNCTION provenance_not(event UUID) RETURNS UUID AS
$$
SELECT provsql.provenance_monus(provsql.gate_one(), event);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/**
* @brief Prefix unary @c ! : alias for @c provenance_not, @c "! event".
*
* Prefix operators are kept on every supported PostgreSQL version (postfix
* operators were removed in PG14), and core PG defines no prefix @c ! on
* @c uuid, so @c "! event" is safe across the CI matrix.
*/
CREATE OPERATOR ! (RIGHTARG=UUID, PROCEDURE=provenance_not);
/**
* @brief Build a per-input order-key string for the inversion-free path.
*
* Emitted by the planner per certified atom: @c K-prefixed, length-prefixed
* @c "K ::", parsed
* back at evaluation by @c safe_cert_key_parse. @p root / @p sec are the
* tuple's root- and secondary-class column values (text-cast by the caller);
* the byte-length prefixes keep the values unambiguous for @em any column type,
* including text containing spaces, colons or digits. @p factor is the atom's
* factor id (or -1 for the shared self-join guard). @c IMMUTABLE so the planner
* can fold it and the marker dedups by content-addressing.
*
* Implemented in C (gate_builders.c); it was an inlined SQL function,
* hence the default cost.
*/
CREATE OR REPLACE FUNCTION inversion_free_key(root TEXT, sec TEXT, factor INT)
RETURNS TEXT AS
'provsql','inversion_free_key' LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
/** @brief Get the text-encoded extra data associated with a circuit gate */
CREATE OR REPLACE FUNCTION get_extra(token UUID)
RETURNS TEXT AS
'provsql','get_extra' LANGUAGE C STABLE PARALLEL SAFE RETURNS NULL ON NULL INPUT;
/**
* @brief Return the total number of materialized gates in the provenance circuit
*
* Input gates for provenance-tracked table rows are created lazily on
* first reference; rows that have never appeared in a query result are
* not counted.
*/
CREATE OR REPLACE FUNCTION get_nb_gates() RETURNS BIGINT AS
'provsql', 'get_nb_gates' LANGUAGE C PARALLEL SAFE;
/**
* @brief Report what does not add up in this database's circuit store
*
* The store lives in four files under the database's directory, outside
* PostgreSQL's WAL and buffer manager, so PostgreSQL's own crash recovery
* says nothing about it. Writes are ordered so that an interrupted one
* leaves a record nothing points at rather than a pointer to a record
* that is not there, and the rehash of the token table is done by
* renaming a complete new file over the old one; this function checks
* that those invariants hold.
*
* Every count is 0 for a healthy store. @c unclean_shutdown is true when
* a file was still marked open-for-writing when it was opened, which an
* immediate shutdown or a crash of the server leaves behind and is not by
* itself a problem. A non-zero @c dangling_indices, @c bad_wires or
* @c bad_extra means the files do not agree with each other -- typically
* a file-level backup taken while the server was running, or a base
* backup; @c provsql.circuit_cleanup() rebuilds the store from what is
* still reachable. @c unreferenced counts gate records the token table
* does not point at: a handful is normal (an interrupted write), a large
* number means the token table is missing entries.
*/
CREATE OR REPLACE FUNCTION check_store(
OUT unclean_shutdown BOOLEAN,
OUT nb_gates BIGINT,
OUT nb_tokens BIGINT,
OUT next_index BIGINT,
OUT dangling_indices BIGINT,
OUT unreferenced BIGINT,
OUT bad_wires BIGINT,
OUT bad_extra BIGINT)
RETURNS record AS
'provsql', 'check_store' LANGUAGE C;
/**
* @brief Rebuild this database's circuit store, keeping only what the
* tokens stored in the database reach
*
* The store only grows: a gate is never removed, because a rolled-back
* transaction leaves an orphan rather than an inconsistency, and because
* the same expression recomputed lands on the same content-addressed
* gate. This is the complement of that -- the one operation allowed to
* remove gates, run explicitly, the way @c VACUUM @c FULL is the
* complement of MVCC. It is also the repair tool for a store an
* interrupted write left inconsistent (see @c provsql.check_store).
*
* It takes the database exclusively: it holds the lock @c DROP
* @c DATABASE holds, so sessions connecting from then on wait, and it
* refuses to run while another session is already connected. That is
* unavoidable -- a query running alongside can adopt an orphan gate a
* moment before the sweep removes it, since gates are re-created
* idempotently and a backend's own cache answers "it exists" without
* asking the store at all.
*
* A root is every value of a @c uuid, @c agg_token or @c random_variable
* column, and of arrays of those, in every table and materialised view of
* the database -- not only columns named @c provsql -- plus the constants
* @c gate_zero, @c gate_one and @c gate_null. A token that lives only
* outside the database is **not** a root: one kept in a notebook cell, a
* deep link, a file, or a @c text / @c jsonb column. Content-addressed
* gates come back by re-running the query that built them; freshly minted
* ones do not.
*
* @param dry_run report what would be kept without writing anything; the
* wire and byte totals are then NULL, since the size of
* the rewrite is not known without doing it
* @param[out] gates_before gate records in the store beforehand
* @param[out] gates_after gate records the roots reach, and so kept
* @param[out] wires_before child wires beforehand
* @param[out] wires_after child wires kept (NULL on a dry run)
* @param[out] extra_bytes_before bytes of gate annotations beforehand
* @param[out] extra_bytes_after bytes of gate annotations kept (NULL on a
* dry run)
*/
CREATE OR REPLACE FUNCTION circuit_cleanup(
dry_run BOOLEAN DEFAULT false,
OUT gates_before BIGINT,
OUT gates_after BIGINT,
OUT wires_before BIGINT,
OUT wires_after BIGINT,
OUT extra_bytes_before BIGINT,
OUT extra_bytes_after BIGINT)
RETURNS record AS
'provsql', 'circuit_cleanup' LANGUAGE C;
/** @} */
/** @defgroup table_management Provenance table management
* Functions for enabling, disabling, and configuring provenance
* tracking on user tables.
* @{
*/
/**
* @brief Trigger function for DELETE statement provenance tracking
*
* Records the deletion and applies monus to provenance tokens of
* deleted rows. This is the version for PostgreSQL < 14.
*/
CREATE OR REPLACE FUNCTION delete_statement_trigger()
RETURNS TRIGGER AS
$$
DECLARE
query_text TEXT;
delete_token UUID;
old_token UUID;
new_token UUID;
r RECORD;
BEGIN
delete_token := public.uuid_generate_v4();
PERFORM create_gate(delete_token, 'input');
SELECT query
INTO query_text
FROM pg_stat_activity
WHERE pid = pg_backend_pid();
INSERT INTO delete_provenance (delete_token, query, deleted_by, deleted_at)
VALUES (delete_token, query_text, current_user, CURRENT_TIMESTAMP);
EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
FOR r IN (SELECT * FROM OLD_TABLE) LOOP
old_token := r.provsql;
new_token := provenance_monus(old_token, delete_token);
EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
USING new_token, old_token;
END LOOP;
RETURN NULL;
END
$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
/**
* @brief Per-relation provenance metadata used by the safe-query
* optimisation.
*
* One row per relation ProvSQL tracks. @c relid is stored as
* @c regclass so a dump carries the relation's *name*: OIDs are not
* stable across databases, and @c pg_dump / @c pg_restore resolve a
* @c regclass value back to whatever OID the relation has in the
* target database. @c kind is one of @c 'tid' / @c 'bid' /
* @c 'opaque' (see @c set_table_info); @c block_key lists the
* block-key column numbers of a BID relation; @c ancestors lists the
* base relations this one's atoms ultimately come from.
*
* Being a heap table, every change follows the transaction that made
* it: a rolled-back @c add_provenance leaves no record, a rolled-back
* @c DROP @c TABLE keeps one, and a concurrent session sees a change
* only once it commits. Marked as a configuration table so
* @c pg_dump carries it.
*/
CREATE TABLE IF NOT EXISTS table_info(
relid regclass PRIMARY KEY,
kind text NOT NULL,
block_key int2[] NOT NULL DEFAULT ARRAY[]::int2[],
ancestors oid[] NOT NULL DEFAULT ARRAY[]::oid[]
);
SELECT pg_catalog.pg_extension_config_dump('table_info', '');
/**
* @brief Row trigger keeping every backend's metadata cache honest.
*
* Each backend caches the metadata of the relations its queries touch
* and drops an entry when PostgreSQL invalidates that relation's
* relcache entry. This trigger issues that invalidation for the
* relation named by every inserted, updated, or deleted row, so a
* hand-written @c UPDATE on the table and the @c COPY a @c pg_restore
* performs are as visible to the caches as the setter functions are.
*/
CREATE OR REPLACE FUNCTION table_info_invalidate()
RETURNS trigger AS
'provsql','provsql_table_info_invalidate' LANGUAGE C;
DROP TRIGGER IF EXISTS table_info_invalidate ON table_info;
CREATE TRIGGER table_info_invalidate
AFTER INSERT OR UPDATE OR DELETE ON table_info
FOR EACH ROW EXECUTE FUNCTION provsql.table_info_invalidate();
/**
* @brief Record per-relation provenance metadata used by the
* safe-query optimisation.
*
* Upserts the @c (relid, kind, block_key) half of the relation's row
* in @c provsql.table_info, preserving its @c ancestors. @p kind is
* one of:
* - @c 'tid' -- independent input leaves (post-@c add_provenance default)
* - @c 'bid' -- block-correlated leaves; rows sharing the same value
* of @p block_key are mutually exclusive. An empty
* @p block_key means the whole table is one block.
* - @c 'opaque' -- arbitrary correlations from a derived source
* (CREATE TABLE AS SELECT, INSERT INTO SELECT,
* UPDATE under provsql.update_provenance); the
* safe-query rewriter must bail on these.
*
* @param relid pg_class OID of the relation.
* @param kind One of @c 'tid' / @c 'bid' / @c 'opaque'.
* @param block_key Block-key column numbers (only meaningful for
* @c 'bid'; ignored otherwise but conventionally
* passed empty).
*/
CREATE OR REPLACE FUNCTION set_table_info(
relid OID, kind TEXT, block_key INT2[] DEFAULT ARRAY[]::INT2[])
RETURNS void AS
'provsql','set_table_info' LANGUAGE C SECURITY DEFINER;
/** @brief Remove a relation's row from @c provsql.table_info.
* No-op when missing. */
CREATE OR REPLACE FUNCTION remove_table_info(relid OID)
RETURNS void AS
'provsql','remove_table_info' LANGUAGE C SECURITY DEFINER;
/**
* @brief Read per-relation provenance metadata.
*
* Returns NULL if no record exists. @c kind is one of @c 'tid' /
* @c 'bid' / @c 'opaque'; @c block_key is the (possibly empty) array
* of block-key column numbers, only meaningful when @c kind = @c 'bid'.
* Used by the planner-time hierarchy detector to gate the safe-query
* rewrite.
*/
CREATE OR REPLACE FUNCTION get_table_info(
relid OID, OUT kind TEXT, OUT block_key INT2[])
RETURNS record AS
'provsql','get_table_info' LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief Record the base-relation ancestor set of a tracked relation.
*
* Base tables created with @c add_provenance / @c repair_key carry
* @c {self}; CTAS-derived tables inherit the union of their sources'
* ancestor sets. The safe-query rewriter consults the registry to
* enforce that joined FROM entries have disjoint base ancestors
* before firing the read-once factoring.
*
* Preserves the relation's existing @c kind / @c block_key half on
* update, and silently no-ops when no row exists for @p relid
* (callers should run @c add_provenance / @c repair_key first). The
* ancestor list is capped at 64 entries (clear error if exceeded).
*
* @param relid pg_class OID of the relation.
* @param ancestors Sorted, deduplicated base-relation OIDs.
*/
CREATE OR REPLACE FUNCTION set_ancestors(
relid OID, ancestors OID[] DEFAULT ARRAY[]::OID[])
RETURNS void AS
'provsql','set_ancestors' LANGUAGE C SECURITY DEFINER;
/** @brief Clear the ancestor half of a per-relation record (keeps kind/block_key).
* No-op when missing. */
CREATE OR REPLACE FUNCTION remove_ancestors(relid OID)
RETURNS void AS
'provsql','remove_ancestors' LANGUAGE C SECURITY DEFINER;
/**
* @brief Copy per-relation metadata out of the legacy
* @c provsql_table_info.mmap file into @c provsql.table_info.
*
* ProvSQL 1.13.0 moved this metadata from a fifth mmap file to a heap
* table, so that it follows the transaction that writes it and is
* carried by @c pg_dump. This function reads the legacy file, if the
* database still has one, and inserts every record it holds that the
* heap table does not already have; it returns the number of rows
* inserted, and 0 when there is no file to read. Idempotent, and a
* no-op on a database that never had one.
*/
CREATE OR REPLACE FUNCTION migrate_table_info()
RETURNS BIGINT AS
'provsql','migrate_table_info' LANGUAGE C SECURITY DEFINER;
/**
* @brief Read the base-relation ancestor set of a tracked relation.
*
* Returns @c NULL when no ancestor record exists for @p relid (or the
* record is empty -- both cases make the safe-query rewriter take
* its conservative refuse path, so they collapse here).
*/
CREATE OR REPLACE FUNCTION get_ancestors(relid OID)
RETURNS OID[] AS
'provsql','get_ancestors' LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief BEFORE INSERT OR UPDATE OF provsql row trigger installed by
* @c add_provenance.
*
* Two jobs:
*
* 1. Fill @c NEW.provsql with a fresh @c uuid_generate_v4 leaf when
* the user did not supply one (a column DEFAULT would not do here:
* it fires before the trigger sees the row, so we could not tell
* "user omitted the column" from "user supplied a value").
* 2. When the user does supply a non-NULL @c provsql on @c INSERT,
* or changes it on @c UPDATE, flip the table's per-table
* metadata to @c OPAQUE. The user is free to write whatever
* UUIDs they want (cross-table reuse, compound tokens minted
* via @c create_gate, ...); the cost is that the safe-query
* rewriter then refuses to fire on this table, because TID
* independence can no longer be assumed. The exception is a
* leaf @c provsql.replace_input / @c replace_block minted in
* this transaction: that one *is* an independent fresh leaf, so
* the kind survives and the maintained mappings follow the
* token to its replacement.
*/
CREATE OR REPLACE FUNCTION provenance_guard()
RETURNS TRIGGER AS $$
DECLARE
_m RECORD;
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.provsql IS NULL THEN
-- A genuine insert: mint a fresh atomic input variable. This is the
-- one place a new input token is born, so it is also where any
-- maintained mapping on this table is extended (keyed to that token).
-- Data-modification re-insertions (INSERT ... SELECT * FROM OLD_TABLE)
-- carry a supplied provsql and take the ELSE branch, so they are
-- correctly skipped: the validity stays keyed to the original input,
-- which is exactly the child a later monus/update gate wraps.
NEW.provsql := public.uuid_generate_v4();
FOR _m IN SELECT mapping, attribute
FROM provsql.provenance_mapping_registry
WHERE source = TG_RELID AND maintained
LOOP
EXECUTE format(
'INSERT INTO %s(value, provenance) SELECT ($1).%I, $2',
_m.mapping::regclass, _m.attribute)
USING NEW, NEW.provsql;
END LOOP;
ELSE
PERFORM provsql.set_table_info(TG_RELID, 'opaque');
END IF;
ELSIF TG_OP = 'UPDATE' THEN
IF NEW.provsql IS DISTINCT FROM OLD.provsql THEN
IF provsql.is_fresh_leaf(NEW.provsql) THEN
-- A replacement leaf minted by provsql.replace_input /
-- replace_block in this transaction: an independent fresh leaf by
-- construction, so the table's kind survives. Carry the
-- maintained mappings over from the token it replaces, the same
-- job the INSERT branch does for a new row.
FOR _m IN SELECT mapping, attribute
FROM provsql.provenance_mapping_registry WHERE source = TG_RELID
LOOP
EXECUTE format(
'INSERT INTO %1$s(value, provenance) '
'SELECT value, $2 FROM %1$s WHERE provenance = $1',
_m.mapping::regclass)
USING OLD.provsql, NEW.provsql;
END LOOP;
ELSE
PERFORM provsql.set_table_info(TG_RELID, 'opaque');
END IF;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
SECURITY DEFINER;
/**
* @brief Enable provenance tracking on an existing table
*
* Adds a provsql UUID column to the table, an index for
* fast UUID-keyed lookups, and a BEFORE INSERT/UPDATE row trigger
* (@c provenance_guard) that mints a fresh @c uuid_generate_v4
* leaf when the user omits the column on INSERT, or flips the
* table's metadata to @c OPAQUE when the user supplies their own
* value. Input gates for existing rows are created lazily when
* first referenced by a query.
*
* @param _tbl the table to add provenance tracking to
*/
CREATE OR REPLACE FUNCTION add_provenance(_tbl regclass)
RETURNS void AS
$$
BEGIN
-- Idempotence: a second add_provenance on an already-tracked table is
-- a no-op with a NOTICE, so setup scripts and notebook cells can be
-- re-run freely.
IF EXISTS (
SELECT 1 FROM pg_attribute
WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
) THEN
RAISE NOTICE 'table % already has provenance tracking', _tbl;
RETURN;
END IF;
-- No DEFAULT: the guard trigger mints the UUID, so the trigger can
-- distinguish "user omitted" (NULL) from "user supplied a value".
-- No UNIQUE: we no longer rely on it to keep the table TID -- the
-- guard does that semantically -- and a UNIQUE would reject the
-- legitimate cross-table UUID copy that just flips the table to
-- OPAQUE. We keep a plain index for fast UUID-keyed lookups.
EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
EXECUTE format(
'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
_tbl);
EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
EXECUTE format(
'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
'ON %s FOR EACH ROW EXECUTE FUNCTION provsql.provenance_guard()',
_tbl);
PERFORM provsql.set_table_info(_tbl::oid, 'tid');
-- Seed the base-ancestor set to {self}: a base TID table's atoms
-- come from itself and no other relation. CTAS-derived tables
-- inherit unions of source ancestor sets; that is handled by the
-- CTAS hook (a separate slice), not here.
PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
/**
* @brief Remove provenance tracking from a table
*
* Drops the provsql column and associated triggers.
*
* @param _tbl the table to remove provenance tracking from
*/
CREATE OR REPLACE FUNCTION remove_provenance(_tbl regclass)
RETURNS void AS
$$
DECLARE
BEGIN
PERFORM provsql.remove_table_info(_tbl::oid);
-- Idempotence, mirroring add_provenance: removing provenance from a
-- table that does not have it is a NOTICE-and-no-op, so setup scripts
-- and notebook cells can be re-run freely. The metadata strip above
-- still runs, so a table left half-tracked is cleaned up.
IF NOT EXISTS (
SELECT 1 FROM pg_attribute
WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
) THEN
RAISE NOTICE 'table % does not have provenance tracking', _tbl;
RETURN;
END IF;
-- Drop the BEFORE INSERT/UPDATE guard first: it has a column
-- dependency on provsql (via the OF provsql clause), so the
-- subsequent DROP COLUMN would otherwise raise.
BEGIN
EXECUTE format('DROP TRIGGER provenance_guard on %s', _tbl);
EXCEPTION WHEN undefined_object THEN
END;
EXECUTE format('ALTER TABLE %s DROP COLUMN provsql', _tbl);
BEGIN
EXECUTE format('DROP TRIGGER add_gate on %s', _tbl);
EXCEPTION WHEN undefined_object THEN
END;
BEGIN
EXECUTE format('DROP TRIGGER insert_statement on %s', _tbl);
EXECUTE format('DROP TRIGGER update_statement on %s', _tbl);
EXECUTE format('DROP TRIGGER delete_statement on %s', _tbl);
EXCEPTION WHEN undefined_object THEN
END;
END
$$ LANGUAGE plpgsql;
/**
* @brief Set up provenance for a table with duplicate key values
*
* When a table has duplicate rows for a given key, this function
* replaces simple input gates with multivalued input (mulinput) gates
* that model a uniform distribution over duplicates. The uniform
* weight is the default a row evaluates at, not a probability written
* on it, so the usual next step -- @c "SELECT set_prob(provenance(),
* p) FROM t" -- gives each row its real probability as a first write.
*
* @param _tbl the table to repair
* @param key_att the key attribute(s) as a comma-separated string, or
* empty string if the whole table is one group
*/
CREATE OR REPLACE FUNCTION repair_key(_tbl regclass, key_att text)
RETURNS void AS
$$
DECLARE
r RECORD;
rows_query TEXT;
block_key_cols INT2[];
BEGIN
-- Resolve the (possibly comma-separated) key_att text into the
-- corresponding pg_attribute.attnum values for the safe-query
-- metadata. Names are trimmed; quoting is not supported because
-- repair_key has never accepted quoted identifiers in key_att.
IF key_att = '' THEN
block_key_cols := ARRAY[]::INT2[];
ELSE
SELECT array_agg(a.attnum ORDER BY t.ord)::INT2[]
INTO block_key_cols
FROM unnest(string_to_array(key_att, ',')) WITH ORDINALITY AS t(name, ord)
JOIN pg_attribute a
ON a.attrelid = _tbl
AND a.attname = trim(t.name)
AND a.attnum > 0
AND NOT a.attisdropped;
IF block_key_cols IS NULL OR array_length(block_key_cols, 1) IS NULL THEN
RAISE EXCEPTION 'repair_key: could not resolve key columns from "%"', key_att
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: repair-key-columns; scope: gap';
END IF;
IF array_length(block_key_cols, 1) > 16 THEN
RAISE EXCEPTION 'repair_key: block key wider than 16 columns is not supported'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: repair-key-too-wide; scope: gap';
END IF;
END IF;
-- Same column shape as add_provenance: no UNIQUE, no DEFAULT past
-- the initial backfill (the guard trigger added after the rename
-- takes over both jobs once the column has been renamed to its
-- final name). The DEFAULT is kept here only so the second pass
-- below can read provsql_temp from the user-visible rows
-- without a separate UPDATE.
EXECUTE format('ALTER TABLE %s ADD COLUMN provsql_temp UUID DEFAULT public.uuid_generate_v4()', _tbl);
-- Build a per-group mapping (key columns + a fresh key_token + the
-- group size) once, then use it for both the create_gate(key_token,
-- 'input') first pass and the per-row mulinput second pass. Going
-- through a temp table avoids re-running uuid_generate_v4() (which
-- would produce different UUIDs the second time). USING (%1$s) on
-- the second pass handles the multi-column case uniformly.
-- ON COMMIT DROP plus the explicit DROP TABLE at the end of this
-- function leave the temp table cleaned up across transactions and
-- across repeated calls in the same transaction.
IF key_att = '' THEN
EXECUTE format(
'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
SELECT public.uuid_generate_v4() AS provsql_key_token,
COUNT(*) AS provsql_group_size
FROM %s', _tbl);
rows_query := format(
'SELECT t.provsql_temp,
k.provsql_key_token AS key_token,
ROW_NUMBER() OVER (ORDER BY t.ctid) AS within_group,
k.provsql_group_size AS group_size
FROM %s t CROSS JOIN provsql_repair_key_tmp k', _tbl);
ELSE
EXECUTE format(
'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
SELECT %1$s,
public.uuid_generate_v4() AS provsql_key_token,
COUNT(*) AS provsql_group_size
FROM %2$s
GROUP BY %1$s', key_att, _tbl);
rows_query := format(
'SELECT t.provsql_temp,
k.provsql_key_token AS key_token,
ROW_NUMBER() OVER (PARTITION BY k.provsql_key_token
ORDER BY t.ctid) AS within_group,
k.provsql_group_size AS group_size
FROM %2$s t
JOIN provsql_repair_key_tmp k USING (%1$s)', key_att, _tbl);
END IF;
-- Pass 1: one input gate per group key.
FOR r IN SELECT provsql_key_token FROM provsql_repair_key_tmp LOOP
PERFORM provsql.create_gate(r.provsql_key_token, 'input');
END LOOP;
-- Pass 2: per row, attach a mulinput gate to its group's key token.
-- The block size goes in info2 rather than the uniform 1/size going
-- in the probability: a repaired row's probability is the user's to
-- write (the documented "repair_key then set_prob(provenance(), p)"
-- pattern), and probabilities are written once. A row nobody gives
-- a probability evaluates at 1/size all the same -- see
-- MMappedCircuit::getProb.
FOR r IN EXECUTE rows_query LOOP
PERFORM provsql.create_gate(r.provsql_temp, 'mulinput', ARRAY[r.key_token],
r.within_group::int, r.group_size::int, NULL);
END LOOP;
DROP TABLE provsql_repair_key_tmp;
EXECUTE format('ALTER TABLE %s ALTER COLUMN provsql_temp DROP DEFAULT', _tbl);
EXECUTE format('ALTER TABLE %s RENAME COLUMN provsql_temp TO provsql', _tbl);
EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
EXECUTE format(
'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
'ON %s FOR EACH ROW EXECUTE FUNCTION provsql.provenance_guard()',
_tbl);
PERFORM provsql.set_table_info(_tbl::oid, 'bid', block_key_cols);
-- Base BID tables also have themselves as their sole ancestor. Same
-- rationale as the @c add_provenance branch above.
PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
END
$$ LANGUAGE plpgsql;
/**
* @brief Event trigger that purges per-table provenance metadata when
* a tracked relation is dropped outside of remove_provenance().
*
* Plain DROP TABLE bypasses remove_provenance() and would otherwise
* leave a stale row in provsql.table_info keyed by a now-recycled
* OID, with confusing consequences for the safe-query rewriter the
* next time the OID is reused. This trigger forwards every dropped
* relation OID to provsql.remove_table_info(), which is a no-op for
* relations that were not tracked. Both the deletion and the
* registry cleanup below roll back with the DROP that triggered
* them.
*/
CREATE OR REPLACE FUNCTION cleanup_table_info()
RETURNS event_trigger AS
$$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT objid FROM pg_event_trigger_dropped_objects()
WHERE object_type IN ('table', 'foreign table', 'materialized view')
LOOP
PERFORM provsql.remove_table_info(r.objid);
-- Forget any maintained mapping whose source or mapping table is gone.
DELETE FROM provsql.provenance_mapping_registry
WHERE source = r.objid OR mapping = r.objid;
END LOOP;
END
$$ LANGUAGE plpgsql;
DROP EVENT TRIGGER IF EXISTS provsql_cleanup_table_info;
-- @c EXECUTE @c PROCEDURE (rather than the PG 11+ @c EXECUTE
-- @c FUNCTION alias) so the extension installs on PG 10 too.
CREATE EVENT TRIGGER provsql_cleanup_table_info ON sql_drop
EXECUTE FUNCTION provsql.cleanup_table_info();
/**
* @brief Registry of provenance mappings
*
* Each row records that mapping table @c mapping was built from the
* @c attribute column of the provenance-tracked @c source table. When
* @c maintained, every genuine insert into @c source also appends
* @c (value, provenance) to it, so the mapping stays current; otherwise
* the mapping is a snapshot and the row is here only so that a token
* *replacement* carries the mapping over (see @c provenance_guard: a leaf
* minted by @c replace_input names the same tuple, so its value is copied
* to the new token in every mapping of the table, maintained or not).
* Keyed on the mapping table, indexed on the source so the guard can look
* up a table's mappings cheaply. Entries are removed when either table is
* dropped (see @c cleanup_table_info).
*/
CREATE TABLE IF NOT EXISTS provsql.provenance_mapping_registry(
mapping oid PRIMARY KEY,
source oid NOT NULL,
attribute name NOT NULL,
maintained boolean NOT NULL DEFAULT false
);
ALTER TABLE provsql.provenance_mapping_registry
ADD COLUMN IF NOT EXISTS maintained boolean NOT NULL DEFAULT false;
CREATE INDEX IF NOT EXISTS provenance_mapping_registry_source_idx
ON provsql.provenance_mapping_registry(source);
/**
* @brief Create a provenance mapping table from an attribute
*
* Creates a new table mapping provenance tokens to values of a given
* attribute, for use with semiring evaluation functions.
* Idempotent: if the mapping table already exists, raises a NOTICE and
* changes nothing (drop it first to rebuild).
*
* @param newtbl name of the mapping table to create
* @param oldtbl source table with provenance tracking
* @param att attribute whose values populate the mapping
* @param preserve_case if true, quote the table name to preserve case
* @param maintained if true, later inserts into @c oldtbl keep the mapping
* current, and it stays correct after data modification
* (deletes/updates rewrite a row's provsql, but the validity stays
* keyed to the original input token). @c att must then be a plain
* column name. When false (the default) the table is a one-off
* snapshot; either way the mapping is recorded in
* @c provenance_mapping_registry, so a row whose input gate is
* replaced by @c provsql.replace_input keeps its value in it.
*/
CREATE OR REPLACE FUNCTION create_provenance_mapping(
newtbl text,
oldtbl regclass,
att text,
preserve_case bool DEFAULT 'f',
maintained bool DEFAULT false
) RETURNS void AS
$$
DECLARE
BEGIN
-- Idempotence: when the mapping table already exists, leave it alone
-- with a NOTICE (re-runnable setup scripts / notebook cells). Drop it
-- first to rebuild a stale mapping.
IF (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
ELSE to_regclass(newtbl) END) IS NOT NULL THEN
RAISE NOTICE 'mapping table % already exists', newtbl;
RETURN;
END IF;
-- ON COMMIT DROP only fires at COMMIT: several mapping creations in
-- one transaction (a notebook cell, a setup script run via psql -1)
-- would otherwise collide on the leftover temp table. The to_regclass
-- probe (rather than DROP IF EXISTS) keeps the first call NOTICE-free.
IF to_regclass('pg_temp.tmp_provsql') IS NOT NULL THEN
DROP TABLE tmp_provsql;
END IF;
EXECUTE format('CREATE TEMP TABLE tmp_provsql ON COMMIT DROP AS TABLE %s', oldtbl);
ALTER TABLE tmp_provsql RENAME provsql TO provenance;
-- The mapping is keyed by gate identity (input-token UUIDs), so peel any
-- transparent annotation wrapper (e.g. the inversion-free certificate a
-- certified query attaches to its row roots) off the captured tokens.
UPDATE tmp_provsql SET provenance = provsql.strip_annotations(provenance)
WHERE provsql.get_gate_type(provenance) = 'annotation';
IF preserve_case THEN
EXECUTE format('CREATE TABLE %I AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
EXECUTE format('CREATE INDEX ON %I(provenance)', newtbl);
ELSE
EXECUTE format('CREATE TABLE %s AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
EXECUTE format('CREATE INDEX ON %s(provenance)', newtbl);
END IF;
-- Register the mapping. When maintained, genuine inserts into oldtbl
-- keep it current (see provenance_guard); keyed to the input token, so
-- it survives the provsql rewrites that data modification performs.
-- A snapshot mapping is registered too, so that replacing a row's input
-- gate (provsql.replace_input) carries the row's value over to the new
-- token: the tuple is the same one, only its token moved.
INSERT INTO provsql.provenance_mapping_registry(mapping, source, attribute, maintained)
VALUES (
(CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
ELSE to_regclass(newtbl) END)::oid,
oldtbl::oid, att, maintained)
ON CONFLICT (mapping)
DO UPDATE SET source = EXCLUDED.source, attribute = EXCLUDED.attribute,
maintained = EXCLUDED.maintained;
END
$$ LANGUAGE plpgsql;
/** @} */
/** @defgroup internal_constants Internal constants
* UUID namespace and identity element functions used for
* deterministic gate generation.
* @{
*/
/** @brief Return the ProvSQL UUID namespace (used for deterministic gate UUIDs) */
CREATE OR REPLACE FUNCTION uuid_ns_provsql() RETURNS uuid AS
$$
-- uuid_generate_v5(uuid_ns_url(),'http://pierre.senellart.com/software/provsql/')
SELECT '920d4f02-8718-5319-9532-d4ab83a64489'::uuid
$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
/** @brief Return the UUID of the semiring zero gate */
CREATE OR REPLACE FUNCTION gate_zero() RETURNS uuid AS
$$
SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'zero');
$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
/** @brief Return the UUID of the semiring one gate */
CREATE OR REPLACE FUNCTION gate_one() RETURNS uuid AS
$$
SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'one');
$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
/**
* @brief Return the UUID of the value gate standing for the NULL value
*
* A constant, like gate_zero() and gate_one(); the gate itself is a
* value gate that displays as NULL. Its UUID is what
* tells it apart from the value gate of the string 'NULL'; the
* seed 'null' is no 'value' || text, so no actual value
* shares it.
*/
CREATE OR REPLACE FUNCTION gate_null() RETURNS uuid AS
$$
SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'null');
$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
/** @} */
/** @defgroup semiring_operations Semiring operations
* Functions that build provenance circuit gates for semiring operations.
* These are called internally by the query rewriter.
*
* They are declared @c IMMUTABLE: each derives its gate UUID
* deterministically from its arguments (a @c uuid5 content address) and
* the @c create_gate write at that address is idempotent, so the token a
* call returns is a pure function of its inputs. The marking matters for
* parallelism: PL/pgSQL runs a non-volatile function's inner SPI
* read-only, so the per-row builders the rewriter injects into a scan do
* not call @c CommandCounterIncrement -- which would raise "cannot start
* commands during a parallel operation" once the enclosing statement has
* gone parallel. A @c VOLATILE builder both blocks that parallel plan and
* loses the query-wide speed-up.
* @{
*/
/**
* @brief Create a times (product) gate from multiple provenance tokens
*
* Filters out NULL and one-gates; returns gate_one() if all tokens
* are trivial, or a single token if only one remains.
*
* When the surviving multiset is one for which this session planted a
* certified equivalent (see @c plant_canonical; the reachability rewriter
* does so for self-join conjunctions of reachability tokens, see
* @c plant_reach_cover), the planted gate is returned. The ordinary
* order-dependent recipe is used otherwise, so ordinary times gates (and
* their formula rendering) are untouched.
*
* Implemented in C (gate_builders.c). The cost is declared as that
* of a PL/pgSQL function, which this function was: the planner then keeps
* evaluating it after the cheaper conditions, and the plans of rewritten
* queries, on which the order of the children of a ⊕ depends, stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_times(VARIADIC tokens uuid[])
RETURNS UUID AS
'provsql','provenance_times' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief Create a monus (difference) gate from two provenance tokens
*
* Implements m-semiring monus. Returns token1 if token2 is NULL
* (used for LEFT OUTER JOIN semantics in the EXCEPT rewriting).
*
* Implemented in C (gate_builders.c), with the declared cost of the
* PL/pgSQL function it was, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_monus(token1 UUID, token2 UUID)
RETURNS UUID AS
'provsql','provenance_monus' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief Create a project gate for where-provenance tracking
*
* Records the mapping between input and output attribute positions.
*
* @param token child provenance token
* @param positions array encoding attribute position mappings
*
* Implemented in C (gate_builders.c): the gate is created together
* with what it records, in one unanswered message.
*/
CREATE OR REPLACE FUNCTION provenance_project(token UUID, VARIADIC positions int[])
RETURNS UUID AS
'provsql','provenance_project' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief Create an equijoin gate for where-provenance tracking
*
* @param token child provenance token
* @param pos1 attribute index in the first relation
* @param pos2 attribute index in the second relation
*
* Implemented in C (gate_builders.c): the gate is created together
* with what it records, in one unanswered message.
*/
CREATE OR REPLACE FUNCTION provenance_eq(token UUID, pos1 int, pos2 int)
RETURNS UUID AS
'provsql','provenance_eq' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief Create a plus (sum) gate from an array of provenance tokens
*
* Filters out NULL and zero-gates; returns gate_zero() if all tokens
* are trivial, or a single token if only one remains. When the
* multiset is one for which this session planted a certified gate
* computing the same sum (see @c plant_canonical), that gate is returned.
* This is how the bounded-hop reachability route keeps the natural
* hop-discarding query on the linear evaluation route: it plants, for a
* vertex's per-length tokens, a certified gate over its native
* within-bound circuit. Otherwise the ordinary order-dependent recipe
* is used, so ordinary plus gates (and their formula rendering) are
* untouched.
*/
CREATE OR REPLACE FUNCTION provenance_plus(tokens uuid[])
RETURNS UUID AS
'provsql','provenance_plus' LANGUAGE C COST 100 STRICT PARALLEL SAFE IMMUTABLE;
/**
* @brief Declare the working table of a recursive CTE, just (re)created
* (internal)
*
* Gates planted from now on belong to it; those planted for the table of
* the same name it replaces, and for working tables that no longer exist,
* are forgotten.
*
* @param work_name name of the temporary working table
*/
CREATE OR REPLACE FUNCTION planted_scope(work_name text)
RETURNS void AS
'provsql','planted_scope' LANGUAGE C STRICT;
/**
* @brief Plant a certified gate in place of a sum or a product (internal)
*
* Creates a @p kind gate with the single child @p target at the canonical
* address of the multiset @p tokens -- the v5 UUID of
* @c 'plus-canonical{sorted tokens}' or @c 'times-canonical{…}', a recipe under which nothing else
* creates gates -- and remembers the address in this session: while the
* working table @p work_name exists, @c provenance_plus /
* @c provenance_times given that very multiset return the planted gate.
* The tokens are row tokens of the working table, a temporary table, so
* the sum or product is computed by the session that planted, never by a
* parallel worker; the store is not consulted, and sessions that planted
* nothing pay nothing.
*
* @param work_name working table the tokens belong to
* @param kind 'plus' or 'times'
* @param tokens the multiset the planted gate stands for
* @param target root of the certified circuit
* @param info1 first info of the planted gate
* @param info2 second info of the planted gate
* @return the address of the planted gate
*/
CREATE OR REPLACE FUNCTION plant_canonical(
work_name text, kind text, tokens uuid[], target uuid,
info1 int, info2 int DEFAULT 0)
RETURNS uuid AS
'provsql','plant_canonical' LANGUAGE C STRICT;
/**
* @brief Address of the @c fixpoint gate reading component @p i of the
* equation system @p sys (internal)
*
* Content-addressed like every other gate, from the system and the
* index, so that evaluation, having solved a system, can name each of its
* components (see @c provenance_evaluate_compiled's solution cache).
*/
CREATE OR REPLACE FUNCTION fixpoint_token(sys uuid, i int) RETURNS uuid AS
$$
SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),
'fixpoint:' || sys::text || ':' || i::text);
$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief The token of each tuple of the equation system @p sys (internal)
*
* Tuples outside every cycle of the system get their right-hand side
* rebuilt over the tokens of the tuples they read, as a fixpoint iteration
* would build it; the tuples of each cyclic component a @c fixpoint gate
* over a system of their own. See @c fix_system.cpp.
*
* @param sys a @c fixsystem gate, with one unknown and one right-hand
* side per tuple
* @return the token of each tuple, in the order of the unknowns
*/
CREATE OR REPLACE FUNCTION resolve_fix_system(sys uuid) RETURNS uuid[] AS
'provsql','resolve_fix_system' LANGUAGE C STRICT VOLATILE;
/**
* @brief Driver for provenance over recursive queries (WITH RECURSIVE),
* as an equation system (internal)
*
* Invoked by the planner hook (@c lower_recursive_cte in @c provsql.c) when
* it lowers a recursive CTE with @c UNION whose body touches
* provenance-tracked relations; the hook deparses the CTE body to SQL and
* scans, in place of the CTE, the table this function fills. Instead of
* unrolling the fixpoint round after round, it records the recursion as one
* equation per derived tuple, x_t = f_t(x), and leaves the value of the
* tuples derived through themselves to evaluation time, where the semiring
* is known:
*
* 1. the derived tuples are computed by PostgreSQL's own recursion, with
* provenance tracking off;
* 2. the working table is loaded with them, each carrying a fresh unknown
* (a @c fixvar gate) as its token;
* 3. the CTE body is run once over that table, through ProvSQL's normal
* rewriting: the token it gives a tuple is the right-hand side f_t of
* its equation, an ordinary circuit over the input tokens and the
* unknowns. A row this round derives that plain recursion did not --
* absent from the database as it is, present in other worlds, as the
* null-padded row of an outer join -- is a tuple of the recursion too:
* it is added with an unknown of its own, and the round run again,
* until it derives no new row;
* 4. a @c fixsystem gate gathers the unknowns and the right-hand sides,
* and @c resolve_fix_system gives each tuple its token: the ordinary
* circuit a fixpoint iteration would reach, for a tuple derived
* through no cycle; a @c fixpoint gate over its cyclic component's own
* system otherwise.
*
* The recursion thus costs plain SQL's rounds plus one, and the circuit
* one round of derivations, whatever the data's cycles; whether a cyclic
* component has a value is decided by the semiring evaluating it (see
* @c GenericCircuit::solveFixSystem): every absorptive semiring, and those
* with a star (counting with an infinite count, min-plus with negative
* cycles, why- and which-provenance); @c sr_formula renders the equations.
*
* This function has no @c SET @c search_path on purpose: @p body_sql is
* the caller's deparsed query and must resolve relation names in the
* caller's path.
*
* @param body_sql the recursive CTE body, e.g.
* @c 'SELECT 1 UNION SELECT e.dst FROM edge e JOIN reach r ON e.src=r.node'
* @param work_name the working relation name @p body_sql references
* @param colnames comma-separated user columns
* @param coldef column definitions for the working table
*/
CREATE OR REPLACE FUNCTION eval_recursive_system(
body_sql text,
work_name text,
colnames text,
coldef text)
RETURNS void AS
$$
DECLARE
seed uuid := public.uuid_generate_v4();
active text := current_setting('provsql.active');
joincond text;
ncols text;
wcols text;
added bigint;
known bigint := 0;
rounds int := 0;
xs uuid[];
fs uuid[];
sys uuid;
BEGIN
EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
DROP TABLE IF EXISTS provsql_rec_new;
DROP TABLE IF EXISTS provsql_rec_eq;
EXECUTE format('CREATE TEMP TABLE %I (%s, provsql uuid) ON COMMIT DROP',
work_name, coldef);
PERFORM provsql.planted_scope(work_name);
EXECUTE format('CREATE TEMP TABLE provsql_rec_new (LIKE %I) ON COMMIT DROP',
work_name);
-- Match two tables on the text form of the user columns: null-safe
-- like IS NOT DISTINCT FROM, but hashable, which a column-wise
-- IS NOT DISTINCT FROM is not (it would make this a nested loop).
SELECT string_agg('n.' || trim(c), ', '), string_agg('w.' || trim(c), ', ')
INTO ncols, wcols
FROM unnest(string_to_array(colnames, ',')) AS c;
joincond := format('ROW(%s)::text = ROW(%s)::text', ncols, wcols);
EXECUTE 'CREATE TEMP TABLE provsql_rec_eq (tid tid, i bigint, x uuid) '
'ON COMMIT DROP';
-- 1. The derived tuples, by plain recursion.
PERFORM set_config('provsql.active', 'off', true);
EXECUTE format(
'INSERT INTO %1$I(%2$s) WITH RECURSIVE %1$I(%2$s) AS (%3$s) '
'SELECT %2$s FROM %1$I',
work_name, colnames, body_sql);
LOOP
-- 2. One unknown for each tuple that has none yet.
PERFORM set_config('provsql.active', 'off', true);
EXECUTE format(
'INSERT INTO provsql_rec_eq '
'SELECT ctid, %2$s + i, public.uuid_generate_v5(%1$L, ''x'' || (%2$s + i)) '
'FROM (SELECT ctid, row_number() OVER () AS i FROM %3$I '
' WHERE provsql IS NULL) t',
seed, known, work_name);
EXECUTE format(
'UPDATE %I w SET provsql = e.x FROM provsql_rec_eq e '
'WHERE w.ctid = e.tid AND w.provsql IS NULL',
work_name);
PERFORM provsql.create_gate(e.x, 'fixvar')
FROM provsql_rec_eq e WHERE e.i > known;
SELECT coalesce(max(i), 0) INTO known FROM provsql_rec_eq;
PERFORM set_config('provsql.active', active, true);
-- 3. One round over the unknowns: each tuple's right-hand side.
EXECUTE 'TRUNCATE provsql_rec_new';
EXECUTE format('INSERT INTO provsql_rec_new(%s) %s', colnames, body_sql);
-- The round may derive rows plain recursion does not: a row that is
-- absent from the database as it is but present in other worlds, which
-- ProvSQL keeps for its provenance (the null-padded row of an outer
-- join, ...). Those are tuples of the recursion too, with equations of
-- their own, and what derives from them as well: add them and run the
-- round again, until it derives no new row. With no such row, which is
-- the usual case, the round runs once.
PERFORM set_config('provsql.active', 'off', true);
EXECUTE format(
'INSERT INTO %1$I(%2$s) SELECT %3$s FROM provsql_rec_new n '
'WHERE NOT EXISTS (SELECT 1 FROM %1$I w WHERE %4$s)',
work_name, colnames, ncols, joincond);
GET DIAGNOSTICS added = ROW_COUNT;
EXIT WHEN added = 0;
rounds := rounds + 1;
IF rounds > 100000 THEN
RAISE EXCEPTION 'ProvSQL: the rows this recursion derives in other '
'worlds than the database as it is do not stop '
'growing';
END IF;
END LOOP;
-- 4. The system, and each tuple's component of its solution.
EXECUTE format(
'SELECT array_agg(e.x ORDER BY e.i), '
'array_agg(coalesce(n.provsql, provsql.gate_zero()) ORDER BY e.i) '
'FROM provsql_rec_eq e JOIN %I w ON w.provsql = e.x '
'LEFT JOIN provsql_rec_new n ON %s',
work_name, joincond) INTO xs, fs;
IF xs IS NOT NULL THEN
sys := public.uuid_generate_v5(seed, 'system');
PERFORM provsql.create_gate(sys, 'fixsystem', xs || fs);
-- Each tuple's token: rebuilt over the others' where it is derived
-- through no cycle, a fixpoint gate over its cyclic component's own
-- system otherwise.
-- Unnested with its index, so that each row reads its own token and
-- not a copy of the whole array.
EXECUTE format(
'UPDATE %I w SET provsql = t.tok '
'FROM provsql_rec_eq e '
'JOIN unnest(provsql.resolve_fix_system(%L)) WITH ORDINALITY AS t(tok, i) '
'ON t.i = e.i '
'WHERE w.provsql = e.x',
work_name, sys);
END IF;
PERFORM set_config('provsql.active', active, true);
END
$$ LANGUAGE plpgsql SET client_min_messages = warning;
/**
* @brief Drive a @c UNION @c ALL recursion, one round per bag of derivations
*
* The bag recursion is not a fixpoint over a set: its rounds are
* @c M0 @c = @c q0 and @c M(i+1) @c = @c q1 over @c Mi -- the PREVIOUS round,
* not what has been derived so far -- and its answer is the bag union of every
* round, which ends when a round derives nothing. Each tuple of it is one
* derivation, annotated by the product along that derivation, and two
* derivations of the same tuple are two rows and are not merged: that is what
* distinguishes it from @c UNION, whose driver (@c eval_recursive_system)
* sums the derivations of a tuple into one row.
*
* @c work_name holds the previous round, which the recursive term reads by the
* name of the CTE; @c all_name accumulates the answer and is what the query
* reads. Ending on an empty round is SQL's own rule, so a recursion PostgreSQL
* runs to completion ends here too, and one it does not is caught by
* @p max_iter as before.
*
* @param q0_sql the non-recursive term, as SQL
* @param q1_sql the recursive term, reading @p work_name
* @param work_name temp table of the previous round (the CTE's name)
* @param all_name temp table accumulating the rounds
* @param colnames comma-separated user column names
* @param coldef column definitions ("name type, ...")
* @param max_iter safety bound on the number of rounds
*/
CREATE OR REPLACE FUNCTION eval_recursive_all(
q0_sql text,
q1_sql text,
work_name text,
all_name text,
colnames text,
coldef text,
max_iter int DEFAULT 1000)
RETURNS void AS
$$
DECLARE
iters int := 0;
new_count int;
BEGIN
EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
EXECUTE format('DROP TABLE IF EXISTS %I', all_name);
DROP TABLE IF EXISTS provsql_rec_new;
EXECUTE format('CREATE TEMP TABLE %I (%s, provsql uuid) ON COMMIT DROP',
work_name, coldef);
EXECUTE format('CREATE TEMP TABLE %I (LIKE %I) ON COMMIT DROP',
all_name, work_name);
EXECUTE format('CREATE TEMP TABLE provsql_rec_new (LIKE %I) ON COMMIT DROP',
work_name);
PERFORM provsql.planted_scope(all_name);
-- The first round: the non-recursive term. A row it derives from no
-- tracked relation gets no token from the INSERT: it is certain, and is
-- given one, as the UNION driver gives it -- left NULL, a grouping above
-- read it as nothing and the row as absent in every world.
EXECUTE format('INSERT INTO provsql_rec_new(%s) %s', colnames, q0_sql);
GET DIAGNOSTICS new_count = ROW_COUNT;
UPDATE provsql_rec_new SET provsql = provsql.gate_one() WHERE provsql IS NULL;
LOOP
EXIT WHEN new_count = 0; -- a round that derives nothing ends the answer
-- Every row of the round is an answer, kept as it is: a second derivation
-- of a tuple is a second row, which is what UNION ALL says.
EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM provsql_rec_new',
all_name, colnames);
-- The round becomes the relation the recursive term reads.
EXECUTE format('TRUNCATE %I', work_name);
EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM provsql_rec_new',
work_name, colnames);
iters := iters + 1;
IF iters > max_iter THEN
/* A bag recursion is defined when a round derives nothing; one whose
* rounds do not end has no answer to give, in SQL either (PostgreSQL
* runs it forever), so this is what the recursion means and not a limit
* of the driver. */
RAISE EXCEPTION 'ProvSQL: the rounds of this UNION ALL recursion do not '
'end (after % of them): its answer is the rows of every '
'round, which SQL itself does not reach either on such '
'data', max_iter
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: recursion-does-not-end; scope: deliberate';
END IF;
EXECUTE format('TRUNCATE provsql_rec_new');
EXECUTE format('INSERT INTO provsql_rec_new(%s) %s', colnames, q1_sql);
GET DIAGNOSTICS new_count = ROW_COUNT;
UPDATE provsql_rec_new SET provsql = provsql.gate_one() WHERE provsql IS NULL;
END LOOP;
END
$$ LANGUAGE plpgsql SET client_min_messages = warning;
/**
* @brief Create a comparison gate for HAVING clause provenance
*
* @param left_token provenance token for the left operand
* @param comparison_op OID of the comparison operator
* @param right_token provenance token for the right operand
*
* Implemented in C (gate_builders.c), with the declared cost of the
* PL/pgSQL function it was, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_cmp(
left_token UUID,
comparison_op OID,
right_token UUID
)
RETURNS UUID AS
'provsql','provenance_cmp' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief The factors of a row annotation an aggregate comparison does not
* subsume.
*
* A lifted comparison entails the existence of the group it ranges over, so it
* supersedes that group's @c gate_delta instead of multiplying with it. This
* reports which factors of @p tokens survive that supersede: a bare δ over the
* compared group disappears, a @c times keeps its other factors, and anything
* else -- an earlier comparison on the same group, an input -- is kept whole.
*/
CREATE FUNCTION cmp_surviving_factors(tokens uuid[], cmp uuid)
RETURNS uuid[] AS
'provsql', 'cmp_surviving_factors' LANGUAGE C PARALLEL SAFE STABLE;
/**
* @brief Combine a lifted aggregate comparison with the row annotation it
* supersedes only part of.
*
* @param cmp Gate of the lifted comparison.
* @param tokens Row-annotation factors at the level owning the comparison.
* @return @c cmp multiplied with whatever of @p tokens it does not subsume.
*/
CREATE OR REPLACE FUNCTION provenance_cmp_times(cmp uuid, tokens uuid[])
RETURNS UUID AS
$$
DECLARE
kept uuid[];
BEGIN
kept := provsql.cmp_surviving_factors(tokens, cmp);
IF kept IS NULL OR array_length(kept, 1) IS NULL THEN
RETURN cmp;
END IF;
RETURN provsql.provenance_times(VARIADIC kept || cmp);
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
/**
* @brief Create an arithmetic gate over scalar-valued provenance children
*
* Builds a deterministic @c gate_arith from an operator tag and an
* ordered list of children. The tag is one of the @c provsql_arith_op
* enum values declared in @c src/provsql_utils.h
* (@c PLUS=0, @c TIMES=1, @c MINUS=2, @c DIV=3, @c NEG=4) and is
* stored in the gate's @c info1 field. Children must be UUIDs of
* scalar-producing gates (@c gate_rv, @c gate_value, or another
* @c gate_arith). The token UUID is derived deterministically from
* @p op and @p children so identical sub-expressions share their gate.
*
* @param op Operator tag (@c provsql_arith_op).
* @param children Ordered list of child gate UUIDs.
* @return UUID of the (possibly pre-existing) @c gate_arith.
*
* Implemented in C (gate_builders.c): the gate is created together
* with what it records, in one unanswered message.
*/
CREATE OR REPLACE FUNCTION provenance_arith(
op INTEGER,
children UUID[]
)
RETURNS UUID AS
'provsql','provenance_arith' LANGUAGE C COST 100 STRICT PARALLEL SAFE IMMUTABLE;
/**
* @brief Create a guarded-selection gate over scalar (RV) children.
*
* Builds a deterministic @c gate_case from the flattened wire list
* @c [guard_1, value_1, ..., guard_k, value_k, default] (odd length): the
* value of the first guard event that holds, else the default (first-match
* semantics). Each guard is a Boolean event token (a @c gate_cmp or Boolean
* combination); each value and the default are scalar-producing gates
* (@c gate_rv, @c gate_value, @c gate_arith, another @c gate_case, ...). The
* token UUID is derived deterministically from @p children so identical
* @c CASE expressions share their gate.
*
* @param children Flattened guard/value wires ending with the default
* (@c array_length must be odd and @c >= 1).
* @return UUID of the (possibly pre-existing) @c gate_case.
*/
CREATE OR REPLACE FUNCTION provenance_case(
children UUID[]
)
RETURNS UUID AS
$$
DECLARE
case_token UUID;
BEGIN
IF array_length(children, 1) IS NULL OR array_length(children, 1) % 2 = 0 THEN
RAISE EXCEPTION 'provenance_case expects an odd number of children '
'(guard/value pairs followed by a default), got %',
coalesce(array_length(children, 1), 0);
END IF;
case_token := public.uuid_generate_v5(
uuid_ns_provsql(),
concat('case', children::text)
);
PERFORM create_gate(case_token, 'case', children);
RETURN case_token;
END
$$ LANGUAGE plpgsql
SET search_path=provsql,pg_temp,public
SECURITY DEFINER
IMMUTABLE
PARALLEL SAFE
STRICT;
/** @} */
/** @defgroup semiring_evaluation Semiring evaluation
* Functions for evaluating provenance circuits over semirings,
* both user-defined (via function references) and compiled (built-in).
* @{
*/
/**
* @brief Evaluate provenance using a compiled (built-in) semiring
*
* This C function handles semiring evaluation entirely in C++ for
* better performance. The semiring is specified by name.
*
* @param token provenance token to evaluate
* @param token2value mapping table from tokens to semiring values
* @param semiring name of the compiled semiring (e.g., "formula", "counting")
* @param element_one identity element of the semiring
*/
CREATE OR REPLACE FUNCTION provenance_evaluate_compiled(
token UUID,
token2value regclass,
semiring TEXT,
element_one anyelement)
RETURNS anyelement AS
'provsql', 'provenance_evaluate_compiled' LANGUAGE C PARALLEL SAFE STABLE;
/**
* @brief Evaluate provenance over a user-defined semiring (PL/pgSQL version)
*
* Recursively walks the provenance circuit and evaluates each gate
* using the provided semiring operations. This is the generic version
* that accepts semiring operations as function references.
*
* @param token provenance token to evaluate
* @param token2value mapping table from tokens to semiring values
* @param element_one identity element of the semiring
* @param value_type OID of the semiring value type
* @param plus_function semiring addition (aggregate)
* @param times_function semiring multiplication (aggregate)
* @param monus_function semiring monus (binary), or NULL
* @param delta_function δ-semiring operator, or NULL
*/
CREATE OR REPLACE FUNCTION provenance_evaluate(
token UUID,
token2value regclass,
element_one anyelement,
value_type regtype,
plus_function regproc,
times_function regproc,
monus_function regproc,
delta_function regproc)
RETURNS anyelement AS
$$
DECLARE
gate_type provenance_gate;
result ALIAS FOR $0;
children UUID[];
-- cmp_value anyelement;
-- temp_result anyelement;
value_text TEXT;
BEGIN
SELECT get_gate_type(token) INTO gate_type;
IF gate_type IS NULL THEN
RETURN NULL;
ELSIF gate_type = 'input' THEN
EXECUTE format('SELECT value FROM %s WHERE provenance=%L', token2value, token)
INTO result;
IF result IS NULL THEN
result := element_one;
END IF;
ELSIF gate_type = 'mulinput' THEN
SELECT concat('{',(get_children(token))[1]::text,'=',(get_infos(token)).info1,'}')
INTO result;
ELSIF gate_type='update' THEN
EXECUTE format('SELECT value FROM %s WHERE provenance=%L',token2value,token) INTO result;
IF result IS NULL THEN
result:=element_one;
END IF;
ELSIF gate_type = 'plus' THEN
EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
plus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
INTO result;
ELSIF gate_type = 'times' THEN
EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
times_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
INTO result;
ELSIF gate_type = 'monus' THEN
IF monus_function IS NULL THEN
RAISE EXCEPTION USING MESSAGE='Provenance with negation evaluated over a semiring without monus function',
DETAIL = 'provsql-reason: semiring-no-monus; scope: deliberate';
ELSE
EXECUTE format('SELECT %s(a1,a2) FROM (SELECT provsql.provenance_evaluate(c[1],%L,%L::%s,%L,%L,%L,%L,%L) AS a1, ' ||
'provsql.provenance_evaluate(c[2],%L,%L::%s,%L,%L,%L,%L,%L) AS a2 FROM get_children(%L) c) tmp',
monus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function,
token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
INTO result;
END IF;
ELSIF gate_type = 'eq' THEN
EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
INTO result;
/* elsif gate_type = 'cmp' then
EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
INTO temp_result;
EXECUTE format('SELECT get_extra((get_children(%L))[2])', token)
INTO cmp_value;
IF temp_result::text = cmp_value::text THEN
SELECT concat('{',temp_result::text,'=',cmp_value::text,'}')
INTO result;
ELSE
RETURN gate_zero()
*/
ELSIF gate_type = 'delta' THEN
IF delta_function IS NULL THEN
RAISE EXCEPTION USING MESSAGE='Provenance with aggregation evaluated over a semiring without delta function',
DETAIL = 'provsql-reason: semiring-no-delta; scope: deliberate';
ELSE
EXECUTE format('SELECT %I(a) FROM (SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L) AS a) tmp',
delta_function, token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
INTO result;
END IF;
ELSIF gate_type = 'zero' THEN
EXECUTE format('SELECT %I(a) FROM (SELECT %L::%I AS a WHERE FALSE) temp', plus_function, element_one, value_type)
INTO result;
ELSIF gate_type = 'one' THEN
EXECUTE format('SELECT %L::%I', element_one, value_type)
INTO result;
ELSIF gate_type = 'project' THEN
EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
INTO result;
ELSIF gate_type = 'annotation' THEN
-- Transparent single-child wrapper (carries the inversion-free certificate
-- / per-input order keys in extra, inert for every semiring): evaluate
-- through to the child, like 'project'.
EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
INTO result;
ELSE
RAISE EXCEPTION USING MESSAGE='provenance_evaluate cannot be called on formulas using ' || gate_type || ' gates; use compiled semirings instead',
DETAIL = 'provsql-reason: evaluate-gate-kind; scope: gap';
END IF;
RETURN result;
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/**
* @brief Evaluate provenance over a user-defined semiring (C version)
*
* Optimized C implementation of provenance_evaluate. Infers the
* value type from element_one. Monus and delta functions are optional.
*
* @param token provenance token to evaluate
* @param token2value mapping table from tokens to semiring values
* @param element_one identity element of the semiring
* @param plus_function semiring addition (aggregate)
* @param times_function semiring multiplication (aggregate)
* @param monus_function semiring monus, or NULL if not needed
* @param delta_function δ-semiring operator, or NULL if not needed
*/
CREATE OR REPLACE FUNCTION provenance_evaluate(
token UUID,
token2value regclass,
element_one anyelement,
plus_function regproc,
times_function regproc,
monus_function regproc = NULL,
delta_function regproc = NULL)
RETURNS anyelement AS
'provsql','provenance_evaluate' LANGUAGE C STABLE;
/** @} */
/** @defgroup circuit_introspection Circuit introspection
* Functions for examining the structure of provenance circuits,
* used by visualization and where-provenance features.
* @{
*/
/** @brief Row type for sub_circuit_with_desc results */
CREATE TYPE gate_with_desc AS (f UUID, t UUID, gate_type provenance_gate, desc_str CHARACTER VARYING, infos INTEGER[], extra TEXT);
/**
* @brief Return the sub-circuit reachable from a token, with descriptions
*
* Recursively traverses the provenance circuit from the given token and
* returns all edges together with input gate descriptions from the
* mapping table.
*
* @param token root provenance token
* @param token2desc mapping table providing descriptions for input gates
*/
CREATE OR REPLACE FUNCTION sub_circuit_with_desc(
token UUID,
token2desc regclass) RETURNS SETOF gate_with_desc AS
$$
BEGIN
RETURN QUERY EXECUTE
'WITH RECURSIVE transitive_closure(f,t,gate_type) AS (
SELECT $1,t,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) AS t
UNION ALL
SELECT p1.t,u,provsql.get_gate_type(p1.t) FROM transitive_closure p1, unnest(provsql.get_children(p1.t)) AS u)
SELECT *, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
SELECT f::uuid,t::uuid,gate_type,NULL FROM transitive_closure
UNION ALL
SELECT p2.provenance::uuid as f, NULL::uuid, ''input'', CAST (p2.value AS varchar) FROM transitive_closure p1 JOIN ' || token2desc || ' AS p2
ON p2.provenance=t
UNION ALL
SELECT provenance::uuid as f, NULL::uuid, ''input'', CAST (value AS varchar) FROM ' || token2desc || ' WHERE provenance=$1
) t'
USING token LOOP;
RETURN;
END
$$ LANGUAGE plpgsql STABLE PARALLEL RESTRICTED;
/**
* @brief Identify which table and how many columns a provenance token belongs to
*
* Searches all provenance-tracked tables for a row matching the given
* token and returns the table name and column count.
*
* @param token provenance token to look up
* @param table_name (OUT) the table containing this token
* @param nb_columns (OUT) number of non-provenance columns in that table
*/
CREATE OR REPLACE FUNCTION identify_token(
token UUID, OUT table_name regclass, OUT nb_columns integer) AS
$$
DECLARE
t RECORD;
found uuid;
BEGIN
table_name:=NULL;
nb_columns:=-1;
FOR t IN
SELECT pg_namespace.nspname, relname, attrelid,
(SELECT count(*) FROM pg_attribute a2 WHERE a2.attrelid=a1.attrelid AND attnum>0 AND atttypid<>0)-1 c
FROM pg_attribute a1 JOIN pg_type ON atttypid=pg_type.oid
JOIN pg_class ON attrelid=pg_class.oid
JOIN pg_namespace ON relnamespace=pg_namespace.oid
WHERE typname='uuid' AND relkind='r'
AND nspname<>'provsql'
AND attname='provsql'
LOOP
-- Qualified by its schema, which need not be on the search_path. A
-- table another session drops, or removes the provenance of, between the
-- catalog read and the query is skipped rather than failing the lookup;
-- so is one another session holds exclusively (a DROP, an ALTER under
-- way): waiting for it, while holding the tables read so far, could
-- deadlock with a statement dropping several of them.
BEGIN
EXECUTE format('LOCK TABLE %I.%I IN ACCESS SHARE MODE NOWAIT',
t.nspname, t.relname);
EXECUTE format('SELECT provsql FROM %I.%I WHERE provsql=%L LIMIT 1',
t.nspname, t.relname, token) INTO found;
EXCEPTION WHEN undefined_table OR undefined_column OR lock_not_available THEN
CONTINUE;
END;
IF found IS NOT NULL THEN
table_name:=t.attrelid::regclass;
nb_columns:=t.c;
EXIT;
END IF;
END LOOP;
END
$$ LANGUAGE plpgsql STRICT;
/**
* @brief Return the sub-circuit for where-provenance computation
*
* Similar to sub_circuit_with_desc but resolves input gates to their
* source table and column count for where-provenance evaluation.
*/
CREATE OR REPLACE FUNCTION sub_circuit_for_where(token UUID)
RETURNS TABLE(f UUID, t UUID, gate_type provenance_gate, table_name REGCLASS, nb_columns INTEGER, infos INTEGER[], extra TEXT) AS
$$
-- A comparison (cmp) is a condition on whether a row is there, which
-- where-provenance ignores: the walk stops at it, leaving it a leaf, and
-- does not enter the aggregates and values it compares.
WITH RECURSIVE transitive_closure(f,t,idx,gate_type) AS (
SELECT $1,t,id,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) WITH ORDINALITY AS a(t,id)
WHERE provsql.get_gate_type($1) <> 'cmp'
UNION ALL
SELECT p1.t,u,id,provsql.get_gate_type(p1.t) FROM transitive_closure p1, unnest(provsql.get_children(p1.t)) WITH ORDINALITY AS a(u, id)
WHERE provsql.get_gate_type(p1.t) <> 'cmp'
) SELECT f, t, gate_type, table_name, nb_columns, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
-- One row per distinct (parent, child, child-position) edge. The
-- recursive closure (UNION ALL) re-emits a gate's outgoing edges once per
-- path that reaches it, so a *shared* non-input gate would otherwise be
-- reported with duplicate edges; DISTINCT on the (f,t,idx) triple
-- collapses those while keeping genuine repeated children (same f,t,
-- different idx, e.g. a self-product). Without this, a shared
-- single-child gate (notably an inversion-free order-marker annotation)
-- gets its child wired k times in the where-circuit -> the locator sets
-- are duplicated k-fold.
SELECT DISTINCT f, t::uuid, idx, gate_type, NULL::regclass AS table_name, NULL::integer AS nb_columns FROM transitive_closure
UNION ALL
-- The leaves, with their own type: an input is traced back to its
-- relation, any other leaf (an aggregation over no row, a constant)
-- is reported as what it is, for where_provenance to refuse by type.
SELECT DISTINCT leaf, NULL::uuid, NULL::int, leaf_type, i.table_name, i.nb_columns FROM (SELECT DISTINCT t AS leaf, provsql.get_gate_type(t) AS leaf_type FROM transitive_closure WHERE t NOT IN (SELECT f FROM transitive_closure)) leaves LEFT JOIN LATERAL provsql.identify_token(leaf) i ON leaf_type = 'input'
UNION ALL
SELECT DISTINCT $1, NULL::uuid, NULL::int, leaf_type, i.table_name, i.nb_columns FROM (SELECT provsql.get_gate_type($1) AS leaf_type WHERE $1 NOT IN (SELECT f FROM transitive_closure)) root LEFT JOIN LATERAL provsql.identify_token($1) i ON leaf_type = 'input'
) t
-- order each parent's edges by child position so the where-circuit's TIMES
-- concatenation reproduces the column order (input rows have idx NULL).
ORDER BY f, idx
$$
LANGUAGE sql;
/**
* @brief BFS expansion of a provenance circuit, capped at @p max_depth
*
* Returns one row per (parent, child) edge in the BFS-bounded subgraph
* rooted at @p root, plus one row for the root with parent and
* child_pos NULL. Provenance circuits are DAGs, so a child gate
* may have several parents within the bound; each such edge is reported
* as a separate row, so callers must deduplicate on node if they
* need a one-row-per-node view.
*
* depth is the node's longest-path distance from @p root
* within the depth bound (the standard circuit-depth notion), so for
* an edge (parent, child) it is the case that
* child.depth >= parent.depth + 1, except at the
* max_depth truncation frontier. A node at
* depth = max_depth is not
* expanded; callers can detect a partial expansion by comparing
* provsql.get_children length against the number of outgoing
* edges reported.
*
* info1 and info2 are the integer values recorded on
* the gate when it was created, formatted as text; their meaning is
* gate-type-specific.
*
* @param root root provenance token
* @param max_depth maximum BFS depth (default 8)
*/
CREATE OR REPLACE FUNCTION circuit_subgraph(root UUID, max_depth INT DEFAULT 8)
RETURNS TABLE(node UUID, parent UUID, child_pos INT, gate_type TEXT, info1 TEXT, info2 TEXT, depth INT) AS
$$
WITH RECURSIVE bfs(node, parent, child_pos, depth) AS (
SELECT root, NULL::UUID, NULL::INT, 0
UNION ALL
SELECT c.t, b.node, c.idx::INT, b.depth + 1
FROM bfs b
CROSS JOIN LATERAL unnest(provsql.get_children(b.node))
WITH ORDINALITY AS c(t, idx)
WHERE b.depth < max_depth
),
-- Each node's canonical depth is its longest-path distance from the
-- root (the standard circuit-depth notion: the longest chain of
-- gates separating the node from the output). The recursive CTE
-- enumerates paths up to @c max_depth, so MAX over those is the
-- longest path of length at most @c max_depth.
node_depth AS (
SELECT node, MAX(depth) AS depth FROM bfs GROUP BY node
),
-- All distinct (parent, child, child_pos) triples seen during the BFS.
-- A child reached from k parents within the bound contributes k rows.
-- Self-joins (times(x, x)) contribute one row per child position.
edges AS (
SELECT DISTINCT parent, node AS child, child_pos
FROM bfs WHERE parent IS NOT NULL
)
SELECT
d.node,
e.parent,
e.child_pos,
provsql.get_gate_type(d.node)::TEXT,
i.info1::TEXT,
i.info2::TEXT,
d.depth
FROM node_depth d
LEFT JOIN edges e ON e.child = d.node
LEFT JOIN LATERAL provsql.get_infos(d.node) i ON TRUE
ORDER BY d.depth, d.node, e.parent;
$$ LANGUAGE sql STABLE PARALLEL SAFE;
/**
* @brief BFS subgraph of the IN-MEMORY simplified circuit rooted at @p root.
*
* Same row shape as @ref circuit_subgraph plus an inline @c extra
* column, but built from the @c GenericCircuit returned by
* @c getGenericCircuit -- i.e. AFTER @c provsql.simplify_on_load
* passes (RangeCheck, ...) have rewritten any decidable @c gate_cmp
* into Bernoulli @c gate_input / @c gate_zero / @c gate_one leaves.
* Lets a renderer show the user what the evaluator actually sees,
* without mutating the persisted DAG.
*
* Returns @c jsonb (an array of objects) rather than @c SETOF record
* to keep the C++ implementation free of SRF / @c FuncCallContext
* boilerplate; callers either consume the array directly or expand
* it via @c jsonb_array_elements.
*
* @param root Root provenance token.
* @param max_depth Maximum BFS depth (default 8).
*/
CREATE OR REPLACE FUNCTION simplified_circuit_subgraph(
root UUID, max_depth INT DEFAULT 8) RETURNS jsonb
AS 'provsql','simplified_circuit_subgraph'
LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief Empirical histogram of a scalar sub-circuit
*
* Returns a jsonb array of @c {bin_lo, bin_hi, count} objects covering
* the observed @c [min, max] range of @p bins equal-width samples from
* the sub-circuit rooted at @p token. Sample count is taken from
* @c provsql.rv_mc_samples; pinning @c provsql.monte_carlo_seed makes
* the result reproducible.
*
* Accepted root gate types are the scalar ones: @c gate_value (Dirac
* at the constant, single bin), @c gate_rv (sampled from the leaf's
* distribution), and @c gate_arith (sampled by recursing through the
* arithmetic DAG, with shared @c gate_rv leaves correctly correlated
* within an iteration). Any other gate type raises.
*
* @param token Root provenance token of a scalar sub-circuit.
* @param bins Number of equal-width histogram bins (default 30).
* @param prov Conditioning event (defaults to @c gate_one() = no
* conditioning). When non-trivial, the histogram is
* over the conditional distribution recovered by
* rejection sampling on the joint circuit with @p token.
*/
CREATE OR REPLACE FUNCTION rv_histogram(
token UUID, bins INT DEFAULT 30, prov UUID DEFAULT gate_one())
RETURNS jsonb
AS 'provsql','rv_histogram'
LANGUAGE C VOLATILE PARALLEL SAFE;
/**
* @brief Sample the closed-form PDF and CDF of a (possibly truncated)
* scalar distribution.
*
* Returns @c {"pdf": [{x, p}, ...], "cdf": [{x, p}, ...]} with @p samples
* evenly-spaced points spanning the distribution's natural display
* range (intersected with the conditioning event's interval when
* @c prov is non-trivial). Used by ProvSQL Studio's Distribution
* profile panel to overlay the analytical curve on the empirical
* histogram from :sqlfunc:`rv_histogram` -- the simplifier's
* analytical wins (e.g. @c c·Exp(λ) folding to @c Exp(λ/c)) become
* visible as a smooth curve riding over the MC-sampled bars.
*
* Returns @c NULL when the root sub-circuit is not a closed-form
* shape (V1: only bare @c gate_rv of Normal / Uniform / Exponential
* / integer-Erlang). The frontend reads @c NULL as "skip overlay"
* without erroring, so the caller can dispatch this in parallel with
* @c rv_histogram regardless of the underlying shape.
*
* @param token Scalar gate token (random_variable's UUID).
* @param samples Number of (x, p) points; must be >= 2.
* @param prov Conditioning event (defaults to @c gate_one() = no
* conditioning). When non-trivial, the curves are
* over the truncated distribution.
*/
CREATE OR REPLACE FUNCTION rv_analytical_curves(
token UUID, samples INT DEFAULT 100, prov UUID DEFAULT gate_one())
RETURNS jsonb
AS 'provsql','rv_analytical_curves'
LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Draw conditional Monte Carlo samples from a scalar gate.
*
* Returns up to @c n samples of the scalar value at @c token; when
* @c prov is not the trivial @c gate_one() event, draws are accepted
* only on iterations where @c prov evaluates true (rejection
* sampling). Shared @c gate_rv leaves between @c token and @c prov
* are loaded into a single joint circuit so the indicator's draw
* and the value's draw share their per-iteration state.
*
* @param token Scalar sub-circuit root.
* @param n Number of accepted samples to attempt.
* @param prov Conditioning event (defaults to @c gate_one() = no
* conditioning).
*
* Emits a @c NOTICE when the conditional acceptance rate yields fewer
* than @c n samples within the @c provsql.rv_mc_samples budget so the
* caller can choose to widen the budget.
*/
CREATE OR REPLACE FUNCTION rv_sample(
token UUID, n integer, prov UUID DEFAULT gate_one())
RETURNS SETOF float8
AS 'provsql','rv_sample'
LANGUAGE C VOLATILE PARALLEL SAFE;
/**
* @brief Resolve an input gate UUID back to its source row
*
* Searches every provenance-tracked relation for a row whose
* provsql column equals @p uuid and returns the relation's
* regclass together with the row encoded as JSONB. Returns zero
* rows when @p uuid is not the provenance token of any tracked row,
* including when it identifies an internal gate (plus,
* times, ...) rather than an input.
*
* Ordinarily exactly one row is returned, but if the same UUID
* happens to appear as a provsql value in several tracked
* tables, all matches are returned.
*
* @param uuid token to resolve
*/
CREATE OR REPLACE FUNCTION resolve_input(uuid UUID)
RETURNS TABLE(relation regclass, row_data JSONB) AS
$$
DECLARE
t RECORD;
rd JSONB;
-- ProvSQL's rewriter unconditionally appends a provsql column to the
-- targetlist of any SELECT reading from a tracked relation; capture and
-- discard it here rather than disabling the rewriter for the whole call.
ign UUID;
BEGIN
FOR t IN
SELECT c.oid::regclass AS regc, ns.nspname, c.relname
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace ns ON c.relnamespace = ns.oid
JOIN pg_type ty ON a.atttypid = ty.oid
WHERE a.attname = 'provsql'
AND ty.typname = 'uuid'
AND c.relkind = 'r'
AND ns.nspname <> 'provsql'
AND a.attnum > 0
LOOP
-- A table another session drops, removes the provenance of, or holds
-- exclusively is skipped (see identify_token).
BEGIN
EXECUTE format('LOCK TABLE %I.%I IN ACCESS SHARE MODE NOWAIT',
t.nspname, t.relname);
FOR rd, ign IN
EXECUTE format(
'SELECT to_jsonb(t) - ''provsql'', t.provsql FROM %I.%I AS t WHERE provsql = $1',
t.nspname, t.relname)
USING uuid
LOOP
relation := t.regc;
row_data := rd;
RETURN NEXT;
END LOOP;
EXCEPTION WHEN undefined_table OR undefined_column OR lock_not_available THEN
CONTINUE;
END;
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
/** @} */
/** @defgroup agg_token_type Type for the result of aggregate queries
*
* Custom type agg_token for a provenance semimodule value, to
* be used in attributes that are computed as a result of aggregation.
* As for provenance tokens, this is simply a UUID, but this UUID is
* displayed in a specific way (as the result of the aggregation
* followed by a "(*)") to help with readability.
*
* The text output is controlled by the
* provsql.aggtoken_text_as_uuid GUC. By default it is off and
* the cell renders as "value (*)". When set to on (typical
* for UI layers such as ProvSQL Studio), the cell renders as the
* underlying UUID instead, so the caller can click through to the
* provenance circuit; the value side is then recovered via
* provsql.agg_token_value_text(uuid).
*
* @{
*/
CREATE TYPE agg_token;
/** @brief Input function for the agg_token type (parses text representation) */
CREATE OR REPLACE FUNCTION agg_token_in(cstring)
RETURNS agg_token
AS 'provsql','agg_token_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Output function for the agg_token type
*
* Default: produces the human-friendly @c "value (*)" form, where
* @c value is the running aggregate state.
*
* When the @c provsql.aggtoken_text_as_uuid GUC is on, returns the
* underlying provenance UUID instead. UI layers (notably ProvSQL
* Studio) flip this on per session so aggregate cells expose the
* circuit root UUID for click-through; the @c "value (*)" display
* string is recovered via @c provsql.agg_token_value_text(uuid).
*
* Marked STABLE rather than IMMUTABLE because the chosen output
* shape now depends on a GUC that the same session can flip at
* runtime.
*/
CREATE OR REPLACE FUNCTION agg_token_out(agg_token)
RETURNS cstring
AS 'provsql','agg_token_out' LANGUAGE C STABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to its text representation */
CREATE OR REPLACE FUNCTION agg_token_cast(agg_token)
RETURNS text
AS 'provsql','agg_token_cast' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE TYPE agg_token (
internallength = 117,
input = agg_token_in,
output = agg_token_out,
alignment = char
);
/** @brief Extract the UUID from an agg_token (implicit cast to UUID) */
CREATE OR REPLACE FUNCTION agg_token_uuid(aggtok agg_token)
RETURNS uuid AS
$$
BEGIN
RETURN agg_token_cast(aggtok)::uuid;
END
$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE PARALLEL SAFE;
/** @brief Implicit PostgreSQL cast from agg_token to UUID (delegates to agg_token_uuid()) */
CREATE CAST (agg_token AS UUID) WITH FUNCTION agg_token_uuid(agg_token) AS IMPLICIT;
/** @brief Whether @p token carries a value but records none in the actual
* data: an aggregate over no row there, a value gate without a constant.
*
* Told apart from a value this reading does not take -- a timestamp, which
* @c agg_gate_value gives up on because it reads numbers, or a random
* variable, which has no value in the actual data at all -- because the two
* call for opposite answers in @c agg_guard_holds: a comparison with a side
* that HAS no value there does not hold, while one whose value is simply not
* read leaves the truth undecided (internal use). */
CREATE OR REPLACE FUNCTION agg_gate_value_missing(token uuid)
RETURNS boolean AS
$$
DECLARE
gt provsql.provenance_gate := provsql.get_gate_type(token);
ch uuid[];
BEGIN
IF gt IN ('agg', 'arith', 'value') THEN
RETURN provsql.get_extra(token) IS NULL;
ELSIF gt = 'semimod' THEN
ch := provsql.get_children(token);
RETURN array_length(ch, 1) = 2
AND provsql.get_extra(ch[2]) IS NULL;
ELSIF gt = 'conditioned' THEN
ch := provsql.get_children(token);
RETURN array_length(ch, 1) >= 1
AND provsql.agg_gate_value_missing(ch[1]);
END IF;
/* Any other gate: nothing is claimed */
RETURN false;
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/**
* @brief The text a value-carrying gate records in the actual data, whatever
* type it is of.
*
* @c agg_gate_value reads that text as a number and gives up on anything else.
* A Boolean aggregate's value is one of those: @c "true" is no number, and yet
* @c = and @c <> compare it, which is what the guards of a
* @c "bool_or(flag)::int" need. Descends the value-carrying gates as
* @c agg_gate_value does, and answers @c NULL for a gate that records no value
* of its own (internal use).
*/
CREATE OR REPLACE FUNCTION agg_gate_value_text(token uuid)
RETURNS text AS
$$
DECLARE
gt provsql.provenance_gate := provsql.get_gate_type(token);
BEGIN
IF gt IN ('agg', 'arith', 'value') THEN
RETURN provsql.get_extra(token);
ELSIF gt = 'semimod' THEN
RETURN provsql.agg_gate_value_text((provsql.get_children(token))[2]);
ELSIF gt = 'conditioned' THEN
RETURN provsql.agg_gate_value_text((provsql.get_children(token))[1]);
END IF;
RETURN NULL;
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/**
* @brief Deterministic truth of a Boolean guard sub-circuit over aggregate
* comparisons, evaluated in the actual world (all input tuples present).
*
* Guards are the shapes @c having_Expr_to_provenance_cmp mints: @c cmp gates
* over aggregate-valued children (comparison-operator OID in @c info1),
* @c times / @c plus combinations (AND / OR, with negation pushed into the
* comparison operators), and the @c one / @c zero indicators of regular
* (aggregate-free) conditions. Uses Kleene three-valued logic: returns
* @c NULL on any other gate shape, or when an operand's deterministic value
* cannot be resolved.
*/
CREATE OR REPLACE FUNCTION agg_guard_holds(token UUID)
RETURNS boolean AS
$$
DECLARE
gt provenance_gate := get_gate_type(token);
ch uuid[];
opname text;
l numeric;
r numeric;
lt text;
rt text;
all_true boolean;
any_true boolean;
any_null boolean;
BEGIN
IF gt = 'one' THEN
RETURN true;
ELSIF gt = 'zero' THEN
RETURN false;
ELSIF gt IN ('times', 'plus') THEN
SELECT bool_and(h), bool_or(h), bool_or(h IS NULL)
INTO all_true, any_true, any_null
FROM (SELECT provsql.agg_guard_holds(c) AS h
FROM unnest(get_children(token)) AS c) AS s;
IF gt = 'times' THEN
-- AND: false dominates unknown (bool_and skips NULL inputs, so it is
-- false exactly when some child is false).
RETURN CASE WHEN NOT all_true THEN false
WHEN any_null THEN NULL
ELSE true END;
ELSE
-- OR: true dominates unknown.
RETURN CASE WHEN any_true THEN true
WHEN any_null THEN NULL
ELSE false END;
END IF;
ELSIF gt = 'cmp' THEN
ch := get_children(token);
l := agg_gate_value(ch[1]);
r := agg_gate_value(ch[2]);
IF l IS NULL OR r IS NULL THEN
/* A side with no value in the actual data -- an aggregate over no row
* there, as a group kept only for other worlds is -- makes the
* comparison unknown there, which is what provenance_cmp annotates
* zero: it does not hold. A side whose value this reading does not
* take, a timestamp or a random variable, leaves the truth undecided
* instead: the value is there, only not as a number. */
IF agg_gate_value_missing(ch[1]) OR agg_gate_value_missing(ch[2]) THEN
RETURN false;
END IF;
/* A Boolean pair: the value is there, only not as a number, and = / <>
* compare it all the same -- the guards a "bool_or(flag)::int" lowers to
* are these. The literals are checked against a list rather than cast
* inside an exception block, which a parallel worker cannot afford, for
* the reason agg_gate_value reads its number with a regex. */
lt := lower(agg_gate_value_text(ch[1]));
rt := lower(agg_gate_value_text(ch[2]));
IF lt IN ('true', 'false', 't', 'f') AND
rt IN ('true', 'false', 't', 'f') THEN
SELECT oprname INTO opname
FROM pg_catalog.pg_operator WHERE oid = (get_infos(token)).info1;
IF opname = '=' THEN
RETURN lt::boolean = rt::boolean;
ELSIF opname = '<>' THEN
RETURN lt::boolean <> rt::boolean;
END IF;
END IF;
RETURN NULL;
END IF;
SELECT oprname INTO opname
FROM pg_catalog.pg_operator WHERE oid = (get_infos(token)).info1;
RETURN CASE opname
WHEN '<' THEN l < r
WHEN '<=' THEN l <= r
WHEN '=' THEN l = r
WHEN '<>' THEN l <> r
WHEN '>=' THEN l >= r
WHEN '>' THEN l > r
END;
ELSIF gt IN ('input', 'delta', 'monus', 'project', 'eq', 'mulinput',
'assumed', 'annotation') THEN
/* An ordinary provenance expression, not a comparison: the guard a
* COALESCE over an aggregate lowers to is the NullTest one,
* delta(+Kn) for IS NOT NULL and 1 - +Kn for IS NULL. Such a guard
* holds in the actual data exactly when its Boolean provenance does
* with every input row present, which is what plain_truth reads. */
RETURN provsql.plain_truth(token);
END IF;
RETURN NULL;
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/**
* @brief Deterministic (actual-world) scalar value of an aggregate-carrying
* gate.
*
* Resolves the value an aggregate expression takes on the actual data -- the
* value an @c agg_token display cell carries: @c agg / @c arith gates record
* it in @c extra (set by aggregate evaluation and @c agg_arith_make), a
* @c value gate carries its constant, a @c semimod wraps a value gate, a
* @c conditioned gate has its target's value, and a @c case gate selects the
* first branch whose guard holds in the actual world (per
* @c agg_guard_holds), else the default. Returns @c NULL when the gate is
* not aggregate-carrying or the value cannot be resolved (e.g. a
* non-numeric aggregate).
*/
CREATE OR REPLACE FUNCTION agg_gate_value(token UUID)
RETURNS numeric AS
$$
DECLARE
gt provenance_gate := get_gate_type(token);
ch uuid[];
n integer;
holds boolean;
extra text;
BEGIN
IF gt IN ('agg', 'arith', 'value') THEN
/* Reading the text as a number without a PL/pgSQL exception block, which
* a parallel worker cannot afford: entering one starts a subtransaction,
* and this function is called from the evaluator, which runs wherever the
* query does (PostgreSQL 11 raises "cannot start subtransactions during a
* parallel operation"). What is not the text of a number is the value of
* a non-numeric aggregate (a min over text, a timestamp), which this
* reading does not take. */
extra := get_extra(token);
IF extra ~ '^\s*([-+]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][-+]?[0-9]+)?|[Nn][Aa][Nn])\s*$' THEN
RETURN extra::numeric;
END IF;
RETURN NULL;
ELSIF gt = 'semimod' THEN
RETURN agg_gate_value((get_children(token))[2]);
ELSIF gt = 'conditioned' THEN
RETURN agg_gate_value((get_children(token))[1]);
ELSIF gt = 'case' THEN
ch := get_children(token);
n := array_length(ch, 1);
FOR i IN 1 .. (n - 1) / 2 LOOP
holds := agg_guard_holds(ch[2 * i - 1]);
IF holds IS NULL THEN
RETURN NULL;
ELSIF holds THEN
RETURN agg_gate_value(ch[2 * i]);
END IF;
END LOOP;
RETURN agg_gate_value(ch[n]);
END IF;
RETURN NULL;
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/**
* @brief Recover the @c "value (*)" display string for an aggregation gate
*
* Companion helper to the @c provsql.aggtoken_text_as_uuid GUC. With
* the GUC on, an @c agg_token cell prints as the underlying provenance
* UUID, which is convenient for tooling that wants to click through to
* the circuit but loses the human-readable aggregate value. This
* function takes such a UUID and returns the original @c "value (*)"
* string by reading the gate's @c extra (set by aggregate evaluation
* for @c agg gates, and by @c agg_arith_make for the @c arith gates
* that agg_token arithmetic mints); for the other aggregate-carrying
* gates (@c case, @c conditioned, @c semimod, @c value) the value is
* resolved through the circuit by @c agg_gate_value. Returns @c NULL
* if @p token does not resolve to an aggregate-carrying gate.
*
* @param token UUID of an @c agg gate (typically obtained from an
* @c agg_token cell when @c aggtoken_text_as_uuid is on,
* or via a manual UUID cast otherwise).
*/
CREATE OR REPLACE FUNCTION agg_token_value_text(token UUID)
RETURNS text AS
$$
SELECT CASE
-- agg gates: extra is set by aggregate evaluation, in the aggregate's own
-- type, so it reads as it is.
WHEN provsql.get_gate_type(token) = 'agg'
THEN provsql.get_extra(token) || ' (*)'
-- arith gates: extra is the value agg_arith_make computed, in numeric
-- whatever the expression's type, so it is read in that type -- a double
-- precision result prints its 16 digits and not numeric's twenty.
WHEN provsql.get_gate_type(token) = 'arith'
THEN CASE provsql.agg_token_value_type(token)
WHEN 'float8'::regtype::oid
THEN (provsql.get_extra(token)::numeric::float8)::text || ' (*)'
WHEN 'float4'::regtype::oid
THEN (provsql.get_extra(token)::numeric::float4)::text || ' (*)'
ELSE provsql.get_extra(token) || ' (*)'
END
-- other aggregate-carrying gates: resolve the actual-world value
-- through the circuit.
WHEN provsql.get_gate_type(token) IN ('case', 'conditioned', 'semimod', 'value')
THEN provsql.agg_gate_value(token)::text || ' (*)'
ELSE NULL
END;
$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to numeric (extracts the aggregate value, loses provenance) */
CREATE OR REPLACE FUNCTION agg_token_to_numeric(agg_token)
RETURNS numeric
AS 'provsql','agg_token_to_numeric' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to double precision (extracts the aggregate value, loses provenance) */
CREATE OR REPLACE FUNCTION agg_token_to_float8(agg_token)
RETURNS double precision
AS 'provsql','agg_token_to_float8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to integer (extracts the aggregate value, loses provenance) */
CREATE OR REPLACE FUNCTION agg_token_to_int4(agg_token)
RETURNS integer
AS 'provsql','agg_token_to_int4' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to bigint (extracts the aggregate value, loses provenance) */
CREATE OR REPLACE FUNCTION agg_token_to_int8(agg_token)
RETURNS bigint
AS 'provsql','agg_token_to_int8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to boolean (extracts the aggregate value, loses provenance) */
CREATE OR REPLACE FUNCTION agg_token_to_bool(agg_token)
RETURNS boolean
AS 'provsql','agg_token_to_bool' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Cast an agg_token to text (extracts the aggregate value, loses provenance) */
CREATE OR REPLACE FUNCTION agg_token_to_text(agg_token)
RETURNS text
AS 'provsql','agg_token_to_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Assignment cast from agg_token to numeric (extracts the scalar
* value, dropping provenance). ASSIGNMENT, not IMPLICIT: provenance-
* preserving arithmetic on aggregates is provided by the native
* agg_token operators below, so an implicit numeric coercion would only
* silently steal `s + 1` away from them (and reroute it differently
* depending on whether provsql is in search_path). Write `s::numeric`
* to opt into the lossy scalar. */
CREATE CAST (agg_token AS numeric) WITH FUNCTION agg_token_to_numeric(agg_token) AS ASSIGNMENT;
-- ---------------------------------------------------------------------
-- Arithmetic on aggregates (agg_token)
--
-- Mirrors the random_variable arithmetic surface: the operators build a
-- `gate_arith` over the operand provenance UUIDs (via provenance_arith,
-- info1 = PROVSQL_ARITH_*), so the arithmetic is recorded symbolically
-- in the circuit and can be resolved when a comparison (gate_cmp) over
-- the result is evaluated. Unlike random_variable (a bare UUID), an
-- agg_token also carries a running scalar value, so each operator
-- additionally computes the resulting value and bundles it back with the
-- new gate.
-- ---------------------------------------------------------------------
/** @brief Running value of an agg_token as numeric, without the
* provenance-loss warning the public cast emits (internal use). */
CREATE OR REPLACE FUNCTION agg_token_value(agg_token)
RETURNS numeric
AS 'provsql','agg_token_value' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Value of an agg_token as text, NULL for a NULL value, without the
* provenance-loss warning the public cast emits: the sort key of an
* ORDER BY on an aggregate result (internal use). */
CREATE FUNCTION agg_token_plain_text(agg_token)
RETURNS text
AS 'provsql','agg_token_plain_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief The children of the aggregation gate @p token, one per
* contribution, to explode an aggregate result into rows (a join on it,
* @c explode_table). Only for @c choose(), whose value is one of its
* contributions; any other aggregate is refused, its value being none of
* them (@c count(*) contributes a 1 per row) (internal use). */
CREATE FUNCTION agg_token_explode_children(token uuid)
RETURNS uuid[] AS
$$
BEGIN
IF provsql.get_gate_type(token) <> 'agg' THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = 'ProvSQL: only the result of an aggregate can be exploded '
'into rows',
DETAIL = 'provsql-reason: explode-not-an-aggregate; scope: deliberate';
END IF;
IF (provsql.get_infos(token)).info1 <>
'provsql.choose(anyelement)'::regprocedure::oid THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() cannot be exploded into '
'rows, one per value it aggregates: only that of '
'choose() is one of them; compare it in a HAVING '
'clause, or mark it plain() to read its plain value',
(SELECT proname FROM pg_catalog.pg_proc
WHERE oid = (provsql.get_infos(token)).info1)),
DETAIL = 'provsql-reason: explode-rows-aggregate-kind; scope: gap';
END IF;
RETURN provsql.get_children(token);
END
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
/** @brief The values the aggregate result @p token takes over the possible
* worlds, as text, to explode that result into one row per value: the value
* of an aggregate read as data (a GROUP BY key, a DISTINCT, an arm of a set
* operation). The planner annotates the row of a value @c v with the
* comparison gate @c [token @c = @c v], so that the rows of one group are
* pairwise exclusive and exactly one of them is in each world where the
* group is.
*
* A @c count() takes every number of its contributions, a @c min(), a
* @c max() and a @c choose() one of their contributed values. Any other
* aggregate is refused (SQLSTATE 0A000): the values of a @c sum() are its
* subset sums, those of a @c string_agg() one per ordering, and reading
* them off the contributions one by one would be wrong. A NULL
* contribution is refused as well: whether the result is NULL is then a
* value of its own, which a comparison cannot express (internal use). */
CREATE FUNCTION agg_possible_values(input anyelement,
with_null boolean DEFAULT false)
RETURNS text[] AS
$$
DECLARE
max_values CONSTANT int := 1000; -- an explosion multiplies the rows
token uuid;
fn text;
ns text;
vals text[];
n int;
counted int;
first int;
sums numeric[];
one numeric;
scalar_agg boolean;
arms uuid[];
cv uuid;
cvals text[];
BEGIN
/* The planner hands the aggregate result itself (an agg_token), whatever
* the type the query declares for that column. */
IF pg_typeof(input) <> 'provsql.agg_token'::regtype THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = 'ProvSQL: only the result of an aggregate can be exploded '
'into one row per value it takes over the possible worlds',
DETAIL = 'provsql-reason: explode-not-an-aggregate; scope: deliberate';
END IF;
token := input::uuid;
IF provsql.get_gate_type(token) = 'case' THEN
/* A guarded selection -- COALESCE(sum(x), 0), a CASE over aggregates --
* takes the values of its arms, [guard_1, value_1, ..., default]: an
* aggregate's own, or a constant's. Each arm is selected where its
* aggregate has a value, so an aggregate arm offers no NULL. */
arms := provsql.get_children(token);
vals := ARRAY[]::text[];
FOR i IN 1 .. coalesce(array_length(arms, 1), 0) LOOP
CONTINUE WHEN i % 2 = 1 AND i < array_length(arms, 1);
CASE provsql.get_gate_type(arms[i])
WHEN 'value' THEN
/* gate_null(), the value of an aggregate with no contribution at
* all, is never selected: its guard, that the aggregate has a value,
* holds in no world. The guarded selections exploded here
* (COALESCE onto a constant) are never NULL. */
IF coalesce(provsql.get_extra(arms[i]), '') NOT IN ('', 'NULL') THEN
vals := vals || provsql.get_extra(arms[i]);
END IF;
WHEN 'agg', 'case' THEN
vals := vals || provsql.agg_possible_values(
provsql.agg_token_make(arms[i], 0));
ELSE
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = 'ProvSQL: an arithmetic expression over aggregate '
'results cannot be exploded into one row per value it '
'takes over the possible worlds',
DETAIL = 'provsql-reason: explode-arithmetic; scope: gap';
END CASE;
END LOOP;
vals := ARRAY(SELECT DISTINCT v FROM unnest(vals) AS v);
IF array_length(vals, 1) > max_values THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: this expression over aggregate results '
'takes more than %s values over the possible worlds, '
'too many to explode it into one row per value',
max_values),
HINT = 'mark it plain() to read its plain value',
DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
END IF;
RETURN ARRAY(SELECT v FROM unnest(vals) AS v
ORDER BY v::numeric NULLS LAST);
END IF;
IF provsql.get_gate_type(token) <> 'agg' THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = 'ProvSQL: an arithmetic expression over aggregate results '
'cannot be exploded into one row per value it takes over '
'the possible worlds',
DETAIL = 'provsql-reason: explode-arithmetic; scope: gap';
END IF;
SELECT p.proname, s.nspname INTO fn, ns
FROM pg_catalog.pg_proc p
JOIN pg_catalog.pg_namespace s ON s.oid = p.pronamespace
WHERE p.oid = (provsql.get_infos(token)).info1;
vals := ARRAY(SELECT provsql.get_extra((provsql.get_children(sm))[2])
FROM unnest(provsql.get_children(token)) AS sm);
n := coalesce(array_length(vals, 1), 0);
/* NULL is a value of the aggregate like any other -- the one it takes where
* no row contributes, which an aggregation over the whole table reaches in
* the world holding none of its rows (a grouped one has no row there at all).
* Whether to offer it is the caller's to decide: the rewriting asks for it
* only where it can annotate that row with the aggregate having no value,
* and never for a count, which is 0 rather than NULL over no row. */
scalar_agg := with_null;
IF (ns, fn) = ('pg_catalog', 'count') THEN
/* A count contributes 1 per row it counts and 0 per row it does not -- a
* row whose value is NULL, as the null-padded row of an outer join is --
* so its values are the numbers of rows counted, up to how many there are
* to count. Zero is one of them where a row that is not counted can be
* the only one there, or where the aggregation is over the whole table; a
* group of counted rows only has none of its rows in no world, a group
* being no group without a row. */
counted := (SELECT count(*) FROM unnest(vals) AS v
WHERE v IS NOT NULL AND v <> '0');
first := CASE WHEN (provsql.get_infos(token)).info2 < 0 OR counted < n
THEN 0 ELSE 1 END;
IF counted + 1 - first > max_values THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() aggregates %s rows, '
'so it takes too many values over the possible '
'worlds to explode it into one row per value',
fn, n),
HINT = 'mark it plain() to read its plain value',
DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
END IF;
RETURN ARRAY(SELECT i::text
FROM generate_series(least(first, counted), counted) AS i);
END IF;
IF (ns, fn) NOT IN (('pg_catalog', 'min'), ('pg_catalog', 'max'),
('pg_catalog', 'sum'), ('provsql', 'choose')) THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() cannot be exploded into '
'one row per value it takes over the possible worlds: '
'only count(), min(), max(), sum() and choose() have '
'values that can be enumerated', fn),
HINT = 'compare it in a HAVING clause, or mark it plain() to read its '
'plain value',
DETAIL = 'provsql-reason: explode-aggregate-kind; scope: gap';
END IF;
/* An aggregate of aggregate results -- sum(revenue) over a view of
* totals -- has contributions that are no constants but aggregate results
* themselves, each taking several values: their own (agg_possible_values
* of their gate), a NULL among them contributing nothing. A sum takes the
* subset sums choosing one value of each contribution there, a minimum
* (maximum, choice) one of their values. The combinations no world
* realises (two contributions that depend on each other) are emitted too,
* and their rows have a provenance of zero. */
IF EXISTS (SELECT 1 FROM unnest(provsql.get_children(token)) AS sm
WHERE provsql.get_gate_type(sm) = 'semimod'
AND provsql.get_gate_type((provsql.get_children(sm))[2])
<> 'value') THEN
sums := ARRAY[]::numeric[];
vals := ARRAY[]::text[];
FOR cv IN SELECT (provsql.get_children(sm))[2]
FROM unnest(provsql.get_children(token)) AS sm
WHERE provsql.get_gate_type(sm) = 'semimod' LOOP
cvals := CASE WHEN provsql.get_gate_type(cv) = 'value'
THEN ARRAY[provsql.get_extra(cv)]
ELSE provsql.agg_possible_values(
provsql.agg_token_make(cv, 0)) END;
cvals := ARRAY(SELECT v FROM unnest(cvals) AS v
WHERE coalesce(v, '') NOT IN ('', 'NULL'));
IF (ns, fn) = ('pg_catalog', 'sum') THEN
sums := ARRAY(SELECT DISTINCT s FROM
(SELECT unnest(sums) AS s
UNION ALL SELECT v::numeric FROM unnest(cvals) AS v
UNION ALL SELECT a + b::numeric
FROM unnest(sums) AS a, unnest(cvals) AS b)
AS u);
ELSE
vals := ARRAY(SELECT DISTINCT v FROM
(SELECT unnest(vals) AS v
UNION ALL SELECT unnest(cvals)) AS u);
END IF;
IF coalesce(array_length(sums, 1), 0) > max_values
OR coalesce(array_length(vals, 1), 0) > max_values THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() takes more than %s '
'values over the possible worlds, too many to '
'explode it into one row per value', fn,
max_values),
HINT = 'mark it plain() to read its plain value',
DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
END IF;
END LOOP;
IF (ns, fn) = ('pg_catalog', 'sum') THEN
RETURN ARRAY(SELECT s::text FROM unnest(sums) AS s ORDER BY s)
|| CASE WHEN scalar_agg THEN ARRAY[NULL::text]
ELSE ARRAY[]::text[] END;
END IF;
RETURN ARRAY(SELECT v FROM unnest(vals) AS v ORDER BY v)
|| CASE WHEN scalar_agg THEN ARRAY[NULL::text]
ELSE ARRAY[]::text[] END;
END IF;
IF EXISTS (SELECT 1 FROM unnest(vals) AS v WHERE v IS NULL) THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() aggregates a NULL value, '
'so it cannot be exploded into one row per value it '
'takes over the possible worlds: a comparison with '
'NULL does not say that the result is NULL', fn),
DETAIL = 'provsql-reason: explode-null-value; scope: gap';
END IF;
IF (ns, fn) = ('pg_catalog', 'sum') THEN
/* The value of a sum is the sum of the rows that are there, so its values
* are its subset sums, reached by adding the contributions one at a time,
* equal sums collapsing (three rows of 1 take three values, not eight).
* The empty subset is the NULL above, which a scalar aggregation takes and
* a grouped one does not (a group without a row is no group). The planner only sends sums over an
* integer column here, whose subset sums the evaluator's own arithmetic
* reaches exactly. */
sums := ARRAY[]::numeric[];
FOREACH one IN ARRAY ARRAY(SELECT v::numeric FROM unnest(vals) AS v) LOOP
sums := ARRAY(SELECT DISTINCT s FROM
(SELECT unnest(sums) AS s
UNION ALL SELECT one
UNION ALL SELECT unnest(sums) + one) AS u);
IF array_length(sums, 1) > max_values THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() takes more than %s '
'values over the possible worlds, too many to '
'explode it into one row per value', fn,
max_values),
HINT = 'mark it plain() to read its plain value',
DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
END IF;
END LOOP;
RETURN ARRAY(SELECT s::text FROM unnest(sums) AS s ORDER BY s)
|| CASE WHEN scalar_agg THEN ARRAY[NULL::text]
ELSE ARRAY[]::text[] END;
END IF;
IF n > max_values THEN
RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
MESSAGE = format('ProvSQL: the result of %s() aggregates %s rows, so '
'it takes too many values over the possible worlds to '
'explode it into one row per value', fn, n),
HINT = 'mark it plain() to read its plain value',
DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
END IF;
/* One row per distinct contributed value: each is the minimum (maximum,
* choice) of the world where only its own row is. */
RETURN ARRAY(SELECT DISTINCT v FROM unnest(vals) AS v ORDER BY v)
|| CASE WHEN scalar_agg THEN ARRAY[NULL::text]
ELSE ARRAY[]::text[] END;
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE;
/** @brief Value of an agg_token as text, NULL for a NULL value, without the
* provenance-loss warning of the public casts: the value of an aggregate
* result read as a plain value where ProvSQL casts it (in a function, an
* operator, a comparison), which the planner reports once as evaluated as
* plain SQL (internal use). */
CREATE FUNCTION agg_token_frozen_value(agg_token)
RETURNS text
AS 'provsql','agg_token_plain_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Bundle a provenance gate UUID with a running value into an
* agg_token (inverse of the agg_token_uuid / agg_token_value
* accessors). */
CREATE OR REPLACE FUNCTION agg_token_make(tok uuid, val numeric)
RETURNS agg_token AS
$$
SELECT format('( %s , %s )', tok::text, val::text)::provsql.agg_token;
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/** @brief Lift a scalar numeric constant into a gate_value leaf and
* return its UUID, so it can be a child of a gate_arith (the agg-side
* analogue of as_random for random_variable). */
CREATE OR REPLACE FUNCTION agg_value_gate(v numeric)
RETURNS uuid AS
$$
DECLARE
token uuid := public.uuid_generate_v5(
provsql.uuid_ns_provsql(), concat('value', v::text));
BEGIN
PERFORM provsql.create_gate(token, 'value', NULL, NULL, NULL, v::text);
RETURN token;
END
$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/**
* @brief The SQL type of the value an @c agg_token carries.
*
* An @c agg gate holds the aggregate's own result type in @c info2, next to
* the scalar flag in its top bit; an @c arith gate holds the result type of
* the operation it stands for, put there by @c agg_arith_make. Every other
* aggregate-carrying gate holds a value the rewriting computed in @c numeric,
* which is what it answers for them.
*/
CREATE OR REPLACE FUNCTION agg_token_value_type(token UUID)
RETURNS oid AS
$$
SELECT CASE provsql.get_gate_type(token)
WHEN 'agg' THEN (i.info2 & 2147483647)::oid
WHEN 'arith' THEN CASE WHEN coalesce(i.info2, 0) = 0
THEN 'numeric'::regtype::oid ELSE i.info2::oid END
ELSE 'numeric'::regtype::oid
END
FROM provsql.get_infos(token) i;
$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/**
* @brief The type the operation @p op over @p children answers in, by SQL's
* own numeric tower.
*
* The gate computes in @c numeric whatever the types are -- it is the value
* that is stored, and a wider one loses nothing -- but the value is READ in
* the type the query's expression has, and @c double @c precision prints 16
* digits where @c numeric prints every one it holds. Recording the type is
* what lets the reading round as SQL does; deriving it from the children makes
* it a function of the gate, so two statements building the same expression
* still agree on its token.
*
* Two operations are not the tower: the integer division SQL writes as @c "/"
* over integers (the @c INTDIV gate) answers in the integer type, and
* @c round(v, d) exists only over @c numeric, so it answers in @c numeric.
*/
CREATE OR REPLACE FUNCTION agg_arith_result_type(op int, children uuid[])
RETURNS oid AS
$$
DECLARE
t oid;
c uuid;
has_float8 boolean := false;
has_float4 boolean := false;
has_numeric boolean := false;
widest_int oid := NULL;
BEGIN
IF children IS NULL THEN
RETURN 'numeric'::regtype::oid;
END IF;
FOREACH c IN ARRAY children LOOP
t := provsql.agg_token_value_type(c);
IF t = 'float8'::regtype::oid THEN has_float8 := true;
ELSIF t = 'float4'::regtype::oid THEN has_float4 := true;
ELSIF t = 'numeric'::regtype::oid THEN has_numeric := true;
ELSIF t = 'int8'::regtype::oid THEN widest_int := 'int8'::regtype::oid;
ELSIF t = 'int4'::regtype::oid THEN
widest_int := coalesce(nullif(widest_int, 'int2'::regtype::oid),
'int4'::regtype::oid);
ELSIF t = 'int2'::regtype::oid THEN
widest_int := coalesce(widest_int, 'int2'::regtype::oid);
END IF;
END LOOP;
IF op = 16 THEN -- the value as double precision reads it
RETURN 'float8'::regtype::oid;
END IF;
IF op = 17 THEN -- ... as real reads it
RETURN 'float4'::regtype::oid;
END IF;
IF op = 11 THEN -- INTDIV
RETURN coalesce(widest_int, 'int8'::regtype::oid);
END IF;
IF op = 12 AND array_length(children, 1) = 2 THEN -- round(v, digits)
RETURN 'numeric'::regtype::oid;
END IF;
IF has_float8 THEN
RETURN 'float8'::regtype::oid;
END IF;
IF has_float4 THEN -- no real-with-numeric operator: SQL widens both
RETURN CASE WHEN has_numeric THEN 'float8'::regtype::oid
ELSE 'float4'::regtype::oid END;
END IF;
IF has_numeric THEN
RETURN 'numeric'::regtype::oid;
END IF;
RETURN coalesce(widest_int, 'numeric'::regtype::oid);
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/** @brief Mint (or reuse) the gate_arith for an agg_token arithmetic
* result and return the agg_token carrying it.
*
* The gate records the computed scalar in its @c extra, in @c numeric --
* exactly what aggregate evaluation does for @c agg gates -- and, in
* @c info2, the SQL type the expression has (@c agg_arith_result_type), so
* that @c agg_token_value_text recovers the @c "value (*)" display from the
* bare UUID, read as SQL reads it (as ProvSQL Studio does for result cells
* under @c provsql.aggtoken_text_as_uuid). A NULL value (a division by zero
* on a row the database as it is may not have) keeps the gate. The gate UUID
* is deterministic in (op, children), so re-recording the (identical) value
* is idempotent. */
CREATE OR REPLACE FUNCTION agg_arith_make(op int, children uuid[], val numeric)
RETURNS agg_token AS
$$
DECLARE
token uuid := public.uuid_generate_v5(
provsql.uuid_ns_provsql(), concat('arith', op::text, children::text));
restype oid;
shown text;
BEGIN
IF op IS NULL OR children IS NULL THEN
RETURN NULL;
END IF;
-- The gate keeps the value as it was computed, in numeric: that is what the
-- evaluators read, and the wider type loses nothing. The type of the
-- expression goes beside it, so the value can be READ as SQL reads it.
restype := provsql.agg_arith_result_type(op, children);
PERFORM provsql.create_gate(token, 'arith', children, op, restype::int,
val::text);
-- A NULL value (a division by zero on a row the database as it is may not
-- have) keeps the gate: its value in the other worlds is in the circuit.
shown := CASE
WHEN val IS NULL THEN 'NULL'
WHEN restype = 'float8'::regtype::oid THEN (val::float8)::text
WHEN restype = 'float4'::regtype::oid THEN (val::float4)::text
ELSE val::text
END;
RETURN format('( %s , %s )', token::text, shown)::provsql.agg_token;
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
-- agg_token \ agg_token --------------------------------------------
/** @brief agg_token + agg_token (gate_arith PLUS). */
CREATE OR REPLACE FUNCTION agg_token_plus(a agg_token, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::uuid, (b)::uuid],
provsql.agg_token_value(a) + provsql.agg_token_value(b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token - agg_token (gate_arith MINUS). */
CREATE OR REPLACE FUNCTION agg_token_minus(a agg_token, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::uuid, (b)::uuid],
provsql.agg_token_value(a) - provsql.agg_token_value(b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token * agg_token (gate_arith TIMES). */
CREATE OR REPLACE FUNCTION agg_token_times(a agg_token, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::uuid, (b)::uuid],
provsql.agg_token_value(a) * provsql.agg_token_value(b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token / agg_token (gate_arith DIV). */
CREATE OR REPLACE FUNCTION agg_token_div(a agg_token, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::uuid, (b)::uuid],
provsql.agg_token_value(a) / NULLIF(provsql.agg_token_value(b), 0)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief Unary -agg_token (gate_arith NEG). */
CREATE OR REPLACE FUNCTION agg_token_neg(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(4, ARRAY[(a)::uuid],
- provsql.agg_token_value(a)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
-- round / floor / ceil / abs of an agg_token ---------------------------
-- Named as the SQL functions they stand for, so that the rewriter's
-- re-resolution of a call whose argument became an agg_token finds them
-- (try_swap_agg_func), exactly as the operators above are found. They compute
-- in numeric, as the operators do, and the gate records the operation so the
-- value is read per possible world; the moment evaluators sample them, since
-- rounding does not commute with expectation.
/** @brief round(agg_token) (gate_arith ROUND). */
CREATE OR REPLACE FUNCTION provsql_round(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(12, ARRAY[(a)::uuid],
pg_catalog.round(provsql.agg_token_value(a))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief round(agg_token, integer): to @p d decimal digits. */
CREATE OR REPLACE FUNCTION provsql_round(a agg_token, d integer)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(12,
ARRAY[(a)::uuid, provsql.agg_value_gate(d::numeric)],
pg_catalog.round(provsql.agg_token_value(a), d)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @cond INTERNAL */
/**
* @brief A cast of an aggregate result to a number, as the function of its
* value that it is.
*
* A cast is a function call like any other, so the rewriting swaps
* @c pg_catalog.numeric(x) over an agg_token for @c provsql.provsql_numeric(agg_token)
* wherever one is declared -- which is how round() and floor() became gate
* operations. Between numbers the value is carried rather than frozen: a
* widening (an integer to @c numeric, to a float) keeps it as it is, and a
* narrowing to an integer is the rounding gate, PostgreSQL's own cast rounding
* half away from zero as that gate does. A cast to text, to a boolean or to a
* date has no arithmetic behind it and stays a reading of the plain value.
*/
CREATE OR REPLACE FUNCTION provsql_numeric(a agg_token)
RETURNS agg_token AS $$ SELECT a $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief float8(agg_token): the value, as a double precision reads it.
*
* Not the identity it once was: the gates compute in @c numeric, and this is
* what says that the expression is a float, so that its value is read as SQL
* reads it -- 2 and not 2.0000000000000000 for @c sum(x)/3 over such a column.
* The gate is the rounding to that type, and every pass that walks arithmetic
* treats it as the value of its child. A token already read in that type is
* returned as it is, so nothing is wrapped twice.
*/
CREATE OR REPLACE FUNCTION provsql_float8(a agg_token)
RETURNS agg_token AS
$$ SELECT CASE
WHEN provsql.agg_token_value_type((a)::uuid) = 'float8'::regtype::oid
THEN a
ELSE provsql.agg_arith_make(16, ARRAY[(a)::uuid],
(provsql.agg_token_value(a)::float8)::numeric)
END $$
LANGUAGE sql STABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief float4(agg_token): the value, as a real reads it (see float8). */
CREATE OR REPLACE FUNCTION provsql_float4(a agg_token)
RETURNS agg_token AS
$$ SELECT CASE
WHEN provsql.agg_token_value_type((a)::uuid) = 'float4'::regtype::oid
THEN a
ELSE provsql.agg_arith_make(17, ARRAY[(a)::uuid],
(provsql.agg_token_value(a)::float4)::numeric)
END $$
LANGUAGE sql STABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief int8(agg_token): the value rounded, as the cast to bigint rounds. */
CREATE OR REPLACE FUNCTION provsql_int8(a agg_token)
RETURNS agg_token AS $$ SELECT provsql.provsql_round(a) $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief int4(agg_token): the value rounded, as the cast to integer rounds. */
CREATE OR REPLACE FUNCTION provsql_int4(a agg_token)
RETURNS agg_token AS $$ SELECT provsql.provsql_round(a) $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief int2(agg_token): the value rounded, as the cast to smallint rounds. */
CREATE OR REPLACE FUNCTION provsql_int2(a agg_token)
RETURNS agg_token AS $$ SELECT provsql.provsql_round(a) $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @endcond */
/** @brief floor(agg_token) (gate_arith FLOOR). */
CREATE OR REPLACE FUNCTION provsql_floor(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(13, ARRAY[(a)::uuid],
pg_catalog.floor(provsql.agg_token_value(a))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief ceil(agg_token) (gate_arith CEIL). */
CREATE OR REPLACE FUNCTION provsql_ceil(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(14, ARRAY[(a)::uuid],
pg_catalog.ceil(provsql.agg_token_value(a))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief ceiling(agg_token), the SQL synonym of ceil. */
CREATE OR REPLACE FUNCTION provsql_ceiling(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(14, ARRAY[(a)::uuid],
pg_catalog.ceil(provsql.agg_token_value(a))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief abs(agg_token) (gate_arith ABS). */
CREATE OR REPLACE FUNCTION provsql_abs(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(15, ARRAY[(a)::uuid],
pg_catalog.abs(provsql.agg_token_value(a))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
-- agg_token \ numeric ----------------------------------------------
/** @brief agg_token + numeric (gate_arith PLUS, constant lifted to a value gate). */
CREATE OR REPLACE FUNCTION agg_token_plus_numeric(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
provsql.agg_token_value(a) + b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token - numeric. */
CREATE OR REPLACE FUNCTION agg_token_minus_numeric(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
provsql.agg_token_value(a) - b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token * numeric. */
CREATE OR REPLACE FUNCTION agg_token_times_numeric(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
provsql.agg_token_value(a) * b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token / numeric. */
CREATE OR REPLACE FUNCTION agg_token_div_numeric(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
provsql.agg_token_value(a) / NULLIF(b, 0)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
-- numeric \ agg_token ----------------------------------------------
/** @brief numeric + agg_token. */
CREATE OR REPLACE FUNCTION numeric_plus_agg_token(a numeric, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(0, ARRAY[provsql.agg_value_gate(a), (b)::uuid],
a + provsql.agg_token_value(b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief numeric - agg_token. */
CREATE OR REPLACE FUNCTION numeric_minus_agg_token(a numeric, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(2, ARRAY[provsql.agg_value_gate(a), (b)::uuid],
a - provsql.agg_token_value(b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief numeric * agg_token. */
CREATE OR REPLACE FUNCTION numeric_times_agg_token(a numeric, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(1, ARRAY[provsql.agg_value_gate(a), (b)::uuid],
a * provsql.agg_token_value(b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief numeric / agg_token. */
CREATE OR REPLACE FUNCTION numeric_div_agg_token(a numeric, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(3, ARRAY[provsql.agg_value_gate(a), (b)::uuid],
a / NULLIF(provsql.agg_token_value(b), 0)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief agg_token / agg_token, in integer division (internal)
*
* The rewriter calls it instead of agg_token_div when both operands were
* integers in the query, as for count(*) / count(x): SQL divides them
* with truncation toward zero, and the displayed value does too. The gate
* is an arith gate of its own operator (11, PROVSQL_ARITH_INTDIV), which
* the evaluators truncate in every world.
*/
CREATE OR REPLACE FUNCTION agg_token_intdiv(a agg_token, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(11, ARRAY[(a)::uuid, (b)::uuid],
trunc(provsql.agg_token_value(a) / NULLIF(provsql.agg_token_value(b), 0))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token / numeric, in integer division (internal; see agg_token_intdiv). */
CREATE OR REPLACE FUNCTION agg_token_intdiv_numeric(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(11, ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
trunc(provsql.agg_token_value(a) / NULLIF(b, 0))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief numeric / agg_token, in integer division (internal; see agg_token_intdiv). */
CREATE OR REPLACE FUNCTION numeric_intdiv_agg_token(a numeric, b agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(11, ARRAY[provsql.agg_value_gate(a), (b)::uuid],
trunc(a / NULLIF(provsql.agg_token_value(b), 0))); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
-- Operator declarations -----------------------------------------------
CREATE OPERATOR + (LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_plus, COMMUTATOR = +);
CREATE OPERATOR - (LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_minus);
CREATE OPERATOR * (LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_times, COMMUTATOR = *);
CREATE OPERATOR / (LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_div);
CREATE OPERATOR - (RIGHTARG=agg_token, PROCEDURE=agg_token_neg);
/* Prefix @ is PostgreSQL's absolute value. Its own procedure is numeric_abs
or int8abs rather than abs, so the swap cannot reach the counterpart by
resolving the operator's function: the operator over agg_token is declared
here instead, and ORDER BY @(2 - max(x)) carries its gate like abs(...). */
CREATE OPERATOR @ (RIGHTARG=agg_token, PROCEDURE=provsql_abs);
/**
* @brief @p fn of the value of @p a, computed in the type the value is read in.
*
* The gates compute in @c numeric, which loses nothing for the four basic
* operations: computing exactly and reading the result as @c double
* @c precision gives what floating-point arithmetic gives, since IEEE requires
* each of them to be correctly rounded. @c sqrt, @c ln and @c exp are not
* like that -- @c numeric computes them to a fixed scale, so @c sqrt(3) comes
* out to fifteen decimals and the value read back differs from SQL's in its
* last digit -- so where the expression is a float, they are computed as SQL
* computes them, in that type, and the numeric the gate stores is that value.
*/
CREATE OR REPLACE FUNCTION agg_transcendental(fn text, a agg_token)
RETURNS numeric AS
$$
SELECT CASE
WHEN provsql.agg_token_value_type((a)::uuid)
IN ('float8'::regtype::oid, 'float4'::regtype::oid)
THEN (CASE fn
WHEN 'sqrt' THEN sqrt(provsql.agg_token_value(a)::float8)
WHEN 'ln' THEN ln(provsql.agg_token_value(a)::float8)
WHEN 'exp' THEN exp(provsql.agg_token_value(a)::float8)
END)::text::numeric /* through the float's own text: the cast to
* numeric keeps fifteen digits, its text
* keeps every one the value has */
ELSE CASE fn
WHEN 'sqrt' THEN sqrt(provsql.agg_token_value(a))
WHEN 'ln' THEN ln(provsql.agg_token_value(a))
WHEN 'exp' THEN exp(provsql.agg_token_value(a))
END
END;
$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public;
/** @brief ln(agg_token) (gate_arith LN): the logarithm of the value the
* aggregate takes, in every world, rather than of the one it takes in the
* database as it is. */
CREATE OR REPLACE FUNCTION provsql_ln(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(8, ARRAY[(a)::uuid],
provsql.agg_transcendental('ln', a)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief exp(agg_token) (gate_arith EXP). */
CREATE OR REPLACE FUNCTION provsql_exp(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(9, ARRAY[(a)::uuid],
provsql.agg_transcendental('exp', a)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief sqrt(agg_token): the square root is the power of one half
* (gate_arith POW, whose exponent is a value gate). */
CREATE OR REPLACE FUNCTION provsql_sqrt(a agg_token)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(7,
ARRAY[(a)::uuid, provsql.agg_value_gate(0.5)],
provsql.agg_transcendental('sqrt', a)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief agg_token ^ numeric (gate_arith POW, constant lifted to a value
* gate). */
CREATE OR REPLACE FUNCTION agg_token_pow_numeric(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(7,
ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
provsql.agg_token_value(a) ^ b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
CREATE OPERATOR ^ (LEFTARG=agg_token, RIGHTARG=numeric,
PROCEDURE=agg_token_pow_numeric);
/** @brief power(agg_token, numeric) (gate_arith POW, the exponent lifted to
* a value gate): what @c power(sum(x), 2) is carried as, the way
* @c sum(x) ^ 2 already is. */
CREATE OR REPLACE FUNCTION provsql_power(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(7,
ARRAY[(a)::uuid, provsql.agg_value_gate(b)],
power(provsql.agg_token_value(a), b)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief power(agg_token, double precision): computed in double precision,
* as SQL computes it, and the exponent stored through its own text, which
* keeps every digit the float has (see @c agg_transcendental). */
CREATE OR REPLACE FUNCTION provsql_power(a agg_token, b double precision)
RETURNS agg_token AS
$$ SELECT provsql.agg_arith_make(7,
ARRAY[(a)::uuid, provsql.agg_value_gate(b::text::numeric)],
power(provsql.agg_token_value(a)::float8, b)::text::numeric); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief pow(agg_token, numeric): @c pow is @c power's other name. */
CREATE OR REPLACE FUNCTION provsql_pow(a agg_token, b numeric)
RETURNS agg_token AS
$$ SELECT provsql.provsql_power(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/** @brief pow(agg_token, double precision). */
CREATE OR REPLACE FUNCTION provsql_pow(a agg_token, b double precision)
RETURNS agg_token AS
$$ SELECT provsql.provsql_power(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
CREATE OPERATOR + (LEFTARG=agg_token, RIGHTARG=numeric, PROCEDURE=agg_token_plus_numeric, COMMUTATOR = +);
CREATE OPERATOR - (LEFTARG=agg_token, RIGHTARG=numeric, PROCEDURE=agg_token_minus_numeric);
CREATE OPERATOR * (LEFTARG=agg_token, RIGHTARG=numeric, PROCEDURE=agg_token_times_numeric, COMMUTATOR = *);
CREATE OPERATOR / (LEFTARG=agg_token, RIGHTARG=numeric, PROCEDURE=agg_token_div_numeric);
CREATE OPERATOR + (LEFTARG=numeric, RIGHTARG=agg_token, PROCEDURE=numeric_plus_agg_token, COMMUTATOR = +);
CREATE OPERATOR - (LEFTARG=numeric, RIGHTARG=agg_token, PROCEDURE=numeric_minus_agg_token);
CREATE OPERATOR * (LEFTARG=numeric, RIGHTARG=agg_token, PROCEDURE=numeric_times_agg_token, COMMUTATOR = *);
CREATE OPERATOR / (LEFTARG=numeric, RIGHTARG=agg_token, PROCEDURE=numeric_div_agg_token);
/** @brief Assignment cast from agg_token to double precision */
CREATE CAST (agg_token AS double precision) WITH FUNCTION agg_token_to_float8(agg_token) AS ASSIGNMENT;
/** @brief Assignment cast from agg_token to integer */
CREATE CAST (agg_token AS integer) WITH FUNCTION agg_token_to_int4(agg_token) AS ASSIGNMENT;
/** @brief Assignment cast from agg_token to bigint */
CREATE CAST (agg_token AS bigint) WITH FUNCTION agg_token_to_int8(agg_token) AS ASSIGNMENT;
/** @brief Assignment cast from agg_token to text (extracts value, not UUID) */
CREATE CAST (agg_token AS text) WITH FUNCTION agg_token_to_text(agg_token) AS ASSIGNMENT;
/** @brief Assignment cast from agg_token to boolean (bool_and, bool_or, every) */
CREATE CAST (agg_token AS boolean) WITH FUNCTION agg_token_to_bool(agg_token) AS ASSIGNMENT;
/**
* @brief Condition a discrete aggregate's distribution on an event:
* @c "SUM(x) | C".
*
* Mirrors @c random_variable_cond for the @c agg_token carrier: returns a
* conditioned @c agg_token that flows onward, its provenance token wrapped in
* the composable two-child @c gate_conditioned @c [agg_target, condition]
* while its running value is preserved. The moment / support dispatchers
* unpack it (@c agg_conditioned_target + @c rv_conditioned_prov) and route
* through the existing @c agg_raw_moment with the condition conjoined into the
* @c prov argument, so @c expected(SUM(x)|C) / @c variance(SUM(x)|C) report
* the conditional aggregate distribution. Nested conditioning folds.
*/
CREATE OR REPLACE FUNCTION agg_token_cond(a agg_token, cond uuid)
RETURNS agg_token AS
$$
DECLARE
tok uuid;
ev uuid;
result uuid;
ch uuid[];
BEGIN
IF cond IS NULL OR cond = gate_one() THEN
RETURN a;
END IF;
tok := (a)::uuid;
IF get_gate_type(tok) = 'conditioned'
AND array_length(get_children(tok), 1) = 2 THEN
ch := get_children(tok);
tok := ch[1];
ev := provenance_times(ch[2], cond);
ELSE
ev := cond;
END IF;
result := public.uuid_generate_v5(uuid_ns_provsql(),
concat('conditioned', tok, ev));
PERFORM create_gate(result, 'conditioned', ARRAY[tok, ev]);
RETURN agg_token_make(result, agg_token_value(a));
END
$$ LANGUAGE plpgsql IMMUTABLE SET search_path=provsql,pg_temp,public
SECURITY DEFINER PARALLEL SAFE;
CREATE OPERATOR | (
LEFTARG = agg_token,
RIGHTARG = uuid,
PROCEDURE = agg_token_cond
);
/**
* @brief Placeholder for @c "SUM(x) | (predicate)" on an agg_token.
*
* Lets the conditioning event be a natural Boolean predicate (e.g.
* @c "SUM(x) | (SUM(x) > 5)") instead of a hand-built gate. Never executes:
* the planner converts the Boolean operand into a condition gate and emits
* @c agg_token_cond.
*/
CREATE OR REPLACE FUNCTION agg_token_cond_predicate(
a agg_token, predicate boolean) RETURNS agg_token AS
$$
BEGIN
RAISE EXCEPTION 'agg_token | (predicate) must be rewritten by the ProvSQL '
'planner hook: the right operand must be a Boolean combination of '
'aggregate / random_variable comparisons (is provsql.active off?)'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR | (
LEFTARG = agg_token,
RIGHTARG = boolean,
PROCEDURE = agg_token_cond_predicate
);
/**
* @brief Unpack the target of a conditioned @c agg_token.
*
* For a @c "SUM(x) | C" whose provenance token is the two-child
* @c gate_conditioned @c [agg_target, condition] returns the agg_token over
* @c agg_target (same running value); for any other agg_token returns it
* unchanged. The conditioning event itself is recovered separately via
* @c rv_conditioned_prov on the token's uuid.
*/
CREATE OR REPLACE FUNCTION agg_conditioned_target(a agg_token)
RETURNS agg_token AS
$$
SELECT CASE
WHEN provsql.get_gate_type((a)::uuid) = 'conditioned'
AND array_length(provsql.get_children((a)::uuid), 1) = 2
THEN provsql.agg_token_make(
(provsql.get_children((a)::uuid))[1], provsql.agg_token_value(a))
ELSE a
END;
$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief Placeholder comparison of agg_token with numeric
*
* This function is never actually called; it exists so the SQL parser
* accepts comparison operators between agg_token and numeric values.
* The ProvSQL query rewriter replaces these comparisons at plan time.
*/
CREATE OR REPLACE FUNCTION agg_token_comp_numeric(a agg_token, b numeric)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RAISE EXCEPTION 'Comparison agg_token-numeric not implemented, should be replaced by ProvSQL behavior'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
END;
$$;
/**
* @brief Placeholder comparison of numeric with agg_token
*
* Symmetric to agg_token_comp_numeric; never actually called.
* The ProvSQL query rewriter replaces these comparisons at plan time.
*/
CREATE OR REPLACE FUNCTION numeric_comp_agg_token(a numeric, b agg_token)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RAISE EXCEPTION 'Comparison numeric-agg_token not implemented, should be replaced by ProvSQL behavior'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
END;
$$;
/** @brief SQL operator agg_token < numeric (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR < (
LEFTARG = agg_token,
RIGHTARG = numeric,
PROCEDURE = agg_token_comp_numeric,
COMMUTATOR = >,
NEGATOR = >=
);
/** @brief SQL operator numeric < agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR < (
LEFTARG = numeric,
RIGHTARG = agg_token,
PROCEDURE = numeric_comp_agg_token,
COMMUTATOR = >,
NEGATOR = >=
);
/** @brief SQL operator agg_token <= numeric (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR <= (
LEFTARG = agg_token,
RIGHTARG = numeric,
PROCEDURE = agg_token_comp_numeric,
COMMUTATOR = >=,
NEGATOR = >
);
/** @brief SQL operator numeric <= agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR <= (
LEFTARG = numeric,
RIGHTARG = agg_token,
PROCEDURE = numeric_comp_agg_token,
COMMUTATOR = >=,
NEGATOR = >
);
/** @brief SQL operator agg_token = numeric (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR = (
LEFTARG = agg_token,
RIGHTARG = numeric,
PROCEDURE = agg_token_comp_numeric,
COMMUTATOR = =,
NEGATOR = <>
);
/** @brief SQL operator numeric = agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR = (
LEFTARG = numeric,
RIGHTARG = agg_token,
PROCEDURE = numeric_comp_agg_token,
COMMUTATOR = =,
NEGATOR = <>
);
/** @brief SQL operator agg_token <> numeric (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR <> (
LEFTARG = agg_token,
RIGHTARG = numeric,
PROCEDURE = agg_token_comp_numeric,
COMMUTATOR = <>,
NEGATOR = =
);
/** @brief SQL operator numeric <> agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR <> (
LEFTARG = numeric,
RIGHTARG = agg_token,
PROCEDURE = numeric_comp_agg_token,
COMMUTATOR = <>,
NEGATOR = =
);
/** @brief SQL operator agg_token >= numeric (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR >= (
LEFTARG = agg_token,
RIGHTARG = numeric,
PROCEDURE = agg_token_comp_numeric,
COMMUTATOR = <=,
NEGATOR = <
);
/** @brief SQL operator numeric >= agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR >= (
LEFTARG = numeric,
RIGHTARG = agg_token,
PROCEDURE = numeric_comp_agg_token,
COMMUTATOR = <=,
NEGATOR = <
);
/** @brief SQL operator agg_token > numeric (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR > (
LEFTARG = agg_token,
RIGHTARG = numeric,
PROCEDURE = agg_token_comp_numeric,
COMMUTATOR = <,
NEGATOR = <=
);
/** @brief SQL operator numeric > agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR > (
LEFTARG = numeric,
RIGHTARG = agg_token,
PROCEDURE = numeric_comp_agg_token,
COMMUTATOR = <,
NEGATOR = <=
);
/**
* @brief Placeholder comparison of two agg_token values (the diagonal)
*
* Never actually called; lets the parser accept agg_token \ agg_token
* (e.g. sum(x) > sum(y) on materialised tokens), which the ProvSQL
* rewriter lowers to a gate_cmp at plan time. Declaring this diagonal
* also disambiguates `s = s2` (otherwise "operator is not unique",
* because both agg_token -> uuid and agg_token -> numeric casts apply).
*/
CREATE OR REPLACE FUNCTION agg_token_comp_agg_token(a agg_token, b agg_token)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RAISE EXCEPTION 'Comparison agg_token-agg_token not implemented, should be replaced by ProvSQL behavior'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
END;
$$;
/** @brief SQL operator agg_token < agg_token (placeholder rewritten at plan time) */
CREATE OPERATOR < (
LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_comp_agg_token,
COMMUTATOR = >, NEGATOR = >=
);
/** @brief SQL operator agg_token <= agg_token (placeholder rewritten at plan time) */
CREATE OPERATOR <= (
LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_comp_agg_token,
COMMUTATOR = >=, NEGATOR = >
);
/** @brief SQL operator agg_token > agg_token (placeholder rewritten at plan time) */
CREATE OPERATOR > (
LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_comp_agg_token,
COMMUTATOR = <, NEGATOR = <=
);
/** @brief SQL operator agg_token >= agg_token (placeholder rewritten at plan time) */
CREATE OPERATOR >= (
LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_comp_agg_token,
COMMUTATOR = <=, NEGATOR = <
);
/** @brief SQL operator agg_token = agg_token (placeholder rewritten at plan time) */
CREATE OPERATOR = (
LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_comp_agg_token,
COMMUTATOR = =, NEGATOR = <>
);
/** @brief SQL operator agg_token <> agg_token (placeholder rewritten at plan time) */
CREATE OPERATOR <> (
LEFTARG=agg_token, RIGHTARG=agg_token, PROCEDURE=agg_token_comp_agg_token,
COMMUTATOR = <>, NEGATOR = =
);
/**
* @brief Placeholder comparison of agg_token with text
*
* This function is never actually called; it exists so the SQL parser
* accepts comparison operators between agg_token and text values.
* The ProvSQL query rewriter replaces these comparisons at plan time.
*/
CREATE OR REPLACE FUNCTION agg_token_comp_text(a agg_token, b text)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RAISE EXCEPTION 'Comparison agg_token-text not implemented, should be replaced by ProvSQL behavior'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
END;
$$;
/**
* @brief Placeholder comparison of text with agg_token
*
* Symmetric to agg_token_comp_text; never actually called.
* The ProvSQL query rewriter replaces these comparisons at plan time.
*/
CREATE OR REPLACE FUNCTION text_comp_agg_token(a text, b agg_token)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RAISE EXCEPTION 'Comparison text-agg_token not implemented, should be replaced by ProvSQL behavior'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
END;
$$;
/** @brief SQL operator agg_token = text (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR = (
LEFTARG = agg_token,
RIGHTARG = text,
PROCEDURE = agg_token_comp_text,
COMMUTATOR = =,
NEGATOR = <>
);
/** @brief SQL operator text = agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR = (
LEFTARG = text,
RIGHTARG = agg_token,
PROCEDURE = text_comp_agg_token,
COMMUTATOR = =,
NEGATOR = <>
);
/** @brief SQL operator agg_token <> text (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR <> (
LEFTARG = agg_token,
RIGHTARG = text,
PROCEDURE = agg_token_comp_text,
COMMUTATOR = <>,
NEGATOR = =
);
/** @brief SQL operator text <> agg_token (placeholder rewritten by ProvSQL at plan time) */
CREATE OPERATOR <> (
LEFTARG = text,
RIGHTARG = agg_token,
PROCEDURE = text_comp_agg_token,
COMMUTATOR = <>,
NEGATOR = =
);
/** @} */
/** @defgroup random_variable_type Type for continuous random variables
*
* Custom type random_variable: a thin wrapper around a
* provenance gate UUID, used to expose continuous probabilistic
* c-tables in SQL. The UUID indexes either a gate_rv
* (an actual distribution) or a gate_value (a
* zero-variance constant produced by provsql.as_random).
* Binary-coercible with uuid (same 16-byte layout), so an
* rv-typed expression flows directly into any function
* expecting a uuid at zero runtime cost.
*
* Constructors live in this group: provsql.normal(μ, σ),
* provsql.uniform(a, b), provsql.exponential(λ),
* provsql.erlang(k, λ), provsql.gamma(k, λ),
* provsql.chi_squared(k), provsql.lognormal(μ, σ),
* provsql.weibull(k, λ), provsql.pareto(xₘ, α),
* provsql.beta(α, β),
* the discrete count constructors (provsql.poisson(λ),
* provsql.binomial(n, p), provsql.geometric(p),
* provsql.hypergeometric(N, K, n),
* provsql.negative_binomial(r, p), all lowering to
* @c categorical via @c categorical_from_log_pmf),
* and provsql.as_random(c).
* Operator overloads
* (+ - * / and the six comparators) are defined further
* below, alongside direct rv_cmp_* UUID constructors for
* callers that want a gate_cmp token without going through
* the planner hook.
* @{
*/
CREATE TYPE random_variable;
/** @brief Input function for the random_variable type */
CREATE OR REPLACE FUNCTION random_variable_in(cstring)
RETURNS random_variable
AS 'provsql','random_variable_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Output function for the random_variable type */
CREATE OR REPLACE FUNCTION random_variable_out(random_variable)
RETURNS cstring
AS 'provsql','random_variable_out' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE TYPE random_variable (
internallength = 16,
input = random_variable_in,
output = random_variable_out,
alignment = char
);
/** @brief Build a random_variable from a UUID (internal). */
CREATE OR REPLACE FUNCTION random_variable_make(tok uuid)
RETURNS random_variable
AS 'provsql','random_variable_make' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Binary-coercible cast random_variable -> uuid.
* A random_variable is byte-for-byte a pg_uuid_t (alignment char,
* length 16), so WITHOUT FUNCTION lets PostgreSQL reinterpret the
* bytes at zero runtime cost. The cast is ASSIGNMENT (not IMPLICIT):
* an implicit cross-domain cast would silently reroute a comparison
* such as `v < w` to `uuid < uuid` (raw byte comparison) whenever
* `provsql` is not in search_path, since operators are resolved
* through search_path but casts are not. Demoting to ASSIGNMENT
* turns that silent wrong result into a clean parse error. Passing a
* random_variable to a uuid-taking function now needs an explicit
* `v::uuid` (function resolution never applies assignment casts). */
CREATE CAST (random_variable AS uuid) WITHOUT FUNCTION AS ASSIGNMENT;
CREATE CAST (uuid AS random_variable) WITHOUT FUNCTION;
/**
* @brief Coerce an @c agg_token to a @c random_variable (its circuit token).
*
* An aggregate over probabilistic tuples IS a random variable: its
* @c agg_token carries the provenance circuit of the aggregate distribution.
* Exposing that as a @c random_variable lets a comparison / conditioning
* predicate mix the two -- e.g. conditioning a latent leaf on a count,
* @c "R | (poisson(lambda) = C)" with @c C a @c count(*) agg_token -- resolve
* to the ordinary @c random_variable comparison operators (which the planner
* hook rewrites into a @c gate_cmp). IMPLICIT so the mixed comparison
* type-checks without an explicit cast; the polymorphic dispatchers keep
* their exact @c agg_token overloads (an exact match beats the cast).
*/
CREATE OR REPLACE FUNCTION agg_token_to_random_variable(a agg_token)
RETURNS random_variable AS
$$ SELECT provsql.random_variable_make(provsql.agg_token_uuid($1)); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE CAST (agg_token AS random_variable)
WITH FUNCTION agg_token_to_random_variable(agg_token) AS IMPLICIT;
/**
* @brief Internal: true iff @p x is a finite (non-NaN, non-±∞) float8.
*
* PostgreSQL's isnan is defined for numeric only,
* not for double precision; we use the inequality form,
* which works because PG defines NaN = NaN as TRUE
* for floats (so NaN <> 'NaN'::float8 is FALSE).
*/
CREATE OR REPLACE FUNCTION is_finite_float8(x double precision)
RETURNS bool AS
$$
SELECT $1 <> 'NaN'::float8 AND $1 <> 'Infinity'::float8 AND $1 <> '-Infinity'::float8;
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/*
* Latent (token-valued) distribution parameters.
*
* A distribution parameter may be a scalar provenance token -- another
* random_variable (or an agg_token cast to uuid) -- rather than a
* concrete double. The parameter is then a random variable itself,
* making the leaf a compound (hierarchical) distribution: e.g.
* normal(M, 1) with M ~ normal(0, 10). The token constructors below
* wire such parameters as children of the gate_rv, encoding each wired
* slot as "$i" in the extra text (a literal slot keeps its decimal text,
* so an all-literal call is byte-identical to the plain numeric
* constructor). Only the Monte Carlo sampler resolves the wires (per
* iteration); every analytic path recognises the wired form and falls
* through to MC.
*/
/**
* @brief Internal: build a two-parameter latent @c gate_rv.
*
* Each parameter is supplied as EITHER a token (@p pN_tok, a scalar
* gate @c uuid) OR a literal (@p pN_lit); exactly one is non-NULL per
* parameter. Token parameters are appended to the gate's wire vector
* in order and referenced as @c "$i" in the @c extra text; literal
* parameters keep their decimal text. Not @c STRICT: the NULLs are the
* literal-vs-token sentinels.
*/
CREATE OR REPLACE FUNCTION rv_parametric2(
family text,
p1_tok uuid, p1_lit double precision,
p2_tok uuid, p2_lit double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
wires uuid[] := ARRAY[]::uuid[];
s1 text;
s2 text;
BEGIN
IF p1_tok IS NOT NULL THEN
wires := wires || p1_tok;
s1 := '$' || (array_length(wires, 1) - 1);
ELSE
IF NOT provsql.is_finite_float8(p1_lit) THEN
RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
family, p1_lit;
END IF;
s1 := p1_lit::text;
END IF;
IF p2_tok IS NOT NULL THEN
wires := wires || p2_tok;
s2 := '$' || (array_length(wires, 1) - 1);
ELSE
IF NOT provsql.is_finite_float8(p2_lit) THEN
RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
family, p2_lit;
END IF;
s2 := p2_lit::text;
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', wires, NULL, NULL,
family || ':' || s1 || ',' || s2);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE;
/**
* @brief Internal: build a one-parameter latent @c gate_rv (rate/scale).
*/
CREATE OR REPLACE FUNCTION rv_parametric1(family text, p_tok uuid)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', ARRAY[p_tok], NULL, NULL, family || ':$0');
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/*
* Token-accepting constructor overloads. For each numeric family the
* three mixed-arity forms (token, literal), (literal, token), (token,
* token) let any parameter be a random_variable; an agg_token parameter
* is passed as @c (agg)::uuid::random_variable. The all-literal call
* still resolves to the plain numeric constructor (an exact match beats
* the implicit numeric->random_variable cast), so the literal fast path
* is unchanged. STRICT: a NULL parameter yields a NULL random_variable.
*/
-- normal(mu, sigma)
CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION normal(mu double precision, sigma random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- logistic(mu, s)
CREATE OR REPLACE FUNCTION logistic(mu random_variable, s double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION logistic(mu double precision, s random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION logistic(mu random_variable, s random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- uniform(a, b)
CREATE OR REPLACE FUNCTION uniform(a random_variable, b double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION uniform(a double precision, b random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION uniform(a random_variable, b random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- exponential(lambda)
CREATE OR REPLACE FUNCTION exponential(lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric1('exponential', ($1)::uuid); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- gamma(k, lambda)
CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION gamma(k double precision, lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- lognormal(mu, sigma)
CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- weibull(k, lambda)
CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION weibull(k double precision, lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- pareto(xm, alpha)
CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- beta(alpha, beta)
CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION beta(alpha double precision, beta random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- inverse_gamma(alpha, beta)
CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
-- inverse_gaussian(mu, lambda)
CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda double precision)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda random_variable)
RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a normal-distribution random variable
*
* Creates a fresh gate_rv with @c "normal:μ,σ" stored in
* the gate's @c extra field, and returns a random_variable
* pointing at it.
*
* Validation:
* - @p mu and @p sigma must be finite (no @c NaN, no @c ±Infinity).
* - @p sigma must be non-negative.
* - When @p sigma is zero the distribution degenerates to the Dirac
* at @p mu; the call is silently routed through @c as_random(mu),
* producing a @c gate_value rather than a zero-variance @c gate_rv.
* This keeps the sampler / moment / boundcheck paths free of σ=0
* special cases and lets normal(x, 0) share its gate with
* as_random(x).
*
* @warning The VOLATILE marking is load-bearing and must
* not be weakened. Each call mints a fresh uuid_generate_v4
* token because two calls to normal(0, 1) are *independent*
* random variables; if PostgreSQL were allowed to fold the function
* (which it would under STABLE / IMMUTABLE), two
* calls in the same query would share a UUID and collapse into a
* single dependent RV, silently breaking the c-table semantics.
* Same warning applies to @c uniform and @c exponential below.
*
* @sa Wikipedia: Normal distribution
*/
CREATE OR REPLACE FUNCTION normal(mu double precision, sigma double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
RAISE EXCEPTION 'provsql.normal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
END IF;
IF sigma < 0 THEN
RAISE EXCEPTION 'provsql.normal: sigma must be non-negative (got %)', sigma;
END IF;
IF sigma = 0 THEN
RETURN provsql.as_random(mu);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'normal:' || mu || ',' || sigma);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a logistic-distribution random variable Logistic(μ, s)
*
* The location-scale family whose CDF is the logistic sigmoid; a threshold
* event over a Logistic(0, 1) noise realises the logit link exactly
* (@c P(eps < score) = 1/(1 + exp(-score))), the natural link for a
* log-odds / latent-utility selection model.
*
* Validation:
* - @p mu and @p s must be finite.
* - @p s (the scale) must be non-negative; s = 0 is the Dirac at
* @p mu, routed through @c as_random(mu) as with @c normal's sigma = 0.
*
* @param mu location (the mean and median).
* @param s scale (> 0); the variance is @f$\pi^2 s^2 / 3@f$.
* @return a @c random_variable token for Logistic(μ, s).
*
* @sa Wikipedia: Logistic distribution
*/
CREATE OR REPLACE FUNCTION logistic(mu double precision, s double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(s) THEN
RAISE EXCEPTION 'provsql.logistic: parameters must be finite (got mu=%, s=%)', mu, s;
END IF;
IF s < 0 THEN
RAISE EXCEPTION 'provsql.logistic: scale s must be non-negative (got %)', s;
END IF;
IF s = 0 THEN
RETURN provsql.as_random(mu);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'logistic:' || mu || ',' || s);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a uniform-distribution random variable on [a, b]
*
* Validation:
* - @p a and @p b must be finite.
* - @p a must be ≤ @p b (reversed bounds are rejected).
* - When a = b the distribution is the Dirac at @p a; the
* call is silently routed through @c as_random(a) for the same
* reason as @c normal with @p sigma = 0.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Continuous uniform distribution
*/
CREATE OR REPLACE FUNCTION uniform(a double precision, b double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(a) OR NOT provsql.is_finite_float8(b) THEN
RAISE EXCEPTION 'provsql.uniform: bounds must be finite (got a=%, b=%)', a, b;
END IF;
IF a > b THEN
RAISE EXCEPTION 'provsql.uniform: a must be <= b (got a=%, b=%)', a, b;
END IF;
IF a = b THEN
RETURN provsql.as_random(a);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'uniform:' || a || ',' || b);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct an exponential-distribution random variable with rate λ
*
* Validation:
* - @p lambda must be finite and strictly positive. No degenerate
* form exists for the exponential distribution, so there is no
* silent route through @c as_random.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Exponential distribution
*/
CREATE OR REPLACE FUNCTION exponential(lambda double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(lambda) THEN
RAISE EXCEPTION 'provsql.exponential: lambda must be finite (got %)', lambda;
END IF;
IF lambda <= 0 THEN
RAISE EXCEPTION 'provsql.exponential: lambda must be strictly positive (got %)', lambda;
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'exponential:' || lambda);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct an Erlang-distribution random variable, sum of
* @p k i.i.d. exponentials with shared rate @p lambda
*
* The Erlang distribution is the sum of @p k independent
* Exp(λ) random variables (equivalently the gamma with
* integer shape). It is the natural closure of i.i.d.
* exponentials under addition, and is materialised here as a single
* gate_rv so the analytic CDF and closed-form moments fire
* directly (rather than the sampler having to draw and sum @p k
* exponential leaves per Monte-Carlo iteration).
*
* Validation:
* - @p k must be ≥ 1. The degenerate @c k=1 case is silently routed
* through @c exponential so erlang(1, λ) shares its gate
* with exponential(λ).
* - @p lambda must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Erlang distribution
*/
CREATE OR REPLACE FUNCTION erlang(k integer, lambda double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF k < 1 THEN
RAISE EXCEPTION 'provsql.erlang: k must be >= 1 (got %)', k;
END IF;
IF NOT provsql.is_finite_float8(lambda) THEN
RAISE EXCEPTION 'provsql.erlang: lambda must be finite (got %)', lambda;
END IF;
IF lambda <= 0 THEN
RAISE EXCEPTION 'provsql.erlang: lambda must be strictly positive (got %)', lambda;
END IF;
IF k = 1 THEN
RETURN provsql.exponential(lambda);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'erlang:' || k || ',' || lambda);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a gamma-distribution random variable with shape @p k
* (any positive real) and rate @p lambda
*
* The gamma distribution generalises Erlang to non-integer shape; its
* CDF is the regularised lower incomplete gamma, evaluated in closed
* form by the analytic passes. Sums of independent gammas with the
* same rate fold to a single gamma in the simplifier.
*
* Validation:
* - @p k must be finite and strictly positive. An integer @p k (in
* @c integer range) is silently routed through @c erlang -- the gamma
* with integer shape *is* Erlang -- so gamma(2, λ) shares
* its gate encoding and closure interplay with erlang(2, λ).
* - @p lambda must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Gamma distribution
*/
CREATE OR REPLACE FUNCTION gamma(k double precision, lambda double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(k) THEN
RAISE EXCEPTION 'provsql.gamma: k must be finite (got %)', k;
END IF;
IF k <= 0 THEN
RAISE EXCEPTION 'provsql.gamma: k must be strictly positive (got %)', k;
END IF;
IF NOT provsql.is_finite_float8(lambda) THEN
RAISE EXCEPTION 'provsql.gamma: lambda must be finite (got %)', lambda;
END IF;
IF lambda <= 0 THEN
RAISE EXCEPTION 'provsql.gamma: lambda must be strictly positive (got %)', lambda;
END IF;
IF k = floor(k) AND k <= 2147483647 THEN
RETURN provsql.erlang(k::integer, lambda);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'gamma:' || k || ',' || lambda);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a chi-squared random variable with @p k degrees of
* freedom: syntactic sugar for gamma(k/2, 1/2)
*
* @p k is accepted as @c double @c precision so fractional degrees of
* freedom work; it must be finite and strictly positive. Even degrees
* of freedom route through @c erlang via @c gamma's integer-shape rule.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Chi-squared distribution
*/
CREATE OR REPLACE FUNCTION chi_squared(k double precision)
RETURNS random_variable AS
$$
BEGIN
IF NOT provsql.is_finite_float8(k) THEN
RAISE EXCEPTION 'provsql.chi_squared: k must be finite (got %)', k;
END IF;
IF k <= 0 THEN
RAISE EXCEPTION 'provsql.chi_squared: k must be strictly positive (got %)', k;
END IF;
RETURN provsql.gamma(k / 2, 0.5);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a log-normal random variable: @c exp of a
* Normal(@p mu, @p sigma), parameterised by the underlying
* normal (so its median is exp(mu) and its mean
* exp(mu + sigma^2/2))
*
* The multiplicative counterpart of @c normal: products of independent
* lognormals fold to a lognormal in the simplifier, and the
* exp(normal(...)) / ln(lognormal(...)) bridges fold
* in both directions, so log-scale models stay closed-form.
*
* Validation mirrors @c normal: both parameters must be finite,
* @p sigma non-negative; the degenerate @c sigma = 0 case is silently
* routed through @c as_random (a Dirac at exp(mu)).
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Log-normal distribution
*/
CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
RAISE EXCEPTION 'provsql.lognormal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
END IF;
IF sigma < 0 THEN
RAISE EXCEPTION 'provsql.lognormal: sigma must be non-negative (got %)', sigma;
END IF;
IF sigma = 0 THEN
RETURN provsql.as_random(exp(mu));
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'lognormal:' || mu || ',' || sigma);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Weibull random variable with shape @p k and
* scale @p lambda
*
* @p lambda is the SCALE (the 63.2% quantile), not a rate: @c k = 1 is
* the exponential with rate 1/lambda, and that case is
* silently routed through @c exponential to share its gate. The shape
* tunes the hazard: @c k < 1 infant mortality, @c k > 1 wear-out.
* Quantiles are exact, truncated moments are closed-form (via the
* regularised incomplete gamma), and the min of i.i.d. Weibulls has a
* closed-form mean (min-stability).
*
* Validation: both parameters must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Weibull distribution
*/
CREATE OR REPLACE FUNCTION weibull(k double precision, lambda double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(k) OR NOT provsql.is_finite_float8(lambda) THEN
RAISE EXCEPTION 'provsql.weibull: parameters must be finite (got k=%, lambda=%)', k, lambda;
END IF;
IF k <= 0 OR lambda <= 0 THEN
RAISE EXCEPTION 'provsql.weibull: parameters must be strictly positive (got k=%, lambda=%)', k, lambda;
END IF;
IF k = 1 THEN
RETURN provsql.exponential(1 / lambda);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'weibull:' || k || ',' || lambda);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Pareto random variable with scale (minimum)
* @p xm and shape @p alpha
*
* The canonical heavy-tailed power law. Raw moments are @b infinite
* for alpha <= k and reported as Infinity (the mean
* for alpha <= 1, the variance for alpha <= 2)
* rather than estimated; quantiles, truncated moments, conditional
* sampling (self-similarity: X | X > a is Pareto(a, alpha)),
* and Pareto-vs-Pareto comparisons are all exact.
*
* Validation: both parameters must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Pareto distribution
*/
CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(xm) OR NOT provsql.is_finite_float8(alpha) THEN
RAISE EXCEPTION 'provsql.pareto: parameters must be finite (got xm=%, alpha=%)', xm, alpha;
END IF;
IF xm <= 0 OR alpha <= 0 THEN
RAISE EXCEPTION 'provsql.pareto: parameters must be strictly positive (got xm=%, alpha=%)', xm, alpha;
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'pareto:' || xm || ',' || alpha);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct an inverse-gamma random variable with shape
* @p alpha and scale @p beta
*
* The distribution of 1/Y for Y ~ gamma(alpha, beta)
* (the conjugate prior for a Gaussian variance). Its CDF is the
* regularised upper incomplete gamma, evaluated in closed form by the
* analytic passes; raw moments are @b infinite for alpha <= k
* and reported as Infinity (the mean for alpha <= 1,
* the variance for alpha <= 2) rather than estimated. Positive
* scalings rescale @p beta in the simplifier.
*
* Validation: both parameters must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Inverse-gamma distribution
*/
CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
END IF;
IF alpha <= 0 OR beta <= 0 THEN
RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'inverse_gamma:' || alpha || ',' || beta);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct an inverse-Gaussian (Wald) random variable with mean
* @p mu and shape @p lambda
*
* The first-passage time of Brownian motion with drift: a positive,
* right-skewed family. Its CDF has a closed form in the standard normal
* @c Phi, so comparisons and quantiles are analytic; all raw moments are
* finite. Positive scalings map c·IG(mu, lambda) to
* IG(c·mu, c·lambda), and a sum of independent inverse
* Gaussians sharing the ratio lambda/mu² folds to a single
* inverse Gaussian in the simplifier. @ref wald is an alias.
*
* Validation: both parameters must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Inverse Gaussian distribution
*/
CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(lambda) THEN
RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be finite (got mu=%, lambda=%)', mu, lambda;
END IF;
IF mu <= 0 OR lambda <= 0 THEN
RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be strictly positive (got mu=%, lambda=%)', mu, lambda;
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'inverse_gaussian:' || mu || ',' || lambda);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Wald distribution: alias for @ref inverse_gaussian.
*
* @sa Wikipedia: Inverse Gaussian distribution
*/
CREATE OR REPLACE FUNCTION wald(mu double precision, lambda double precision)
RETURNS random_variable AS
$$
SELECT provsql.inverse_gaussian(mu, lambda);
$$ LANGUAGE sql VOLATILE PARALLEL UNSAFE;
/**
* @brief Build a discrete (categorical) random variable from outcomes
* and UNNORMALISED log-masses
*
* The shared back end of the discrete count constructors (@c poisson,
* @c binomial, @c geometric, @c hypergeometric,
* @c negative_binomial), and directly usable for any custom discrete
* pmf: the log-masses are shifted by their maximum (so only relative
* magnitudes matter and no @c exp underflows), outcomes whose relative
* mass is below 1e-15 are dropped, and the rest is
* renormalised before being handed to @c categorical. Working in log
* space keeps arbitrarily large parameters stable (e.g. a
* Poisson(1000) pmf whose linear-space recurrence would
* underflow at @c exp(-1000)).
*
* @param outcomes outcome values, same length as @p log_pmf
* @param log_pmf natural logs of the (unnormalised) masses
*/
CREATE OR REPLACE FUNCTION categorical_from_log_pmf(
outcomes double precision[], log_pmf double precision[])
RETURNS random_variable AS
$$
DECLARE
n int := array_length(outcomes, 1);
max_lp double precision := '-Infinity';
kept_o double precision[] := '{}';
kept_p double precision[] := '{}';
total double precision := 0;
v double precision;
i int;
BEGIN
IF n IS NULL OR n = 0 OR n <> coalesce(array_length(log_pmf, 1), 0) THEN
RAISE EXCEPTION 'provsql.categorical_from_log_pmf: outcomes and log_pmf must be non-empty arrays of the same length';
END IF;
FOR i IN 1..n LOOP
IF log_pmf[i] > max_lp THEN max_lp := log_pmf[i]; END IF;
END LOOP;
IF max_lp = '-Infinity' THEN
RAISE EXCEPTION 'provsql.categorical_from_log_pmf: all masses are zero';
END IF;
FOR i IN 1..n LOOP
v := exp(log_pmf[i] - max_lp);
IF v >= 1e-15 THEN
kept_o := array_append(kept_o, outcomes[i]);
kept_p := array_append(kept_p, v);
total := total + v;
END IF;
END LOOP;
FOR i IN 1..array_length(kept_p, 1) LOOP
kept_p[i] := kept_p[i] / total;
END LOOP;
RETURN provsql.categorical(kept_p, kept_o);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Poisson random variable with mean @p lambda, as a
* truncated categorical
*
* The pmf is enumerated over [max(0, λ-12√λ), λ+12√λ+30] (the
* omitted tails carry ~1e-30 of mass) by the log-space recurrence
* ln p(k+1) = ln p(k) + ln λ - ln(k+1) and handed to
* @c categorical_from_log_pmf, so moments, quantiles, and (in)equality
* comparisons are exact over the enumerated support. @c lambda = 0 is
* a Dirac at @c 0 (routed through @c as_random); supports up to 10000
* outcomes (λ up to ~170000), beyond which it raises -- approximate
* huge means by @c normal(λ, √λ) instead.
*
* @sa Wikipedia: Poisson distribution
*/
CREATE OR REPLACE FUNCTION poisson(lambda double precision)
RETURNS random_variable AS
$$
DECLARE
lo int;
hi int;
outcomes double precision[] := '{}';
lps double precision[] := '{}';
lp double precision := 0;
k int;
BEGIN
IF NOT provsql.is_finite_float8(lambda) OR lambda < 0 THEN
RAISE EXCEPTION 'provsql.poisson: lambda must be finite and non-negative (got %)', lambda;
END IF;
IF lambda = 0 THEN
RETURN provsql.as_random(0);
END IF;
lo := greatest(0, floor(lambda - 12 * sqrt(lambda)))::int;
hi := ceil(lambda + 12 * sqrt(lambda))::int + 30;
IF hi - lo + 1 > 10000 THEN
RAISE EXCEPTION 'provsql.poisson: support window of % outcomes exceeds 10000; approximate with normal(%, sqrt(%))', hi - lo + 1, lambda, lambda;
END IF;
-- ln p(0) = -λ; walk the recurrence, keeping only the window.
lp := -lambda;
FOR k IN 1..hi LOOP
lp := lp + ln(lambda) - ln(k::double precision);
IF k >= lo THEN
outcomes := array_append(outcomes, k::double precision);
lps := array_append(lps, lp);
END IF;
END LOOP;
IF lo = 0 THEN
outcomes := array_prepend(0::double precision, outcomes);
lps := array_prepend(-lambda, lps);
END IF;
RETURN provsql.categorical_from_log_pmf(outcomes, lps);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Poisson with a LATENT rate: @c poisson(random_variable).
*
* A latent (token-valued) rate cannot be enumerated into a categorical at
* construction, so this builds a parametric @c gate_rv leaf (family
* @c "poisson") wiring the rate, exactly like the continuous latent
* constructors. Only the Monte Carlo sampler resolves the rate (per draw,
* then draws a Poisson); @c observe weights by the Poisson pmf; the mean is
* exact (E[Poisson(Λ)] = E[Λ], affine). Unblocks discrete-likelihood
* posteriors such as @c "R | (poisson(120*R) = observed_count)".
*/
CREATE OR REPLACE FUNCTION poisson(lambda random_variable)
RETURNS random_variable AS
$$ SELECT provsql.rv_parametric1('poisson', ($1)::uuid); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Beta(α, β) random variable on the unit interval
*
* The conjugate prior of Bernoulli / binomial success probabilities:
* closed-form moments, CDF through the regularised incomplete beta,
* quantiles through the generic CDF bisection over the finite
* @c [0, 1] support, and closed-form truncated moments (interval
* conditioning). Beta(1, 1) IS Uniform(0, 1) and is
* silently routed through @c uniform to share its richer closed forms.
*
* Validation: both shapes must be finite and strictly positive.
*
* @warning VOLATILE is load-bearing; see the warning on
* @ref normal.
*
* @sa Wikipedia: Beta distribution
*/
CREATE OR REPLACE FUNCTION beta(alpha double precision, beta double precision)
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
RAISE EXCEPTION 'provsql.beta: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
END IF;
IF alpha <= 0 OR beta <= 0 THEN
RAISE EXCEPTION 'provsql.beta: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
END IF;
IF alpha = 1 AND beta = 1 THEN
RETURN provsql.uniform(0, 1);
END IF;
token := public.uuid_generate_v4();
PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'beta:' || alpha || ',' || beta);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Binomial(n, p) random variable (number of
* successes in @p n independent trials), as a categorical
*
* Enumerated over {0..n} by the log-space recurrence
* ln p(k+1) = ln p(k) + ln((n-k)/(k+1)) + ln(p/(1-p))
* (outcomes below 1e-15 relative mass are dropped). @c p = 0 /
* @c p = 1 are Diracs at @c 0 / @c n; @c n is capped at 10000.
*
* @sa Wikipedia: Binomial distribution
*/
CREATE OR REPLACE FUNCTION binomial(n integer, p double precision)
RETURNS random_variable AS
$$
DECLARE
outcomes double precision[] := '{}';
lps double precision[] := '{}';
lp double precision;
k int;
BEGIN
IF n IS NULL OR n < 0 THEN
RAISE EXCEPTION 'provsql.binomial: n must be non-negative (got %)', n;
END IF;
IF NOT provsql.is_finite_float8(p) OR p < 0 OR p > 1 THEN
RAISE EXCEPTION 'provsql.binomial: p must be in [0, 1] (got %)', p;
END IF;
IF n > 10000 THEN
RAISE EXCEPTION 'provsql.binomial: n = % exceeds 10000; approximate with normal(n*p, sqrt(n*p*(1-p)))', n;
END IF;
IF n = 0 OR p = 0 THEN
RETURN provsql.as_random(0);
END IF;
IF p = 1 THEN
RETURN provsql.as_random(n);
END IF;
lp := n * ln(1 - p); -- ln p(0)
outcomes := array_append(outcomes, 0::double precision);
lps := array_append(lps, lp);
FOR k IN 0..(n - 1) LOOP
lp := lp + ln((n - k)::double precision / (k + 1)) + ln(p / (1 - p));
outcomes := array_append(outcomes, (k + 1)::double precision);
lps := array_append(lps, lp);
END LOOP;
RETURN provsql.categorical_from_log_pmf(outcomes, lps);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Binomial with a fixed trial count and a LATENT success
* probability: @c binomial(integer, random_variable).
*
* @p n is a literal trial count; @p p is a latent (token-valued) success
* probability (e.g. @c "40.0 / N" for a latent population size @c N).
* Builds a parametric @c gate_rv leaf (family @c "binomial", @c extra
* @c "binomial:n,$0") the Monte Carlo sampler resolves per draw; @c observe
* weights by the Binomial pmf. Unblocks capture-recapture-style posteriors
* such as @c "N | (binomial(50, 40.0/N) = recaptured_count)".
*/
CREATE OR REPLACE FUNCTION binomial(n integer, p random_variable)
RETURNS random_variable AS
$$ SELECT provsql.rv_parametric2('binomial', NULL, $1::double precision,
($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Geometric(p) random variable -- the number of
* TRIALS up to and including the first success (support
* starting at 1; subtract 1 for the failures convention)
*
* P(X = k) = (1-p)^{k-1} p, enumerated up to the 1e-15
* relative-mass tail and renormalised. @c p = 1 is a Dirac at @c 1.
*
* @sa Wikipedia: Geometric distribution
*/
CREATE OR REPLACE FUNCTION geometric(p double precision)
RETURNS random_variable AS
$$
DECLARE
k_max int;
outcomes double precision[] := '{}';
lps double precision[] := '{}';
k int;
BEGIN
IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
RAISE EXCEPTION 'provsql.geometric: p must be in (0, 1] (got %)', p;
END IF;
IF p = 1 THEN
RETURN provsql.as_random(1);
END IF;
k_max := 1 + ceil(ln(1e-15) / ln(1 - p))::int;
IF k_max > 10000 THEN
RAISE EXCEPTION 'provsql.geometric: support window of % outcomes exceeds 10000 (p = % is too small); approximate with exponential(%)', k_max, p, p;
END IF;
FOR k IN 1..k_max LOOP
outcomes := array_append(outcomes, k::double precision);
lps := array_append(lps, (k - 1) * ln(1 - p) + ln(p));
END LOOP;
RETURN provsql.categorical_from_log_pmf(outcomes, lps);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Geometric with a LATENT success probability: @c geometric(random_variable).
*
* A latent (token-valued) @p p cannot be enumerated at construction, so this
* builds a parametric @c gate_rv leaf (family @c "geometric") wiring the
* probability, resolved per draw by the sampler. @c observe weights by the
* geometric pmf; unblocks a Beta-Geometric conjugate posterior.
*/
CREATE OR REPLACE FUNCTION geometric(p random_variable)
RETURNS random_variable AS
$$ SELECT provsql.rv_parametric1('geometric', ($1)::uuid); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a Hypergeometric(N, K, n) random variable: the
* number of marked items among @p n draws WITHOUT replacement
* from a population of @p pop_n items of which @p k_marked are
* marked
*
* The exact finite support [max(0, n-(N-K)), min(n, K)] is
* enumerated by the pmf ratio recurrence (in log space, so large
* populations cannot overflow) and normalised -- exact "sampling
* without replacement" probabilities with no combinatorial functions
* needed.
*
* @sa Wikipedia: Hypergeometric distribution
*/
CREATE OR REPLACE FUNCTION hypergeometric(pop_n integer, k_marked integer, n integer)
RETURNS random_variable AS
$$
DECLARE
lo int;
hi int;
outcomes double precision[] := '{}';
lps double precision[] := '{}';
lp double precision := 0; -- relative log-mass; normalised later
k int;
BEGIN
IF pop_n IS NULL OR k_marked IS NULL OR n IS NULL
OR pop_n < 0 OR k_marked < 0 OR n < 0
OR k_marked > pop_n OR n > pop_n THEN
RAISE EXCEPTION 'provsql.hypergeometric: need 0 <= k_marked, n <= pop_n (got pop_n=%, k_marked=%, n=%)', pop_n, k_marked, n;
END IF;
lo := greatest(0, n - (pop_n - k_marked));
hi := least(n, k_marked);
IF hi - lo + 1 > 10000 THEN
RAISE EXCEPTION 'provsql.hypergeometric: support window of % outcomes exceeds 10000', hi - lo + 1;
END IF;
outcomes := array_append(outcomes, lo::double precision);
lps := array_append(lps, lp);
FOR k IN lo..(hi - 1) LOOP
-- pmf(k+1)/pmf(k) = (K-k)(n-k) / ((k+1)(N-K-n+k+1))
lp := lp + ln((k_marked - k)::double precision * (n - k))
- ln((k + 1)::double precision * (pop_n - k_marked - n + k + 1));
outcomes := array_append(outcomes, (k + 1)::double precision);
lps := array_append(lps, lp);
END LOOP;
RETURN provsql.categorical_from_log_pmf(outcomes, lps);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Construct a negative-binomial random variable: the number of
* FAILURES before the @p r-th success (support starting at 0),
* with real @p r > 0 allowed (the Polya / overdispersed-count
* parameterisation, the Poisson-Gamma mixture)
*
* P(X = k) = C(k+r-1, k) p^r (1-p)^k, enumerated by the
* log-space recurrence
* ln p(k+1) = ln p(k) + ln((k+r)/(k+1)) + ln(1-p) up to the
* 1e-15 relative-mass tail. @c p = 1 is a Dirac at @c 0.
*
* @sa Wikipedia: Negative binomial distribution
*/
CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p double precision)
RETURNS random_variable AS
$$
DECLARE
outcomes double precision[] := '{}';
lps double precision[] := '{}';
lp double precision;
max_lp double precision;
mean double precision;
k int := 0;
BEGIN
IF NOT provsql.is_finite_float8(r) OR r <= 0 THEN
RAISE EXCEPTION 'provsql.negative_binomial: r must be finite and strictly positive (got %)', r;
END IF;
IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
RAISE EXCEPTION 'provsql.negative_binomial: p must be in (0, 1] (got %)', p;
END IF;
IF p = 1 THEN
RETURN provsql.as_random(0);
END IF;
mean := r * (1 - p) / p;
lp := r * ln(p); -- ln p(0)
max_lp := lp;
outcomes := array_append(outcomes, 0::double precision);
lps := array_append(lps, lp);
LOOP
lp := lp + ln((k + r) / (k + 1)) + ln(1 - p);
k := k + 1;
IF lp > max_lp THEN max_lp := lp; END IF;
outcomes := array_append(outcomes, k::double precision);
lps := array_append(lps, lp);
EXIT WHEN k > mean AND lp < max_lp + ln(1e-15);
IF k >= 10000 THEN
RAISE EXCEPTION 'provsql.negative_binomial: support window exceeds 10000 outcomes (r=%, p=%)', r, p;
END IF;
END LOOP;
RETURN provsql.categorical_from_log_pmf(outcomes, lps);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/*
* NegativeBinomial with a LATENT parameter: the count r (number of successes)
* stays a plain number while the success probability p is a random_variable,
* built as a parametric gate_rv leaf (family "negative_binomial") -- the
* Beta-NegativeBinomial conjugate shape. The all-random and latent-r forms
* are provided for uniformity with the continuous constructors.
*/
CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p random_variable)
RETURNS random_variable AS
$$ SELECT provsql.rv_parametric2('negative_binomial', NULL, $1, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p double precision)
RETURNS random_variable AS
$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::uuid, NULL, NULL, $2); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p random_variable)
RETURNS random_variable AS
$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::uuid, NULL, ($2)::uuid, NULL); $$
LANGUAGE sql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Catalog of the registered continuous-distribution families.
*
* One row per @c gate_rv family known to this build of the extension:
* @c name is the on-disk token (the part before the colon in the gate's
* @c extra encoding), @c nparams the parameter count, @c param_names the
* conventional parameter symbols in @c extra order (e.g.
* {μ, σ}), and @c label a short display glyph (e.g. @c "N",
* @c "Γ"). UI clients (ProvSQL Studio's circuit inspector) read this to
* render families they were not hard-coded for, so a newly added family
* shows up without a client release.
*/
CREATE OR REPLACE FUNCTION rv_families()
RETURNS TABLE(name TEXT, nparams INT, param_names TEXT[], label TEXT) AS
'provsql','rv_families' LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief Construct a probabilistic-mixture random variable.
*
* Returns a @c random_variable whose distribution is a Bernoulli
* mixture of two scalar RV roots: with probability P(p = true)
* the mixture samples @p x, with the complementary probability it
* samples @p y. The mixing token @p p is a @c gate_input Bernoulli
* whose probability has been pinned with @c set_prob, and the same
* @p p can be shared with other branches of the circuit -- the
* Monte-Carlo sampler's per-iteration cache couples every reference
* to the same draw, so users can build joint conditional structures
* (e.g. mixture(p, X1, Y1) + mixture(p, X2, Y2) samples
* X1 + X2 with prob π and Y1 + Y2 with prob 1-π).
*
* @p x and @p y may be any scalar RV root: a base @c gate_rv
* (@c normal / @c uniform / @c exponential / @c erlang), a
* @c gate_value Dirac (@c as_random), a @c gate_arith expression, or
* another @c mixture. N-ary mixtures are built by composition --
* mixture(p1, A, mixture(p2, B, C)) realises a 3-component
* mixture with effective weights π1, (1-π1)·π2, (1-π1)·(1-π2).
*
* Validation:
* - @p p must point to a Boolean gate (@c input, @c mulinput,
* @c update, @c plus, @c times, @c monus, @c project, @c eq,
* @c cmp, @c zero, @c one). Compound Boolean gates derive their
* probability from their atoms via the active probability-evaluation
* method; a bare @c gate_input's probability is whatever @c set_prob
* pinned (@c set_prob is responsible for keeping it in [0, 1]).
* - @p x and @p y must be scalar RV roots; aggregate / Boolean roots
* are rejected at construction.
*
* Two calls to @c mixture with the same @c (p, x, y) operands collapse
* to the same @c gate_mixture node by v5-hash, exactly like
* @c arith(PLUS, X, Y). Draw independence is controlled by @p p:
* sharing @p p couples branch selection across consumers via the
* sampler's @c bool_cache_; minting independent Bernoullis (e.g. via
* the @c mixture(p_value, …) overload) decouples them.
*
* @sa Wikipedia: Mixture distribution
*/
CREATE OR REPLACE FUNCTION mixture(
p uuid, x random_variable, y random_variable)
RETURNS random_variable AS
$$
DECLARE
token uuid;
p_kind provsql.provenance_gate;
x_uuid uuid;
y_uuid uuid;
x_kind provsql.provenance_gate;
y_kind provsql.provenance_gate;
BEGIN
p_kind := provsql.get_gate_type(p);
IF p_kind NOT IN ('input','mulinput','update',
'plus','times','monus',
'project','eq','cmp',
'zero','one') THEN
RAISE EXCEPTION 'provsql.mixture: p must be a Boolean gate '
'(input/mulinput/update/plus/times/monus/project/eq/cmp/zero/one), got %', p_kind
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: mixture-argument-kind; scope: out-of-scope';
END IF;
x_uuid := (x)::uuid;
y_uuid := (y)::uuid;
x_kind := provsql.get_gate_type(x_uuid);
y_kind := provsql.get_gate_type(y_uuid);
IF x_kind NOT IN ('rv','value','arith','mixture') THEN
RAISE EXCEPTION 'provsql.mixture: x must be a scalar RV root (rv / value / arith / mixture), got %', x_kind
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: mixture-argument-kind; scope: out-of-scope';
END IF;
IF y_kind NOT IN ('rv','value','arith','mixture') THEN
RAISE EXCEPTION 'provsql.mixture: y must be a scalar RV root (rv / value / arith / mixture), got %', y_kind
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: mixture-argument-kind; scope: out-of-scope';
END IF;
token := public.uuid_generate_v5(
provsql.uuid_ns_provsql(),
concat('mixture', p, x_uuid, y_uuid));
PERFORM provsql.create_gate(token, 'mixture', ARRAY[p, x_uuid, y_uuid]);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
/**
* @brief Ad-hoc mixture constructor that mints a fresh anonymous
* @c gate_input Bernoulli with probability @p p_value.
*
* Sugar over the @c mixture(uuid, x, y) form: when the caller doesn't
* care about reusing the Bernoulli token elsewhere in the circuit
* (which is the common case – "give me a 0.3 / 0.7 weighted GMM,
* I don't need to share the coin"), this overload creates the
* underlying @c gate_input on the fly with a fresh
* @c uuid_generate_v4() token, pins @p p_value via @c set_prob, and
* threads everything into the uuid-keyed constructor.
*
* A probability of exactly 1 or 0 is a constant, not a coin: the
* mixture is then @p x or @p y itself, and no gate is created.
*
* Each call mints a NEW Bernoulli, so two calls to
* mixture(0.5, X, Y) are *independent* mixtures whose branch
* selections are uncorrelated. When coupling is desired (e.g. two
* mixtures sharing a coin), use the @c mixture(uuid, x, y) form with a
* user-managed @c gate_input token.
*
* @warning VOLATILE is load-bearing for the same reason as
* @ref normal and the other RV constructors -- folding under
* @c STABLE / @c IMMUTABLE would collapse two independent draws into
* one shared gate.
*
* @sa Wikipedia: Mixture distribution
*/
CREATE OR REPLACE FUNCTION mixture(
p_value double precision,
x random_variable,
y random_variable)
RETURNS random_variable AS
$$
DECLARE
p_token uuid;
BEGIN
IF p_value IS NULL OR p_value <> p_value OR p_value < 0 OR p_value > 1 THEN
RAISE EXCEPTION 'provsql.mixture: probability must be in [0,1] (got %)', p_value;
END IF;
IF p_value = 1 THEN
RETURN x;
ELSIF p_value = 0 THEN
RETURN y;
END IF;
p_token := public.uuid_generate_v4();
PERFORM provsql.create_gate(p_token, 'input');
PERFORM provsql.set_prob(p_token, p_value);
RETURN provsql.mixture(p_token, x, y);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Categorical-RV constructor over explicit (probabilities,
* values) arrays.
*
* Builds a categorical-form @c gate_mixture directly: a fresh
* @c gate_input "key" anchor and one @c gate_mulinput per outcome with
* positive mass, all sharing the key. The wires
* [key, mul_1, ..., mul_n] are what downstream evaluators
* (@c Expectation, @c MonteCarloSampler, @c AnalyticEvaluator,
* @c RangeCheck) recognise via @c isCategoricalMixture and treat as a
* scalar RV with the categorical distribution @p probs over
* @p outcomes.
*
* Validation:
* - @p probs and @p outcomes must be non-null, same length, length ≥ 1.
* - Each @c probs[i] must be finite, in [0, 1], and the array
* must sum to 1 within @c 1e-9.
* - Each @c outcomes[i] must be finite.
*
* Each call mints a fresh key gate and a fresh set of mulinputs, so
* two calls to @c categorical with the same arrays are *independent*
* categorical RVs. The marking is @c VOLATILE accordingly.
*
* Degenerate case: a categorical with exactly one positive-mass
* outcome reduces to @c as_random(v) at construction (the block would
* just be a single mulinput, which is operationally a Dirac point
* mass). Two such calls share the @c gate_value UUID via the v5
* convention @c as_random already uses.
*
* @sa @c mixture for the Bernoulli-weighted choice constructor.
* @sa Wikipedia: Categorical distribution
*/
CREATE OR REPLACE FUNCTION categorical(
probs double precision[],
outcomes double precision[])
RETURNS random_variable AS
$$
DECLARE
n integer;
p_sum double precision := 0.0;
i integer;
key_token uuid;
mix_token uuid;
mul_token uuid;
mul_tokens uuid[] := ARRAY[]::uuid[];
mix_wires uuid[];
pi_i double precision;
vi_i double precision;
BEGIN
IF probs IS NULL OR outcomes IS NULL THEN
RAISE EXCEPTION 'provsql.categorical: probs and outcomes must be non-null';
END IF;
n := array_length(probs, 1);
IF n IS NULL OR n < 1 THEN
RAISE EXCEPTION 'provsql.categorical: probs must be non-empty';
END IF;
IF array_length(outcomes, 1) <> n THEN
RAISE EXCEPTION 'provsql.categorical: probs and outcomes must have the same length (got % and %)',
n, array_length(outcomes, 1);
END IF;
FOR i IN 1..n LOOP
pi_i := probs[i];
vi_i := outcomes[i];
-- PostgreSQL diverges from IEEE 754: NaN = NaN is TRUE there, so
-- the canonical x <> x NaN test doesn't fire. Compare against the
-- literal 'NaN'::float8 instead, and reject ±Infinity for outcomes
-- explicitly.
IF pi_i IS NULL OR pi_i = 'NaN'::float8 OR pi_i < 0 OR pi_i > 1 THEN
RAISE EXCEPTION 'provsql.categorical: probs[%] must be in [0,1] (got %)', i, pi_i;
END IF;
IF vi_i IS NULL OR vi_i = 'NaN'::float8
OR vi_i = 'Infinity'::float8 OR vi_i = '-Infinity'::float8 THEN
RAISE EXCEPTION 'provsql.categorical: outcomes[%] must be finite (got %)', i, vi_i;
END IF;
p_sum := p_sum + pi_i;
END LOOP;
IF abs(p_sum - 1.0) > 1e-9 THEN
RAISE EXCEPTION 'provsql.categorical: probs must sum to 1 within 1e-9 (got %)', p_sum;
END IF;
-- Degenerate case: exactly one positive-mass outcome (the rest are
-- zero). The "categorical" is then a Dirac point mass; skip the
-- block-allocation entirely and return @c as_random(v), which yields
-- a shared, v5-keyed gate_value -- exactly what downstream
-- evaluators (rv_moment, AnalyticEvaluator, rv_support) treat
-- specially. Saves a key gate and a mulinput per call, and lets
-- two calls to @c categorical({1.0}, {v}) collide on the same
-- gate_value UUID instead of producing distinct anonymous blocks.
DECLARE
nb_positive integer := 0;
only_idx integer := 0;
BEGIN
FOR i IN 1..n LOOP
IF probs[i] > 0.0 THEN
nb_positive := nb_positive + 1;
only_idx := i;
END IF;
END LOOP;
IF nb_positive = 1 THEN
RETURN provsql.as_random(outcomes[only_idx]);
END IF;
END;
-- Mint the block's key anchor. Probability 1.0 matches the
-- joint-table convention: the categorical mass lives on the
-- mulinputs, the key just identifies the block.
key_token := public.uuid_generate_v4();
PERFORM provsql.create_gate(key_token, 'input');
PERFORM provsql.set_prob(key_token, 1.0);
-- One mulinput per positive-probability outcome. Zero-probability
-- entries contribute no mass and are skipped: the gate_mixture's
-- wire vector is otherwise polluted with no-op leaves.
FOR i IN 1..n LOOP
pi_i := probs[i];
IF pi_i <= 0.0 THEN CONTINUE; END IF;
mul_token := public.uuid_generate_v4();
PERFORM provsql.create_gate(mul_token, 'mulinput', ARRAY[key_token],
i - 1, NULL, outcomes[i]::text);
PERFORM provsql.set_prob(mul_token, pi_i);
mul_tokens := mul_tokens || mul_token;
END LOOP;
mix_wires := ARRAY[key_token] || mul_tokens;
mix_token := public.uuid_generate_v4();
PERFORM provsql.create_gate(mix_token, 'mixture', mix_wires);
RETURN provsql.random_variable_make(mix_token);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Gaussian-mixture-model (GMM) constructor.
*
* Packages the common fitted-density pattern -- a categorical choice
* among Normal components -- into one call:
*
* @code
* provsql.gmm(weights => ARRAY[0.3, 0.5, 0.2],
* means => ARRAY[120.0, 380.0, 1200.0],
* stddevs => ARRAY[40.0, 90.0, 250.0])
* @endcode
*
* No new gate: the mixture decomposes into a stick-breaking cascade of
* Bernoulli @c gate_mixture nodes over @c gate_rv Normal leaves
* (component @c i is selected with conditional probability
* @c w_i / (w_i + ... + w_n), so the joint selection probabilities are
* exactly @p weights), which every evaluator already handles: moments
* are closed-form through the mixture recursion, sampling is exact,
* and comparisons ride the existing mixture machinery. Zero-weight
* components are skipped; a single positive-weight component returns
* its Normal directly (no mixture node).
*
* Validation mirrors @c categorical: same-length non-empty arrays,
* weights finite in [0, 1] summing to 1 within @c 1e-9; the
* component parameters are validated by @c provsql.normal (finite
* @c mu, non-negative @c sigma; @c sigma @c = @c 0 degenerates to a
* Dirac component).
*
* @sa @c mixture, @c categorical, @c normal
* @sa Wikipedia: Mixture model
*/
CREATE OR REPLACE FUNCTION gmm(
weights double precision[],
means double precision[],
stddevs double precision[])
RETURNS random_variable AS
$$
DECLARE
n integer;
w_sum double precision := 0.0;
i integer;
acc random_variable := NULL;
remaining double precision := 0.0;
BEGIN
IF weights IS NULL OR means IS NULL OR stddevs IS NULL THEN
RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must be non-null';
END IF;
n := array_length(weights, 1);
IF n IS NULL OR n < 1 THEN
RAISE EXCEPTION 'provsql.gmm: weights must be non-empty';
END IF;
IF array_length(means, 1) <> n OR array_length(stddevs, 1) <> n THEN
RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must have the same length (got %, %, %)',
n, array_length(means, 1), array_length(stddevs, 1);
END IF;
FOR i IN 1..n LOOP
IF weights[i] IS NULL OR weights[i] = 'NaN'::float8
OR weights[i] < 0 OR weights[i] > 1 THEN
RAISE EXCEPTION 'provsql.gmm: weights[%] must be in [0,1] (got %)',
i, weights[i];
END IF;
w_sum := w_sum + weights[i];
END LOOP;
IF abs(w_sum - 1.0) > 1e-9 THEN
RAISE EXCEPTION 'provsql.gmm: weights must sum to 1 within 1e-9 (got %)', w_sum;
END IF;
-- Stick-breaking, built back to front: acc holds the mixture of
-- components i+1..n, and prepending component i selects it with
-- conditional probability w_i / (w_i + ... + w_n).
FOR i IN REVERSE n..1 LOOP
IF weights[i] <= 0.0 THEN
CONTINUE;
END IF;
IF acc IS NULL THEN
acc := provsql.normal(means[i], stddevs[i]);
remaining := weights[i];
ELSE
remaining := remaining + weights[i];
acc := provsql.mixture(least(1.0, weights[i] / remaining),
provsql.normal(means[i], stddevs[i]), acc);
END IF;
END LOOP;
RETURN acc;
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Empirical-samples constructor: the ecdf of a sample bundle as
* a @c random_variable.
*
* Loads a Monte Carlo / MCMC / bootstrap sample array as the discrete
* distribution putting mass @c 1/n on each draw (duplicates merge, so a
* value drawn @c k times carries @c k/n) -- the standard empirical
* distribution. Reduces entirely to @ref categorical, so the whole
* exact discrete surface applies: moments are the sample moments,
* comparisons against constants are decided analytically ("fraction of
* samples below c"), and quantiles are the exact empirical quantiles.
*
* @code
* -- Bulk load via array_agg over a sample table
* INSERT INTO model_posteriors
* SELECT param, provsql.empirical_samples(array_agg(value))
* FROM mcmc_chain GROUP BY param;
* @endcode
*
* At most 10000 distinct values (the categorical block cap): thin the
* chain or bin the samples (e.g. with @c width_bucket) beyond that.
*
* @sa @ref categorical, @ref empirical_cdf
* @sa Wikipedia: Empirical distribution function
*/
CREATE OR REPLACE FUNCTION empirical_samples(samples double precision[])
RETURNS random_variable AS
$$
DECLARE
n integer;
sorted double precision[];
outcomes double precision[] := '{}';
probs double precision[] := '{}';
v double precision;
prev double precision;
run integer := 0;
started boolean := false;
BEGIN
n := array_length(samples, 1);
IF n IS NULL OR n < 1 THEN
RAISE EXCEPTION 'provsql.empirical_samples: samples must be non-empty';
END IF;
sorted := ARRAY(SELECT s FROM unnest(samples) AS s ORDER BY 1);
FOREACH v IN ARRAY sorted LOOP
IF v IS NULL OR v = 'NaN'::float8
OR v = 'Infinity'::float8 OR v = '-Infinity'::float8 THEN
RAISE EXCEPTION
'provsql.empirical_samples: samples must be finite (got %)', v;
END IF;
IF started AND v = prev THEN
run := run + 1;
ELSE
IF started THEN
outcomes := outcomes || prev;
probs := probs || (run::double precision / n);
END IF;
prev := v;
run := 1;
started := true;
END IF;
END LOOP;
outcomes := outcomes || prev;
probs := probs || (run::double precision / n);
IF array_length(outcomes, 1) > 10000 THEN
RAISE EXCEPTION
'provsql.empirical_samples: at most 10000 distinct values are '
'supported (got %); thin the chain or bin the samples (e.g. with '
'width_bucket)', array_length(outcomes, 1);
END IF;
RETURN provsql.categorical(probs, outcomes);
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Empirical-CDF constructor: a piecewise-linear CDF table as a
* @c random_variable.
*
* Loads a tabulated CDF -- simulation output percentile tables, risk
* models, expert-elicited forecasts -- as the distribution whose CDF is
* @c cdf[i] at @c grid[i], linear in between: mass
* @c cdf[i+1] @c - @c cdf[i] spread uniformly over
* (grid[i], grid[i+1]), plus (when @c cdf[1] @c > @c 0) an
* atom of mass @c cdf[1] at @c grid[1] for the probability at or below
* the grid start. Packaged, like @ref gmm, as a stick-breaking cascade
* of Bernoulli @ref mixture nodes over @ref uniform components (and the
* optional @ref as_random atom), so moments and sampling are exact
* through the existing mixture machinery; comparisons ride Monte Carlo.
*
* @code
* provsql.empirical_cdf(
* grid => ARRAY[0.0, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0],
* cdf => ARRAY[0.32, 0.51, 0.67, 0.82, 0.94, 0.99, 1.0])
* @endcode
*
* Validation: same-length arrays of at least two entries, @p grid
* strictly increasing and finite, @p cdf non-decreasing within
* [0, 1] and ending at @c 1 within @c 1e-9.
*
* @sa @ref gmm, @ref empirical_samples
* @sa Wikipedia: Cumulative distribution function
*/
CREATE OR REPLACE FUNCTION empirical_cdf(grid double precision[],
cdf double precision[])
RETURNS random_variable AS
$$
DECLARE
n integer;
i integer;
acc random_variable := NULL;
remaining double precision := 0.0;
w double precision;
comp random_variable;
BEGIN
n := array_length(grid, 1);
IF n IS NULL OR n < 2 THEN
RAISE EXCEPTION 'provsql.empirical_cdf: grid must have at least two entries';
END IF;
IF array_length(cdf, 1) <> n THEN
RAISE EXCEPTION 'provsql.empirical_cdf: grid and cdf must have the same length (got % and %)',
n, array_length(cdf, 1);
END IF;
IF n > 10000 THEN
RAISE EXCEPTION 'provsql.empirical_cdf: at most 10000 grid points are supported (got %)', n;
END IF;
FOR i IN 1..n LOOP
IF grid[i] IS NULL OR grid[i] = 'NaN'::float8
OR grid[i] = 'Infinity'::float8 OR grid[i] = '-Infinity'::float8 THEN
RAISE EXCEPTION 'provsql.empirical_cdf: grid[%] must be finite (got %)', i, grid[i];
END IF;
IF i > 1 AND NOT grid[i] > grid[i-1] THEN
RAISE EXCEPTION 'provsql.empirical_cdf: grid must be strictly increasing (grid[%] = %, grid[%] = %)',
i-1, grid[i-1], i, grid[i];
END IF;
IF cdf[i] IS NULL OR cdf[i] = 'NaN'::float8 OR cdf[i] < 0 OR cdf[i] > 1 THEN
RAISE EXCEPTION 'provsql.empirical_cdf: cdf[%] must be in [0,1] (got %)', i, cdf[i];
END IF;
IF i > 1 AND cdf[i] < cdf[i-1] THEN
RAISE EXCEPTION 'provsql.empirical_cdf: cdf must be non-decreasing (cdf[%] = %, cdf[%] = %)',
i-1, cdf[i-1], i, cdf[i];
END IF;
END LOOP;
IF abs(cdf[n] - 1.0) > 1e-9 THEN
RAISE EXCEPTION 'provsql.empirical_cdf: cdf must end at 1 within 1e-9 (got %)', cdf[n];
END IF;
-- Stick-breaking cascade, back to front: component i = 1 is the atom
-- at the grid start (mass cdf[1]); component i >= 2 is
-- uniform(grid[i-1], grid[i]) with mass cdf[i] - cdf[i-1].
FOR i IN REVERSE n..1 LOOP
w := CASE WHEN i = 1 THEN cdf[1] ELSE cdf[i] - cdf[i-1] END;
IF w <= 0.0 THEN
CONTINUE;
END IF;
comp := CASE WHEN i = 1 THEN provsql.as_random(grid[1])
ELSE provsql.uniform(grid[i-1], grid[i]) END;
IF acc IS NULL THEN
acc := comp;
remaining := w;
ELSE
remaining := remaining + w;
acc := provsql.mixture(least(1.0, w / remaining), comp, acc);
END IF;
END LOOP;
RETURN acc;
END
$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL UNSAFE;
/**
* @brief Lift a deterministic constant into a random_variable
*
* Creates a gate_value carrying the constant's text form so
* that comparisons against a random_variable column produce
* the same circuit shape regardless of whether the operand is an
* actual RV or a literal constant.
*
* Marked IMMUTABLE: the gate UUID is derived deterministically
* from the constant via the same v5 convention as provenance_semimod's
* inline value gate (concat('value', CAST(c AS VARCHAR))), so
* as_random(2) always resolves to the same gate, and any other
* code path that already creates a value gate for the same constant
* (e.g. provenance_semimod) shares the UUID.
* create_gate is idempotent on already-mapped tokens, so
* repeat invocations are harmless.
*
* @sa Wikipedia: Degenerate distribution (Dirac point mass)
*/
CREATE OR REPLACE FUNCTION as_random(c double precision)
RETURNS random_variable AS
$$
DECLARE
-- Canonicalise -0.0 to +0.0: IEEE 754 defines x + 0.0 = +0.0 for
-- both signed zeros, and is identity for finite, NaN, and ±Infinity.
-- Without this, as_random(-0.0) and as_random(+0.0) would produce
-- different gate UUIDs (their CAST AS VARCHAR text representations
-- differ: '-0' vs '0') even though they denote the same constant.
c_canon double precision := c + 0.0;
c_text varchar := CAST(c_canon AS VARCHAR);
token uuid := public.uuid_generate_v5(
provsql.uuid_ns_provsql(), concat('value', c_text));
BEGIN
PERFORM provsql.create_gate(token, 'value', NULL, NULL, NULL, c_text);
RETURN provsql.random_variable_make(token);
END
$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
/**
* @brief Implicit cast double precision -> random_variable (lifts a
* scalar literal to a constant RV).
*
* Lets users write WHERE reading > 2.5::float8 instead of
* WHERE reading > provsql.as_random(2.5); the planner-hook
* rewriter then sees a uniform random_variable on both sides.
* Sibling casts below cover @c integer and @c numeric literals so
* plain WHERE reading > 2 and WHERE reading > 2.5
* also work; PostgreSQL's operator resolution does not chain casts
* across more than one step, so each numeric-source type needs its
* own direct cast.
*/
CREATE CAST (double precision AS random_variable)
WITH FUNCTION as_random(double precision) AS IMPLICIT;
/** @brief @c as_random for @c integer (delegates to the @c float8 form). */
CREATE OR REPLACE FUNCTION as_random(c integer)
RETURNS random_variable AS
$$ SELECT provsql.as_random(c::double precision); $$
LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
/** @brief @c as_random for @c numeric (delegates to the @c float8 form). */
CREATE OR REPLACE FUNCTION as_random(c numeric)
RETURNS random_variable AS
$$ SELECT provsql.as_random(c::double precision); $$
LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
/** @brief Implicit cast integer -> random_variable. */
CREATE CAST (integer AS random_variable)
WITH FUNCTION as_random(integer) AS IMPLICIT;
/** @brief Implicit cast numeric -> random_variable. */
CREATE CAST (numeric AS random_variable)
WITH FUNCTION as_random(numeric) AS IMPLICIT;
/**
* @name Arithmetic and comparison on random_variable
*
* Each binary operator below is declared on @c (random_variable,
* random_variable) only; mixed shapes such as rv + 2 or
* 2.5 > rv resolve through the implicit casts from
* @c integer / @c numeric / @c double @c precision to
* @c random_variable declared above. This avoids the resolution
* ambiguity that would arise if both (rv, numeric) and
* (rv, rv) overloads were declared while implicit casts also
* existed.
*
* Arithmetic operators build a @c gate_arith via @c provenance_arith
* and return a new @c random_variable wrapping its UUID.
*
* Comparison operators are placeholders that return @c boolean and
* raise if executed -- the @c boolean return type is required so that
* PostgreSQL accepts WHERE rv > 2 at parse-analyze. The
* planner hook intercepts every such @c OpExpr (matched by
* @c opfuncid against @c constants_t::OID_FUNCTION_RV_CMP) and rewrites
* it into a @c provenance_cmp call whose UUID is conjoined into the
* tuple's @c provsql column via @c provenance_times. Code that needs
* a @c gate_cmp UUID directly (without going through the planner hook)
* uses the @c rv_cmp_* family below, which call @c provenance_cmp
* with the matching float8-comparator OID.
*
* @{
*/
/** @brief @c random_variable + @c random_variable (gate_arith PLUS). */
CREATE OR REPLACE FUNCTION random_variable_plus(
a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
0, -- PROVSQL_ARITH_PLUS
ARRAY[(a)::uuid,
(b)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief @c random_variable - @c random_variable (gate_arith MINUS). */
CREATE OR REPLACE FUNCTION random_variable_minus(
a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
2, -- PROVSQL_ARITH_MINUS
ARRAY[(a)::uuid,
(b)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief @c random_variable * @c random_variable (gate_arith TIMES). */
CREATE OR REPLACE FUNCTION random_variable_times(
a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
1, -- PROVSQL_ARITH_TIMES
ARRAY[(a)::uuid,
(b)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief @c random_variable / @c random_variable (gate_arith DIV). */
CREATE OR REPLACE FUNCTION random_variable_div(
a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
3, -- PROVSQL_ARITH_DIV
ARRAY[(a)::uuid,
(b)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Unary @c -random_variable (gate_arith NEG). */
CREATE OR REPLACE FUNCTION random_variable_neg(a random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
4, -- PROVSQL_ARITH_NEG
ARRAY[(a)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief @c random_variable ^ @c random_variable (gate_arith POW).
*
* Real-valued branch only: evaluation raises if a negative base is
* drawn together with a non-integer exponent (write
* pow(greatest(x, 0), p) for the non-negative branch).
*/
CREATE OR REPLACE FUNCTION random_variable_pow(
a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
7, -- PROVSQL_ARITH_POW
ARRAY[(a)::uuid,
(b)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Natural logarithm of a @c random_variable (gate_arith LN).
*
* Defined on @c [0, +Infinity): evaluation raises if a negative value
* is drawn (restrict the argument's support); a draw of exactly @c 0
* yields @c -Infinity.
*/
CREATE OR REPLACE FUNCTION ln(a random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
8, -- PROVSQL_ARITH_LN
ARRAY[(a)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief @c e^x for a @c random_variable (gate_arith EXP). Total. */
CREATE OR REPLACE FUNCTION exp(a random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(
provsql.provenance_arith(
9, -- PROVSQL_ARITH_EXP
ARRAY[(a)::uuid]));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief @c pow / @c power spellings of the @c ^ operator, mirroring
* PostgreSQL's numeric surface. Scalar exponents resolve
* through the implicit numeric-to-rv casts:
* pow(x, 0.5) is x ^ 0.5.
*/
CREATE OR REPLACE FUNCTION pow(a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_pow(a, b);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION power(a random_variable, b random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_pow(a, b);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Square root of a @c random_variable: sugar for
* x ^ 0.5 (no gate or opcode of its own). Evaluation
* raises on a negative draw, like any non-integer exponent.
*/
CREATE OR REPLACE FUNCTION sqrt(a random_variable)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_pow(a, provsql.as_random(0.5));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Internal helper: float8-comparator OID for a given symbol.
*
* Wraps the @c '<sym>(double precision,double precision)'::regoperator
* lookup so the per-comparator functions read uniformly. Marked
* @c IMMUTABLE because the resolved OID is fixed at catalog level
* (the float8 comparators are core PG and never re-installed).
*/
CREATE OR REPLACE FUNCTION random_variable_cmp_oid(sym text)
RETURNS oid AS
$$
SELECT (sym || '(double precision,double precision)')::regoperator::oid;
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/* The six @c random_variable_{lt,le,eq,ne,ge,gt} functions below are
* boolean placeholders -- they exist only so the @c (rv, rv) operators
* can be declared at all (PostgreSQL needs a procedure to bind to the
* operator definition, and a procedure returning anything but @c boolean
* would be rejected by parse-analyze in a WHERE position). They MUST
* NOT be invoked directly: the planner hook in @c src/provsql.c
* intercepts every @c OpExpr whose @c opfuncid matches one of these and
* rewrites it into a @c provenance_cmp() call against the row's
* provenance. If the executor ever reaches one of these, it means the
* planner hook was bypassed (e.g. @c provsql.active was off), in which
* case raising is the right behaviour. */
/** @brief Placeholder body shared by every random_variable_*
* comparison procedure. Raises with a uniform message. */
CREATE OR REPLACE FUNCTION random_variable_cmp_placeholder(
a random_variable, b random_variable)
RETURNS boolean AS
$$
BEGIN
RAISE EXCEPTION 'random_variable comparison must be rewritten by the '
'ProvSQL planner hook (is provsql.active off?)'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: rv-operator-not-rewritten; scope: out-of-scope';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION random_variable_lt(
a random_variable, b random_variable) RETURNS boolean AS
$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION random_variable_le(
a random_variable, b random_variable) RETURNS boolean AS
$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION random_variable_eq(
a random_variable, b random_variable) RETURNS boolean AS
$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION random_variable_ne(
a random_variable, b random_variable) RETURNS boolean AS
$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION random_variable_ge(
a random_variable, b random_variable) RETURNS boolean AS
$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OR REPLACE FUNCTION random_variable_gt(
a random_variable, b random_variable) RETURNS boolean AS
$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/* Direct UUID constructors -- used by tests and any caller that wants
* a @c gate_cmp without going through the planner hook (e.g. building
* a circuit fragment in a SELECT list). Each delegates to
* @c provenance_cmp with the matching float8-comparator OID. */
/** @brief Build a @c gate_cmp for a < b and return its UUID. */
CREATE OR REPLACE FUNCTION rv_cmp_lt(
a random_variable, b random_variable) RETURNS uuid AS
$$
SELECT provsql.provenance_cmp(
(a)::uuid,
provsql.random_variable_cmp_oid('<'),
(b)::uuid);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Build a @c gate_cmp for a ≤ b and return its UUID. */
CREATE OR REPLACE FUNCTION rv_cmp_le(
a random_variable, b random_variable) RETURNS uuid AS
$$
SELECT provsql.provenance_cmp(
(a)::uuid,
provsql.random_variable_cmp_oid('<='),
(b)::uuid);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Build a @c gate_cmp for a = b and return its UUID. */
CREATE OR REPLACE FUNCTION rv_cmp_eq(
a random_variable, b random_variable) RETURNS uuid AS
$$
SELECT provsql.provenance_cmp(
(a)::uuid,
provsql.random_variable_cmp_oid('='),
(b)::uuid);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Build a @c gate_cmp for a <> b and return its UUID. */
CREATE OR REPLACE FUNCTION rv_cmp_ne(
a random_variable, b random_variable) RETURNS uuid AS
$$
SELECT provsql.provenance_cmp(
(a)::uuid,
provsql.random_variable_cmp_oid('<>'),
(b)::uuid);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Build a @c gate_cmp for a ≥ b and return its UUID. */
CREATE OR REPLACE FUNCTION rv_cmp_ge(
a random_variable, b random_variable) RETURNS uuid AS
$$
SELECT provsql.provenance_cmp(
(a)::uuid,
provsql.random_variable_cmp_oid('>='),
(b)::uuid);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Build a @c gate_cmp for a > b and return its UUID. */
CREATE OR REPLACE FUNCTION rv_cmp_gt(
a random_variable, b random_variable) RETURNS uuid AS
$$
SELECT provsql.provenance_cmp(
(a)::uuid,
provsql.random_variable_cmp_oid('>'),
(b)::uuid);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR + (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_plus,
COMMUTATOR = +
);
CREATE OPERATOR - (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_minus
);
CREATE OPERATOR * (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_times,
COMMUTATOR = *
);
CREATE OPERATOR / (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_div
);
/** @brief Prefix unary minus on @c random_variable. */
CREATE OPERATOR - (
RIGHTARG = random_variable,
PROCEDURE = random_variable_neg
);
CREATE OPERATOR ^ (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_pow
);
CREATE OPERATOR < (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_lt,
COMMUTATOR = >,
NEGATOR = >=
);
CREATE OPERATOR <= (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_le,
COMMUTATOR = >=,
NEGATOR = >
);
CREATE OPERATOR = (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_eq,
COMMUTATOR = =,
NEGATOR = <>
);
CREATE OPERATOR <> (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_ne,
COMMUTATOR = <>,
NEGATOR = =
);
CREATE OPERATOR >= (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_ge,
COMMUTATOR = <=,
NEGATOR = <
);
/**
* @brief Order @c agg_token values by the value each carries.
*
* With a cast of an aggregate carried rather than frozen, a column of
* @c agg_token is what a query sorts, groups or takes the DISTINCT of, and
* those need an ordering. It is the ordering of the values on the data as it
* is -- the one ProvSQL already documents for @c ORDER @c BY on an aggregate
* -- and the comparison says so, once for the statement.
*/
CREATE OR REPLACE FUNCTION agg_token_btree_cmp(a agg_token, b agg_token)
RETURNS integer AS 'MODULE_PATHNAME', 'agg_token_btree_cmp'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR CLASS agg_token_ops
DEFAULT FOR TYPE agg_token USING btree AS
OPERATOR 1 <,
OPERATOR 2 <=,
OPERATOR 3 =,
OPERATOR 4 >=,
OPERATOR 5 >,
FUNCTION 1 agg_token_btree_cmp(agg_token, agg_token);
CREATE OPERATOR > (
LEFTARG = random_variable,
RIGHTARG = random_variable,
PROCEDURE = random_variable_gt,
COMMUTATOR = <,
NEGATOR = <=
);
/**
* @brief btree comparison support for @c random_variable -- always an error.
*
* A @c random_variable is a distribution, not a scalar, so it has no total
* order: sorting (@c ORDER @c BY), de-duplicating (@c DISTINCT), grouping, and
* the built-in @c GREATEST / @c LEAST all reduce to this btree comparison
* proc, which raises a clear diagnostic rather than a placeholder message.
*
* The proc exists only so a DEFAULT btree operator class can be declared for
* @c random_variable -- which is what lets PostgreSQL's @c GREATEST / @c LEAST
* grammar parse over random variables so the planner hook can lift it into a
* @c gate_arith @c MAX / @c MIN order statistic. When the hook is active the
* @c GREATEST / @c LEAST node is rewritten before it ever calls this proc.
*/
CREATE OR REPLACE FUNCTION random_variable_btree_cmp(
a random_variable, b random_variable) RETURNS integer AS
$$
BEGIN
RAISE EXCEPTION 'comparison or ordering of random_variable values is '
'meaningless: a random_variable is a distribution, not a scalar'
USING HINT =
'Compare them as a probabilistic event -- in a WHERE / JOIN clause or '
'with probability(x > y); take order statistics with provsql.greatest / '
'provsql.least (or the min / max aggregates); summarise numerically with '
'expected / variance / support.',
DETAIL = 'provsql-reason: rv-comparison; scope: out-of-scope';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
-- DEFAULT btree operator class over the (planner-hook-lifted) comparison
-- operators. Its only purpose is to make GREATEST / LEAST over random_variable
-- parse; every actual comparison it would drive (ORDER BY, DISTINCT, an
-- un-rewritten GREATEST) funnels through random_variable_btree_cmp above and
-- raises the "meaningless" diagnostic.
CREATE OPERATOR CLASS random_variable_ops
DEFAULT FOR TYPE random_variable USING btree AS
OPERATOR 1 <,
OPERATOR 2 <=,
OPERATOR 3 =,
OPERATOR 4 >=,
OPERATOR 5 >,
FUNCTION 1 random_variable_btree_cmp(random_variable, random_variable);
/**
* @brief Condition a random variable on an event: @c "X | C".
*
* Returns a conditioned distribution that flows onward like any other
* @c random_variable: it can be stored, re-conditioned, and queried with
* @c expected / @c variance / @c moment / @c support, which then report the
* conditional distribution. @p cond is a Boolean-event provenance token,
* typically a comparison over the variable itself (@c "X | rv_cmp_gt(X,
* as_random(3))" -- a truncation) or any external event.
*
* Unlike the uuid carrier's terminal @c cond, the random-variable form is a
* composable two-child @c gate_conditioned @c [target, condition]: the moment
* / support dispatchers unpack it and route through the existing conditional
* evaluator (@c rv_moment over the joint of the target and the condition).
* Nested conditioning folds: @c "(X|A)|B = X|(A∧B)".
*/
CREATE OR REPLACE FUNCTION random_variable_cond(rv random_variable, cond uuid)
RETURNS random_variable AS
$$
DECLARE
tgt uuid;
ev uuid;
result uuid;
ch uuid[];
BEGIN
IF cond IS NULL OR cond = gate_one() THEN
RETURN rv;
END IF;
-- A point-equality "Y = c" on a bare random-variable leaf is an
-- OBSERVATION, not a truncation: rewrite it to the internal likelihood-
-- weighting evidence (its density / mass at c). This is what lets
-- "X | (normal(mu,1) = 8)" (a continuous point event, measure-zero as a
-- Boolean selection) condition as the disintegration rather than fold to
-- an infeasible event.
cond := provsql.evidence_as_observation(cond);
tgt := (rv)::uuid;
IF get_gate_type(tgt) = 'conditioned'
AND array_length(get_children(tgt), 1) = 2 THEN
-- Fold (X|A)|B = X|(A∧B): the rv-carrier conditioned gate is the
-- two-child [target, condition] shape; accumulate the new event.
ch := get_children(tgt);
tgt := ch[1];
ev := provenance_times(ch[2], cond);
ELSE
ev := cond;
END IF;
result := public.uuid_generate_v5(uuid_ns_provsql(),
concat('conditioned', tgt, ev));
PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev]);
RETURN (result)::random_variable;
END
$$ LANGUAGE plpgsql VOLATILE SET search_path=provsql,pg_temp,public
SECURITY DEFINER PARALLEL UNSAFE;
CREATE OPERATOR | (
LEFTARG = random_variable,
RIGHTARG = uuid,
PROCEDURE = random_variable_cond
);
/**
* @brief Placeholder for @c "X | (predicate)" -- conditioning a random
* variable on a Boolean comparison written naturally.
*
* Lets one write @c "X | (X > 3)" instead of
* @c "X | rv_cmp_gt(X, as_random(3))". Never executes: the ProvSQL planner
* hook rewrites the Boolean operand (a combination of random_variable
* comparisons) into the corresponding condition gate and emits
* @c random_variable_cond. Reaching it at runtime means the rewriter was
* inactive or the predicate was not a random_variable comparison.
*/
CREATE OR REPLACE FUNCTION random_variable_cond_predicate(
rv random_variable, predicate boolean) RETURNS random_variable AS
$$
BEGIN
RAISE EXCEPTION 'random_variable | (predicate) must be rewritten by the '
'ProvSQL planner hook: the right operand must be a Boolean combination '
'of random_variable comparisons (is provsql.active off?)'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: rv-operator-not-rewritten; scope: out-of-scope';
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR | (
LEFTARG = random_variable,
RIGHTARG = boolean,
PROCEDURE = random_variable_cond_predicate
);
/**
* @brief Unpack the target of a random-variable conditioning gate.
*
* For a two-child @c gate_conditioned @c [target, condition] (the @c "X | C"
* shape) returns @p target; for any other token returns it unchanged. Used
* by the moment / support dispatchers to route a conditioned distribution
* through the existing conditional evaluator.
*/
CREATE OR REPLACE FUNCTION rv_conditioned_target(token uuid) RETURNS uuid AS
$$
SELECT CASE
WHEN provsql.get_gate_type(token) = 'conditioned'
AND array_length(provsql.get_children(token), 1) = 2
THEN (provsql.get_children(token))[1]
ELSE token
END;
$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief Combine a conditioning gate's event with an explicit @p prov.
*
* For a two-child @c gate_conditioned @c [target, condition] returns
* @c "condition ∧ prov"; otherwise returns @p prov unchanged. Lets a stored
* @c "X | C" be queried as @c expected(X|C) (prov defaulting to one) or have
* an extra condition conjoined as @c expected(X|C, extra_prov).
*/
CREATE OR REPLACE FUNCTION rv_conditioned_prov(token uuid, prov uuid)
RETURNS uuid AS
$$
SELECT CASE
WHEN provsql.get_gate_type(token) = 'conditioned'
AND array_length(provsql.get_children(token), 1) = 2
THEN provsql.provenance_times((provsql.get_children(token))[2], prov)
ELSE prov
END;
$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/*
* Latent-variable posterior inference.
*
* Likelihood weighting (self-normalised importance sampling): bind an
* observed datum to a latent-dependent random-variable leaf with observe,
* conjoin the per-observation evidence with and_agg into a single evidence
* token, and pass it as the prov conditioning argument of any moment /
* quantile / sample readout. Latents are drawn from the prior (the
* existing forward recursion) and each draw is weighted by the observed
* leaves' densities at the data; the readouts then report the posterior.
* It is the continuous generalisation of the rejection-based conditioning:
* a Boolean event in the evidence contributes a 0/1 weight, an observe
* contributes a pdf weight -- same evidence conjunction, same
* "P(query AND evidence)/P(evidence)" normaliser, now weighted.
*/
/**
* @brief Internal: rewrite a point-equality conditioning event into an
* observation. If @p ev is a @c gate_cmp with the @c "=" operator,
* one side a bare @c gate_rv leaf and the other a constant, return
* @c observe(leaf, const); otherwise return @p ev unchanged.
*
* This is the bridge that makes the natural equality form the surface for
* likelihood-weighting conditioning: @c "X | (Y = c)" and @c "given(Y = c)"
* both produce a @c gate_cmp, which this turns into density evidence. A
* point event on a bare leaf is only meaningful as an observation (a
* continuous @c "Y = c" is measure-zero as a Boolean selection), so the
* rewrite is unambiguous. Non-equality / non-leaf events pass through as
* ordinary Boolean conditioning.
*/
CREATE OR REPLACE FUNCTION evidence_as_observation(ev uuid) RETURNS uuid AS
$$
DECLARE
ch uuid[];
i1 integer;
leaf uuid;
datum_gate uuid;
BEGIN
IF ev IS NULL OR provsql.get_gate_type(ev) <> 'cmp' THEN
RETURN ev;
END IF;
ch := provsql.get_children(ev);
IF array_length(ch, 1) <> 2 THEN
RETURN ev;
END IF;
-- The cmp stores the comparison OPERATOR's OID in info1; match on its name
-- '=' the same way the C-side cmpOpFromOid does (get_opname), rather than a
-- fixed operator OID (which varies per install / carrier type).
SELECT info1 INTO i1 FROM provsql.get_infos(ev);
IF (SELECT oprname FROM pg_catalog.pg_operator WHERE oid = i1) IS DISTINCT FROM '=' THEN
RETURN ev; -- not an equality
END IF;
IF provsql.get_gate_type(ch[1]) = 'rv'
AND provsql.get_gate_type(ch[2]) = 'value' THEN
leaf := ch[1]; datum_gate := ch[2];
ELSIF provsql.get_gate_type(ch[2]) = 'rv'
AND provsql.get_gate_type(ch[1]) = 'value' THEN
leaf := ch[2]; datum_gate := ch[1];
ELSE
RETURN ev; -- not a bare-leaf-vs-constant point event
END IF;
RETURN provsql.observe((leaf)::random_variable,
provsql.get_extra(datum_gate)::double precision);
END
$$ LANGUAGE plpgsql VOLATILE
SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL UNSAFE;
/**
* @brief Internal: bind an observed datum to a random-variable leaf --
* the likelihood-weighting evidence behind @c "X | (Y = d)".
*
* @p x MUST be a bare @c gate_rv leaf (typically a latent-parameterised
* one, e.g. @c normal(mu, 1) sharing a latent @c mu across rows).
* Returns an @b evidence uuid -- a @c gate_observe wrapping the leaf with
* the datum in @c extra -- that composes with other evidence through
* @c and_agg (a @c gate_times conjunction) and is consumed by the
* importance-sampling weight walk, contributing the factor @c f_X(d).
*
* Internal: the user-facing surface is the equality form @c "X | (Y = d)"
* (single conditioning) and @c "given(Y = d)" (per-row evidence for
* @c and_agg), both of which route here through @c evidence_as_observation.
*
* A fresh gate is minted per call (each observation is a distinct
* evidence atom, so a repeated @c (leaf, datum) contributes its density
* factor once per row -- and each is a separate Shapley atom). Observing
* a derived quantity (@c observe(X+Y, d)) is out of scope: it needs a
* change-of-variables density; a non-leaf argument is refused.
*/
CREATE OR REPLACE FUNCTION observe(x random_variable, datum double precision)
RETURNS uuid AS
$$
DECLARE
leaf uuid := (x)::uuid;
result uuid;
BEGIN
IF provsql.get_gate_type(leaf) <> 'rv' THEN
RAISE EXCEPTION 'provsql.observe: the argument must be a bare '
'random-variable leaf (a gate_rv), got a % gate', provsql.get_gate_type(leaf)
USING HINT = 'observe binds a datum to a single distribution leaf; '
'observing a derived quantity (a sum, product, or comparison) needs '
'a change-of-variables density and is out of scope.',
DETAIL = 'provsql-reason: observe-argument-kind; scope: out-of-scope';
END IF;
IF NOT provsql.is_finite_float8(datum) THEN
RAISE EXCEPTION 'provsql.observe: datum must be finite (got %)', datum;
END IF;
result := public.uuid_generate_v4();
PERFORM provsql.create_gate(result, 'observe', ARRAY[leaf], NULL, NULL, datum::text);
RETURN result;
END
$$ LANGUAGE plpgsql VOLATILE
SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL UNSAFE;
/**
* @brief Conjunction state function for @c and_agg (evidence @c gate_times).
*
* Not @c STRICT: @c provenance_times maps a @c NULL operand to the times
* neutral, so an empty group leaves the state @c NULL (no evidence) and a
* first row seeds it with that row's evidence.
*/
CREATE OR REPLACE FUNCTION and_agg_sfunc(state uuid, ev uuid)
RETURNS uuid AS
$$
SELECT provsql.provenance_times(state, ev);
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/**
* @brief Conjoin per-row evidence tokens into one evidence circuit.
*
* The evidence-conjunction counterpart used to fold one @c observe (or any
* Boolean conditioning event) per row into a single @c gate_times root, to
* be passed as the @c prov argument of the moment / quantile / sample
* readouts. An empty group yields @c NULL (no evidence).
*/
CREATE AGGREGATE and_agg(uuid) (
SFUNC = and_agg_sfunc,
STYPE = uuid
);
/**
* @brief Marginal likelihood @c P(data) of an evidence circuit.
*
* The mean raw importance weight over @c provsql.rv_mc_samples prior draws
* -- the same quantity rejection conditioning computes as @c P(C), now the
* product of the observations' densities. @p evidence is an @c and_agg
* conjunction of @c observe tokens (and/or Boolean events).
*/
CREATE OR REPLACE FUNCTION evidence(evidence uuid)
RETURNS double precision
AS 'provsql','rv_evidence' LANGUAGE C STRICT PARALLEL SAFE;
/**
* @brief The @c observe atoms of an evidence circuit.
*
* Collects every @c gate_observe leaf reachable through the @c gate_times
* conjunction spine (the shape @c and_agg builds -- a possibly left-nested
* tree, since @c provenance_times does not flatten). Used by
* @c shapley_observe to recover the flat observation set regardless of the
* conjunction's nesting.
*/
CREATE OR REPLACE FUNCTION observe_atoms(evidence uuid)
RETURNS uuid[] AS
$$
WITH RECURSIVE walk(tok) AS (
SELECT evidence
UNION
SELECT c
FROM walk, LATERAL unnest(provsql.get_children(walk.tok)) AS c
WHERE provsql.get_gate_type(walk.tok) = 'times'
)
SELECT array_agg(tok ORDER BY tok)
FROM walk
WHERE provsql.get_gate_type(tok) = 'observe';
$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief Shapley attribution of each observation to a posterior moment.
*
* "Which observation most shifted my posterior?" Because the importance
* weight is a product of per-observation density factors, dropping an
* observation is dropping one factor: the classical Shapley value of each
* @c gate_observe atom over the coalitional value function
* @c "v(S) = payoff(target | observations in S)" is the attribution, a
* byproduct of the same likelihood-weighting machinery (see the
* explainable-inference angle in the continuous-distributions notes).
*
* @p target is the latent (its @c uuid); @p evidence is the @c and_agg
* conjunction of @c observe atoms; @p payoff is @c 'expected' or
* @c 'variance'. Returns each observation atom with its Shapley value; the
* values sum to @c "payoff(target | all data) - payoff(target)" (Shapley
* efficiency: the total shift from prior to posterior).
*
* Exact enumeration over the @c 2^n observation subsets, so it is capped at
* @c n = 12 observations (sampling-based attribution for larger sets is
* future work); pin @c provsql.monte_carlo_seed so the coalitional value
* functions share common random numbers (lower-variance differences).
*/
CREATE OR REPLACE FUNCTION shapley_observe(
target uuid, evidence uuid, payoff text DEFAULT 'expected')
RETURNS TABLE(observation uuid, value double precision) AS
$$
DECLARE
atoms uuid[];
n int;
nmasks int;
pv double precision[];
popc int[];
fact double precision[];
mask int;
i int;
j int;
cnt int;
subset uuid[];
ev_s uuid;
sh double precision;
bit int;
s_size int;
BEGIN
IF payoff NOT IN ('expected', 'variance') THEN
RAISE EXCEPTION 'provsql.shapley_observe: payoff must be ''expected'' or '
'''variance'' (got %)', payoff;
END IF;
atoms := provsql.observe_atoms(evidence);
n := coalesce(array_length(atoms, 1), 0);
IF n = 0 THEN
RAISE EXCEPTION 'provsql.shapley_observe: evidence contains no observe() '
'atoms (got a % gate)', provsql.get_gate_type(evidence)
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: shapley-observe-no-evidence; scope: out-of-scope';
END IF;
IF n > 12 THEN
RAISE EXCEPTION 'provsql.shapley_observe: exact attribution over % '
'observations is exponential; capped at 12 (sampling-based '
'attribution is future work)', n
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: shapley-observe-too-many; scope: out-of-scope';
END IF;
-- factorials 0!..n! (fact[k+1] = k!)
fact := ARRAY[1::double precision];
FOR i IN 1..n LOOP fact := fact || (fact[i] * i); END LOOP;
nmasks := (1 << n);
pv := array_fill(NULL::double precision, ARRAY[nmasks]);
popc := array_fill(0, ARRAY[nmasks]);
-- Payoff value function for every subset of observations.
FOR mask IN 0 .. nmasks - 1 LOOP
subset := ARRAY[]::uuid[];
cnt := 0;
FOR i IN 0 .. n - 1 LOOP
IF (mask >> i) & 1 = 1 THEN
subset := subset || atoms[i + 1];
cnt := cnt + 1;
END IF;
END LOOP;
popc[mask + 1] := cnt;
IF cnt = 0 THEN
ev_s := provsql.gate_one(); -- prior (no evidence)
ELSE
ev_s := provsql.provenance_times(VARIADIC subset);
END IF;
IF payoff = 'expected' THEN
pv[mask + 1] := provsql.rv_moment(target, 1, false, ev_s);
ELSE
pv[mask + 1] := provsql.rv_moment(target, 2, true, ev_s);
END IF;
END LOOP;
-- Shapley value of each observation atom.
FOR i IN 0 .. n - 1 LOOP
sh := 0;
bit := (1 << i);
FOR mask IN 0 .. nmasks - 1 LOOP
IF (mask >> i) & 1 = 0 THEN -- subsets S not containing i
s_size := popc[mask + 1];
-- weight |S|! (n-|S|-1)! / n!
sh := sh + (fact[s_size + 1] * fact[n - s_size] / fact[n + 1])
* (pv[(mask | bit) + 1] - pv[mask + 1]);
END IF;
END LOOP;
observation := atoms[i + 1];
value := sh;
RETURN NEXT;
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/**
* @name Order statistics over random_variable
*
* Same-row @c greatest / @c least over @c random_variable arguments: the
* order-statistic counterpart of the element-wise @c "+ - * /" operators.
* They lower to a single @c gate_arith with the @c MAX / @c MIN opcode over
* the argument circuits, the same n-ary shape the @c max / @c min aggregates
* build. Evaluation is Monte-Carlo-correct out of the box (@c std::max /
* @c std::min over the jointly-sampled children, so shared base RVs stay
* coupled); closed forms for i.i.d. families come from the analytic
* order-statistic pass.
*
* PostgreSQL's built-in @c GREATEST / @c LEAST are dedicated syntax (a
* @c MinMaxExpr requiring a btree comparison), not overloadable functions, so
* the surface is the schema-qualified @c provsql.greatest(...) /
* @c provsql.least(...). @c NULL arguments are ignored, matching the built-in
* (an all-@c NULL / empty call returns @c NULL).
* @{
*/
-- "greatest" / "least" are col_name keywords, so the CREATE FUNCTION name
-- must be quoted; callers reach them qualified as provsql.greatest(...).
-- Idempotence: max / min ignore repeats, so identical children (same gate)
-- are de-duplicated -- greatest(x, x, y) == greatest(x, y) -- and a single
-- surviving child collapses to itself -- greatest(x) == x. DISTINCT also sorts
-- the children, so the argument order does not matter for gate sharing. (Two
-- independent draws of the same distribution are distinct gates and are NOT
-- de-duplicated.)
CREATE OR REPLACE FUNCTION "greatest"(VARIADIC args random_variable[])
RETURNS random_variable AS
$$
DECLARE
children uuid[];
BEGIN
IF args IS NULL THEN
RETURN NULL;
END IF;
SELECT array_agg(DISTINCT (a)::uuid) INTO children
FROM unnest(args) a WHERE a IS NOT NULL;
IF children IS NULL OR array_length(children, 1) IS NULL THEN
RETURN NULL;
END IF;
IF array_length(children, 1) = 1 THEN
RETURN provsql.random_variable_make(children[1]);
END IF;
RETURN provsql.random_variable_make(
provsql.provenance_arith(5, children)); -- 5 = PROVSQL_ARITH_MAX
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
CREATE OR REPLACE FUNCTION "least"(VARIADIC args random_variable[])
RETURNS random_variable AS
$$
DECLARE
children uuid[];
BEGIN
IF args IS NULL THEN
RETURN NULL;
END IF;
SELECT array_agg(DISTINCT (a)::uuid) INTO children
FROM unnest(args) a WHERE a IS NOT NULL;
IF children IS NULL OR array_length(children, 1) IS NULL THEN
RETURN NULL;
END IF;
IF array_length(children, 1) = 1 THEN
RETURN provsql.random_variable_make(children[1]);
END IF;
RETURN provsql.random_variable_make(
provsql.provenance_arith(6, children)); -- 6 = PROVSQL_ARITH_MIN
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
/**
* @brief Build a @c random_variable from a guarded-selection @c gate_case.
*
* Thin @c random_variable wrapper over @c provenance_case (defined with the
* other gate builders, since it is uuid-only), the target of the planner-hook
* @c CASE-over-RV rewrite: the hook flattens the branches into
* @c [guard_1, value_1, ..., default] and emits this call so an RV-typed
* @c CASE surfaces as a first-class @c random_variable.
*/
CREATE OR REPLACE FUNCTION rv_case(
children UUID[]
)
RETURNS random_variable AS
$$
SELECT provsql.random_variable_make(provsql.provenance_case(children));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Build an @c agg_token from a guarded-selection @c gate_case.
*
* The aggregate-carrier analogue of @c rv_case: a thin @c agg_token wrapper
* over the carrier-agnostic @c provenance_case, the target of the planner-hook
* lowering of a searched @c CASE whose guards are aggregate comparisons and
* whose branches are aggregates. The branches (and default) are already
* flattened into @c [guard_1, value_1, ..., default] UUIDs. The display cell
* carries the actual-world CASE value -- the branch selected on the actual
* data, resolved by @c agg_gate_value, exactly as a bare aggregate's cell
* carries its actual-world value. The probabilistic result is produced by
* the measure evaluators (``expected`` / ``probability`` / possible-worlds /
* Monte Carlo) from the gate, not the token's cell.
*/
CREATE OR REPLACE FUNCTION agg_case(
children UUID[]
)
RETURNS agg_token AS
$$
SELECT format('( %s , %s )', t::text,
coalesce(provsql.agg_gate_value(t)::text, ''))::provsql.agg_token
FROM (SELECT provsql.provenance_case(children) AS t) AS s;
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @} */
/**
* @name Aggregates over random_variable
*
* An overload of the standard
* @c sum aggregate that takes a @c random_variable per row and returns
* the @c random_variable representing the (provenance-weighted) sum.
* Lives in the @c provsql schema so a @c sum(random_variable) call
* resolves to it without colliding with the built-in numeric @c sum
* overloads in @c pg_catalog.
*
* Direct calls outside a provenance-tracked query treat each row's
* contribution unconditionally (no per-row Boolean selector). When
* the planner hook sees a @c provsql.sum @c Aggref over a
* provenance-tracked query, it wraps the per-row argument @c x in
* provsql.mixture(prov_token, x, provsql.as_random(0)) so the
* aggregate's effective semantics become
* @f$\mathrm{SUM}(x) = \sum_i \mathbf{1}\{\varphi_i\} \cdot X_i@f$,
* the natural extension of semimodule-provenance to RV-valued M.
*
* The internal state is the array of UUIDs of the per-row mixtures.
* The final function builds a single @c gate_arith @c PLUS over them
* (or returns @c as_random(0) for an empty group, the additive
* identity). Sharing on @c provenance_arith's v5 hash means two
* @c sum invocations over the same set of rows collide on the same
* gate.
*
* @{
*/
/**
* @brief Per-row helper: wrap an RV in @c mixture(prov, rv, as_random(0)).
*
* Internal helper used by the planner-hook rewriter to lift a
* @c sum(random_variable) argument into its provenance-aware form.
* Encodes one row's contribution to the SUM as a Bernoulli mixture
* over the row's provenance: with probability @c P(prov) the mixture
* samples @c rv, otherwise it samples the additive identity
* @c as_random(0). Exposed as a regular SQL function so the planner
* can construct a @c FuncExpr by name without needing to disambiguate
* @c mixture / @c as_random overloads at OID-lookup time.
*/
CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
prov uuid, rv random_variable)
RETURNS random_variable AS
$$
SELECT provsql.mixture(prov, rv, provsql.as_random(0::double precision));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Identity-parameterised per-row wrap for an RV-returning aggregate.
*
* Generalises the two-argument @ref rv_aggregate_semimod. The else-branch
* (a row's contribution when its provenance is false) is
* @c as_random(@p identity) instead of the additive @c as_random(0). The
* planner-hook rewrite bakes each aggregate's own identity element into the
* wrap -- @c 1 for @c product, @f$-\infty@f$ / @f$+\infty@f$ for @c max /
* @c min -- so the aggregate's final function is a plain fold over the
* per-row mixtures with no gate inspection. @c sum keeps the two-argument
* form (@c identity @c = @c 0).
*/
CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
prov uuid, rv random_variable, identity double precision)
RETURNS random_variable AS
$$
SELECT provsql.mixture(prov, rv, provsql.as_random(identity));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Per-row denominator wrap for @c avg(random_variable): the
* provenance indicator @f$\mathbf{1}\{\varphi\}@f$.
*
* The row contributes @c 1 to the running count when present and @c 0 when
* absent, so @c sum over these wraps is the provenance-weighted count
* @f$\sum_i \mathbf{1}\{\varphi_i\}@f$. The planner-hook rewrites
* @c avg(x) into @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
* @c sum(rv_aggregate_indicator(prov)) -- the "@c AVG @c = @c SUM @c /
* @c COUNT" identity lifted into the @c random_variable algebra -- so
* @c avg rides entirely on @c sum's fold and never inspects a gate.
*/
CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov uuid)
RETURNS random_variable AS
$$
SELECT provsql.rv_aggregate_semimod(prov, provsql.as_random(1::double precision));
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Value-aware presence indicator: NULL when the row's aggregated
* value is NULL.
*
* SQL aggregates skip NULL inputs, so a NULL @c random_variable cell must
* not count in @c avg's denominator: the wrap yields NULL (which the
* @c sum fold skips) exactly when the value is NULL, and the plain
* one-argument indicator otherwise. The planner-hook @c avg rewrite
* emits this form; the one-argument indicator remains for the internal
* public-form defaults.
*/
CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov uuid, rv random_variable)
RETURNS random_variable AS
$$
SELECT CASE WHEN rv IS NULL THEN NULL
ELSE provsql.rv_aggregate_indicator(prov) END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/**
* @brief State-transition function for @c sum(random_variable).
*
* Appends the input RV's UUID to the running array. NULL inputs are
* skipped (matching standard SUM semantics). The aggregate's INITCOND
* is @c '{}' so the FINALFUNC always runs and can tell an empty group
* (state @c '{}') apart from a group whose every input was NULL -- both
* of which SQL reports as @c NULL.
*/
CREATE OR REPLACE FUNCTION sum_rv_sfunc(
state uuid[], rv random_variable)
RETURNS uuid[] AS
$$
SELECT CASE
WHEN rv IS NULL THEN state
ELSE array_append(state, (rv)::uuid)
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/**
* @brief Final function for @c sum(random_variable): build a
* @c gate_arith PLUS root.
*
* Empty group (@c state = @c '{}'): return @c NULL, as SQL's @c sum
* does over zero rows -- the same answer the @c agg_token path gives
* for @c sum over an empty aggregation.
*
* Singleton group: return the single child directly without minting a
* useless single-child @c gate_arith.
*
* Otherwise: build @c gate_arith(PLUS, state) via @c provenance_arith.
*/
CREATE OR REPLACE FUNCTION sum_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
arith_token uuid;
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
IF array_length(state, 1) = 1 THEN
RETURN provsql.random_variable_make(state[1]);
END IF;
arith_token := provsql.provenance_arith(0, state); -- 0 = PROVSQL_ARITH_PLUS
RETURN provsql.random_variable_make(arith_token);
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE sum(random_variable) (
SFUNC = sum_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = sum_rv_ffunc
);
/**
* @brief Numerator final function for the @c avg rewrite: @c sum,
* @c NULL on an empty group.
*
* Behaviourally identical to @ref sum_rv_ffunc; kept as a separate
* catalog entry because the @c avg rewrite names it explicitly. The
* planner-hook @c avg rewrite emits
* @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
* @c sum(rv_aggregate_indicator(prov)); @c random_variable_div is
* @c STRICT, so an empty group propagates the numerator's @c NULL and
* @c avg is @c NULL -- the standard SQL @c AVG convention -- while a
* non-empty group behaves exactly like @c sum.
*/
CREATE OR REPLACE FUNCTION rv_sum_or_null_ffunc(state uuid[])
RETURNS random_variable AS
$$
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
IF array_length(state, 1) = 1 THEN
RETURN provsql.random_variable_make(state[1]);
END IF;
RETURN provsql.random_variable_make(
provsql.provenance_arith(0, state)); -- 0 = PROVSQL_ARITH_PLUS
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE rv_sum_or_null(random_variable) (
SFUNC = sum_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = rv_sum_or_null_ffunc
);
/**
* @brief Final function for @c avg(random_variable).
*
* @c avg lifts the "@c AVG @c = @c SUM @c / @c COUNT" identity into the
* @c random_variable algebra:
* @f[
* \mathrm{AVG}(x) \;=\; \frac{\sum_i \mathbf{1}\{\varphi_i\} \cdot X_i}
* {\sum_i \mathbf{1}\{\varphi_i\}}.
* @f]
* In a provenance-tracked query the planner-hook rewrites @c avg(x) into
* @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
* @c sum(rv_aggregate_indicator(prov)) (see
* @c make_rv_aggregate_expression), so both the numerator and the
* provenance-weighted count denominator are built by @c sum's fold and no
* gate is inspected. This FFUNC is therefore reached only on an
* @em untracked call, where every row is unconditionally present: the
* numerator is @c sum over the raw per-row RVs and the denominator is the
* plain row count @c n (each row contributing @c as_random(1)).
*
* Empty group: returns @c NULL, matching standard SQL @c AVG (and unlike
* @c sum, whose empty group is the additive identity @c as_random(0)):
* the caller cannot otherwise disambiguate "0 rows" from "rows summing
* to 0".
*/
CREATE OR REPLACE FUNCTION avg_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
n integer;
i integer;
num_token uuid;
denom_token uuid;
denom_state uuid[] := '{}';
one_uuid uuid;
BEGIN
IF state IS NULL THEN
RETURN NULL;
END IF;
n := array_length(state, 1);
IF n IS NULL THEN
RETURN NULL;
END IF;
one_uuid := (provsql.as_random(1::double precision))::uuid;
FOR i IN 1..n LOOP
denom_state := array_append(denom_state, one_uuid);
END LOOP;
IF n = 1 THEN
num_token := state[1];
denom_token := denom_state[1];
ELSE
num_token := provsql.provenance_arith(0, state); -- 0 = PLUS
denom_token := provsql.provenance_arith(0, denom_state); -- 0 = PLUS
END IF;
RETURN provsql.random_variable_make(
provsql.provenance_arith(
3, -- 3 = PROVSQL_ARITH_DIV
ARRAY[num_token, denom_token]));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE avg(random_variable) (
SFUNC = sum_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = avg_rv_ffunc
);
/**
* @brief Final function for @c product(random_variable): fold a
* @c gate_arith TIMES root over the per-row contributions.
*
* Multiplicative analogue of @c sum(random_variable):
* @f[
* \mathrm{PRODUCT}(x) \;=\; \prod_i \big(\mathbf{1}\{\varphi_i\} \cdot X_i
* + \mathbf{1}\{\neg\varphi_i\} \cdot 1\big)
* \;=\; \prod_{i : \varphi_i} X_i.
* @f]
* Each per-row contribution already carries the multiplicative identity
* as its absent-row value: a provenance-tracked query wraps the argument
* as @c mixture(prov_i, X_i, as_random(1)) (identity baked in by the
* three-argument @ref rv_aggregate_semimod), and an untracked call passes
* the raw RV through. So the FFUNC is a plain fold with no gate
* inspection: @c gate_arith(TIMES, state).
*
* Reuses @c sum_rv_sfunc as the state-transition function. Empty group:
* @c NULL, by symmetry with @c sum / @c avg / @c min / @c max, which take
* it from their standard-SQL counterparts. The multiplicative identity
* @c as_random(1) stays the absent-row value inside the fold, where it
* belongs; a group containing no row has no product to report.
* Singleton group: the single child directly, without a one-child TIMES
* root.
*/
CREATE OR REPLACE FUNCTION product_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
IF array_length(state, 1) = 1 THEN
RETURN provsql.random_variable_make(state[1]);
END IF;
RETURN provsql.random_variable_make(
provsql.provenance_arith(1, state)); -- 1 = PROVSQL_ARITH_TIMES
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE product(random_variable) (
SFUNC = sum_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = product_rv_ffunc
);
/**
* @brief Final function for @c max(random_variable) / @c min(random_variable):
* fold a @c gate_arith MAX / MIN root over the per-row contributions.
*
* The order-statistic analogues of @c sum / @c product:
* @f[
* \mathrm{MAX}(x) = \max_{i : \varphi_i} X_i, \qquad
* \mathrm{MIN}(x) = \min_{i : \varphi_i} X_i.
* @f]
* A row absent in a world (its provenance @f$\varphi_i@f$ false) must not
* perturb the extremum, so it contributes the order-statistic identity
* @f$\mp\infty@f$. That identity is baked into each per-row contribution
* upstream: a provenance-tracked query wraps the argument as
* @c mixture(prov_i, X_i, as_random(∓∞)) (via the three-argument
* @ref rv_aggregate_semimod), and an untracked call passes the raw RV
* through. So the FFUNC is a plain fold with no gate inspection:
* @c gate_arith(@p op, state).
*
* Empty group: @c NULL, as SQL's @c min / @c max report over zero rows.
* @p identity belongs to the catalog signature and describes the per-row
* absent contribution baked in upstream; the empty group does not consult
* it, since @f$\mp\infty@f$ is an artefact of the fold rather than a value
* the group actually contains.
* Singleton group: the single child directly.
*/
CREATE OR REPLACE FUNCTION extremum_rv_ffunc(
state uuid[], op integer, identity double precision)
RETURNS random_variable AS
$$
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
IF array_length(state, 1) = 1 THEN
RETURN provsql.random_variable_make(state[1]);
END IF;
RETURN provsql.random_variable_make(
provsql.provenance_arith(op, state));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
CREATE OR REPLACE FUNCTION max_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
-- 5 = PROVSQL_ARITH_MAX; empty-group / row-absent identity -inf.
SELECT provsql.extremum_rv_ffunc(state, 5, '-Infinity'::double precision);
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
CREATE OR REPLACE FUNCTION min_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
-- 6 = PROVSQL_ARITH_MIN; empty-group / row-absent identity +inf.
SELECT provsql.extremum_rv_ffunc(state, 6, 'Infinity'::double precision);
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE max(random_variable) (
SFUNC = sum_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = max_rv_ffunc
);
CREATE AGGREGATE min(random_variable) (
SFUNC = sum_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = min_rv_ffunc
);
-- ---------------------------------------------------------------------
-- SQL-standard statistic aggregates over random_variable rows:
-- covar_pop / covar_samp / corr (two-argument), stddev_pop / stddev_samp
-- (one-argument), and the ordered-set percentile_cont.
--
-- Row presence is carried by a per-row 0/1 indicator RV: the public
-- aggregates use the certain indicator as_random(1) (every row present),
-- and a provenance-tracked query is rewritten by the planner hook
-- (make_rv_aggregate_expression) to the rv_*_impl aggregates whose extra
-- leading argument is rv_aggregate_indicator(prov), so a row absent in a
-- world drops out of every sum, the count, and the percentile member set.
-- The moment statistics are built from indicator-weighted power sums with
-- existing gate_arith opcodes (e.g. covar_pop = SXY/N - (SX/N)(SY/N)); a
-- world where the statistic is undefined (N = 0, or N = 1 for the sample
-- forms) divides by zero, which leaves the world without a value, as SQL
-- gives NULL there; the moment estimators skip such worlds. percentile_cont is the one gate the arithmetic
-- cannot express: it mints the PROVSQL_ARITH_PERCENTILE gate_arith
-- (interleaved [ind_1, x_1, ...] wires, fraction in extra) that the Monte
-- Carlo sampler evaluates by sorting each draw's present values and
-- interpolating.
-- ---------------------------------------------------------------------
/** @brief State transition for the one-argument RV statistic aggregates
* (@c stddev_pop / @c stddev_samp): append the certain indicator and the
* row's RV as a pair. NULL rows are skipped (standard SQL). */
CREATE OR REPLACE FUNCTION rv_stat1_sfunc(state uuid[], x random_variable)
RETURNS uuid[] AS
$$
SELECT CASE
WHEN x IS NULL THEN state
ELSE state || ARRAY[(provsql.as_random(1::double precision))::uuid,
(x)::uuid]
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/** @brief State transition for the two-argument RV statistic aggregates
* (@c covar_pop / @c covar_samp / @c corr): append the certain indicator
* and the row's RV pair as a triple. Rows with either side NULL are
* skipped (standard SQL covariance semantics). */
CREATE OR REPLACE FUNCTION rv_stat2_sfunc(
state uuid[], x random_variable, y random_variable)
RETURNS uuid[] AS
$$
SELECT CASE
WHEN x IS NULL OR y IS NULL THEN state
ELSE state || ARRAY[(provsql.as_random(1::double precision))::uuid,
(x)::uuid, (y)::uuid]
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/** @brief Indicator-carrying state transition for the one-argument
* @c rv_*_impl statistic aggregates: the planner-hook rewrite passes the
* row's provenance indicator @c rv_aggregate_indicator(prov) as @p ind. */
CREATE OR REPLACE FUNCTION rv_stat1_impl_sfunc(
state uuid[], ind random_variable, x random_variable)
RETURNS uuid[] AS
$$
SELECT CASE
WHEN x IS NULL THEN state
ELSE state || ARRAY[coalesce((ind)::uuid,
(provsql.as_random(1::double precision))::uuid),
(x)::uuid]
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/** @brief Indicator-carrying state transition for the two-argument
* @c rv_*_impl statistic aggregates. */
CREATE OR REPLACE FUNCTION rv_stat2_impl_sfunc(
state uuid[], ind random_variable, x random_variable, y random_variable)
RETURNS uuid[] AS
$$
SELECT CASE
WHEN x IS NULL OR y IS NULL THEN state
ELSE state || ARRAY[coalesce((ind)::uuid,
(provsql.as_random(1::double precision))::uuid),
(x)::uuid, (y)::uuid]
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/**
* @brief Mint the indicator-weighted power-sum gates shared by the
* covariance / stddev final functions.
*
* @p state is the flat interleaved aggregate state -- pairs
* @c [ind, x, ...] (@p stride 2) or triples @c [ind, x, y, ...]
* (@p stride 3). Emits @c gate_arith tokens for
* @f$N = \sum_i \mathbf{1}_i@f$, @f$SX = \sum_i \mathbf{1}_i x_i@f$,
* @f$SXX = \sum_i \mathbf{1}_i x_i^2@f$ and, at stride 3, @f$SY@f$,
* @f$SXY@f$, @f$SYY@f$. The per-row indicator gate is shared between
* @f$N@f$ and every product it weighs, so the Monte Carlo per-iteration
* cache keeps the row's presence coupled across all the sums (and a
* repeated child @c [ind, x, x] reuses the same draw of @c x, giving
* @f$x^2@f$, not two independent draws).
*/
CREATE OR REPLACE FUNCTION rv_stat_sum_tokens(
state uuid[], stride integer,
OUT n_tok uuid, OUT sx_tok uuid, OUT sxx_tok uuid,
OUT sy_tok uuid, OUT sxy_tok uuid, OUT syy_tok uuid)
AS
$$
DECLARE
nrows integer := coalesce(array_length(state, 1), 0) / stride;
inds uuid[] := '{}';
xs uuid[] := '{}';
xxs uuid[] := '{}';
ys uuid[] := '{}';
xys uuid[] := '{}';
yys uuid[] := '{}';
ind uuid;
x uuid;
y uuid;
BEGIN
FOR i IN 1..nrows LOOP
ind := state[(i-1) * stride + 1];
x := state[(i-1) * stride + 2];
inds := array_append(inds, ind);
xs := array_append(xs, provenance_arith(1, ARRAY[ind, x]));
xxs := array_append(xxs, provenance_arith(1, ARRAY[ind, x, x]));
IF stride = 3 THEN
y := state[(i-1) * stride + 3];
ys := array_append(ys, provenance_arith(1, ARRAY[ind, y]));
xys := array_append(xys, provenance_arith(1, ARRAY[ind, x, y]));
yys := array_append(yys, provenance_arith(1, ARRAY[ind, y, y]));
END IF;
END LOOP;
n_tok := provenance_arith(0, inds);
sx_tok := provenance_arith(0, xs);
sxx_tok := provenance_arith(0, xxs);
IF stride = 3 THEN
sy_tok := provenance_arith(0, ys);
sxy_tok := provenance_arith(0, xys);
syy_tok := provenance_arith(0, yys);
END IF;
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/** @brief Population-variance gate @f$SXX/N - (SX/N)^2@f$ from the
* power-sum tokens. */
CREATE OR REPLACE FUNCTION rv_stat_var_pop_token(
n_tok uuid, s_tok uuid, ss_tok uuid)
RETURNS uuid AS
$$
SELECT provsql.provenance_arith(2, ARRAY[
provsql.provenance_arith(3, ARRAY[ss_tok, n_tok]),
provsql.provenance_arith(1, ARRAY[
provsql.provenance_arith(3, ARRAY[s_tok, n_tok]),
provsql.provenance_arith(3, ARRAY[s_tok, n_tok])])]);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Sample-variance gate @f$(SXX - SX^2/N) / (N - 1)@f$ from the
* power-sum tokens (no value in a world with @f$N \le 1@f$, where it
* divides by zero, as SQL gives NULL there). */
CREATE OR REPLACE FUNCTION rv_stat_var_samp_token(
n_tok uuid, s_tok uuid, ss_tok uuid)
RETURNS uuid AS
$$
SELECT provsql.provenance_arith(3, ARRAY[
provsql.provenance_arith(2, ARRAY[
ss_tok,
provsql.provenance_arith(3, ARRAY[
provsql.provenance_arith(1, ARRAY[s_tok, s_tok]), n_tok])]),
provsql.provenance_arith(2, ARRAY[
n_tok, (provsql.as_random(1::double precision))::uuid])]);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief @f$\sqrt{\max(v, 0)}@f$ gate over a variance token: the max-clamp
* removes the tiny negative values float error can produce (variance is
* mathematically non-negative), so the POW domain guard never fires. */
CREATE OR REPLACE FUNCTION rv_stat_sqrt_token(v_tok uuid)
RETURNS uuid AS
$$
SELECT provsql.provenance_arith(7, ARRAY[
provsql.provenance_arith(5, ARRAY[
v_tok, (provsql.as_random(0::double precision))::uuid]),
(provsql.as_random(0.5::double precision))::uuid]);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Population-covariance gate @f$SXY/N - (SX/N)(SY/N)@f$ from the
* power-sum tokens. */
CREATE OR REPLACE FUNCTION rv_stat_covar_pop_token(
n_tok uuid, sx_tok uuid, sy_tok uuid, sxy_tok uuid)
RETURNS uuid AS
$$
SELECT provsql.provenance_arith(2, ARRAY[
provsql.provenance_arith(3, ARRAY[sxy_tok, n_tok]),
provsql.provenance_arith(1, ARRAY[
provsql.provenance_arith(3, ARRAY[sx_tok, n_tok]),
provsql.provenance_arith(3, ARRAY[sy_tok, n_tok])])]);
$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Final function for @c covar_pop(random_variable, random_variable). */
CREATE OR REPLACE FUNCTION covar_pop_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
t record;
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
RETURN random_variable_make(
rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/** @brief Final function for @c covar_samp(random_variable, random_variable):
* @f$(SXY - SX\,SY/N) / (N-1)@f$. */
CREATE OR REPLACE FUNCTION covar_samp_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
t record;
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
RETURN random_variable_make(
provenance_arith(3, ARRAY[
provenance_arith(2, ARRAY[
t.sxy_tok,
provenance_arith(3, ARRAY[
provenance_arith(1, ARRAY[t.sx_tok, t.sy_tok]), t.n_tok])]),
provenance_arith(2, ARRAY[
t.n_tok, (as_random(1::double precision))::uuid])]));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/** @brief Final function for @c corr(random_variable, random_variable):
* @f$\mathrm{covar\_pop} / \sqrt{\max(v_x v_y, 0)}@f$ (a zero-variance
* world divides by zero and has no value, matching SQL's NULL for a
* zero-stddev input). */
CREATE OR REPLACE FUNCTION corr_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
t record;
vx uuid;
vy uuid;
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
vx := rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok);
vy := rv_stat_var_pop_token(t.n_tok, t.sy_tok, t.syy_tok);
RETURN random_variable_make(
provenance_arith(3, ARRAY[
rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok),
rv_stat_sqrt_token(provenance_arith(1, ARRAY[vx, vy]))]));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/** @brief Final function for @c stddev_pop(random_variable). */
CREATE OR REPLACE FUNCTION stddev_pop_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
t record;
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
RETURN random_variable_make(
rv_stat_sqrt_token(
rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok)));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/** @brief Final function for @c stddev_samp(random_variable). */
CREATE OR REPLACE FUNCTION stddev_samp_rv_ffunc(state uuid[])
RETURNS random_variable AS
$$
DECLARE
t record;
BEGIN
IF state IS NULL OR array_length(state, 1) IS NULL THEN
RETURN NULL;
END IF;
SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
RETURN random_variable_make(
rv_stat_sqrt_token(
rv_stat_var_samp_token(t.n_tok, t.sx_tok, t.sxx_tok)));
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
CREATE AGGREGATE covar_pop(random_variable, random_variable) (
SFUNC = rv_stat2_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = covar_pop_rv_ffunc
);
CREATE AGGREGATE covar_samp(random_variable, random_variable) (
SFUNC = rv_stat2_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = covar_samp_rv_ffunc
);
CREATE AGGREGATE corr(random_variable, random_variable) (
SFUNC = rv_stat2_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = corr_rv_ffunc
);
CREATE AGGREGATE stddev_pop(random_variable) (
SFUNC = rv_stat1_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = stddev_pop_rv_ffunc
);
CREATE AGGREGATE stddev_samp(random_variable) (
SFUNC = rv_stat1_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = stddev_samp_rv_ffunc
);
-- The indicator-carrying rewrite targets (planner hook only; never called
-- directly by users).
CREATE AGGREGATE rv_covar_pop_impl(
random_variable, random_variable, random_variable) (
SFUNC = rv_stat2_impl_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = covar_pop_rv_ffunc
);
CREATE AGGREGATE rv_covar_samp_impl(
random_variable, random_variable, random_variable) (
SFUNC = rv_stat2_impl_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = covar_samp_rv_ffunc
);
CREATE AGGREGATE rv_corr_impl(
random_variable, random_variable, random_variable) (
SFUNC = rv_stat2_impl_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = corr_rv_ffunc
);
CREATE AGGREGATE rv_stddev_pop_impl(random_variable, random_variable) (
SFUNC = rv_stat1_impl_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = stddev_pop_rv_ffunc
);
CREATE AGGREGATE rv_stddev_samp_impl(random_variable, random_variable) (
SFUNC = rv_stat1_impl_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = stddev_samp_rv_ffunc
);
/**
* @brief Mint the @c PROVSQL_ARITH_PERCENTILE gate: the continuous
* percentile (SQL @c percentile_cont) over a group of RV rows.
*
* @p pairs is the interleaved wire list @c [ind_1, x_1, ..., ind_n, x_n]
* (each @p ind_i a 0/1 presence-indicator RV). The @p fraction is
* text-encoded in the gate's @c extra and participates in the token UUID
* (two percentiles of the same group at different fractions are distinct
* gates). Per Monte Carlo draw, the sampler collects the values whose
* indicator draws 1, sorts them, and linearly interpolates at the
* fraction; a draw with no present value has no value (SQL's NULL).
*/
CREATE OR REPLACE FUNCTION rv_percentile_make(fraction double precision,
pairs uuid[])
RETURNS random_variable AS
$$
DECLARE
token uuid;
BEGIN
IF fraction IS NULL THEN
RETURN NULL;
END IF;
IF fraction < 0 OR fraction > 1 THEN
RAISE EXCEPTION
'percentile_cont: fraction must be between 0 and 1 (got %)', fraction;
END IF;
token := public.uuid_generate_v5(
uuid_ns_provsql(),
concat('arith', '10', pairs::text, fraction::text));
-- 10 = PROVSQL_ARITH_PERCENTILE
PERFORM create_gate(token, 'arith', pairs, 10, NULL, fraction::text);
RETURN random_variable_make(token);
END
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/** @brief State transition for the public ordered-set
* @c percentile_cont(float8) WITHIN GROUP (ORDER BY random_variable):
* append the certain indicator and the row's RV. Only reachable on
* untracked input (a provenance-tracked query is rewritten to
* @c rv_percentile_impl before planning), where the sort over
* @c random_variable raises the ordering-is-meaningless diagnostic
* first -- so in practice this runs only for empty input. */
CREATE OR REPLACE FUNCTION percentile_cont_rv_sfunc(
state uuid[], x random_variable)
RETURNS uuid[] AS
$$
SELECT provsql.rv_stat1_sfunc(state, x);
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/** @brief Final function for the public ordered-set @c percentile_cont:
* receives the direct @p fraction argument after the state. */
CREATE OR REPLACE FUNCTION percentile_cont_rv_ffunc(
state uuid[], fraction double precision)
RETURNS random_variable AS
$$
SELECT CASE
WHEN state IS NULL OR array_length(state, 1) IS NULL THEN NULL
ELSE provsql.rv_percentile_make(fraction, state)
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE percentile_cont(double precision ORDER BY random_variable) (
SFUNC = percentile_cont_rv_sfunc,
STYPE = uuid[],
INITCOND = '{}',
FINALFUNC = percentile_cont_rv_ffunc
);
/** @brief Transition state for @c rv_percentile_impl: the fraction (from
* the first row) plus the interleaved indicator/value token pairs. */
CREATE TYPE rv_percentile_state AS (
fraction double precision,
tokens uuid[]
);
/** @brief State transition for @c rv_percentile_impl, the planner-hook
* rewrite target of a provenance-tracked @c percentile_cont: stashes the
* (group-constant) fraction and appends the indicator/value pair. */
CREATE OR REPLACE FUNCTION rv_percentile_impl_sfunc(
state rv_percentile_state, fraction double precision,
ind random_variable, x random_variable)
RETURNS rv_percentile_state AS
$$
SELECT ROW(
coalesce((state).fraction, fraction),
CASE
WHEN x IS NULL THEN (state).tokens
ELSE (state).tokens ||
ARRAY[coalesce((ind)::uuid,
(provsql.as_random(1::double precision))::uuid),
(x)::uuid]
END)::provsql.rv_percentile_state;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/** @brief Final function for @c rv_percentile_impl. */
CREATE OR REPLACE FUNCTION rv_percentile_impl_ffunc(state rv_percentile_state)
RETURNS random_variable AS
$$
SELECT CASE
WHEN state IS NULL OR array_length((state).tokens, 1) IS NULL THEN NULL
ELSE provsql.rv_percentile_make((state).fraction, (state).tokens)
END;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
CREATE AGGREGATE rv_percentile_impl(
double precision, random_variable, random_variable) (
SFUNC = rv_percentile_impl_sfunc,
STYPE = rv_percentile_state,
INITCOND = '(,"{}")',
FINALFUNC = rv_percentile_impl_ffunc
);
/** @} */
/** @} */
/** @} */
/** @defgroup aggregate_provenance Aggregate provenance
* Functions for building and evaluating aggregate (GROUP BY) provenance,
* including the δ-semiring operator and semimodule multiplication.
* @{
*/
/**
* @brief Create a δ-semiring gate wrapping a provenance token
*
* Used internally for aggregate provenance. Returns the token unchanged
* if it is gate_zero() or gate_one(), and gate_one() if the token is NULL.
*
* Implemented in C (gate_builders.c), with the declared cost of the
* PL/pgSQL function it was, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_delta
(token UUID)
RETURNS UUID AS
'provsql','provenance_delta' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief Build an aggregate provenance gate from grouped tokens
*
* Called internally by the query rewriter for GROUP BY queries.
* Creates an agg gate linking all contributing tokens and records
* the aggregate function OID and the computed scalar value. The gate's
* address hashes everything it records, result type and value included:
* what is recorded is written once, and two aggregations that differ only
* there (a polymorphic aggregate over two types with the same texts, a
* floating-point sum rounded differently by two plans) must be two gates.
*
* @param aggfnoid OID of the SQL aggregate function
* @param aggtype OID of the aggregate result type
* @param val computed aggregate value
* @param tokens array of provenance tokens being aggregated
* @param is_scalar true for a scalar (no GROUP BY) aggregation, whose
* output row exists even when no tuple is present; stored in the
* high bit of info2
*
* Implemented in C (gate_builders.c), with the declared cost of the
* PL/pgSQL function it was, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_aggregate(
aggfnoid integer,
aggtype integer,
val anyelement,
tokens uuid[],
is_scalar boolean DEFAULT false)
RETURNS agg_token AS
'provsql','provenance_aggregate' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief Create a semimodule scalar multiplication gate
*
* Pairs a scalar value with a provenance token, used internally by
* the query rewriter for aggregate provenance.
*
* @param val the scalar value
* @param token the provenance token to multiply
*
* Implemented in C (gate_builders.c), with the declared cost of the
* PL/pgSQL function it was, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_semimod(val anyelement, token UUID)
RETURNS UUID AS
'provsql','provenance_semimod' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief The contribution of an aggregate result to an aggregate of another
* kind over it (internal)
*
* For a row of token @p token whose value @p val is the result of an
* aggregate of another kind than the one aggregating it (an @c avg of a
* @c count, a @c max of a @c sum, an aggregate of an arithmetic expression
* over aggregates): @c semimod(g, token), where @c g is the inner
* aggregate's own gate. Its value is one per possible world, not one value
* of the database, so the evaluators that read a value per world resolve it
* and the closed forms decline it.
*/
CREATE FUNCTION provenance_semimod_nested(val agg_token, token uuid)
RETURNS uuid AS
'provsql','provenance_semimod_nested' LANGUAGE C PARALLEL SAFE IMMUTABLE;
/**
* @brief The contributions of an aggregate result to an aggregate of the
* same kind over it (internal)
*
* For a row of token @p token whose value @p val is the result of
* @c sum / @c count / @c max / @c min over a group, aggregated again by
* @c sum / @c max / @c min: the contributions @c semimod(v_i, token ⊗ k_i)
* of the group's own contributions @c semimod(v_i, k_i), so that
* @c sum(sum(x)) is @c sum(x) over the rows of the groups -- semimodule
* scalar multiplication, in every semiring. Collected by
* @c provenance_contributions_cat.
*/
CREATE FUNCTION provenance_semimod_flat(val agg_token, token uuid)
RETURNS uuid[] AS
'provsql','provenance_semimod_flat' LANGUAGE C PARALLEL SAFE IMMUTABLE;
/** @brief Concatenation of the arrays of contributions of
* @c provenance_semimod_flat (internal) */
CREATE AGGREGATE provenance_contributions_cat(uuid[]) (
SFUNC = array_cat,
STYPE = uuid[],
INITCOND = '{}'
);
/**
* @brief Semimodule gate for an aggregate that sees its NULL inputs
*
* Variant of provenance_semimod() used by the query rewriter for
* array_agg, json_agg and the like, whose result lists
* every input, NULLs included: a NULL value still yields a semimod gate,
* over the constant value gate gate_null().
*
* @param val the scalar value, possibly NULL
* @param token the provenance token to multiply
*
* Implemented in C (gate_builders.c), with the declared cost of the
* PL/pgSQL function it was, so that plans stay the same.
*/
CREATE OR REPLACE FUNCTION provenance_semimod_nullable(val anyelement, token UUID)
RETURNS UUID AS
'provsql','provenance_semimod_nullable' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
/**
* @brief The rank of a row standing for its row number (internal)
*
* Called by the query rewriter for row_number() OVER (…) over
* provenance-tracked relations, which is tracked as rank(): the two
* are equal when the ORDER BY of the window leaves no ties. Returns
* @p rank, with a warning, once per statement, when @p row_number differs
* from it.
*
* @param rank the agg_token of the rank of the row
* @param row_number the row number of the row among those of the database
* as it is, or NULL for a row absent from it (no check then)
* @param tied whether the row ties on the ORDER BY with another row
* of any world, which warns as well
*/
CREATE OR REPLACE FUNCTION row_number_as_rank(rank agg_token, row_number bigint,
tied boolean DEFAULT false)
RETURNS agg_token
AS 'provsql','row_number_as_rank' LANGUAGE C VOLATILE PARALLEL SAFE;
/** @brief Transition function of order_determined (internal) */
CREATE OR REPLACE FUNCTION order_determined_transfn(internal, integer, VARIADIC "any")
RETURNS internal
AS 'provsql','order_determined_transfn' LANGUAGE C PARALLEL SAFE;
/** @brief Final function of order_determined (internal) */
CREATE OR REPLACE FUNCTION order_determined_finalfn(internal)
RETURNS boolean
AS 'provsql','order_determined_finalfn' LANGUAGE C PARALLEL SAFE;
/**
* @brief Whether an order-dependent aggregate reads its rows in an order the
* query determines (internal)
*
* Called by the query rewriter beside @c array_agg, @c string_agg, the
* @c json_agg family and @c xmlagg over provenance-tracked relations, with
* the aggregate's own arguments (the first @p nvals ones) and @c ORDER
* @c BY, over every row of the group, those present only in other worlds
* included: false when two rows tie on the @c ORDER @c BY (or there is
* none) with different values.
*/
CREATE AGGREGATE order_determined(nvals integer, VARIADIC "any") (
SFUNC = order_determined_transfn,
STYPE = internal,
FINALFUNC = order_determined_finalfn
);
/**
* @brief The value of an order-dependent aggregate, with a warning where its
* order is not determined (internal)
*
* Returns @p val, with a warning, once per statement, when @p determined
* (from @c order_determined) is false: the order of the rows in the
* database as it is is the one read in every world. @p is_window for a
* window function (@c lag, @c first_value, ...), which warns as such.
*/
CREATE OR REPLACE FUNCTION order_checked(val anyelement, determined boolean,
is_window boolean DEFAULT false)
RETURNS anyelement
AS 'provsql','order_checked' LANGUAGE C VOLATILE PARALLEL SAFE;
/**
* @brief The bucket of a row read over its rank, for @c ntile() (internal)
*
* Called by the query rewriter for ntile(n) OVER (…) over
* provenance-tracked relations, whose bucket is computed from the rank of the
* row: rows that tie on the ORDER BY share a bucket, where SQL may
* split them. Returns @p bucket, with a warning, once per statement, when
* @p sql_bucket differs from it.
*
* @param bucket the agg_token of the tracked bucket
* @param sql_bucket the bucket PostgreSQL gave the row
*/
CREATE OR REPLACE FUNCTION ntile_as_rank(bucket agg_token, sql_bucket bigint)
RETURNS agg_token
AS 'provsql','ntile_as_rank' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief The event that an aggregate result is NULL, or is not (internal)
*
* Called by the query rewriter for an IS [NOT] NULL on an aggregate
* column of a subquery, where the aggregate's own call is not at hand. A
* row whose value is NULL contributes nothing to the aggregate's gate, so,
* the row of the aggregate being there, its value is NULL exactly when none
* of the rows its gate holds is: 1 ⊖ ⊕K over their tokens, and
* ⊕K for IS NOT NULL. A NULL @p aggtok (a padded row) is NULL.
*
* @param aggtok the aggregate result
* @param want_null true for IS NULL, false for IS NOT NULL
*/
CREATE OR REPLACE FUNCTION agg_token_null_event(aggtok agg_token, want_null boolean)
RETURNS uuid AS
$$
DECLARE
g uuid;
p uuid;
BEGIN
IF aggtok IS NULL THEN
RETURN CASE WHEN want_null THEN provsql.gate_one() ELSE provsql.gate_zero() END;
END IF;
RETURN provsql.agg_gate_null_event(provsql.agg_token_uuid(aggtok), want_null);
END
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief Whether a count may be 0 where its group is there (internal)
*
* The companion count(arg) the explosion of an aggregate value gives a
* grouped aggregate has one contribution per row of the group, 1 where arg
* has a value and 0 where it is NULL: the aggregate can be NULL only if one
* row contributes 0. Its rows are all there, those the query keeps only for
* their provenance included, which a count of the data would miss.
*
* @param cnt the count
*/
CREATE OR REPLACE FUNCTION agg_count_can_be_zero(cnt agg_token)
RETURNS boolean AS
$$
SELECT cnt IS NULL OR provsql.get_gate_type(provsql.agg_token_uuid(cnt)) <> 'agg'
OR NOT EXISTS (SELECT 1 FROM unnest(provsql.get_children(provsql.agg_token_uuid(cnt))) c
WHERE provsql.get_gate_type(c) = 'semimod')
OR EXISTS (SELECT 1
FROM unnest(provsql.get_children(provsql.agg_token_uuid(cnt))) c
WHERE provsql.get_gate_type(c) = 'semimod'
AND provsql.get_extra((provsql.get_children(c))[2]) = '0')
$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief The event that the value of gate @p g is NULL, or is not (internal)
*
* An aggregate is NULL where none of the rows its gate holds is there (a row
* whose value is NULL contributes nothing); arithmetic is NULL where one of
* its operands is; a value, where it is NULL. See @c agg_token_null_event.
*
* @param g the gate
* @param want_null true for IS NULL, false for IS NOT NULL
*/
CREATE OR REPLACE FUNCTION agg_gate_null_event(g uuid, want_null boolean)
RETURNS uuid AS
$$
DECLARE
t provsql.provenance_gate;
p uuid;
parts uuid[];
prev uuid[];
ch uuid[];
region uuid;
arm uuid;
BEGIN
t := provsql.get_gate_type(g);
IF t = 'agg' THEN
SELECT provsql.provenance_plus(coalesce(array_agg((provsql.get_children(c))[1]),
ARRAY[]::uuid[]))
INTO p
FROM unnest(provsql.get_children(g)) AS c
WHERE provsql.get_gate_type(c) = 'semimod';
RETURN CASE WHEN want_null THEN provsql.provenance_monus(provsql.gate_one(), p)
ELSE p END;
ELSIF t = 'arith' THEN
SELECT array_agg(provsql.agg_gate_null_event(c, want_null))
INTO parts
FROM unnest(provsql.get_children(g)) AS c;
IF parts IS NULL THEN
RETURN CASE WHEN want_null THEN provsql.gate_zero() ELSE provsql.gate_one() END;
END IF;
/* NULL where one operand is; not NULL where every one is not */
RETURN CASE WHEN want_null THEN provsql.provenance_plus(parts)
ELSE provsql.provenance_times(VARIADIC parts) END;
ELSIF t = 'value' THEN
RETURN CASE WHEN (coalesce(provsql.get_extra(g), '') IN ('', 'NULL')) = want_null
THEN provsql.gate_one() ELSE provsql.gate_zero() END;
ELSIF t = 'case' THEN
/* A guarded selection, [guard_1, value_1, ..., default]: NULL (not NULL)
* where the arm it selects is -- the arm's region, its guard and none of
* the earlier ones (the default: none of them), times the arm's own
* event. COALESCE(sum(x), 0) selects the sum where it has a value and 0
* elsewhere, so is NULL nowhere. */
parts := ARRAY[]::uuid[];
prev := ARRAY[]::uuid[];
ch := provsql.get_children(g);
FOR i IN 1 .. coalesce(array_length(ch, 1), 0) BY 2 LOOP
IF i = array_length(ch, 1) THEN
region := CASE WHEN cardinality(prev) = 0 THEN provsql.gate_one()
ELSE provsql.provenance_monus(provsql.gate_one(),
provsql.provenance_plus(prev))
END;
arm := ch[i];
ELSE
region := CASE WHEN cardinality(prev) = 0 THEN ch[i]
ELSE provsql.provenance_times(
ch[i],
provsql.provenance_monus(provsql.gate_one(),
provsql.provenance_plus(prev)))
END;
arm := ch[i + 1];
prev := prev || ch[i];
END IF;
parts := parts || provsql.provenance_times(
region, provsql.agg_gate_null_event(arm, want_null));
END LOOP;
RETURN provsql.provenance_plus(parts);
END IF;
RAISE EXCEPTION USING
MESSAGE = 'ProvSQL: IS [NOT] NULL on this expression over aggregates, read '
'from a subquery, is not supported',
DETAIL = 'provsql-reason: nulltest-not-aggregate; scope: gap',
ERRCODE = 'feature_not_supported';
END
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
/**
* @brief The contributions of the distinct values of a window frame
* (internal)
*
* Called by the query rewriter for dense_rank() OVER (…) over
* provenance-tracked relations, which counts the distinct ordering values
* before the current row: one provenance_semimod(1, ⊕ tokens) per
* distinct value of @p vals, the ⊕ of the tokens of the rows that have it,
* which is present when one of them is. Values are compared with the
* equality of their type, as the peers of a window are, NULLs being equal.
*
* @param vals the ordering values of the rows of the frame
* @param tokens the provenance tokens of these rows, in the same order
*/
CREATE OR REPLACE FUNCTION window_distinct_tokens(vals anyarray, tokens uuid[])
RETURNS uuid[] AS
$$
SELECT array_agg(s ORDER BY s)
FROM (SELECT provsql.provenance_semimod(1, provsql.provenance_plus(array_agg(tokens[i]))) AS s
FROM generate_subscripts(vals, 1) AS i GROUP BY vals[i]) g
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/**
* @brief Mark a part of a query to be evaluated as plain SQL, not tracked
*
* Over provenance-tracked relations, ORDER BY … LIMIT k keeps, in
* each possible world, the rows that fewer than @p k present rows precede:
* every row that may be among them is output, annotated with that condition.
* LIMIT plain(k) (FETCH FIRST plain(k) ROWS,
* OFFSET plain(m)) instead truncates the result as computed on the
* actual data, and each row kept carries its provenance in the full result.
* The function returns its argument.
*
* @param value the marked value
*/
CREATE OR REPLACE FUNCTION plain(value anyelement)
RETURNS anyelement AS
$$ SELECT value $$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/** @} */
/** @defgroup probability Probability and Shapley values
* Functions for computing probabilities, expected values, and
* game-theoretic contribution measures (Shapley/Banzhaf values)
* from provenance circuits.
* @{
*/
/**
* @brief Compute the probability of a provenance token
*
* Compiles the provenance circuit to d-DNNF and evaluates the
* probability. The compilation method can be selected explicitly.
*
* @ref probability() "probability" is a shorter alias bound to the same C symbol, so
* @c probability(token) is exactly @c probability_evaluate(token); it is
* usually preferable, and additionally carries a @c (boolean) predicate
* overload (e.g. @c probability(x @c > @c y)).
*
* @param token provenance token to evaluate
* @param method knowledge compilation method (NULL for default)
* @param arguments additional arguments for the method
*/
CREATE OR REPLACE FUNCTION probability_evaluate(
token UUID,
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS
'provsql','probability_evaluate' LANGUAGE C VOLATILE PARALLEL UNSAFE;
/**
* @brief Short alias of @ref probability_evaluate.
*
* Bound to the same C symbol as @ref probability_evaluate, so
* @c probability(token) is exactly @c probability_evaluate(token).
* Provided to match the concise polymorphic surface of @ref expected,
* @ref variance, and @ref support "support": callers are not forced to
* spell out @c probability_evaluate.
*
* @param token provenance token to evaluate
* @param method knowledge compilation method (NULL for default)
* @param arguments additional arguments for the method
*/
CREATE OR REPLACE FUNCTION probability(
token UUID,
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS
'provsql','probability_evaluate' LANGUAGE C VOLATILE PARALLEL UNSAFE;
/**
* @brief Probability of a Boolean event over random variables.
*
* The @c (boolean) overload of @c probability lets a query ask for the
* probability of an event with the natural infix grammar, e.g.
* @c probability(x @c > @c y @c AND @c x @c < @c z). When the argument
* carries a probabilistic (random_variable / aggregate) comparison, the
* planner hook intercepts the call and rewrites it into
* @c probability_evaluate over the argument's event token (a @c gate_cmp /
* Boolean combination); the body below is then never reached.
*
* When the argument is a purely deterministic Boolean (no probabilistic
* comparison) the hook leaves the call alone and the body runs, so the
* probability of a definite event is simply @c 1 when it holds and @c 0 when
* it does not (@c NULL propagates). This makes @c probability total over
* Booleans -- @c probability(1 @c > @c 0) is @c 1, @c probability(region @c =
* @c 'north') is a per-row @c 0/1 -- and it works even with
* @c provsql.active off. @c NOT strict so a default-NULL @c method does not
* short-circuit the cast.
*
* The predicate surface deliberately lives only on the short @c probability
* name, not on @c probability_evaluate: a Boolean overload of the latter
* would make @c probability_evaluate('') ambiguous (an unknown
* literal matches both the @c uuid and the @c boolean overload), breaking
* existing string-literal callers. @c probability is new, so it carries the
* predicate overload without that hazard.
*/
CREATE OR REPLACE FUNCTION probability(
predicate boolean,
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS
$$
SELECT predicate::integer::double precision;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
/**
* @brief Cheap certified probability interval of a DNF-shaped circuit.
*
* Returns @c [lower,upper] with @c lower <= probability_evaluate(token) <=
* @c upper, computed without compiling the circuit (the Olteanu-Huang d-tree
* leaf bound). Errors when @p token is not a monotone DNF over input leaves.
*/
CREATE OR REPLACE FUNCTION probability_bounds(
token UUID,
OUT lower DOUBLE PRECISION,
OUT upper DOUBLE PRECISION) AS
'provsql','probability_bounds' LANGUAGE C STABLE;
/**
* @brief Compute the expected value of a probabilistic scalar
*
* Computes E[input | prov] for either an @c agg_token (discrete
* SUM/MIN/MAX aggregation over Boolean-input gate_agg circuits, with
* @c prov as the Boolean conditioning event) or a @c random_variable
* (continuous distribution, traversed by the analytical / MC
* evaluator from @c Expectation.cpp).
*
* Implementation: thin wrapper over @c moment(input, 1, prov, method,
* arguments). Both branches converge on the same machinery; the
* agg_token side computes E[X] as the @f$k=1@f$ instance of the
* @f$n^k@f$-tuple enumeration in @c agg_raw_moment, the
* random_variable side calls @c compute_expectation through
* @c rv_moment.
*
* @param input aggregate expression or random variable to compute E[·] of
* @param prov provenance condition (defaults to gate_one(), i.e., unconditional)
* @param method knowledge compilation method (agg_token path only)
* @param arguments additional arguments for the method (agg_token path only)
*/
CREATE OR REPLACE FUNCTION expected(
input ANYELEMENT,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS $$
SELECT moment(input, 1, prov, method, arguments);
$$ LANGUAGE sql PARALLEL UNSAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Internal: shared C entry point for variance / moment / central_moment.
*
* The @c expected() SQL function reaches the Expectation evaluator
* through @c provenance_evaluate_compiled(..., 'expectation', ...).
* The variance / raw-moment / central-moment SQL functions need an
* extra @p k integer argument that does not fit that dispatcher's
* signature, so they go through this dedicated entry point. Returns
* E[X^k] when @p central is FALSE, or E[(X - E[X])^k] when TRUE; NULL
* when the value is defined in no world.
*/
CREATE OR REPLACE FUNCTION rv_moment(
token uuid, k integer, central boolean,
prov uuid DEFAULT gate_one())
RETURNS double precision
AS 'provsql','rv_moment' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/** @brief Exact E[AVG^k | COUNT >= 1] over independent rows (the joint
* (sum, count) fold); NULL when the shape is out of scope (shared
* leaves, compound contributors), signalling @c agg_raw_moment's avg
* arm to fall back to the Monte-Carlo scalar path. */
CREATE OR REPLACE FUNCTION agg_avg_moment_exact(token uuid, k integer)
RETURNS double precision
AS 'provsql','agg_avg_moment_exact' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Collapsed (Rao-Blackwellised) raw moment E[C^k] of a correlated
* COUNT / SUM whose per-row selection events are coupled through a single
* shared continuous latent: 1-D quadrature over the latent, closed-form
* per-row CDF given it (O(G·n), exact up to the grid). NULL when the
* circuit does not match the shared-latent pattern (caller falls back to
* the exact n^k enumeration). k in {1, 2}. */
CREATE OR REPLACE FUNCTION agg_collapsed_moment(token uuid, k integer)
RETURNS double precision
AS 'provsql','agg_collapsed_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief Both collapsed raw moments {E[C], E[C^2]} of a correlated COUNT / SUM
* from a single circuit load and plan build; NULL when the shared-latent
* pattern does not match. @c variance() uses this so a mean+variance readout
* traverses the circuit once rather than calling @c agg_collapsed_moment twice
* (the load and O(n) plan build dominate once the grid loop is arithmetic). */
CREATE OR REPLACE FUNCTION agg_collapsed_moments(token uuid)
RETURNS double precision[]
AS 'provsql','agg_collapsed_moments' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/** @brief For each contribution of an aggregation, in the order of its
* children, the number of its independence component: contributions reaching
* a common source of randomness (an input, a block of a multivalued input, a
* random-variable leaf or the parameters of a latent one) share one, and
* contributions of different components are independent. NULL for a gate
* that is no aggregation (internal, used by agg_raw_moment). */
CREATE OR REPLACE FUNCTION agg_row_components(token uuid)
RETURNS integer[]
AS 'provsql','agg_row_components' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Boolean event "this aggregate-carrying gate's value is defined
* (non-NULL) in the world".
*
* Backs the conditional-on-defined convention of the aggregate moment
* readouts: a scalar @c count (no @c GROUP BY) and constants have a value
* in every world -- a scalar count over no row is the real value @c 0 --
* so their defined event is @c gate_one(); every other aggregate (a
* @c sum over no row is SQL @c NULL, a grouped one has no row at all) is
* defined only where "some contributing row is present", the OR of the
* semimod children's row tokens; a @c case gate's value is defined iff its first-match
* selected branch's value is (the same region walk as the moment
* evaluator, conjoined per branch). Anything else (an @c arith
* composite, whose agg_token running value is total) counts as always
* defined.
*/
CREATE OR REPLACE FUNCTION agg_defined_event(token uuid)
RETURNS uuid AS $$
DECLARE
gt provenance_gate := get_gate_type(token);
fname varchar;
toks uuid[];
wires uuid[];
nw integer;
m integer;
i integer;
running_neg uuid := gate_one();
parts uuid[] := '{}';
BEGIN
IF token = gate_null() THEN
RETURN gate_zero(); -- the NULL value: never defined
END IF;
IF gt = 'agg' THEN
SELECT proname INTO fname
FROM pg_proc WHERE oid = (get_infos(token)).info1;
-- A scalar COUNT has a row in every world, counting a real 0 over none;
-- every other aggregate (a SUM over no row is SQL NULL, a grouped one has
-- no row at all) is defined only where a contributing row is.
IF fname = 'count' AND (get_infos(token)).info2 < 0 THEN
RETURN gate_one();
END IF;
SELECT array_agg((get_children(c))[1]) INTO toks
FROM unnest(get_children(token)) AS c;
IF toks IS NULL THEN
RETURN gate_zero(); -- structurally empty aggregate: never defined
END IF;
RETURN provenance_plus(toks);
ELSIF gt = 'case' THEN
wires := get_children(token);
nw := array_length(wires, 1);
m := (nw - 1) / 2;
FOR i IN 1..m LOOP
parts := parts || provenance_times(
running_neg, wires[2 * i - 1],
agg_defined_event(wires[2 * i]));
running_neg := provenance_times(running_neg,
provenance_not(wires[2 * i - 1]));
END LOOP;
parts := parts || provenance_times(running_neg,
agg_defined_event(wires[nw]));
RETURN provenance_plus(parts);
ELSIF gt = 'arith' THEN
-- Arithmetic is STRICT: a value exists where every operand's does. Saying
-- gate_one here made a var_pop moment count the world where the group has
-- no row. Its CASE guards a count against 0 and against 1, and a
-- comparison over an aggregate that has no value holds in no world, so in
-- the empty world neither guard fires and the FORMULA arm is the one
-- selected -- an arith over the sums, whose operands have no value there.
-- Declared defined, that world was counted, and expected(var_pop(x)) came
-- out NaN for a one-row group whose value is 0 wherever it exists.
SELECT array_agg(agg_defined_event(c)) INTO parts
FROM unnest(get_children(token)) AS c;
IF parts IS NULL OR array_length(parts, 1) IS NULL THEN
RETURN gate_one();
END IF;
RETURN provenance_times(VARIADIC parts);
END IF;
-- value / anything else: a value exists in every world (gate_null, the one
-- value that never does, is answered at the top).
RETURN gate_one();
END
$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
SET search_path=provsql,pg_temp,public SECURITY DEFINER;
/**
* @brief Compute the raw moment E[X^k | prov] of an agg_token aggregate
*
* Sister of @c expected() for the agg_token side of the polymorphic
* @c moment / @c variance / @c central_moment dispatch. Supports the
* same aggregation functions as @c expected: SUM (which COUNT
* normalises to at the gate level via @c Aggregation.cpp:322), MIN,
* MAX, and AVG (exact over independent / laminar rows via the joint
* (sum, count) distribution, Monte-Carlo scalar fallback otherwise).
* MIN / MAX / AVG are NULL on an empty group, so their moments are
* CONDITIONAL on the aggregate being defined -- NULL only when it never
* is; SUM / COUNT treat the empty world as the real value 0.
*
* Strategy:
* - SUM: with X = Σᵢ Iᵢ·vᵢ (Iᵢ the per-row inclusion indicator,
* vᵢ the row's value), expanding X^k and taking expectation gives
* @f$E[X^k] = \sum_{(i_1,\ldots,i_k) \in \{1..n\}^k} v_{i_1}\cdots v_{i_k}
* \cdot P(\bigwedge_{i \in \text{distinct}(i_1..i_k)} I_i)@f$.
* We enumerate the @f$n^k@f$ tuples, conjoin the distinct inclusion
* tokens (and @p prov when conditioning), and evaluate the
* probability via @c probability_evaluate.
* - MIN / MAX: replace @c v with @c v^k in the rank-based
* enumeration that @c expected already uses; @c MAX is handled by
* sign-flipping per the existing trick (negate vs. rerank), with
* the outer multiplier becoming @f$(-1)^k@f$ instead of just @f$-1@f$.
*
* Cost: SUM is @f$O(n^k)@f$ probability evaluations -- tractable for
* small @p k or small @p n; for larger sizes, prefer reaching for the
* sampler. MIN / MAX stay linear in @p n.
*/
CREATE OR REPLACE FUNCTION agg_raw_moment(
token agg_token,
k integer,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS $$
DECLARE
aggregation_function VARCHAR;
child_pairs uuid[];
pair_children uuid[];
n integer;
i integer;
j integer;
vals float8[];
toks uuid[];
total float8;
total_probability float8;
tup integer[];
d integer;
prod_v float8;
distinct_tok uuid[];
conj_token uuid;
prob float8;
sign_max float8;
is_scalar boolean;
defined_tok uuid;
BEGIN
IF token IS NULL OR k IS NULL THEN
RETURN NULL;
END IF;
IF k < 0 THEN
RAISE EXCEPTION 'agg_raw_moment(): k must be non-negative (got %)', k;
END IF;
-- Aggregate-carrier CASE (a gate_case over aggregate branches): a first-match
-- guarded selection. The moment is CONDITIONAL on the CASE's value being
-- defined (NULL only when it never is, mirroring the MIN/MAX convention):
-- E[pick^k | defined ∧ prov]
-- = Σ_i P(region_i ∧ def_i) · E[value_i^k | region_i ∧ def_i]
-- / Σ_i P(region_i ∧ def_i),
-- where region_i = (¬g_1 ∧ … ∧ ¬g_{i-1}) ∧ g_i ∧ prov is the world set that
-- selects branch i (the default's region is "all guards false") and def_i is
-- the branch's defined event (agg_defined_event: gate_one for sum / count /
-- constants, "some row present" for min / max / avg, recursive for a nested
-- CASE). Both factors are exact: probability() over the region ∧ def event,
-- and the conditional aggregate moment (a recursive agg_raw_moment on the
-- branch aggregate, which conditions on its own definedness within the
-- region, so the two factors weigh the same worlds). The regions are
-- mutually exclusive, so the terms sum with no inclusion-exclusion, and
-- correlation between a guard and its branch (shared input tuples) is
-- carried by the conditioning, exactly as HAVING carries it. When every
-- branch is defined everywhere, the defined mass equals P(prov) and the
-- formula reduces to the plain region-weighted sum.
IF get_gate_type(token) = 'case' THEN
IF k = 0 THEN
RETURN 1;
END IF;
DECLARE
wires uuid[] := get_children(token);
nw integer := array_length(get_children(token), 1);
m integer := (array_length(get_children(token), 1) - 1) / 2;
running_neg uuid := gate_one();
region_full uuid;
prov_p float8;
p float8;
total float8 := 0;
def_mass float8 := 0;
ci integer;
vuid uuid;
bm float8;
BEGIN
prov_p := probability(prov);
IF prov_p IS NULL OR prov_p <= 0 THEN
RETURN NULL; -- impossible conditioning event
END IF;
-- Branches 1..m are the guarded WHENs; branch m+1 is the ELSE default,
-- whose region is "all guards false".
FOR ci IN 1 .. m + 1 LOOP
IF ci <= m THEN
region_full := provenance_times(running_neg, wires[2 * ci - 1], prov);
vuid := wires[2 * ci];
running_neg :=
provenance_times(running_neg, provenance_not(wires[2 * ci - 1]));
ELSE
region_full := provenance_times(running_neg, prov);
vuid := wires[nw];
END IF;
p := probability(provenance_times(region_full,
agg_defined_event(vuid)));
IF p > 0 THEN
-- E[value_i^k | region_i ∧ def_i]: a constant branch is a Dirac
-- (c^k, exact); a single aggregate or nested CASE is exact via
-- agg_raw_moment (whose MIN/MAX/CASE arms condition on their own
-- definedness within the region); an arithmetic / composite branch
-- takes the Monte-Carlo scalar path (which composes with the
-- aggregate leaves).
IF get_gate_type(vuid) = 'value' THEN
bm := power(CAST(get_extra(vuid) AS float8), k);
ELSIF get_gate_type(vuid) IN ('agg', 'case') THEN
bm := agg_raw_moment(agg_token_make(vuid, 0), k, region_full,
method, arguments);
ELSE
bm := rv_moment(vuid, k, false, region_full);
END IF;
total := total + p * bm;
def_mass := def_mass + p;
END IF;
END LOOP;
IF def_mass <= 0 THEN
RETURN NULL; -- the CASE's value is never defined under prov
END IF;
RETURN total / def_mass;
END;
END IF;
IF get_gate_type(token) <> 'agg' THEN
IF get_gate_type(token) IN ('arith', 'conditioned') THEN
-- An arithmetic combination of aggregates (SUM(x) + SUM(y), SUM(x) / 2),
-- or a conditioning of one: the scalar evaluator, exact over the
-- possible worlds of few inputs, sampled otherwise
RETURN rv_moment((token)::uuid, k, false, prov);
ELSE
RAISE EXCEPTION USING MESSAGE='Wrong gate type for agg_raw_moment computation',
DETAIL = 'provsql-reason: moment-not-an-aggregate; scope: deliberate';
END IF;
END IF;
IF k = 0 THEN
RETURN 1;
END IF;
SELECT pp.proname::varchar FROM pg_proc pp
WHERE oid=(get_infos(token)).info1
INTO aggregation_function;
child_pairs := get_children(token);
n := COALESCE(array_length(child_pairs, 1), 0);
-- A contribution whose value is itself an aggregate (an avg of a count, a
-- max of a sum, an aggregate of an arithmetic expression over aggregates:
-- provenance_semimod_nested) takes one value per possible world, not a
-- constant read off its gate, so none of the closed forms below applies to
-- it. The scalar evaluator reads such a value in every world.
IF EXISTS (SELECT 1 FROM unnest(child_pairs) AS c
WHERE get_gate_type((get_children(c))[2]) <> 'value') THEN
RETURN rv_moment((token)::uuid, k, false, prov);
END IF;
IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
-- count(*) and count(col) both keep the COUNT identity at the gate level,
-- their value being the SUM of per-row 1 / 0-or-1 indicators, so their
-- moments are computed exactly like SUM.
--
-- The value exists only where a contributing row does: SUM over no row is
-- SQL NULL, and a grouped aggregation has no row there at all. So the
-- moment conditions on that event, as the MIN / MAX arms do, and
-- @c expected(sum(x)) of a group equals @c expected(sum(x), provenance()).
-- A scalar COUNT is the exception: its row is always there and counts a
-- real 0 over no row.
is_scalar := (get_infos(token)).info2 < 0; -- the scalar flag, high bit
-- Extract per-child token + value arrays.
vals := ARRAY[]::float8[];
toks := ARRAY[]::uuid[];
FOR i IN 1..n LOOP
pair_children := get_children(child_pairs[i]);
toks := toks || pair_children[1];
vals := vals || CAST(get_extra(pair_children[2]) AS float8);
END LOOP;
defined_tok := CASE
WHEN aggregation_function = 'count' AND is_scalar THEN gate_one()
WHEN n = 0 THEN gate_zero()
ELSE provenance_plus(toks) END;
IF n = 0 THEN
-- No contributing row at all: a real 0 for a scalar COUNT, SQL NULL
-- (never defined) otherwise.
RETURN CASE WHEN defined_tok = gate_one() THEN 0 ELSE NULL END;
END IF;
-- Collapsed fast path: a correlated COUNT / SUM whose per-row selection
-- events share a single continuous latent has an O(G·n) 1-D quadrature,
-- vastly cheaper than the O(n^k) tuple enumeration below (which is the
-- O(n^2) pair-probability bottleneck for the variance). Only fires
-- unconditionally (prov = one) and for k in {1, 2}; agg_collapsed_moment
-- returns NULL when the shared-latent pattern does not match, and we
-- fall through to the exact enumeration. Its moment counts the worlds
-- without a row as 0, which contribute nothing, so it only needs the
-- conditional normalisation below.
IF prov = gate_one() AND k <= 2 THEN
total := agg_collapsed_moment((token)::uuid, k);
IF total IS NOT NULL THEN
IF defined_tok = gate_one() THEN
RETURN total;
END IF;
prob := probability_evaluate(defined_tok, method, arguments);
IF prob IS NULL OR prob <= 0 THEN
RETURN NULL;
END IF;
RETURN total / prob;
END IF;
END IF;
-- Independent rows: the first two moments from one probability per row.
-- Rows of different independence components (agg_row_components: no
-- source of randomness in common) are independent, so a pair of them is
-- present with the product of their probabilities, and
-- E[S] = sum_i v_i p_i,
-- E[S^2] = sum_i v_i^2 p_i + (sum_i v_i p_i)^2 - sum_c (sum_{i in c} v_i p_i)^2
-- + sum_{i <> j in one component} v_i v_j P(t_i and t_j):
-- n probability evaluations, plus one per pair within a component,
-- instead of one per pair -- n^2, each a Monte Carlo estimate when the
-- rows' events compare random variables. Unconditional only (an outer
-- event couples the rows).
IF prov = gate_one() AND k <= 2 THEN
DECLARE
comp integer[] := agg_row_components((token)::uuid);
p float8[] := ARRAY[]::float8[];
s1 float8 := 0;
sc float8[];
ncomp integer;
pair_tok uuid[];
BEGIN
FOR i IN 1..n LOOP
p := p || probability_evaluate(toks[i], method, arguments);
s1 := s1 + vals[i] * p[i];
END LOOP;
IF k = 1 THEN
total := s1;
ELSE
ncomp := (SELECT max(c) FROM unnest(comp) AS c);
sc := array_fill(0::float8, ARRAY[ncomp]);
total := 0;
FOR i IN 1..n LOOP
total := total + vals[i] * vals[i] * p[i];
sc[comp[i]] := sc[comp[i]] + vals[i] * p[i];
END LOOP;
total := total + s1 * s1;
FOR i IN 1..ncomp LOOP
total := total - sc[i] * sc[i];
END LOOP;
FOR i IN 1..n LOOP
FOR j IN 1..n LOOP
CONTINUE WHEN i = j OR comp[i] <> comp[j];
pair_tok := ARRAY(SELECT DISTINCT unnest(ARRAY[toks[i], toks[j]]));
total := total + vals[i] * vals[j] * CASE
WHEN array_length(pair_tok, 1) = 1 THEN p[i]
ELSE probability_evaluate(provenance_times(VARIADIC pair_tok),
method, arguments) END;
END LOOP;
END LOOP;
END IF;
IF defined_tok = gate_one() THEN
RETURN total;
END IF;
prob := probability_evaluate(defined_tok, method, arguments);
IF prob IS NULL OR prob <= 0 THEN
RETURN NULL; -- never defined: SQL NULL
END IF;
RETURN total / prob;
END;
END IF;
-- Enumerate all k-tuples (i_1, ..., i_k) in {1..n}^k. tup is the
-- current tuple; we step through them in lexicographic order.
total := 0;
tup := array_fill(1, ARRAY[k]);
LOOP
prod_v := 1;
FOR j IN 1..k LOOP
prod_v := prod_v * vals[tup[j]];
END LOOP;
SELECT array_agg(DISTINCT toks[idx]) INTO distinct_tok
FROM unnest(tup) AS idx;
IF prov <> gate_one() THEN
distinct_tok := distinct_tok || prov;
END IF;
conj_token := provenance_times(VARIADIC distinct_tok);
prob := probability_evaluate(conj_token, method, arguments);
total := total + prod_v * prob;
d := k;
WHILE d >= 1 AND tup[d] = n LOOP
tup[d] := 1;
d := d - 1;
END LOOP;
EXIT WHEN d = 0;
tup[d] := tup[d] + 1;
END LOOP;
-- Conditional on the value existing (and on prov): every k-tuple of the
-- sum above names a row, so the worlds without one contribute 0 and only
-- the normalisation is left.
IF defined_tok <> gate_one() THEN
IF prov <> gate_one() THEN
defined_tok := provenance_times(prov, defined_tok);
END IF;
prob := probability_evaluate(defined_tok, method, arguments);
IF prob IS NULL OR prob <= 0 THEN
RETURN NULL; -- never defined: SQL NULL
END IF;
RETURN total / prob; -- already conditional; skip the generic norm
END IF;
ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
-- Rank enumeration: per distinct value v, P(MIN = v) is the
-- probability that some t_i with v_i=v is true and all t_j with
-- smaller v are false. For MAX we negate values so the same
-- "smaller-than" rank logic computes MIN-of-negated, then flip.
-- The outer multiplier picks up the right sign for the k-th moment
-- of MAX: E[MAX^k] = (-1)^k * E[MIN(-v)^k], so sign_max = (-1)^k.
sign_max := CASE
WHEN aggregation_function = 'max'
THEN power(-1::float8, k)
ELSE 1
END;
-- MIN/MAX over the empty input world are NULL (no elements), not ±Infinity:
-- SQL returns one row with a NULL value. The moment is therefore CONDITIONAL
-- on the aggregate being defined (non-empty) -- the empty world is excluded
-- and the result renormalised by P(prov AND non-empty). (count, whose empty
-- value 0 is a real value, keeps the empty world; sum keeps it too, as 0.)
IF n = 0 THEN
RETURN NULL; -- structurally empty: MIN/MAX undefined
END IF;
-- Numerator E[MIN^k . 1{prov AND non-empty}] (the rank sum naturally omits
-- the empty world, since every term requires a present token).
WITH tok_value AS (
SELECT (get_children(c))[1] AS tok,
(CASE WHEN aggregation_function='max' THEN -1 ELSE 1 END)
* CAST(get_extra((get_children(c))[2]) AS DOUBLE PRECISION) AS v
FROM UNNEST(child_pairs) AS c
) SELECT sign_max * COALESCE(SUM(p * power(v, k)), 0) FROM (
SELECT t1.v AS v,
probability_evaluate(
CASE WHEN prov = gate_one()
THEN provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
provenance_plus(ARRAY_AGG(t2.tok)))
ELSE provenance_times(prov,
provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
provenance_plus(ARRAY_AGG(t2.tok)))) END,
method, arguments) AS p
FROM tok_value t1 LEFT OUTER JOIN tok_value t2 ON t1.v > t2.v
GROUP BY t1.v) tmp
INTO total;
-- Denominator P(prov AND non-empty) = P(prov (x) (+) tokens).
SELECT probability_evaluate(
CASE WHEN prov = gate_one()
THEN provenance_plus(ARRAY_AGG(tok))
ELSE provenance_times(prov, provenance_plus(ARRAY_AGG(tok))) END,
method, arguments)
FROM (SELECT (get_children(c))[1] AS tok FROM UNNEST(child_pairs) AS c) s
INTO total_probability;
IF total_probability <= 0 THEN
RETURN NULL; -- never defined under prov: MIN/MAX undefined
END IF;
RETURN total / total_probability; -- already conditional; skip generic norm
ELSIF aggregation_function = 'avg' THEN
-- AVG = SUM/COUNT is a ratio of two correlated world-dependent
-- quantities, so the k-tuple expansion above does not apply. Like
-- MIN/MAX, AVG over the empty world is NULL, so its moment conditions
-- on the aggregate being defined (COUNT >= 1), NULL when it never is.
-- Two routes:
-- * EXACT (independent rows, unconditional): the joint (sum, count)
-- PMF folded in C by agg_avg_moment_exact --
-- E[AVG^k | COUNT>=1] = Σ_{(s,c), c>=1} (s/c)^k pmf(s,c) / P(c>=1).
-- * Monte-Carlo scalar fallback otherwise (an outer conditioning
-- event, shared leaves, compound contributors): rv_moment samples
-- the agg gate per world; its NaN-skip on empty draws implements
-- the same conditional-on-defined convention, at the
-- provsql.rv_mc_samples budget (0 raises, per convention).
IF n = 0 THEN
RETURN NULL; -- structurally empty: AVG undefined
END IF;
-- Conditioning on an event that AVG being defined implies -- some row
-- among a set holding AVG's rows present, as provenance() of a GROUP BY
-- row is, the group possibly having rows whose value is NULL -- is what
-- the moment already does: the exact route applies.
IF prov <> gate_one() THEN
DECLARE
inner_prov uuid := prov;
prov_toks uuid[];
agg_toks uuid[];
BEGIN
IF get_gate_type(inner_prov) = 'delta' THEN
inner_prov := (get_children(inner_prov))[1];
END IF;
IF get_gate_type(inner_prov) = 'plus' THEN
prov_toks := get_children(inner_prov);
ELSE
prov_toks := ARRAY[inner_prov];
END IF;
SELECT array_agg((get_children(c))[1]) INTO agg_toks
FROM unnest(child_pairs) AS c;
IF agg_toks <@ prov_toks THEN
prov := gate_one();
END IF;
END;
END IF;
IF prov = gate_one() THEN
total := agg_avg_moment_exact((token)::uuid, k);
IF total IS NOT NULL THEN
RETURN total;
END IF;
END IF;
RETURN rv_moment((token)::uuid, k, false, prov);
ELSE
RAISE EXCEPTION USING MESSAGE=
'Cannot compute moment for aggregation function ' || aggregation_function,
DETAIL = 'provsql-reason: moment-aggregate-kind; scope: gap';
END IF;
-- Conditional normalisation: E[X^k · 1_A] / P(A) = E[X^k | A].
IF prov <> gate_one()
AND total <> 0
AND total <> 'Infinity'::float8
AND total <> '-Infinity'::float8 THEN
total := total / probability_evaluate(prov, method, arguments);
END IF;
RETURN total;
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Compute the variance Var[X | prov] of a probabilistic scalar
*
* Polymorphic dispatcher that mirrors @c expected: @c random_variable
* inputs go through the analytical / MC evaluator
* (@c rv_moment(uuid, 2, true)); @c agg_token inputs go through the
* @c agg_raw_moment helper, computing
* @f$\mathrm{Var}[X|A] = E[X^2|A] - E[X|A]^2@f$. Conditioning on
* @c prov is supported for @c agg_token (matching @c expected) but
* not yet for @c random_variable.
*/
CREATE OR REPLACE FUNCTION variance(
input ANYELEMENT,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS $$
DECLARE
m1 float8;
m2 float8;
BEGIN
IF pg_typeof(input) = 'random_variable'::regtype THEN
IF input IS NULL THEN
RETURN NULL;
END IF;
-- Conditioning on prov is handled inside rv_moment: when prov
-- resolves to gate_one() (the default, or load-time
-- simplification of any always-true sub-circuit) the
-- unconditional analytical path runs unchanged; otherwise the
-- joint-circuit loader unifies shared gate_rv leaves between
-- input and prov, and the conditional path runs either
-- truncated-distribution closed form or MC rejection.
RETURN provsql.rv_moment(
rv_conditioned_target((input::random_variable)::uuid), 2, true,
rv_conditioned_prov((input::random_variable)::uuid, prov));
END IF;
IF pg_typeof(input) = 'agg_token'::regtype THEN
IF input IS NULL THEN
RETURN NULL;
END IF;
-- Collapsed fast path: E[C] and E[C^2] from a single circuit load and plan
-- build, instead of two agg_raw_moment() calls that each reload. Mirrors
-- the guard in agg_raw_moment (unconditional only, prov = one); on any
-- mismatch agg_collapsed_moments returns NULL and we fall through to the
-- generic per-order path (which handles conditioning, SUM enumeration, ...).
IF rv_conditioned_prov(input::uuid, prov) = gate_one() THEN
DECLARE ms float8[];
BEGIN
ms := agg_collapsed_moments(
(agg_conditioned_target(input::agg_token))::uuid);
IF ms IS NOT NULL THEN
RETURN ms[2] - ms[1] * ms[1];
END IF;
END;
END IF;
m1 := agg_raw_moment(agg_conditioned_target(input::agg_token), 1,
rv_conditioned_prov(input::uuid, prov), method, arguments);
m2 := agg_raw_moment(agg_conditioned_target(input::agg_token), 2,
rv_conditioned_prov(input::uuid, prov), method, arguments);
IF m1 IS NULL OR m2 IS NULL THEN
RETURN NULL;
END IF;
RETURN m2 - m1 * m1;
END IF;
-- Bernoulli event token (see moment()): Var[X] = p(1 - p).
IF pg_typeof(input) = 'uuid'::regtype THEN
IF input IS NULL THEN
RETURN NULL;
END IF;
m1 := provsql.probability_evaluate(provsql.cond(input::uuid, prov),
method, arguments);
RETURN m1 * (1 - m1);
END IF;
RAISE EXCEPTION 'variance() is not yet supported for input type %', pg_typeof(input)
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: moment-input-type; scope: gap';
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Compute the raw moment E[X^k | prov] of a probabilistic scalar
*
* @c k must be a non-negative integer. @c k = 0 returns 1; @c k = 1
* is equivalent to @c expected(input). Polymorphic dispatcher: routes
* @c random_variable through @c rv_moment (analytical / MC) and
* @c agg_token through @c agg_raw_moment (SUM via tuple enumeration,
* MIN / MAX via rank enumeration, AVG via the joint (sum, count)
* distribution over independent / laminar rows with a Monte-Carlo
* fallback).
*/
CREATE OR REPLACE FUNCTION moment(
input ANYELEMENT,
k integer,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS $$
BEGIN
IF pg_typeof(input) = 'random_variable'::regtype THEN
IF input IS NULL OR k IS NULL THEN
RETURN NULL;
END IF;
-- See variance() above: rv_moment handles the conditional/unconditional
-- dispatch internally based on the resolved prov gate type.
RETURN provsql.rv_moment(
rv_conditioned_target((input::random_variable)::uuid), k, false,
rv_conditioned_prov((input::random_variable)::uuid, prov));
END IF;
IF pg_typeof(input) = 'agg_token'::regtype THEN
RETURN agg_raw_moment(agg_conditioned_target(input::agg_token), k,
rv_conditioned_prov(input::uuid, prov), method, arguments);
END IF;
-- A bare provenance event token (a gate_cmp lifted from an RV comparison,
-- e.g. expected(x <= c)) is a Bernoulli indicator: X in {0,1}, so every raw
-- moment E[X^k] with k >= 1 equals P(event), and E[X^0] = 1. cond() applies
-- the optional conditioning prov (a no-op for the default gate_one()).
IF pg_typeof(input) = 'uuid'::regtype THEN
IF input IS NULL OR k IS NULL THEN
RETURN NULL;
END IF;
IF k = 0 THEN
RETURN 1;
END IF;
RETURN provsql.probability_evaluate(provsql.cond(input::uuid, prov),
method, arguments);
END IF;
RAISE EXCEPTION 'moment() is not yet supported for input type %', pg_typeof(input)
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: moment-input-type; scope: gap';
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Internal: rv-side quantile computation.
*
* C entry point behind the polymorphic @c quantile dispatcher.
* Closed-form inverse CDF where the family has one (Normal via
* Beasley-Springer-Moro polished by Newton steps, Uniform and
* Exponential by algebraic inversion), generic monotone-CDF bisection
* otherwise (Erlang, Gamma), exact generalised inverse for categorical
* mixtures, and the empirical Monte Carlo quantile for compound scalar
* circuits. A non-trivial @p prov conditions (truncates) the
* distribution first, in closed form when the event reduces to an
* interval on a bare @c gate_rv.
*/
CREATE OR REPLACE FUNCTION rv_quantile(
token uuid, p double precision,
prov uuid DEFAULT gate_one())
RETURNS double precision
AS 'provsql','rv_quantile' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Compute the p-quantile (inverse CDF) of a probabilistic scalar
*
* @f$F^{-1}(p) = \min\{x : P(X \le x) \ge p\}@f$ for @f$p \in [0,1]@f$:
* medians (@c p = 0.5), percentiles, Value-at-Risk, and credible
* intervals. @c p = 0 / @c p = 1 return the (possibly infinite)
* support edges. Polymorphic dispatcher mirroring @c expected /
* @c moment: @c random_variable routes through @c rv_quantile
* (analytical inverse CDF / MC), plain numerics are their own quantile
* (a Dirac's inverse CDF is constant), and the optional @p prov
* argument conditions on a provenance event, e.g.
* quantile(x | (x > 0), 0.5) for the median of a truncated
* distribution.
*/
CREATE OR REPLACE FUNCTION quantile(
input ANYELEMENT,
p double precision,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS $$
BEGIN
IF p IS NULL THEN
RETURN NULL;
END IF;
IF p <> p OR p < 0 OR p > 1 THEN
RAISE EXCEPTION 'quantile: p must be in [0, 1] (got %)', p;
END IF;
IF pg_typeof(input) = 'random_variable'::regtype THEN
IF input IS NULL THEN
RETURN NULL;
END IF;
-- See variance(): rv_quantile handles the conditional/unconditional
-- dispatch internally based on the resolved prov gate type.
RETURN provsql.rv_quantile(
rv_conditioned_target((input::random_variable)::uuid), p,
rv_conditioned_prov((input::random_variable)::uuid, prov));
END IF;
IF pg_typeof(input) IN ('smallint'::regtype, 'integer'::regtype,
'bigint'::regtype, 'numeric'::regtype,
'real'::regtype, 'double precision'::regtype) THEN
-- A deterministic scalar is a Dirac: every quantile is the value.
RETURN input::double precision;
END IF;
RAISE EXCEPTION 'quantile() is not yet supported for input type %', pg_typeof(input)
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: quantile-input-type; scope: gap';
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Internal: rv-side support computation
*
* Lifts @c provsql::compute_support out of @c RangeCheck.cpp -- the
* same interval-arithmetic propagation @c runRangeCheck uses to
* decide @c gate_cmps. Returns @c [-Infinity, +Infinity] when the
* tightest bound is the conservative all-real interval (e.g. for a
* normal RV, or any sub-circuit that mixes a normal in).
*/
CREATE OR REPLACE FUNCTION rv_support(
token uuid, prov uuid DEFAULT gate_one(),
OUT lo float8, OUT hi float8)
AS 'provsql','rv_support' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
/**
* @brief Compute the support interval @c [lo, hi] of a probabilistic
* (or deterministic) scalar
*
* Polymorphic dispatcher mirroring @c expected / @c variance /
* @c moment / @c central_moment, with two extra "free" branches:
*
* - Plain numeric (@c smallint / @c integer / @c bigint /
* @c numeric / @c real / @c double @c precision): degenerate
* point support @f$[c, c]@f$. Lets callers ask for the support
* of a literal without round-tripping through @c as_random.
* - @c random_variable / bare @c uuid (any provenance gate
* token; the @c random_variable branch reinterprets the value via
* the binary-coercible @c random_variable @c -> @c uuid cast):
* routes to @c rv_support, which propagates distribution
* supports (uniform exact, exponential @c [0,+∞), normal
* @c (-∞,+∞)) through @c gate_arith via interval arithmetic.
* @c gate_value gives the same @f$[c, c]@f$ point support as the
* numeric branch; any non-scalar gate (Boolean gates, aggregates,
* ...) safely falls back to the conservative all-real interval
* without raising. Conditioning on @c prov is not yet supported.
*
* - @c agg_token: closed-form per aggregation function, over the worlds
* where the aggregate has a value (an empty group has none, except for
* a scalar @c COUNT, which is 0 there):
* - @c SUM and @c COUNT (the sum of per-row 1s, or 0s for @c NULL
* counted columns): @f$[\sum_i \min(0,v_i), \sum_i \max(0,v_i)]@f$,
* with a row always present adding @f$v_i@f$ itself (every other row
* is independently in or out of the included set; the extreme sums
* include only its positive or only its negative values). Unless a
* row is always present or the empty world counts, a bound moves in
* from 0 to @f$\min_i v_i@f$ when every @f$v_i > 0@f$, and to
* @f$\max_i v_i@f$ when every @f$v_i < 0@f$.
* - @c MIN : @f$[\min_i v_i, \max_i v_i]@f$ in the non-empty
* subsets, plus @c +Infinity if the empty subset has positive
* probability under @c prov.
* - @c MAX : symmetric -- @c -Infinity if empty has positive
* probability under @c prov, otherwise @c min_i v_i; @c hi is
* always @c max_i v_i.
*
* Other aggregation functions raise.
*
* Returns the composite record @c (lo, hi) via the function's
* @c OUT parameters, with @c -Infinity / @c +Infinity marking
* unbounded ends.
*/
CREATE OR REPLACE FUNCTION support(
input ANYELEMENT,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL,
OUT lo float8,
OUT hi float8)
AS $$
DECLARE
aggregation_function VARCHAR;
child_pairs uuid[];
values_arr float8[];
total_probability float8;
vmin float8;
vmax float8;
some_always boolean;
BEGIN
IF input IS NULL THEN
lo := NULL; hi := NULL; RETURN;
END IF;
-- Plain numeric: degenerate point support. Lets `support(2.5)` /
-- `support(42)` / etc. return (2.5, 2.5) without making the user
-- wrap in `as_random`.
IF pg_typeof(input) IN (
'smallint'::regtype, 'integer'::regtype, 'bigint'::regtype,
'numeric'::regtype, 'real'::regtype, 'double precision'::regtype) THEN
lo := input::double precision;
hi := input::double precision;
RETURN;
END IF;
-- random_variable is binary-coercible to uuid (explicit cast
-- below), so a single rv_support call covers both shapes.
-- rv_support handles
-- gate_value (point), gate_rv (distribution), gate_arith
-- (propagated), and falls back to the conservative all-real
-- interval for any other gate kind. Conditioning on prov is not
-- supported (would require restricting the underlying joint
-- distribution by the indicator of prov, which has no closed form
-- for the basic distributions we ship).
IF pg_typeof(input) IN ('random_variable'::regtype, 'uuid'::regtype) THEN
-- Conditional support: rv_support folds the AND-conjunct interval
-- constraints from prov into the unconditional support. When
-- prov is gate_one() the unconditional support is returned
-- unchanged.
SELECT r.lo, r.hi INTO lo, hi
FROM provsql.rv_support(
rv_conditioned_target(input::uuid),
rv_conditioned_prov(input::uuid, prov)) r;
RETURN;
END IF;
IF pg_typeof(input) = 'agg_token'::regtype THEN
-- A conditioned aggregate SUM(x)|C: the value-range support is that of
-- the target aggregate (conditioning can only tighten it; the
-- conservative range stays valid), so unpack to the target gate.
DECLARE
atok agg_token := agg_conditioned_target(input::agg_token);
BEGIN
IF get_gate_type(atok) <> 'agg' THEN
RAISE EXCEPTION USING MESSAGE='Wrong gate type for support computation',
DETAIL = 'provsql-reason: support-not-an-aggregate; scope: deliberate';
END IF;
SELECT pp.proname::varchar FROM pg_proc pp
WHERE oid=(get_infos(atok)).info1
INTO aggregation_function;
child_pairs := get_children(atok);
IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
-- COUNT is the SUM of its per-row values: 1, or 0 for a row whose
-- counted column is NULL. A row always present adds its value; any
-- other adds it or nothing.
IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
lo := 0; hi := 0; RETURN;
END IF;
SELECT sum(CASE WHEN always THEN v ELSE LEAST(v, 0::float8) END),
sum(CASE WHEN always THEN v ELSE GREATEST(v, 0::float8) END),
min(v), max(v), bool_or(always)
INTO lo, hi, vmin, vmax, some_always
FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v,
get_gate_type((get_children(c))[1]) = 'one' AS always
FROM unnest(child_pairs) AS c) sub;
-- The support covers the worlds where the aggregate has a value. An
-- empty group has none for SUM, and none for a grouped COUNT either,
-- there being no row to carry it; only a scalar COUNT (the top bit of
-- info2) is 0 there. Unless a row is always present or the empty world
-- counts, at least one row is, so a bound moves in from 0 when every
-- value is on the same side of it.
IF NOT some_always
AND NOT (aggregation_function = 'count' AND (get_infos(atok)).info2 < 0) THEN
IF vmin > 0 THEN lo := vmin; END IF;
IF vmax < 0 THEN hi := vmax; END IF;
END IF;
ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
-- MIN/MAX over the empty input world are NULL, not ±Infinity (matching the
-- moment surface): the empty world carries no value, so the support is just
-- the range of the per-row values [min(v), max(v)]. A structurally empty
-- aggregate has no defined value at all -> NULL support.
IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
lo := NULL; hi := NULL; RETURN;
END IF;
SELECT min(v), max(v)
INTO lo, hi
FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
FROM UNNEST(child_pairs) AS c) sub;
ELSE
RAISE EXCEPTION USING MESSAGE=
'Cannot compute support for aggregation function ' || aggregation_function,
DETAIL = 'provsql-reason: support-aggregate-kind; scope: gap';
END IF;
RETURN;
END;
END IF;
RAISE EXCEPTION 'support() is not yet supported for input type %', pg_typeof(input)
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: support-input-type; scope: gap';
END
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Compute the central moment E[(X - E[X|prov])^k | prov]
*
* @c k = 0 returns 1; @c k = 1 returns 0; @c k = 2 is equivalent to
* @c variance(input, prov, ...). Polymorphic dispatcher: routes
* @c random_variable through @c rv_moment, and @c agg_token through
* the binomial expansion
* @f$E[(X-\mu)^k|A] = \sum_{i=0}^{k} \binom{k}{i} (-\mu)^{k-i} E[X^i|A]@f$
* with @f$\mu = E[X|A]@f$, where each @f$E[X^i|A]@f$ comes from
* @c agg_raw_moment.
*/
CREATE OR REPLACE FUNCTION central_moment(
input ANYELEMENT,
k integer,
prov UUID = gate_one(),
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS $$
DECLARE
mu float8;
total float8;
i integer;
raw_i float8;
binom float8;
-- iterative binomial coefficient C(k, i)
k_double float8;
BEGIN
IF pg_typeof(input) = 'random_variable'::regtype THEN
IF input IS NULL OR k IS NULL THEN
RETURN NULL;
END IF;
-- See variance() above: rv_moment handles the conditional/unconditional
-- dispatch internally based on the resolved prov gate type.
RETURN provsql.rv_moment(
rv_conditioned_target((input::random_variable)::uuid), k, true,
rv_conditioned_prov((input::random_variable)::uuid, prov));
END IF;
IF pg_typeof(input) = 'agg_token'::regtype THEN
IF input IS NULL OR k IS NULL THEN
RETURN NULL;
END IF;
IF k < 0 THEN
RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
END IF;
IF k = 0 THEN RETURN 1; END IF;
IF k = 1 THEN RETURN 0; END IF;
mu := agg_raw_moment(agg_conditioned_target(input::agg_token), 1,
rv_conditioned_prov(input::uuid, prov), method, arguments);
IF mu IS NULL THEN RETURN NULL; END IF;
-- mu may be ±Infinity for empty MIN / MAX with positive empty
-- probability; central_moment is undefined in that case.
IF mu = 'Infinity'::float8 OR mu = '-Infinity'::float8 THEN
RETURN mu;
END IF;
total := 0;
binom := 1; -- C(k, 0)
k_double := k;
FOR i IN 0..k LOOP
raw_i := agg_raw_moment(agg_conditioned_target(input::agg_token), i,
rv_conditioned_prov(input::uuid, prov), method, arguments);
IF raw_i IS NULL THEN RETURN NULL; END IF;
total := total + binom * power(-mu, k - i) * raw_i;
-- C(k, i+1) = C(k, i) * (k - i) / (i + 1)
IF i < k THEN
binom := binom * (k_double - i) / (i + 1);
END IF;
END LOOP;
RETURN total;
END IF;
-- Bernoulli event token (see moment()): with p = P(event),
-- E[(X-p)^k] = (1-p)(-p)^k + p(1-p)^k; k = 0 -> 1, k = 1 -> 0.
IF pg_typeof(input) = 'uuid'::regtype THEN
IF input IS NULL OR k IS NULL THEN
RETURN NULL;
END IF;
IF k < 0 THEN
RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
END IF;
IF k = 0 THEN RETURN 1; END IF;
IF k = 1 THEN RETURN 0; END IF;
mu := provsql.probability_evaluate(provsql.cond(input::uuid, prov),
method, arguments);
RETURN (1 - mu) * power(-mu, k) + mu * power(1 - mu, k);
END IF;
RAISE EXCEPTION 'central_moment() is not yet supported for input type %', pg_typeof(input)
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: moment-input-type; scope: gap';
END
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SET search_path=provsql SECURITY DEFINER;
/** @brief C entry point behind @ref covariance (uuid-level binding). */
CREATE OR REPLACE FUNCTION rv_covariance(x uuid, y uuid, prov uuid)
RETURNS double precision
AS 'provsql','rv_covariance' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Covariance Cov(X, Y) = E[XY] − E[X]·E[Y] of two random variables.
*
* The bivariate readout complementing the univariate moment surface
* (@ref expected / @ref variance / @ref moment / @ref central_moment).
* Exact tiers: an exact @c 0 when the two arguments' stochastic-leaf
* footprints are structurally independent (given @p prov), a variance
* readout when the two arguments coincide, and the closed-form
* @c E[XY] − E[X]·E[Y] whenever every factor decomposes analytically.
* When some factor has no closed form, a SINGLE coupled Monte-Carlo pass
* over the joint circuit draws @c (x, y) pairs (shared leaves produce one
* draw both observe) and returns the sample covariance -- the estimator's
* noise then scales with the covariance signal itself, not with the
* product of the means as the naive three-run E[XY] − E[X]·E[Y]
* subtraction would.
*
* @param x the first random variable.
* @param y the second random variable.
* @param prov optional conditioning event (a provenance @c uuid); the
* default @c gate_one() is the unconditional covariance. Conditioning
* is applied jointly: the Monte-Carlo pass rejection-samples the pair on
* @p prov, giving @c Cov(X, Y | prov).
*/
CREATE OR REPLACE FUNCTION covariance(
x random_variable, y random_variable, prov uuid DEFAULT gate_one())
RETURNS double precision AS $$
SELECT provsql.rv_covariance((x)::uuid, (y)::uuid, prov);
$$ LANGUAGE sql PARALLEL SAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Standard deviation σ(X) = √Var(X) of a random variable.
*
* A thin numeric readout over @ref variance. The square root is taken on
* the scalar @c double result, so no RV-level @c sqrt is involved and this
* carries no dependency on RV function application (@c pow / @c sqrt).
* @c NULL propagates from a @c NULL input; the order-2 central moment is
* non-negative by construction, so the root is always real.
*
* @param x the random variable.
* @param prov optional conditioning event; default @c gate_one()
* (unconditional).
*/
CREATE OR REPLACE FUNCTION stddev(
x random_variable, prov uuid DEFAULT gate_one())
RETURNS double precision AS $$
SELECT sqrt(provsql.variance(x, prov));
$$ LANGUAGE sql PARALLEL UNSAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/** @brief C entry point behind @ref correlation (uuid-level binding). */
CREATE OR REPLACE FUNCTION rv_correlation(x uuid, y uuid, prov uuid)
RETURNS double precision
AS 'provsql','rv_correlation' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Pearson correlation ρ(X, Y) = Cov(X, Y) / (σ(X)·σ(Y)).
*
* Same exact tiers as @ref covariance; on the Monte-Carlo path the
* covariance and BOTH standard deviations are read off the same coupled
* pass, instead of stacking five independent estimates (three for the
* covariance, one per standard deviation). Returns @c NULL when either
* standard deviation is @c 0 (a degenerate / constant variable, for which
* correlation is undefined) rather than raising a division-by-zero.
*
* @param x the first random variable.
* @param y the second random variable.
* @param prov optional conditioning event; default @c gate_one()
* (unconditional).
*/
CREATE OR REPLACE FUNCTION correlation(
x random_variable, y random_variable, prov uuid DEFAULT gate_one())
RETURNS double precision AS $$
SELECT provsql.rv_correlation((x)::uuid, (y)::uuid, prov);
$$ LANGUAGE sql PARALLEL SAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/** @brief C entry point behind @ref entropy (uuid-level binding). */
CREATE OR REPLACE FUNCTION rv_entropy(token uuid, prov uuid)
RETURNS double precision
AS 'provsql','rv_entropy' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Entropy H(X) of a random variable, in nats.
*
* Shannon entropy for a discrete distribution (a categorical / discrete
* count / constant -- a point mass has entropy @c 0), differential
* entropy for a continuous one (quadrature of @c -f ln f over the
* family's integration range; also exact through independent-arm
* Bernoulli mixture trees such as @ref gmm's). Shapes with no
* closed density (arithmetic composites) and the conditional form fall
* back to a Monte Carlo histogram plug-in estimate at the
* @c provsql.rv_mc_samples budget.
*
* @param x the random variable.
* @param prov optional conditioning event; default @c gate_one()
* (unconditional).
*/
CREATE OR REPLACE FUNCTION entropy(
x random_variable, prov uuid DEFAULT gate_one())
RETURNS double precision AS $$
SELECT provsql.rv_entropy((x)::uuid, prov);
$$ LANGUAGE sql PARALLEL SAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/** @brief C entry point behind @ref kl (uuid-level binding). */
CREATE OR REPLACE FUNCTION rv_kl(p uuid, q uuid)
RETURNS double precision
AS 'provsql','rv_kl' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Kullback-Leibler divergence KL(P || Q), in nats.
*
* Exact: the defining sum for two discrete distributions (matching
* outcomes by value) and the defining integral (quadrature over P's
* integration window) for two continuous ones, including
* independent-arm mixture trees. Returns @c Infinity when P is not
* absolutely continuous with respect to Q -- an outcome of P that Q
* gives zero mass, mismatched kinds (discrete vs continuous), or a
* region of P's support where Q's density (under)flows to zero. Both
* arguments must resolve to closed-form densities; arithmetic
* composites and conditioned variables raise.
*/
CREATE OR REPLACE FUNCTION kl(p random_variable, q random_variable)
RETURNS double precision AS $$
SELECT provsql.rv_kl((p)::uuid, (q)::uuid);
$$ LANGUAGE sql PARALLEL SAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/** @brief C entry point behind @ref mutual_information (uuid-level
* binding). */
CREATE OR REPLACE FUNCTION rv_mutual_information(x uuid, y uuid)
RETURNS double precision
AS 'provsql','rv_mutual_information' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
/**
* @brief Mutual information I(X; Y), in nats.
*
* Exactly @c 0 for structurally independent variables (disjoint
* stochastic-leaf footprints, the same test the moment evaluators use);
* @c H(X) for a discrete variable paired with itself and @c Infinity
* for a continuous one (I(X;X) diverges). A genuinely correlated pair
* (shared leaves) is estimated by a 2-D histogram plug-in over coupled
* joint Monte Carlo draws -- both roots evaluated against the same
* per-iteration cache, so shared leaves keep their joint law -- at the
* @c provsql.rv_mc_samples budget.
*/
CREATE OR REPLACE FUNCTION mutual_information(
x random_variable, y random_variable)
RETURNS double precision AS $$
SELECT provsql.rv_mutual_information((x)::uuid, (y)::uuid);
$$ LANGUAGE sql PARALLEL SAFE VOLATILE SET search_path=provsql SECURITY DEFINER;
/**
* @brief Compute the Shapley value of an input variable
*
* Measures the contribution of a specific input variable to the
* truth of a provenance expression, using game-theoretic Shapley values.
*
* @param token provenance token to evaluate
* @param variable UUID of the input variable
* @param method knowledge compilation method
* @param arguments additional arguments for the method
* @param banzhaf if true, compute the Banzhaf value instead
*/
CREATE OR REPLACE FUNCTION shapley(
token UUID,
variable UUID,
method text = NULL,
arguments text = NULL,
banzhaf BOOLEAN = 'f')
RETURNS DOUBLE PRECISION AS
'provsql','shapley' LANGUAGE C STABLE;
/** @brief Compute Shapley values for all input variables at once */
CREATE OR REPLACE FUNCTION shapley_all_vars(
IN token UUID,
IN method text = NULL,
IN arguments text = NULL,
IN banzhaf BOOLEAN = 'f',
OUT variable UUID,
OUT value DOUBLE PRECISION)
RETURNS SETOF record AS
'provsql', 'shapley_all_vars'
LANGUAGE C STABLE;
/** @brief Compute the Banzhaf power index of an input variable */
CREATE OR REPLACE FUNCTION banzhaf(
token UUID,
variable UUID,
method text = NULL,
arguments text = NULL)
RETURNS DOUBLE PRECISION AS
$$ SELECT provsql.shapley(token, variable, method, arguments, 't') $$
LANGUAGE SQL;
/** @brief Compute Banzhaf power indices for all input variables at once */
CREATE OR REPLACE FUNCTION banzhaf_all_vars(
IN token UUID,
IN method text = NULL,
IN arguments text = NULL,
OUT variable UUID,
OUT value DOUBLE PRECISION)
RETURNS SETOF record AS
$$ SELECT * FROM provsql.shapley_all_vars(token, method, arguments, 't') $$
LANGUAGE SQL;
/**
* @brief Exact reachability probability over bounded-treewidth data
* (columnar form)
*
* Computes the probability that @p target is reachable from @p source in
* the probabilistic graph given by the parallel edge arrays
* (two-terminal network reliability). Unlike
* @c probability_evaluate(), which compiles the provenance circuit
* built along the relational query plan, this compiles the query
* along a tree decomposition of the *data* graph (in the spirit of the
* provenance refinement of Courcelle's theorem), producing a d-DNNF
* whose size is linear in the number of edges for data of bounded
* treewidth. Exact, and linear-time, on cyclic data as well -- where
* the recursive-query fixpoint cannot terminate structurally.
*
* Edges are independent events. Two array positions may share a token
* only if they are mutual reverses (the natural encoding of an
* undirected edge in a directed edge relation); they are then treated
* as a single bidirectional edge. This is an internal/testing surface:
* the user-facing route is a plain @c WITH @c RECURSIVE reachability
* query under the 'absorptive' (or 'boolean') provenance class, which
* the query rewriter compiles through @c eval_reachability() /
* @c reachability_materialize().
*
* @param sources source vertex of each edge (dense integer IDs)
* @param destinations destination vertex of each edge
* @param tokens provenance token of each edge tuple
* @param probabilities probability of each edge tuple
* @param source the vertex reachability starts from
* @param target the vertex whose reachability is evaluated
* @param directed if false, each edge can be traversed both ways
*/
CREATE OR REPLACE FUNCTION reachability_evaluate(
sources INT[],
destinations INT[],
tokens UUID[],
probabilities DOUBLE PRECISION[],
source INT,
target INT,
directed BOOLEAN)
RETURNS DOUBLE PRECISION AS
'provsql','reachability_evaluate' LANGUAGE C IMMUTABLE PARALLEL SAFE;
/**
* @brief Reachability probability plus compilation statistics
* (columnar form)
*
* Same compilation as @c reachability_evaluate(), returning the
* probability together with the structural statistics that
* substantiate the bounded-treewidth guarantee: the treewidth of the
* min-fill decomposition of the data graph, its number of bags, the
* maximum number of dynamic-programming states at any decomposition
* node, and the size of the emitted d-DNNF (linear in the number of
* edges for fixed data treewidth).
*
* @param sources source vertex of each edge (dense integer IDs)
* @param destinations destination vertex of each edge
* @param tokens provenance token of each edge tuple
* @param probabilities probability of each edge tuple
* @param source the vertex reachability starts from
* @param target the vertex whose reachability is evaluated
* @param directed if false, each edge can be traversed both ways
* @param[out] probability the reachability probability
* @param[out] data_treewidth treewidth of the min-fill decomposition of the
* data graph
* @param[out] nb_bags number of bags in the decomposition
* @param[out] max_states maximum number of dynamic-programming states at any
* decomposition node
* @param[out] nb_gates number of gates in the emitted d-DNNF
* @param[out] nb_variables number of variables in the emitted d-DNNF
*/
CREATE OR REPLACE FUNCTION reachability_compile_stats(
IN sources INT[],
IN destinations INT[],
IN tokens UUID[],
IN probabilities DOUBLE PRECISION[],
IN source INT,
IN target INT,
IN directed BOOLEAN,
OUT probability DOUBLE PRECISION,
OUT data_treewidth INT,
OUT nb_bags BIGINT,
OUT max_states BIGINT,
OUT nb_gates BIGINT,
OUT nb_variables BIGINT)
AS 'provsql','reachability_compile_stats'
LANGUAGE C IMMUTABLE PARALLEL SAFE;
/**
* @brief Boolean UCQ probability plus compilation statistics
* (columnar form, internal)
*
* Same compilation as @c ucq_joint_compile_stats(query jsonb, ...),
* returning the probability together with the three width columns that
* substantiate thesis Prop. 4.2.11 empirically -- the adversarial family
* has small data and circuit widths but large joint width -- and the
* structural statistics.
*
* @param disjunct_nvars number of query variables of each disjunct
* @param atom_disjunct disjunct index of each atom (parallel to @p atom_rel)
* @param atom_rel relation id of each atom
* @param atom_vars query-variable indices of all atom columns, concatenated
* @param atom_arity number of columns of each atom (slices @p atom_vars)
* @param fact_rel relation id of each fact
* @param fact_elems element ids of all fact columns, concatenated
* @param fact_arity number of columns of each fact (slices @p fact_elems)
* @param fact_tokens provenance token of each fact
* @param fact_probs probability of each fact
* @param[out] probability the exact UCQ probability
* @param[out] joint_treewidth width of the min-fill decomposition found
* @param[out] data_treewidth_lb degeneracy lower bound of the data-only graph
* @param[out] circuit_treewidth_lb degeneracy lower bound of the slice-only graph
* @param[out] n_bags number of bags in the decomposition
* @param[out] max_states peak number of DP states at any node
* @param[out] dd_size number of gates in the emitted d-D
* @param[out] n_enumerating maximum number of essential (enumerating) query
* variables over the disjuncts -- the @c e of the @f$2^{O(k^e)}@f$
* bound, with variables functionally determined by others (via FDs
* mined from the data) removed
*/
CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
IN disjunct_nvars INT[],
IN atom_disjunct INT[],
IN atom_rel INT[],
IN atom_vars INT[],
IN atom_arity INT[],
IN fact_rel INT[],
IN fact_elems INT[],
IN fact_arity INT[],
IN fact_tokens UUID[],
IN fact_probs DOUBLE PRECISION[],
OUT probability DOUBLE PRECISION,
OUT joint_treewidth INT,
OUT data_treewidth_lb INT,
OUT circuit_treewidth_lb INT,
OUT n_bags BIGINT,
OUT max_states BIGINT,
OUT dd_size BIGINT,
OUT n_enumerating INT)
AS 'provsql','ucq_joint_compile_stats'
LANGUAGE C IMMUTABLE PARALLEL SAFE;
/**
* @brief Boolean UCQ probability plus statistics from a JSON specification
*
* JSON-spec wrapper over the columnar @c ucq_joint_compile_stats()
* (see @c ucq_joint_evaluate(query jsonb, ...) for the JSON format).
*/
CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
IN query JSONB,
IN fact_rel INT[],
IN fact_elems INT[],
IN fact_arity INT[],
IN fact_tokens UUID[],
IN fact_probs DOUBLE PRECISION[],
OUT probability DOUBLE PRECISION,
OUT joint_treewidth INT,
OUT data_treewidth_lb INT,
OUT circuit_treewidth_lb INT,
OUT n_bags BIGINT,
OUT max_states BIGINT,
OUT dd_size BIGINT,
OUT n_enumerating INT)
AS $$
DECLARE
dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
avars INT[] := '{}'; aarity INT[] := '{}';
d JSONB; a JSONB; v TEXT; didx INT := 0;
BEGIN
FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
dnv := dnv || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
adisj := adisj || didx;
arel := arel || (a->>'rel')::int;
aarity := aarity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
avars := avars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
s.n_enumerating
INTO probability, joint_treewidth, data_treewidth_lb,
circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s;
END;
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
/**
* @brief Correlated Boolean UCQ probability plus compilation statistics
* (columnar form, internal)
*
* Same compilation as @c ucq_joint_evaluate_tracked(); the three width
* columns substantiate thesis Prop. 4.2.11 on real correlated data (the
* data-only and circuit-only degeneracy bounds can be small while the
* joint width is large).
*/
CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
IN disjunct_nvars INT[],
IN atom_disjunct INT[],
IN atom_rel INT[],
IN atom_vars INT[],
IN atom_arity INT[],
IN fact_rel INT[],
IN fact_elems INT[],
IN fact_arity INT[],
IN fact_tokens UUID[],
OUT probability DOUBLE PRECISION,
OUT joint_treewidth INT,
OUT data_treewidth_lb INT,
OUT circuit_treewidth_lb INT,
OUT n_bags BIGINT,
OUT max_states BIGINT,
OUT dd_size BIGINT,
OUT n_enumerating INT)
AS 'provsql','ucq_joint_compile_stats_tracked'
LANGUAGE C STABLE PARALLEL SAFE;
/**
* @brief Correlated Boolean UCQ probability plus statistics from a JSON spec
*/
CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
IN query JSONB,
IN fact_rel INT[],
IN fact_elems INT[],
IN fact_arity INT[],
IN fact_tokens UUID[],
OUT probability DOUBLE PRECISION,
OUT joint_treewidth INT,
OUT data_treewidth_lb INT,
OUT circuit_treewidth_lb INT,
OUT n_bags BIGINT,
OUT max_states BIGINT,
OUT dd_size BIGINT,
OUT n_enumerating INT)
AS $$
DECLARE
dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
avars INT[] := '{}'; aarity INT[] := '{}';
d JSONB; a JSONB; v TEXT; didx INT := 0;
BEGIN
FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
dnv := dnv || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
adisj := adisj || didx;
arel := arel || (a->>'rel')::int;
aarity := aarity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
avars := avars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
s.n_enumerating
INTO probability, joint_treewidth, data_treewidth_lb,
circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
FROM ucq_joint_compile_stats_tracked(dnv, adisj, arel, avars, aarity,
fact_rel, fact_elems, fact_arity, fact_tokens) s;
END;
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
/**
* @brief Compile a correlated UCQ and materialise its certified d-D,
* returning the root provenance token (columnar form, internal)
*
* The architecturally-primary route: the compiler builds the
* deterministic, decomposable circuit and materialises it as ordinary
* @c plus / @c times / @c monus provenance gates (carrying the d-DNNF
* certificate); the answer is then obtained through the standard entry
* points on the returned token -- @c probability_evaluate(token),
* @c shapley(token, ...), expectation -- so the joint-width path shares
* the one evaluation pipeline. The token is the exact Boolean
* provenance of the UCQ (no @c 'absorptive' marker).
*/
CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
disjunct_nvars INT[],
atom_disjunct INT[],
atom_rel INT[],
atom_vars INT[],
atom_arity INT[],
fact_rel INT[],
fact_elems INT[],
fact_arity INT[],
fact_tokens UUID[])
RETURNS UUID AS
'provsql','ucq_joint_materialize_tracked' LANGUAGE C VOLATILE;
/**
* @brief Compile a correlated UCQ and materialise its certified d-D
* from a JSON spec, returning the root provenance token
*
* JSON-spec wrapper over @c ucq_joint_materialize_tracked(). Evaluate
* the answer with the standard surface, e.g.
* @c probability_evaluate(ucq_joint_materialize_tracked(query, ...)).
*/
CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
query JSONB,
fact_rel INT[],
fact_elems INT[],
fact_arity INT[],
fact_tokens UUID[])
RETURNS UUID AS $$
DECLARE
dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
avars INT[] := '{}'; aarity INT[] := '{}';
d JSONB; a JSONB; v TEXT; didx INT := 0;
BEGIN
FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
dnv := dnv || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
adisj := adisj || didx;
arel := arel || (a->>'rel')::int;
aarity := aarity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
avars := avars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
RETURN ucq_joint_materialize_tracked(dnv, adisj, arel, avars, aarity,
fact_rel, fact_elems, fact_arity, fact_tokens);
END;
$$ LANGUAGE plpgsql VOLATILE;
/**
* @brief Compile a UCQ over named relations into a materialised certified
* d-D, gathering the facts from the store -- the descriptor-driven engine
*
* The query-surface bridge for the joint-width compiler: instead of
* hand-built columnar arrays, a JSON @p descriptor names the relations
* and how their columns map to query variables, and this function
* gathers the facts itself (the provenance rewriting is disabled around
* the gather), builds the value-based element dictionary shared across
* the relations (so equal join values get the same dense id), compiles
* and materialises the certified d-D, and returns its provenance token.
* The answer is then any standard evaluation on that token --
* @c probability_evaluate(ucq_joint_provenance(...)),
* @c shapley(...), expectation. This is also the engine the planner-time
* query recogniser drives once it builds the descriptor from a query's
* abstract syntax.
*
* Descriptor shape:
* @verbatim
* { "disjuncts": [ { "n_vars": k,
* "atoms": [ {"rel": , "vars": [..]}, ... ] }, ... ],
* "relations": [ "schema.r", "schema.s", ... ], -- relidx -> relation
* "elem_cols": [ ["x"], ["x","y"], ... ] } -- per relation: the
* element columns, in
* the atom's var order
* @endverbatim
*
* @param descriptor the UCQ + the relations and their element columns
* @param fallback token returned if the joint-width compiler declines
* @return the materialised joint-width provenance token (NULL UUID-free
* exact Boolean provenance of the UCQ)
*/
CREATE OR REPLACE FUNCTION ucq_joint_provenance(
descriptor JSONB, fallback UUID DEFAULT NULL)
RETURNS UUID AS $$
DECLARE
legs text; sql text; saved text;
fact_rel int[]; fact_elems int[]; fact_arity int[]; fact_tokens uuid[];
dnv int[]:='{}'; adisj int[]:='{}'; arel int[]:='{}';
avars int[]:='{}'; aarity int[]:='{}';
d jsonb; a jsonb; v text; didx int:=0;
BEGIN
-- Parse the UCQ structure into the columnar query arrays.
FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
dnv := dnv || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
adisj := adisj || didx; arel := arel || (a->>'rel')::int;
aarity := aarity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
avars := avars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
-- One UNION ALL leg per relation: (relation index, text element array,
-- provenance token). No temp tables: a single gather query, with the
-- value-based dense element dictionary built inline.
SELECT string_agg(
format('SELECT %s, ARRAY[%s]::text[], provsql FROM %s%s',
rn - 1,
(SELECT string_agg(format('(%I)::text', c), ',')
FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::int) c),
rel,
-- the lifted single-relation selection (a pre-filter), already
-- deparsed to SQL by the recogniser; '' / absent = unfiltered.
CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::int,'') <> ''
THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::int)
ELSE '' END),
' UNION ALL ')
INTO legs
FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
sql := format($q$
WITH facts(rel,elems,tok) AS (%s),
ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
(SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
(SELECT array_agg(tok ORDER BY ord) FROM ord),
(SELECT array_agg(dd.id ORDER BY o.ord, e.k)
FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
JOIN dict dd ON dd.val = e.val)
$q$, legs);
-- Read the raw rows with provenance rewriting disabled (we only read
-- the existing provsql column; this internal gather is not tracked).
saved := current_setting('provsql.active', true);
PERFORM set_config('provsql.active','off', true);
EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
PERFORM set_config('provsql.active', saved, true);
RETURN ucq_joint_materialize_tracked(dnv,adisj,arel,avars,aarity,
fact_rel,fact_elems,fact_arity,fact_tokens);
EXCEPTION WHEN OTHERS THEN
-- The joint-width compiler declined (unsupported gate type, joint
-- width too large, ...): fall back to the normal provenance so the
-- query never fails. Both give the same probability.
RETURN fallback;
END;
$$ LANGUAGE plpgsql VOLATILE;
-- ===========================================================================
-- Safe-UCQ Möbius-inversion route (mobius_evaluate.cpp).
--
-- The last missing exact route of the Dalvi-Suciu dichotomy: UCQs that are
-- safe only because the \#P-hard terms of their inclusion-exclusion expansion
-- carry a zero Möbius value on the CNF lattice and cancel (canonical witness:
-- QW / q9). Same TID gather as ucq_joint, then the lattice-walking compiler
-- materialises a gate_mobius-rooted circuit (a signed combination over
-- certified-independent islands), answered in PTIME data complexity by the
-- standard probability path.
-- ===========================================================================
/**
* @brief Materialise the safe-UCQ Möbius circuit and return its root token.
* Columnar (TID) interface; see ucq_mobius_provenance for the gather.
*/
CREATE OR REPLACE FUNCTION ucq_mobius_materialize_tracked(
disjunct_nvars INT[],
atom_disjunct INT[],
atom_rel INT[],
atom_vars INT[],
atom_arity INT[],
fact_rel INT[],
fact_elems INT[],
fact_arity INT[],
fact_tokens UUID[],
lineage UUID DEFAULT NULL)
RETURNS UUID AS
'provsql','ucq_mobius_materialize_tracked' LANGUAGE C VOLATILE;
/**
* @brief Compile the Möbius circuit and return the lattice statistics plus the
* probability (the demonstrability surface). @c cancelled_hard is the
* single number that makes the mechanism legible: for q9 the 1 cancelled
* element is \#P-hard, so the query is easy only because its hard part
* cancels.
*/
CREATE OR REPLACE FUNCTION ucq_mobius_compile_stats(
IN disjunct_nvars INT[],
IN atom_disjunct INT[],
IN atom_rel INT[],
IN atom_vars INT[],
IN atom_arity INT[],
IN fact_rel INT[],
IN fact_elems INT[],
IN fact_arity INT[],
IN fact_tokens UUID[],
OUT probability DOUBLE PRECISION,
OUT n_components INT,
OUT n_cnf_conjuncts INT,
OUT lattice_size INT,
OUT n_nonzero INT,
OUT n_cancelled INT,
OUT cancelled_hard BOOLEAN,
OUT dd_size BIGINT,
OUT memo_hits BIGINT)
AS 'provsql','ucq_mobius_compile_stats'
LANGUAGE C VOLATILE;
/**
* @brief Pass a token through iff it is a @c gate_mobius, else return NULL.
*
* The Möbius-precedence dispatch (see @c make_provenance_expression) wraps the
* Möbius call in this and then @c COALESCE\ s it before the joint-width call:
* a Möbius *success* always roots a @c gate_mobius (the compiler wraps even a
* thin selector around the lineage), so it short-circuits and the joint-width
* compiler never runs; a Möbius *decline* returns the literal lineage (never a
* @c gate_mobius), so this yields NULL and @c COALESCE falls through to
* joint-width. The lineage token is a plain plus/times/input, so the test is
* unambiguous.
*/
CREATE OR REPLACE FUNCTION mobius_or_null(tok UUID)
RETURNS UUID AS $$
SELECT CASE WHEN tok IS NOT NULL AND provsql.get_gate_type(tok) = 'mobius'
THEN tok END
$$ LANGUAGE sql STABLE;
/**
* @brief Möbius-route provenance from a descriptor (the planner-substituted
* entry point, and the manual one). Same descriptor and TID gather as
* @c ucq_joint_provenance; on any decline (unsafe shape, cap, not TID)
* returns @p fallback, so a recognised query never fails.
*/
CREATE OR REPLACE FUNCTION ucq_mobius_provenance(
descriptor JSONB, fallback UUID DEFAULT NULL)
RETURNS UUID AS $$
DECLARE
legs text; sql text; saved text;
fact_rel int[]; fact_elems int[]; fact_arity int[]; fact_tokens uuid[];
dnv int[]:='{}'; adisj int[]:='{}'; arel int[]:='{}';
avars int[]:='{}'; aarity int[]:='{}';
d jsonb; a jsonb; v text; didx int:=0;
BEGIN
FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
dnv := dnv || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
adisj := adisj || didx; arel := arel || (a->>'rel')::int;
aarity := aarity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
avars := avars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
SELECT string_agg(
format('SELECT %s, ARRAY[%s]::text[], provsql FROM %s%s',
rn - 1,
(SELECT string_agg(format('(%I)::text', c), ',')
FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::int) c),
rel,
CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::int,'') <> ''
THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::int)
ELSE '' END),
' UNION ALL ')
INTO legs
FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
sql := format($q$
WITH facts(rel,elems,tok) AS (%s),
ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
(SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
(SELECT array_agg(tok ORDER BY ord) FROM ord),
(SELECT array_agg(dd.id ORDER BY o.ord, e.k)
FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
JOIN dict dd ON dd.val = e.val)
$q$, legs);
saved := current_setting('provsql.active', true);
PERFORM set_config('provsql.active','off', true);
EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
PERFORM set_config('provsql.active', saved, true);
-- Pass the normal-provenance fallback as the lineage: it is carried on the
-- gate_mobius so the token still answers Shapley / semiring / PROV on the
-- literal lineage (the Möbius combination is a probability-only shortcut).
RETURN ucq_mobius_materialize_tracked(dnv,adisj,arel,avars,aarity,
fact_rel,fact_elems,fact_arity,fact_tokens, fallback);
EXCEPTION WHEN OTHERS THEN
RETURN fallback;
END;
$$ LANGUAGE plpgsql VOLATILE;
/**
* @brief Möbius lattice statistics + probability from a descriptor: the
* demonstrability SRF. Gathers
* the same TID facts as @c ucq_mobius_provenance, then runs the columnar
* @c ucq_mobius_compile_stats.
*/
CREATE OR REPLACE FUNCTION mobius_compile_stats(
IN descriptor JSONB,
OUT probability DOUBLE PRECISION,
OUT n_components INT,
OUT n_cnf_conjuncts INT,
OUT lattice_size INT,
OUT n_nonzero INT,
OUT n_cancelled INT,
OUT cancelled_hard BOOLEAN,
OUT dd_size BIGINT,
OUT memo_hits BIGINT)
RETURNS RECORD AS $$
DECLARE
legs text; sql text; saved text;
fact_rel int[]; fact_elems int[]; fact_arity int[]; fact_tokens uuid[];
dnv int[]:='{}'; adisj int[]:='{}'; arel int[]:='{}';
avars int[]:='{}'; aarity int[]:='{}';
d jsonb; a jsonb; v text; didx int:=0;
BEGIN
FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
dnv := dnv || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
adisj := adisj || didx; arel := arel || (a->>'rel')::int;
aarity := aarity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
avars := avars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
SELECT string_agg(
format('SELECT %s, ARRAY[%s]::text[], provsql FROM %s%s',
rn - 1,
(SELECT string_agg(format('(%I)::text', c), ',')
FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::int) c),
rel,
CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::int,'') <> ''
THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::int)
ELSE '' END),
' UNION ALL ')
INTO legs
FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
sql := format($q$
WITH facts(rel,elems,tok) AS (%s),
ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
(SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
(SELECT array_agg(tok ORDER BY ord) FROM ord),
(SELECT array_agg(dd.id ORDER BY o.ord, e.k)
FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
JOIN dict dd ON dd.val = e.val)
$q$, legs);
saved := current_setting('provsql.active', true);
PERFORM set_config('provsql.active','off', true);
EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
PERFORM set_config('provsql.active', saved, true);
SELECT s.probability, s.n_components, s.n_cnf_conjuncts, s.lattice_size,
s.n_nonzero, s.n_cancelled, s.cancelled_hard, s.dd_size, s.memo_hits
INTO probability, n_components, n_cnf_conjuncts, lattice_size,
n_nonzero, n_cancelled, cancelled_hard, dd_size, memo_hits
FROM ucq_mobius_compile_stats(dnv,adisj,arel,avars,aarity,
fact_rel,fact_elems,fact_arity,fact_tokens) s;
END;
$$ LANGUAGE plpgsql VOLATILE;
/**
* @brief Internal gather for the per-answer joint route: parse @p descriptor
* into the columnar UCQ arrays and gather every fact (relation index,
* dense element ids, provenance token) with the value dictionary.
*
* Used only by the planner-substituted @c ucq_joint_provenance_answer (the C
* single-DP entry point), which calls it ONCE per query and then computes all
* answers in one sweep. No head pinning: the single DP discovers the answers.
* @c val_by_id maps a dense element id back to its text value (so an answer's
* head ids can be matched to the @c GROUP @c BY head text).
*/
CREATE OR REPLACE FUNCTION ucq_joint_gather(
descriptor JSONB,
OUT disjunct_nvars INT[], OUT atom_disjunct INT[], OUT atom_rel INT[],
OUT atom_vars INT[], OUT atom_arity INT[],
OUT fact_rel INT[], OUT fact_elems INT[], OUT fact_arity INT[],
OUT fact_tokens UUID[], OUT val_by_id TEXT[])
AS $$
DECLARE
legs text; sql text; saved text; d jsonb; a jsonb; v text; didx int := 0;
BEGIN
disjunct_nvars:='{}'; atom_disjunct:='{}'; atom_rel:='{}';
atom_vars:='{}'; atom_arity:='{}';
FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
disjunct_nvars := disjunct_nvars || (d->>'n_vars')::int;
FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
atom_disjunct := atom_disjunct || didx;
atom_rel := atom_rel || (a->>'rel')::int;
atom_arity := atom_arity || jsonb_array_length(a->'vars');
FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
atom_vars := atom_vars || v::int;
END LOOP;
END LOOP;
didx := didx + 1;
END LOOP;
SELECT string_agg(
format('SELECT %s, ARRAY[%s]::text[], provsql FROM %s%s', rn - 1,
(SELECT string_agg(format('(%I)::text', c), ',')
FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::int) c),
rel,
CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::int,'') <> ''
THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::int)
ELSE '' END),
' UNION ALL ')
INTO legs
FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
sql := format($q$
WITH facts(rel,elems,tok) AS (%s),
ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
(SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
(SELECT array_agg(tok ORDER BY ord) FROM ord),
(SELECT array_agg(dd.id ORDER BY o.ord, e.k)
FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
JOIN dict dd ON dd.val = e.val),
(SELECT array_agg(val ORDER BY id) FROM dict)
$q$, legs);
saved := current_setting('provsql.active', true);
PERFORM set_config('provsql.active','off', true);
EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems, val_by_id;
PERFORM set_config('provsql.active', saved, true);
END;
$$ LANGUAGE plpgsql VOLATILE;
/**
* @brief Per-answer joint-width provenance via the TOP-DOWN single DP
* (planner-substituted, C).
*
* The transparent per-answer rewrite substitutes one call per output group.
* On the FIRST call of a query the function gathers the facts once
* (@c ucq_joint_gather), runs the single DP, and materialises EVERY answer's
* certified d-D into the store, caching @c head_vals -> token in @c fn_extra;
* each subsequent group call is an O(1) lookup -- so the whole GROUP BY costs
* one gather + one decomposition + one sweep, not @p k of each. On any
* decline (joint width too large) the @p fallback token (the normal
* per-answer provenance) is returned, so the query never fails. The answer's
* marginal / Shapley / expectation is then the standard evaluation on the
* returned token -- one pipeline for the whole system.
*/
CREATE OR REPLACE FUNCTION ucq_joint_provenance_answer(
descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
RETURNS UUID AS 'provsql','ucq_joint_provenance_answer'
LANGUAGE C STABLE;
/**
* @brief Per-answer safe-UCQ Möbius provenance (planner-substituted): one
* head-pinned Möbius circuit per output group. On the first call the
* facts are gathered once (ucq_joint_gather) and cached; each group pins
* @p head_vars to @p head_vals and compiles, caching head -> token. On
* any decline returns @p fallback. STABLE: it caches per fn-call
* context, so it is not re-evaluated within one scan.
*/
CREATE OR REPLACE FUNCTION ucq_mobius_provenance_answer(
descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
RETURNS UUID AS 'provsql','ucq_mobius_provenance_answer'
LANGUAGE C STABLE;
/**
* @brief Compile and materialise the reachability provenance of every
* vertex (columnar form, internal)
*
* All-targets variant of @c reachability_evaluate(): compiles, along a
* tree decomposition of the data graph, one certified provenance
* circuit per vertex reachable from some source in the all-edges-present
* world, materialises the (shared, linear-size) circuits in the
* provenance store -- @c plus / @c times gates carrying the d-DNNF
* certificate, negated edges as @c monus(one, edge) -- and returns one
* @c (vertex, token) row per such vertex. Sources form a possibly
* *probabilistic source set*: each source arc is gated by the source
* tuple's token, the nil UUID marking a certain (always present)
* source. This is the engine behind the rewriter's
* recursive-reachability route; the returned tokens are ordinary
* provenance tokens usable with the whole evaluation surface, wrapped
* in the 'absorptive' assumption marker (the compiled circuit is the
* exact Boolean lineage but only the absorptive quotient of the
* infinite recursive semiring provenance: probability and absorptive
* semiring evaluations -- e.g. nonnegative min-plus -- are exact,
* counting and why-provenance refuse).
*
* @param sources source vertex of each edge (dense integer IDs)
* @param destinations destination vertex of each edge
* @param tokens provenance token of each edge tuple
* @param probabilities probability of each edge tuple
* @param block_keys per-edge BID key variable (nil UUID = independent
* tuple; alternatives sharing a key are mutually exclusive, e.g.
* from repair_key)
* @param block_indices per-edge outcome index within its block
* @param source_vertices the source vertices
* @param source_tokens per-source provenance token (nil UUID = certain)
* @param source_probabilities per-source probability
* @param directed if false, each edge can be traversed both ways
* @param[out] vertex a vertex reachable from some source
* @param[out] token the materialised reachability provenance token of @c vertex
*/
CREATE OR REPLACE FUNCTION reachability_materialize(
IN sources INT[],
IN destinations INT[],
IN tokens UUID[],
IN probabilities DOUBLE PRECISION[],
IN block_keys UUID[],
IN block_indices INT[],
IN source_vertices INT[],
IN source_tokens UUID[],
IN source_probabilities DOUBLE PRECISION[],
IN directed BOOLEAN,
OUT vertex INT,
OUT token UUID)
RETURNS SETOF record AS
'provsql','reachability_materialize' LANGUAGE C VOLATILE;
/**
* @brief Bounded-hop variant of @c reachability_materialize() (internal)
*
* Compiles, along a tree decomposition of the data graph, one certified
* provenance circuit per (vertex, walk length) pair achievable within
* @p hop_bound edges -- the rows a hop-counting recursive CTE derives,
* row @c (v,h) meaning "some *walk* of exactly @c h edges connects a
* present source to @c v" -- and returns them as @c (vertex, hops,
* token) with @p hop_seed added to the lengths (the CTE base arm's hop
* constant). Also pre-creates, per vertex, the certified gate that a
* hop-discarding query's deduplication will address, wired to the
* compilation's native within-bound root, so the natural "within k
* hops" probability evaluates through the linear certified route.
*
* @param sources source vertex of each edge (dense integer IDs)
* @param destinations destination vertex of each edge
* @param tokens provenance token of each edge tuple
* @param probabilities probability of each edge tuple
* @param block_keys per-edge BID key variable (nil UUID = independent)
* @param block_indices per-edge outcome index within its block
* @param source_vertices the source vertices
* @param source_tokens per-source provenance token (nil UUID = certain)
* @param source_probabilities per-source probability
* @param directed if false, each edge can be traversed both ways
* @param hop_bound maximum walk length
* @param hop_seed hop value of the base arm (added to reported lengths)
* @param[out] vertex a reachable vertex
* @param[out] hops the walk length at which @c vertex is reached
* @param[out] token the materialised provenance token of the @c (vertex, hops) pair
*/
CREATE OR REPLACE FUNCTION reachability_materialize_hops(
IN sources INT[],
IN destinations INT[],
IN tokens UUID[],
IN probabilities DOUBLE PRECISION[],
IN block_keys UUID[],
IN block_indices INT[],
IN source_vertices INT[],
IN source_tokens UUID[],
IN source_probabilities DOUBLE PRECISION[],
IN directed BOOLEAN,
IN hop_bound INT,
IN hop_seed INT,
OUT vertex INT,
OUT hops INT,
OUT token UUID)
RETURNS SETOF record AS
'provsql','reachability_materialize_hops' LANGUAGE C VOLATILE;
/**
* @brief Per-group "some member reachable" compilation (columnar form,
* internal)
*
* For each distinct group in the parallel @p group_ids /
* @p member_vertices arrays, compiles the certified circuit of "some
* member vertex is reachable from a present source" along the data
* decomposition -- the disjunction over the group's *correlated*
* per-vertex reachability events, deterministic by construction
* through the set-reachability state bit -- materialises it, and
* returns one @c (group_id, token) row per group. Engine behind the
* rewriter's cross-vertex aggregation planting.
*
* @param sources source vertex of each edge (dense integer IDs)
* @param destinations destination vertex of each edge
* @param tokens provenance token of each edge tuple
* @param probabilities probability of each edge tuple
* @param block_keys per-edge BID key variable (nil UUID = independent)
* @param block_indices per-edge outcome index within its block
* @param source_vertices the source vertices
* @param source_tokens per-source provenance token (nil UUID = certain)
* @param source_probabilities per-source probability
* @param directed if false, each edge can be traversed both ways
* @param group_ids group identifier of each member row
* @param member_vertices member vertex of each member row
* @param[out] group_id a group whose every member is reachable
* @param[out] token the materialised all-members-reachable provenance token of
* @c group_id
*/
CREATE OR REPLACE FUNCTION reachability_materialize_any(
IN sources INT[],
IN destinations INT[],
IN tokens UUID[],
IN probabilities DOUBLE PRECISION[],
IN block_keys UUID[],
IN block_indices INT[],
IN source_vertices INT[],
IN source_tokens UUID[],
IN source_probabilities DOUBLE PRECISION[],
IN directed BOOLEAN,
IN group_ids INT[],
IN member_vertices INT[],
OUT group_id INT,
OUT token UUID)
RETURNS SETOF record AS
'provsql','reachability_materialize_any' LANGUAGE C VOLATILE;
/**
* @brief Compile and materialise the "every member vertex reachable"
* (k-terminal / coverage) circuit (columnar form, internal)
*
* Arguments as @c reachability_materialize_any() with a single member
* set: compiles the certified circuit of "every member vertex is
* reachable from a present source" -- the conjunction over the
* members' *correlated* per-vertex events, deterministic by
* construction through the pending rescuer-set congruence --
* materialises it, and returns its token, wrapped in the
* @c 'absorptive' assumption marker. Probability evaluation gives the
* k-terminal reliability; nonnegative min-plus the cost of the
* cheapest covering subgraph (directed Steiner cost), shared edges
* paid once. A member vertex absent from the graph is unreachable:
* the circuit is then constant false.
*
* @param sources source vertex of each edge (dense integer IDs)
* @param destinations destination vertex of each edge
* @param tokens provenance token of each edge tuple
* @param probabilities probability of each edge tuple
* @param block_keys per-edge BID key variable (nil UUID = independent)
* @param block_indices per-edge outcome index within its block
* @param source_vertices the source vertices
* @param source_tokens per-source provenance token (nil UUID = certain)
* @param source_probabilities per-source probability
* @param directed if false, each edge can be traversed both ways
* @param member_vertices the member vertices (dense IDs)
*/
CREATE OR REPLACE FUNCTION reachability_materialize_cover(
sources INT[],
destinations INT[],
tokens UUID[],
probabilities DOUBLE PRECISION[],
block_keys UUID[],
block_indices INT[],
source_vertices INT[],
source_tokens UUID[],
source_probabilities DOUBLE PRECISION[],
directed BOOLEAN,
member_vertices INT[])
RETURNS UUID AS
'provsql','reachability_materialize_cover' LANGUAGE C VOLATILE;
/**
* @brief Plant certified any-member-reachable gates for a grouped
* reachability aggregation (internal)
*
* Called (at plan time, over SPI) by the recursive-CTE lowering when
* the outer query aggregates a reachability working table by a column
* of a joined, untracked member relation: @c GROUP @c BY collapses
* each group's per-vertex reach tokens with @c provenance_plus, whose
* disjuncts are correlated (they share edges) and would otherwise
* leave the certified route. For each multi-member group this
* pre-creates, at the canonical address of the group's token multiset,
* a certified single-child plus over the group's native
* any-member-reachable circuit (@c reachability_materialize_any), so
* the natural aggregation stays on the linear evaluation route.
* Best-effort: any failure leaves the generic path untouched (notice
* under verbosity 10).
*
* @param work_name the lowered CTE's working table
* @param node_attribute its vertex column
* @param member_rel the joined member relation (must be untracked)
* @param member_attribute the member relation's join column
* @param group_attribute the member relation's grouping column
* @param edge_rel the tracked edge relation (as for eval_reachability)
* @param source_attribute name of the source-vertex column
* @param destination_attribute name of the destination-vertex column
* @param source_value the base arm's constant, as text
* @param directed if false, each edge can be traversed both ways
* @param edge_quals optional deterministic filter over edge columns
* @param source_rel source relation of a multi-source base arm
* @param source_rel_attribute the source relation's vertex column
* @param edge_sql deparsed edge subquery (join-defined edges)
* @param member_quals optional deterministic filter over the member
* relation's columns (table-qualified as @c t.column), restricting
* which members participate in each group
*/
CREATE OR REPLACE FUNCTION plant_reach_any_groups(
work_name text,
node_attribute text,
member_rel regclass,
member_attribute text,
group_attribute text,
edge_rel regclass,
source_attribute text,
destination_attribute text,
source_value text,
directed boolean,
edge_quals text DEFAULT NULL,
source_rel regclass DEFAULT NULL,
source_rel_attribute text DEFAULT NULL,
edge_sql text DEFAULT NULL,
member_quals text DEFAULT NULL)
RETURNS void AS
$$
DECLARE
e record;
grp record;
m record;
sv text[];
st uuid[];
sp double precision[];
gids int[] := ARRAY[]::int[];
mids int[] := ARRAY[]::int[];
vid int;
verbosity int := coalesce(current_setting('provsql.verbose_level', true)::int, 0);
BEGIN
BEGIN
-- A tracked member relation would make the aggregated tokens
-- per-row products, not the bare reach tokens: nothing to plant.
IF EXISTS (SELECT 1 FROM pg_attribute
WHERE attrelid = member_rel AND attname = 'provsql'
AND atttypid = 'uuid'::regtype AND NOT attisdropped) THEN
RETURN;
END IF;
IF source_rel IS NOT NULL THEN
SELECT g.source_values, g.source_tokens, g.source_probabilities
INTO sv, st, sp
FROM provsql.gather_reachability_sources(source_rel,
source_rel_attribute) g;
IF sv IS NULL THEN
sv := ARRAY[]::text[];
st := ARRAY[]::uuid[];
sp := ARRAY[]::float8[];
END IF;
ELSE
sv := ARRAY[source_value];
st := ARRAY['00000000-0000-0000-0000-000000000000'::uuid];
sp := ARRAY[1.0::float8];
END IF;
e := provsql.gather_reachability_edges(edge_rel, source_attribute,
destination_attribute,
sv, edge_quals, edge_sql);
-- The groups, replicating the user's join semantics: per group, the
-- member vertices and the multiset of their reach tokens (with the
-- multiplicity the join produces). Single-member groups need no
-- planting (provenance_plus passes a single token through).
-- Two steps: materialise the joined rows with their per-row tokens
-- (tracked CTAS, then strip the automatic provsql column), and only
-- then aggregate the now-plain table -- aggregating provenance()
-- inside a grouped tracked query would be rewritten as a
-- provenance-aware aggregation, which is not what the planting
-- needs.
DROP TABLE IF EXISTS provsql_reach_any_flat_tmp;
EXECUTE format(
'CREATE TEMP TABLE provsql_reach_any_flat_tmp AS '
|| 'SELECT w.%1$I::text AS node_val, provsql.provenance() AS tok, '
|| ' t.%5$I AS grp_key '
|| 'FROM %2$I w JOIN %3$s t ON w.%1$I = t.%4$I'
-- The member-relation filter restricts which members participate
-- (deparsed table-qualified as t.column); the working table side
-- carries no provenance distinction here.
|| coalesce(' WHERE ' || member_quals, ''),
node_attribute, work_name, member_rel::text, member_attribute,
group_attribute);
PERFORM provsql.remove_provenance('provsql_reach_any_flat_tmp');
DROP TABLE IF EXISTS provsql_reach_any_groups_tmp;
CREATE TEMP TABLE provsql_reach_any_groups_tmp AS
SELECT (row_number() OVER ())::int AS gid, members, toks FROM (
SELECT array_agg(node_val) AS members, array_agg(tok) AS toks
FROM provsql_reach_any_flat_tmp
GROUP BY grp_key HAVING count(*) >= 2) g;
DROP TABLE provsql_reach_any_flat_tmp;
FOR grp IN SELECT gid, members FROM provsql_reach_any_groups_tmp LOOP
FOR m IN SELECT DISTINCT unnest(grp.members) AS val LOOP
vid := array_position(e.vertices, m.val);
IF vid IS NOT NULL THEN
gids := gids || grp.gid;
mids := mids || vid;
END IF;
END LOOP;
END LOOP;
IF cardinality(gids) = 0 THEN
DROP TABLE provsql_reach_any_groups_tmp;
RETURN;
END IF;
FOR grp IN
SELECT a.group_id, a.token AS any_token, t.toks
FROM provsql.reachability_materialize_any(
e.sources, e.destinations, e.tokens, e.probabilities,
e.block_keys, e.block_indices, e.extra_ids, st, sp,
directed, gids, mids) a
JOIN provsql_reach_any_groups_tmp t ON t.gid = a.group_id
LOOP
PERFORM provsql.plant_canonical(work_name, 'plus', grp.toks,
grp.any_token, 1);
END LOOP;
DROP TABLE provsql_reach_any_groups_tmp;
IF verbosity >= 20 THEN
-- Lift the function-level client_min_messages = warning for the
-- one RAISE; the function-level SET restores the caller's value.
PERFORM set_config('client_min_messages', 'notice', true);
RAISE NOTICE 'ProvSQL: certified any-member gates planted for the aggregation of "%" by %.%',
work_name, member_rel, group_attribute;
PERFORM set_config('client_min_messages', 'warning', true);
END IF;
EXCEPTION WHEN OTHERS THEN
IF verbosity >= 10 THEN
PERFORM set_config('client_min_messages', 'notice', true);
RAISE NOTICE 'ProvSQL: any-member planting for "%" skipped (%)',
work_name, SQLERRM;
PERFORM set_config('client_min_messages', 'warning', true);
END IF;
END;
END
-- No SET search_path: the deparsed edge subquery must resolve against
-- the caller's path; ProvSQL internals are schema-qualified.
$$ LANGUAGE plpgsql SET client_min_messages = warning;
/**
* @brief Plant the certified all-members-reachable gate for a
* reachability self-join conjunction (internal)
*
* Called (at plan time, over SPI) by the recursive-CTE lowering when
* the outer query self-joins a reachability working table with one
* constant node binding per reference -- "are these k vertices all
* reachable" -- whose row provenance @c provenance_times() computes as
* the product of *correlated* per-vertex reach tokens (they share
* edges). This pre-creates, at the times-canonical address of that
* token multiset, a certified single-child times over the native
* all-members-reachable circuit (@c reachability_materialize_cover),
* so the natural conjunction stays on the linear certified route --
* with the joint-worlds semantics: probability evaluation gives the
* k-terminal reliability, and nonnegative min-plus the cost of the
* cheapest covering subgraph (directed Steiner cost), shared edges
* paid once where the raw product would pay them once per factor.
* Best-effort: any failure leaves the generic path untouched (notice
* under verbosity 10).
*
* @param work_name the lowered CTE's working table
* @param node_attribute its vertex column
* @param edge_rel the tracked edge relation (as for eval_reachability)
* @param source_attribute name of the source-vertex column
* @param destination_attribute name of the destination-vertex column
* @param source_value the base arm's constant, as text
* @param directed if false, each edge can be traversed both ways
* @param node_values the constant node bindings, as text (multiset:
* one per self-join reference)
* @param edge_quals optional deterministic filter over edge columns
* @param source_rel source relation of a multi-source base arm
* @param source_rel_attribute the source relation's vertex column
* @param edge_sql deparsed edge subquery (join-defined edges)
*/
CREATE OR REPLACE FUNCTION plant_reach_cover(
work_name text,
node_attribute text,
edge_rel regclass,
source_attribute text,
destination_attribute text,
source_value text,
directed boolean,
node_values text[],
edge_quals text DEFAULT NULL,
source_rel regclass DEFAULT NULL,
source_rel_attribute text DEFAULT NULL,
edge_sql text DEFAULT NULL)
RETURNS void AS
$$
DECLARE
e record;
sv text[];
st uuid[];
sp double precision[];
val text;
vid int;
vids int[] := ARRAY[]::int[];
tok uuid;
toks uuid[] := ARRAY[]::uuid[];
cover_token uuid;
verbosity int := coalesce(current_setting('provsql.verbose_level', true)::int, 0);
BEGIN
BEGIN
IF source_rel IS NOT NULL THEN
SELECT g.source_values, g.source_tokens, g.source_probabilities
INTO sv, st, sp
FROM provsql.gather_reachability_sources(source_rel,
source_rel_attribute) g;
IF sv IS NULL THEN
sv := ARRAY[]::text[];
st := ARRAY[]::uuid[];
sp := ARRAY[]::float8[];
END IF;
ELSE
sv := ARRAY[source_value];
st := ARRAY['00000000-0000-0000-0000-000000000000'::uuid];
sp := ARRAY[1.0::float8];
END IF;
e := provsql.gather_reachability_edges(edge_rel, source_attribute,
destination_attribute,
sv, edge_quals, edge_sql);
-- The bound vertices and their per-row reach tokens, with the
-- multiplicity the self-join produces. A vertex absent from the
-- graph, or from the working table, means the join is empty: no
-- row will exist, nothing to plant.
FOREACH val IN ARRAY node_values LOOP
vid := array_position(e.vertices, val);
IF vid IS NULL THEN
RETURN;
END IF;
vids := vids || vid;
EXECUTE format('SELECT provsql FROM %I WHERE %I::text = $1',
work_name, node_attribute)
INTO tok USING val;
IF tok IS NULL THEN
RETURN;
END IF;
toks := toks || tok;
END LOOP;
cover_token := provsql.reachability_materialize_cover(
e.sources, e.destinations, e.tokens, e.probabilities,
e.block_keys, e.block_indices, e.extra_ids, st, sp,
directed, vids);
PERFORM provsql.plant_canonical(work_name, 'times', toks, cover_token, 1);
IF verbosity >= 20 THEN
-- Lift the function-level client_min_messages = warning for the
-- one RAISE; the function-level SET restores the caller's value.
PERFORM set_config('client_min_messages', 'notice', true);
RAISE NOTICE 'ProvSQL: certified all-members gate planted for the self-join of "%"',
work_name;
PERFORM set_config('client_min_messages', 'warning', true);
END IF;
EXCEPTION WHEN OTHERS THEN
IF verbosity >= 10 THEN
PERFORM set_config('client_min_messages', 'notice', true);
RAISE NOTICE 'ProvSQL: all-members planting for "%" skipped (%)',
work_name, SQLERRM;
PERFORM set_config('client_min_messages', 'warning', true);
END IF;
END;
END
-- No SET search_path: the deparsed edge subquery must resolve against
-- the caller's path; ProvSQL internals are schema-qualified.
$$ LANGUAGE plpgsql SET client_min_messages = warning;
/**
* @brief Input leaves of a conjunction-shaped provenance token (internal)
*
* Descends a token's circuit through the conjunctive gate types
* (@c times, and the pass-through @c project / @c eq where-provenance
* wrappers) down to @c input leaves. Returns the distinct leaves, or
* NULL when the circuit contains any other gate type (a disjunctive or
* aggregate shape, which is not a conjunction of independent tuples).
* Used by the reachability gathering to accept join-defined edges:
* a derived edge whose token is a pure conjunction of base tuples.
*
* @param token the provenance token
*/
CREATE OR REPLACE FUNCTION token_conjunctive_leaves(token uuid)
RETURNS uuid[] AS
$$
WITH RECURSIVE walk(g) AS (
SELECT token
UNION
SELECT c FROM walk w, unnest(provsql.get_children(w.g)) AS c
WHERE provsql.get_gate_type(w.g) IN ('times', 'project', 'eq', 'annotation')
)
SELECT CASE WHEN bool_and(provsql.get_gate_type(g)
IN ('times', 'project', 'eq', 'annotation', 'input'))
THEN array_agg(DISTINCT g)
FILTER (WHERE provsql.get_gate_type(g) = 'input')
ELSE NULL END
FROM walk;
$$ LANGUAGE sql STABLE;
/**
* @brief Gather the edges of a tracked relation in the columnar form
* expected by reachability_evaluate (internal)
*
* Materializes the edge relation with its provenance tokens and
* probabilities, maps arbitrary vertex values (compared as text) onto
* dense integer IDs, and checks that every edge tuple carries a base
* input token (independent tuples): reachability compilation along the
* data is only correct when the edges are independent events, so views
* or query results with derived provenance are rejected.
*
* @param rel the provenance-tracked edge relation
* @param source_attribute name of the source-vertex column
* @param destination_attribute name of the destination-vertex column
* @param extra_vertices vertex values (as text) that must be part of
* the dense ID space even when they touch no edge -- the source
* set in particular; their IDs come back in @c extra_ids
* (aligned with the input)
* @param edge_quals optional deterministic filter over the edge
* relation's columns (SQL text, deparsed by the rewriter from
* the recursive arm's WHERE clause), restricting which edges
* participate
* @param rel_sql deparsed edge subquery to gather from instead of
* @p rel (join-defined edges); the tokens are then conjunctions
* of base tuples, validated for shape and disjoint supports
*
* The @c vertices output maps the dense IDs back to the original
* vertex values (as text, 1-indexed), for callers that need to label
* per-vertex results.
*
* @param[out] sources source vertex (dense ID) of each gathered edge
* @param[out] destinations destination vertex (dense ID) of each edge
* @param[out] tokens provenance token of each edge tuple
* @param[out] probabilities probability of each edge tuple
* @param[out] block_keys per-edge BID key variable (nil UUID = independent)
* @param[out] block_indices per-edge outcome index within its block
* @param[out] extra_ids dense IDs assigned to the @p extra_vertices
* @param[out] vertices dense-ID-to-original-value map (text, 1-indexed)
*/
CREATE OR REPLACE FUNCTION gather_reachability_edges(
IN rel regclass,
IN source_attribute TEXT,
IN destination_attribute TEXT,
IN extra_vertices TEXT[],
IN edge_quals TEXT DEFAULT NULL,
IN rel_sql TEXT DEFAULT NULL,
OUT sources INT[],
OUT destinations INT[],
OUT tokens UUID[],
OUT probabilities DOUBLE PRECISION[],
OUT block_keys UUID[],
OUT block_indices INT[],
OUT extra_ids INT[],
OUT vertices TEXT[])
AS
$$
DECLARE
tkind text;
bkey_expr text;
sel_probs text;
sel_bkeys text;
sel_bidx text;
verbosity int := coalesce(current_setting('provsql.verbose_level', true)::int, 0);
BEGIN
-- Consult the per-table characterisation registry (TID / BID / OPAQUE,
-- maintained by add_provenance / repair_key and the CTAS lineage hook):
-- a TID relation is certified all-independent-inputs, a BID relation
-- holds input or mulinput rows with the block structure given by the
-- registry's key columns. Derived (OPAQUE), unregistered, or
-- subquery-defined edges take the fully dynamic per-token path.
IF rel IS NOT NULL AND rel_sql IS NULL THEN
tkind := (provsql.get_table_info(rel::oid)).kind;
END IF;
IF tkind NOT IN ('tid', 'bid') THEN
tkind := NULL;
END IF;
IF tkind = 'bid' THEN
SELECT string_agg(quote_ident(a.attname) || '::text', ' || '','' || '
ORDER BY k.ord)
INTO bkey_expr
FROM unnest((provsql.get_table_info(rel::oid)).block_key)
WITH ORDINALITY AS k(attnum, ord)
JOIN pg_attribute a ON a.attrelid = rel AND a.attnum = k.attnum;
-- An empty registry key means the whole table is one block.
bkey_expr := coalesce(bkey_expr, quote_literal(''));
END IF;
IF tkind IS NOT NULL AND verbosity >= 20 THEN
-- The function-level client_min_messages = warning (which silences
-- the CTAS / DROP TABLE chatter) would also swallow this notice;
-- lift it for the one RAISE. The function-level SET restores the
-- caller's value at exit regardless.
PERFORM set_config('client_min_messages', 'notice', true);
RAISE NOTICE 'ProvSQL: catalog characterises % as %', rel, upper(tkind);
PERFORM set_config('client_min_messages', 'warning', true);
END IF;
-- Materialize the edges with their tokens; the planner hook resolves
-- provenance() over the tracked relation, and remove_provenance strips
-- the automatic provsql column so the later aggregation is plain SQL.
-- For a BID relation the synthetic per-block key (a v5 UUID over the
-- registry key columns' values) is computed here, while the columns
-- are in scope.
DROP TABLE IF EXISTS provsql_reachability_edges_tmp;
EXECUTE format(
'CREATE TEMP TABLE provsql_reachability_edges_tmp AS '
|| 'SELECT %1$I::text AS u, %2$I::text AS v, '
|| 'provsql.strip_annotations(provsql.provenance()) AS token%5$s '
|| 'FROM %3$s WHERE %1$I IS NOT NULL AND %2$I IS NOT NULL%4$s',
source_attribute, destination_attribute,
CASE WHEN rel_sql IS NULL THEN rel::text
ELSE '(' || rel_sql || ') AS provsql_edge_subquery' END,
CASE WHEN edge_quals IS NULL THEN ''
ELSE ' AND (' || edge_quals || ')' END,
CASE WHEN tkind = 'bid'
THEN ', public.uuid_generate_v5(provsql.uuid_ns_provsql(), '
|| quote_literal('bidblock' || rel::text || ':')
|| ' || ' || bkey_expr || ') AS bkey'
ELSE ', NULL::uuid AS bkey' END);
PERFORM provsql.remove_provenance('provsql_reachability_edges_tmp');
DROP TABLE IF EXISTS provsql_reachability_support_tmp;
IF tkind IS NULL THEN
-- Dynamic path: validate the token shapes and, for conjunction-shaped
-- (join-defined) tokens, the pairwise disjointness of their supports.
IF EXISTS (SELECT 1 FROM provsql_reachability_edges_tmp
WHERE provsql.get_gate_type(token) NOT IN ('input', 'mulinput', 'times',
'project', 'eq')) THEN
DROP TABLE provsql_reachability_edges_tmp;
RAISE EXCEPTION 'reachability: the provenance of % must consist of base input, repair_key, or conjunctive join tokens', coalesce(rel::text, 'the edge query')
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: reachability-provenance-shape; scope: gap';
END IF;
CREATE TEMP TABLE provsql_reachability_support_tmp AS
SELECT t.token, l.leaf
FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp
WHERE provsql.get_gate_type(token) IN ('times', 'project', 'eq')) t,
LATERAL unnest(provsql.token_conjunctive_leaves(t.token)) AS l(leaf);
IF EXISTS (SELECT 1
FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp) t
WHERE provsql.get_gate_type(t.token) IN ('times', 'project', 'eq')
AND provsql.token_conjunctive_leaves(t.token) IS NULL) THEN
DROP TABLE provsql_reachability_support_tmp;
DROP TABLE provsql_reachability_edges_tmp;
RAISE EXCEPTION 'reachability: a join-defined edge token is not a pure conjunction of base tuples'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: reachability-edge-not-conjunction; scope: gap';
END IF;
IF EXISTS (SELECT 1 FROM (
SELECT leaf FROM provsql_reachability_support_tmp
UNION ALL
SELECT DISTINCT token FROM provsql_reachability_edges_tmp
WHERE provsql.get_gate_type(token) = 'input'
) all_leaves
GROUP BY leaf HAVING count(*) > 1) THEN
DROP TABLE provsql_reachability_support_tmp;
DROP TABLE provsql_reachability_edges_tmp;
RAISE EXCEPTION 'reachability: join-defined edges share base tuples (their supports overlap), so they are not independent'
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: reachability-edges-share-tuples; scope: gap';
END IF;
END IF;
-- Per-kind classification expressions for the final aggregation: a TID
-- relation needs no per-row gate introspection at all; a BID relation
-- one get_gate_type per row (the input/mulinput split), block keys from
-- the precomputed column-derived key and indices by numbering within
-- the block; the dynamic path reads the gates.
IF tkind = 'tid' THEN
sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
sel_bkeys := $sql$'00000000-0000-0000-0000-000000000000'::uuid$sql$;
sel_bidx := '0';
ELSIF tkind = 'bid' THEN
sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
THEN e.bkey
ELSE '00000000-0000-0000-0000-000000000000'::uuid END$sql$;
sel_bidx := 'e.bidx';
ELSE
sel_probs := $sql$CASE WHEN provsql.get_gate_type(e.token) IN ('times','project','eq')
THEN (SELECT CASE WHEN bool_or(coalesce(provsql.get_prob(s.leaf),1.0) = 0)
THEN 0.0
ELSE exp(sum(ln(coalesce(provsql.get_prob(s.leaf),1.0)))) END
FROM provsql_reachability_support_tmp s
WHERE s.token = e.token)
ELSE coalesce(provsql.get_prob(e.token), 1.0) END$sql$;
sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
THEN (provsql.get_children(e.token))[1]
ELSE '00000000-0000-0000-0000-000000000000'::uuid END$sql$;
sel_bidx := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
THEN (provsql.get_infos(e.token)).info1 ELSE 0 END$sql$;
END IF;
EXECUTE format(
$sql$
WITH verts AS (
SELECT u AS x FROM provsql_reachability_edges_tmp
UNION SELECT v FROM provsql_reachability_edges_tmp
UNION SELECT unnest($1)),
ids AS (
SELECT x, (row_number() OVER (ORDER BY x))::int AS id FROM verts)
SELECT array_agg(iu.id), array_agg(iv.id),
array_agg(e.token),
array_agg(%s),
array_agg(%s),
array_agg(%s),
(SELECT array_agg(i.id ORDER BY ev.ord)
FROM unnest($1) WITH ORDINALITY AS ev(x, ord)
JOIN ids i ON i.x = ev.x),
(SELECT array_agg(x ORDER BY id) FROM ids)
FROM (SELECT t.*,
(row_number() OVER (PARTITION BY t.bkey))::int AS bidx
FROM provsql_reachability_edges_tmp t) e
JOIN ids iu ON iu.x = e.u
JOIN ids iv ON iv.x = e.v
$sql$, sel_probs, sel_bkeys, sel_bidx)
INTO sources, destinations, tokens, probabilities, block_keys,
block_indices, extra_ids, vertices
USING extra_vertices;
DROP TABLE provsql_reachability_edges_tmp;
DROP TABLE IF EXISTS provsql_reachability_support_tmp;
END
-- No SET search_path: the deparsed edge subquery (and the regclass
-- rendering) must resolve against the caller's search_path; the ProvSQL
-- calls above are schema-qualified instead.
$$ LANGUAGE plpgsql SET client_min_messages = warning;
/**
* @brief Gather a source relation's vertices, tokens and probabilities
* (internal)
*
* For a provenance-tracked source relation, every tuple must carry a
* base @c input token (a *probabilistic source set*); for an untracked
* relation the sources are certain and the tokens come back as the nil
* UUID. Vertex values are returned as text, for the shared dense-ID
* mapping of @c gather_reachability_edges().
*
* @param rel the source relation
* @param source_attribute name of the vertex column
* @param[out] source_values vertex value of each source tuple (as text)
* @param[out] source_tokens per-source base @c input token (nil UUID = certain)
* @param[out] source_probabilities per-source probability
*/
CREATE OR REPLACE FUNCTION gather_reachability_sources(
IN rel regclass,
IN source_attribute TEXT,
OUT source_values TEXT[],
OUT source_tokens UUID[],
OUT source_probabilities DOUBLE PRECISION[])
AS
$$
DECLARE
tracked boolean;
tkind text;
BEGIN
SELECT EXISTS (
SELECT 1 FROM pg_attribute
WHERE attrelid = rel AND attname = 'provsql'
AND atttypid = 'uuid'::regtype AND NOT attisdropped)
INTO tracked;
-- Registry consultation: a TID source relation is certified
-- all-base-input, so the per-row gate check can be skipped; a BID one
-- holds block-correlated tuples, which a probabilistic source set
-- cannot model -- reject it before gathering anything.
IF tracked THEN
tkind := (get_table_info(rel::oid)).kind;
IF tkind = 'bid' THEN
RAISE EXCEPTION 'reachability: % is block-independent (repair_key); block-correlated source sets are not supported', rel
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: reachability-block-correlated; scope: gap';
END IF;
END IF;
DROP TABLE IF EXISTS provsql_reachability_sources_tmp;
IF tracked THEN
EXECUTE format(
'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
|| 'SELECT %1$I::text AS x, provenance() AS token '
|| 'FROM %2$s WHERE %1$I IS NOT NULL',
source_attribute, rel);
PERFORM remove_provenance('provsql_reachability_sources_tmp');
IF tkind IS DISTINCT FROM 'tid'
AND EXISTS (SELECT 1 FROM provsql_reachability_sources_tmp
WHERE get_gate_type(token) <> 'input') THEN
DROP TABLE provsql_reachability_sources_tmp;
RAISE EXCEPTION 'reachability: the provenance of % must consist of base input tokens (independent tuples); views or query results are not supported', rel
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: reachability-not-base-inputs; scope: gap';
END IF;
SELECT array_agg(x), array_agg(token),
array_agg(coalesce(get_prob(token), 1.0))
INTO source_values, source_tokens, source_probabilities
FROM provsql_reachability_sources_tmp;
DROP TABLE provsql_reachability_sources_tmp;
ELSE
EXECUTE format(
'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
|| 'SELECT DISTINCT %1$I::text AS x FROM %2$s WHERE %1$I IS NOT NULL',
source_attribute, rel);
SELECT array_agg(x),
array_agg('00000000-0000-0000-0000-000000000000'::uuid),
array_agg(1.0::float8)
INTO source_values, source_tokens, source_probabilities
FROM provsql_reachability_sources_tmp;
DROP TABLE provsql_reachability_sources_tmp;
END IF;
END
$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SET client_min_messages = warning;
/**
* @brief Fixpoint driver for the recursive reachability shape:
* decomposition-aligned compilation with fallback to eval_recursive_system
*
* Called (at plan time, over SPI) by the recursive-CTE lowering when
* the provenance class is 'absorptive' or 'boolean'
* (@c provsql.provenance) and the CTE matches the linear
* reachability shape over a tracked base edge relation. Attempts the
* decomposition-aligned route -- gather the edges, compile every
* reachable vertex's certified provenance circuit along a tree
* decomposition of the data graph, materialise them, and fill the
* working table with one tokenised row per reachable vertex. On any
* failure (data treewidth above the cap, per-node state bound, edges
* that are not independent base tuples...), falls back to the generic
* @c eval_recursive_system() driver, preserving its behaviour exactly.
*
* @param edge_rel the provenance-tracked edge relation
* @param source_attribute name of the source-vertex column
* @param destination_attribute name of the destination-vertex column
* @param source_value the base arm's constant, as text
* @param directed if false, each edge can be traversed both ways
* @param work_name name of the working temp table (the CTE name)
* @param colnames comma-separated user column names (for the fallback)
* @param coldef column definitions of the working table
* @param coltype type of the CTE's single column
* @param body_sql deparsed CTE body (for the fallback)
* @param edge_quals optional deterministic filter over edge columns
* (deparsed from the recursive arm's WHERE clause)
* @param source_rel source relation of a multi-source base arm
* (@c SELECT col FROM sources), NULL for the constant form;
* tracked sources form a probabilistic source set, untracked
* ones are certain
* @param source_rel_attribute the source relation's vertex column
* @param edge_sql deparsed edge subquery when the recursive arm joins a
* derived (join-defined) edge relation instead of a base one;
* NULL for the regclass form
* @param hop_bound maximum number of recursive steps for the
* hop-counting CTE shape (NULL for plain reachability)
* @param hop_seed the base arm's hop constant (hop-counting shape)
* @param hops_position 1-based position of the hop column among the
* CTE's two columns (hop-counting shape)
*/
CREATE OR REPLACE FUNCTION eval_reachability(
edge_rel regclass,
source_attribute text,
destination_attribute text,
source_value text,
directed boolean,
work_name text,
colnames text,
coldef text,
coltype text,
body_sql text,
edge_quals text DEFAULT NULL,
source_rel regclass DEFAULT NULL,
source_rel_attribute text DEFAULT NULL,
edge_sql text DEFAULT NULL,
hop_bound int DEFAULT NULL,
hop_seed int DEFAULT NULL,
hops_position int DEFAULT NULL)
RETURNS void AS
$$
DECLARE
e record;
sv text[];
st uuid[];
sp double precision[];
verbosity int := coalesce(current_setting('provsql.verbose_level', true)::int, 0);
BEGIN
BEGIN
IF source_rel IS NOT NULL THEN
-- Multi-source: gather the source relation (probabilistic when
-- tracked, certain otherwise).
SELECT g.source_values, g.source_tokens, g.source_probabilities
INTO sv, st, sp
FROM provsql.gather_reachability_sources(source_rel,
source_rel_attribute) g;
IF sv IS NULL THEN
sv := ARRAY[]::text[];
st := ARRAY[]::uuid[];
sp := ARRAY[]::float8[];
END IF;
ELSE
-- Constant base arm: one certain source.
sv := ARRAY[source_value];
st := ARRAY['00000000-0000-0000-0000-000000000000'::uuid];
sp := ARRAY[1.0::float8];
END IF;
e := provsql.gather_reachability_edges(edge_rel, source_attribute,
destination_attribute,
sv, edge_quals, edge_sql);
IF to_regclass(work_name) IS NOT NULL THEN
EXECUTE format('DROP TABLE %I', work_name);
END IF;
/* Dropped with the statement, as the other drivers' tables are. */
EXECUTE format('CREATE TEMP TABLE %I (%s, provsql uuid) ON COMMIT DROP',
work_name, coldef);
PERFORM provsql.planted_scope(work_name);
IF hop_bound IS NULL THEN
EXECUTE format(
'INSERT INTO %I SELECT ($1::text[])[m.vertex]::%s, m.token '
|| 'FROM provsql.reachability_materialize($2, $3, $4, $5, $6, $7, $8, $9, $10, $11) m',
work_name, coltype)
USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
e.block_keys, e.block_indices, e.extra_ids, st, sp, directed;
ELSE
-- Hop-counting shape: one row per (vertex, walk length), the hop
-- column in its CTE position.
EXECUTE format(
'INSERT INTO %I SELECT %s, m.token '
|| 'FROM provsql.reachability_materialize_hops($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) m',
work_name,
CASE WHEN hops_position = 1
THEN format('m.hops, ($1::text[])[m.vertex]::%s', coltype)
ELSE format('($1::text[])[m.vertex]::%s, m.hops', coltype) END)
USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
e.block_keys, e.block_indices, e.extra_ids, st, sp, directed,
hop_bound, hop_seed;
END IF;
IF verbosity >= 20 THEN
RAISE NOTICE 'ProvSQL: recursive CTE "%" compiled along a tree decomposition of %',
regexp_replace(work_name, '^provsql_rec_[0-9]+a?_', ''),
coalesce(edge_rel::text, 'the join-defined edge query');
END IF;
EXCEPTION WHEN OTHERS THEN
IF verbosity >= 10 THEN
/* Named as the user named the CTE: the working table carries a name of
ours (provsql_rec__), which is no business of a message. */
RAISE NOTICE 'ProvSQL: reachability route for "%" fell back to the generic fixpoint (%)',
regexp_replace(work_name, '^provsql_rec_[0-9]+a?_', ''), SQLERRM;
END IF;
PERFORM provsql.eval_recursive_system(body_sql, work_name, colnames, coldef);
END;
END
$$ LANGUAGE plpgsql;
/** @} */
/** @defgroup provenance_output Provenance output
* Functions for visualizing and exporting provenance circuits
* in various formats.
* @{
*/
/**
* @brief Return a DOT or text visualization of the provenance circuit
*
* @param token root provenance token
* @param token2desc mapping table for gate descriptions
* @param dbg debug level (0 = normal)
*/
CREATE OR REPLACE FUNCTION view_circuit(
token UUID,
token2desc regclass,
dbg int = 0)
RETURNS TEXT AS
'provsql','view_circuit' LANGUAGE C;
/**
* @brief Return a DOT visualisation of the d-DNNF compiled from the
* provenance circuit
*
* Runs the requested external knowledge compiler and renders the
* resulting d-DNNF as a GraphViz digraph.
*
* @param token root provenance token
* @param compiler external compiler or in-process meta-route to invoke;
* empty (the default) picks the highest-preference available compiler
*/
CREATE OR REPLACE FUNCTION compile_to_ddnnf_dot(
token UUID,
compiler TEXT = '')
RETURNS TEXT AS
'provsql','compile_to_ddnnf_dot' LANGUAGE C;
/**
* @brief Return the compiled d-DNNF of a provenance circuit in the
* c2d / d4 ".nnf" text interchange format.
*
* Companion to compile_to_ddnnf_dot (DOT, for viewing): this is the
* machine-readable form, suitable for feeding to an external d-DNNF
* reasoner / verifier or saving next to tseytin_cnf (same variable
* numbering). Accepts the same compiler / meta-route names.
*
* @param token root provenance token
* @param compiler compiler or in-process meta-route to use; empty (the
* default) picks the highest-preference available compiler
*/
CREATE OR REPLACE FUNCTION compile_to_ddnnf(
token UUID,
compiler TEXT = '')
RETURNS TEXT AS
'provsql','compile_to_ddnnf' LANGUAGE C;
/**
* @brief Structural statistics of the d-DNNF a compiler produces for a
* provenance circuit.
*
* Compiles the circuit with the given compiler / meta-route (same names
* as compile_to_ddnnf_dot: d4, d4v2, c2d, minic2d, dsharp, panini-*,
* tree-decomposition, interpret-as-dd, default) and returns a jsonb
* object: nodes, edges, and / or / not / inputs counts, smooth, depth
* (longest path), treewidth (null when not computable), and compile_ms.
* Lets clients compare what each compiler produces on the same circuit.
*
* @param token root provenance token
* @param compiler compiler or in-process meta-route to use; empty (the
* default) picks the highest-preference available compiler
*/
CREATE OR REPLACE FUNCTION ddnnf_stats(
token UUID,
compiler TEXT = '')
RETURNS jsonb AS
'provsql','ddnnf_stats' LANGUAGE C;
/**
* @brief Return the DIMACS CNF (Tseytin transformation) of the provenance circuit
*
* Returns the same encoding the extension writes to a temp file before
* invoking d4 / c2d / minic2d / dsharp. With @c weighted true (the
* default), per-input probability weights are appended as @c w lines.
*
* @param token root provenance token
* @param weighted include probability weights when true
* @param mapping prepend "c input " comment lines
* documenting which provenance input each variable stands for
*/
CREATE OR REPLACE FUNCTION tseytin_cnf(
token UUID,
weighted BOOLEAN = TRUE,
mapping BOOLEAN = TRUE)
RETURNS TEXT AS
'provsql','tseytin_cnf' LANGUAGE C;
/**
* @brief Map each DIMACS variable of tseytin_cnf back to its
* provenance input.
*
* Returns one row per input gate: the variable index (matching
* tseytin_cnf and compile_to_ddnnf's NNF), the original-circuit UUID
* of that input, and its probability. Lets a satisfying assignment or
* weighted model count obtained from an external tool be read against
* the provenance circuit.
*
* @param token root provenance token
*/
CREATE OR REPLACE FUNCTION tseytin_cnf_mapping_json(token UUID)
RETURNS jsonb AS
'provsql','tseytin_cnf_mapping_json' LANGUAGE C;
CREATE OR REPLACE FUNCTION tseytin_cnf_mapping(token UUID)
RETURNS TABLE(variable INT, gate UUID, probability FLOAT8) AS $$
SELECT variable, gate, probability
FROM jsonb_to_recordset(tseytin_cnf_mapping_json(token))
AS x(variable INT, gate UUID, probability FLOAT8)
ORDER BY variable
$$ LANGUAGE SQL STABLE;
/**
* @brief Return a DOT visualisation of the tree decomposition of the
* provenance circuit
*
* Computes the min-fill decomposition used by the in-process
* knowledge compiler. The first line of the output is a comment of
* the form @c "// treewidth=".
*
* @param token root provenance token
*/
CREATE OR REPLACE FUNCTION tree_decomposition_dot(
token UUID)
RETURNS TEXT AS
'provsql','tree_decomposition_dot' LANGUAGE C;
/**
* @brief Report whether an external tool is on the backend's resolved PATH
*
* Uses the same @c find_external_tool() helper that the compilers
* (d4 / c2d / minic2d / dsharp / panini), model counters (ganak /
* sharpsat-td / dpmc via htb+dmc / weightmc), and visualisation
* wrappers (graph-easy, dot) themselves consult, so the result
* reflects exactly what a subsequent @c probability_evaluate or
* @c view_circuit call would see, including the
* @c provsql.tool_search_path GUC prepended to @c $PATH.
*
* Names with a slash are treated as paths and tested directly via
* @c access(X_OK); bare names are resolved through @c /bin/sh's
* @c command -v under the backend's PATH.
*
* @param name bare executable (e.g. @c 'd4') or an absolute path
* @return true iff the tool resolves to an executable file
*/
CREATE OR REPLACE FUNCTION tool_available(name TEXT)
RETURNS BOOLEAN AS
'provsql','tool_available' LANGUAGE C STRICT;
/* ----------------------------------------------------------------------
* External-tool registry
*
* A catalog of the external tools ProvSQL can invoke (the knowledge
* compilers, weighted model counters, and the graph-easy DOT renderer).
* The default tools and their invocations are compiled in (seeded in C), so
* out-of-the-box behaviour is unchanged with no configuration.
*
* Administrators may add / repoint / reorder / disable tools at run time;
* those changes are persisted in the @c provsql.tool_overrides table below
* and overlaid on the compiled seed, so they survive across sessions and
* backends (and dump/restore). An empty overrides table means exactly the
* compiled defaults. The mutators are superuser-only because a tool record
* names an executable run as the PostgreSQL OS user (the same trust level as
* provsql.tool_search_path).
* ---------------------------------------------------------------------- */
/**
* @brief Persistent overrides overlaid on the compiled-in tool seed.
*
* Each row is the complete desired record for a tool (added or modified) keyed
* by logical @c name, or a tombstone (@c removed = true) hiding a seeded
* default. The effective registry is the compiled seed with tombstoned names
* removed and the remaining rows upserted over it. Written only by the
* superuser-only register_tool / unregister_tool / set_tool_* functions;
* read back into each backend's in-memory registry on demand. Marked as a
* configuration table so pg_dump carries an operator's registrations.
*/
CREATE TABLE IF NOT EXISTS tool_overrides(
name TEXT PRIMARY KEY,
removed BOOLEAN NOT NULL DEFAULT false,
kind TEXT,
executable TEXT,
operations TEXT[],
input_formats TEXT[],
output_format TEXT,
parser TEXT,
preference INT,
enabled BOOLEAN,
dependencies TEXT[],
argtpl TEXT,
argtpl_circuit TEXT,
endpoint TEXT
);
SELECT pg_catalog.pg_extension_config_dump('tool_overrides', '');
/**
* @brief Set-returning listing backing the @c provsql.tools view.
*
* @c operations / @c input_formats / @c output_format use the KCMCP
* shared-registry names (see the KCMCP server protocol), so a CLI record and
* a future kcmcp-server record are comparable; @c parser is the CLI-only tag
* for how to decode the tool's raw output. @c argtpl is the command template
* ({in}/{out}/... placeholders). @c available is true iff @c executable
* (when set) and every dependency currently resolve on the backend's PATH.
*/
CREATE OR REPLACE FUNCTION tool_registry_list()
RETURNS TABLE(name TEXT, kind TEXT, executable TEXT, operations TEXT[],
input_formats TEXT[], output_format TEXT, parser TEXT,
preference INT, enabled BOOLEAN, argtpl TEXT,
argtpl_circuit TEXT, endpoint TEXT, available BOOLEAN) AS
'provsql','tool_registry_list' LANGUAGE C STABLE;
/**
* @brief Read-only view of the registered tools.
*/
CREATE OR REPLACE VIEW tools AS
SELECT name, kind, executable, operations, input_formats, output_format,
parser, preference, enabled, argtpl, argtpl_circuit, endpoint,
available
FROM tool_registry_list();
/**
* @brief Register a tool, or replace the record with the same logical name.
*
* @param name logical id (e.g. @c 'd4-jm62300'); also the value
* @c provsql.fallback_compiler / the wmc tool selector use
* @param executable executable to resolve on PATH (defaults to @c name)
* @param kind @c 'cli' (spawn @c executable) or @c 'kcmcp' (talk to
* the KCMCP server at @c endpoint)
* @param operations capabilities (KCMCP names): @c 'compile' / @c 'wmc'
* (and ProvSQL-local @c 'render')
* @param input_formats accepted inputs (KCMCP names): @c 'dimacs-cnf',
* @c 'circuit-bcs12' (listing @c 'circuit-bcs12' enables
* the native-circuit fast path)
* @param output_format result encoding (KCMCP names): @c 'ddnnf-nnf',
* @c 'decimal', @c 'rational', ... (local @c 'panini-dd'
* / @c 'ascii' where KCMCP has no code)
* @param parser CLI-only decode tag: @c 'nnf' (the tolerant d4 / c2d
* NNF reader), @c 'panini-dd', @c 'wmc-line',
* @c 'weightmc', @c 'ascii'
* @param argtpl command template; placeholders @c {in} / @c {out}
* (and @c {binary} / @c {tmpdir} / @c {pivotAC}). When
* it omits @c {binary}, the executable is prepended.
* @param argtpl_circuit command used when the @c 'circuit-bcs12' input is
* selected (a BC-S1.2 circuit rather than a CNF); only a
* tool accepting that input needs it
* @param preference ordering within an operation (higher first)
* @param enabled whether the dispatchers may select it
* @param endpoint for a @c 'kcmcp' record, the server address:
* @c 'unix:/path' or @c 'host:port'
*
* Superuser-only: a CLI record runs an arbitrary command as the PostgreSQL
* OS user, and a kcmcp record names a socket the server connects to.
*/
CREATE OR REPLACE FUNCTION register_tool(
name TEXT,
executable TEXT DEFAULT NULL,
kind TEXT DEFAULT 'cli',
operations TEXT[] DEFAULT NULL,
input_formats TEXT[] DEFAULT NULL,
output_format TEXT DEFAULT NULL,
parser TEXT DEFAULT NULL,
argtpl TEXT DEFAULT NULL,
argtpl_circuit TEXT DEFAULT NULL,
preference INT DEFAULT 0,
enabled BOOLEAN DEFAULT true,
endpoint TEXT DEFAULT NULL)
RETURNS void AS
'provsql','tool_registry_register' LANGUAGE C;
/** @brief Unregister a tool; errors on an unknown tool name. Superuser-only. */
CREATE OR REPLACE FUNCTION unregister_tool(name TEXT)
RETURNS void AS
'provsql','tool_registry_unregister' LANGUAGE C STRICT;
/** @brief Enable/disable a tool; errors on an unknown tool name. Superuser-only. */
CREATE OR REPLACE FUNCTION set_tool_enabled(name TEXT, enabled BOOLEAN)
RETURNS void AS
'provsql','tool_registry_set_enabled' LANGUAGE C STRICT;
/** @brief Set a tool's preference; errors on an unknown tool name. Superuser-only. */
CREATE OR REPLACE FUNCTION set_tool_preference(name TEXT, preference INT)
RETURNS void AS
'provsql','tool_registry_set_preference' LANGUAGE C STRICT;
-- The mutators guard at the C level too, but revoke from PUBLIC so the
-- superuser requirement is visible in the catalog.
REVOKE ALL ON FUNCTION register_tool(TEXT, TEXT, TEXT, TEXT[], TEXT[], TEXT, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION unregister_tool(TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION set_tool_enabled(TEXT, BOOLEAN) FROM PUBLIC;
REVOKE ALL ON FUNCTION set_tool_preference(TEXT, INT) FROM PUBLIC;
/**
* @brief Return an XML representation of the provenance circuit
*
* @param token root provenance token
* @param token2desc optional mapping table for gate descriptions
*/
CREATE OR REPLACE FUNCTION to_provxml(
token UUID,
token2desc regclass = NULL)
RETURNS TEXT AS
'provsql','to_provxml' LANGUAGE C;
/** @brief Return the provenance token of the current query result tuple */
CREATE OR REPLACE FUNCTION provenance() RETURNS UUID AS
'provsql', 'provenance' LANGUAGE C;
/**
* @brief Compute where-provenance for a result tuple
*
* Returns a text representation showing which input columns
* contributed to each output column.
*/
CREATE OR REPLACE FUNCTION where_provenance(token UUID)
RETURNS text AS
'provsql','where_provenance' LANGUAGE C;
/** @} */
/** @defgroup circuit_init Circuit initialization
* Functions and statements executed at extension load time to
* reset internal caches and create the constant zero/one gates.
* @{
*/
/** @brief Reset the internal cache of OID constants used by the query rewriter */
CREATE OR REPLACE FUNCTION reset_constants_cache()
RETURNS void AS
'provsql', 'reset_constants_cache' LANGUAGE C;
SELECT reset_constants_cache();
SELECT create_gate(gate_zero(), 'zero');
SELECT create_gate(gate_one(), 'one');
SELECT create_gate(gate_null(), 'value', NULL, NULL, NULL, 'NULL');
/** @} */
/** @brief Types of update operations tracked for temporal provenance */
CREATE TYPE query_type_enum AS ENUM (
'INSERT', -- Row was inserted
'DELETE', -- Row was deleted
'UPDATE', -- Row was updated
'UNDO', -- Previous operation was undone
'TRANSACTION', -- The transaction the statements below belong to
'REPLACE' -- An update gate given a different probability
);
/** @defgroup compiled_semirings Compiled semirings
* Definitions of compiled semirings
* @{
*/
/** @brief Evaluate provenance as a symbolic formula (e.g., "a ⊗ b ⊕ c") */
-- The mapping is optional (as for sr_boolexpr): formula renders whatever
-- circuit it is given, and the measure-carrier circuits it is most useful
-- on (random variables, arithmetic, mixtures) have no leaf mapping at all.
-- Without one, input leaves render as the semiring's 𝟙.
CREATE FUNCTION sr_formula(token ANYELEMENT, token2value regclass = NULL)
RETURNS VARCHAR AS
$$
BEGIN
IF token IS NULL THEN
RETURN NULL;
END IF;
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'formula',
'𝟙'::VARCHAR
);
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the counting semiring (ℕ) */
CREATE FUNCTION sr_counting(token ANYELEMENT, token2value regclass)
RETURNS INT AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'counting',
1
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as why-provenance (set of witness sets) */
CREATE FUNCTION sr_why(token ANYELEMENT, token2value regclass)
RETURNS VARCHAR AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'why',
'{}'::VARCHAR
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as how-provenance (canonical polynomial provenance ℕ[X], universal commutative-semiring provenance) */
CREATE FUNCTION sr_how(token ANYELEMENT, token2value regclass)
RETURNS VARCHAR AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'how',
'{}'::VARCHAR
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as which-provenance (lineage: a single set of contributing labels) */
CREATE FUNCTION sr_which(token ANYELEMENT, token2value regclass)
RETURNS VARCHAR AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'which',
'{}'::VARCHAR
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as a Boolean expression
*
* The optional @p token2value mapping labels the leaves of the
* formula: when omitted, leaves are rendered as bare @c x@
* placeholders.
*/
CREATE FUNCTION sr_boolexpr(token ANYELEMENT, token2value regclass = NULL)
RETURNS VARCHAR AS
$$
BEGIN
IF token IS NULL THEN
RETURN NULL;
END IF;
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'boolexpr',
'⊤'::VARCHAR
);
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the Boolean semiring (true/false)
*
* The optional @p token2value mapping gives the Boolean value of the
* leaves; a leaf it does not map, and every leaf when it is omitted, is
* true. Without a mapping, the result is whether the token holds in the
* database as it is, every input tuple present.
*/
CREATE FUNCTION sr_boolean(token ANYELEMENT, token2value regclass = NULL)
RETURNS BOOLEAN AS
$$
BEGIN
IF token IS NULL THEN
RETURN NULL;
END IF;
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'boolean',
TRUE
);
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/**
* @brief Whether a token holds in the database as it is (internal)
*
* @c sr_boolean(token) without a mapping, with a fast path for leaves. The
* rewriter filters by it the rows a displayed aggregate value reads, so that
* the value is the one plain SQL computes on the same data.
*/
CREATE FUNCTION plain_truth(token uuid)
RETURNS boolean AS
'provsql', 'plain_truth' LANGUAGE C PARALLEL SAFE STABLE;
/** @brief Structural universal-zero test (C backend of nonzero's default mode) */
CREATE FUNCTION true_nonzero(token uuid)
RETURNS boolean AS
'provsql', 'true_nonzero' LANGUAGE C PARALLEL SAFE STABLE;
/**
* @brief Test whether a provenance annotation is nonzero.
*
* Returns false only on a *proof* that the annotation is zero; true
* otherwise, so filtering with WHERE nonzero(provenance()) never
* discards a row whose annotation could be nonzero.
*
* The default mode (@p semiring NULL) tests *universal* zero-ness: zero in
* every (m-)semiring under every leaf valuation, decided by sound
* structural rules (zero propagation through the gates; a comparison gate
* whose satisfying-world set is empty). Filtering on it can never
* contradict any downstream semiring evaluation.
*
* A named @p semiring evaluates the circuit there and tests against that
* semiring's zero: 'boolean' is presence in the vanilla SQL answer on this
* instance (the mode that filters, e.g., the null-padded arm of a
* difference), 'counting' is bag multiplicity. An absent @p mapping reads
* every leaf as the semiring's one (true / 1); with a mapping, leaves take
* their mapped values.
*
* A NULL @p token reads as the neutral 1 (an untracked row): true.
*
* @param token provenance token to test
* @param semiring NULL (universal zero test), 'boolean', or 'counting'
* @param mapping optional mapping table from tokens to leaf values
*/
CREATE FUNCTION nonzero(token uuid,
semiring text DEFAULT NULL,
mapping regclass DEFAULT NULL)
RETURNS boolean AS
$$
BEGIN
IF token IS NULL THEN
RETURN true;
END IF;
IF semiring IS NULL THEN
RETURN provsql.true_nonzero(token);
ELSIF semiring = 'boolean' THEN
IF mapping IS NULL THEN
/* Every leaf true: that truth is plain_truth, which walks the gates
* whose truth follows from their children's and reads a comparison of
* aggregate results off the values they record. Reading such a
* comparison over the worlds of what it aggregates, as the evaluation
* below does, costs one term per subset of its contributions. */
RETURN provsql.plain_truth(token);
END IF;
RETURN provsql.provenance_evaluate_compiled(token, mapping, 'boolean', TRUE);
ELSIF semiring = 'counting' THEN
RETURN provsql.provenance_evaluate_compiled(token, mapping, 'counting', 1) <> 0;
ELSE
RAISE EXCEPTION 'nonzero: unsupported semiring "%" (supported: boolean, counting; NULL for the universal zero test)', semiring
USING ERRCODE = 'feature_not_supported',
DETAIL = 'provsql-reason: nonzero-semiring; scope: gap';
END IF;
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/**
* @brief Presence in the vanilla SQL answer on this instance.
*
* WHERE present(provenance()) restores the result set the query
* has without provenance tracking, filtering the zero-annotated extras
* (antijoin arms, failed HAVING groups, unknown comparisons) that the
* rewriting keeps visible.
*
* Shorthand for nonzero(token, 'boolean') with every leaf true,
* which reads that truth off the gates (@c plain_truth).
*/
CREATE FUNCTION present(token uuid)
RETURNS boolean AS
$$
SELECT provsql.nonzero(token, 'boolean');
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the tropical (min-plus) m-semiring
*
* Inputs are read as %float8 cost values; the additive identity
* is 'Infinity'::%float8 and the multiplicative identity is 0.
* Returns the cost of the cheapest derivation.
*
* With @p nonnegative, input costs are checked nonnegative and the
* semiring is *absorptive*: evaluation then also accepts circuits
* carrying the @c 'absorptive' assumption marker -- notably the
* reachability circuits of the bounded-treewidth route, giving exact
* min-cost reachability on cyclic data.
*/
CREATE FUNCTION sr_tropical(token ANYELEMENT, token2value regclass,
nonnegative BOOLEAN = false)
RETURNS FLOAT AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
CASE WHEN nonnegative THEN 'tropical_nonneg' ELSE 'tropical' END,
0::FLOAT
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as a regular expression over the input labels
*
* Inputs are read as labels (an empty label is the empty word); a
* derivation is the word of its labels, in the order of the query (that
* of the @c FROM clause for a join), and a tuple's value is the language
* of its derivations: @c | for union, @c · for concatenation, @c * for
* the Kleene star, @c ε and @c ∅ for the empty word and language.
*
* Concatenation does not commute. A recursion on cyclic data has the
* regular expression of its derivations as exact value, provided every
* recursive @c FROM clause has the recursive relation at the same place
* (first, or last); otherwise it is refused.
*/
CREATE FUNCTION sr_regex(token ANYELEMENT, token2value regclass)
RETURNS VARCHAR AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'regex',
''::VARCHAR
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief C implementation of @c sr_tropical_paths */
CREATE FUNCTION provenance_evaluate_tropical_paths(
token UUID, costs regclass, labels regclass,
OUT cost FLOAT, OUT paths TEXT)
AS 'provsql', 'provenance_evaluate_tropical_paths'
LANGUAGE C PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as the least cost of a derivation, with the
* derivations of that cost
*
* Inputs are read as %float8 costs from @p costs (0 if absent) and as
* labels from @p labels (the empty word if absent). Returns the least
* cost, as @c sr_tropical over nonnegative costs, and the derivations of
* that cost as a regular expression over the labels, as @c sr_regex: on a
* path query, the shortest paths. A cycle of cost zero can be followed any
* number of times on a shortest path, and appears under a star; one of
* negative cost is refused.
*/
CREATE FUNCTION sr_tropical_paths(token ANYELEMENT, costs regclass,
labels regclass,
OUT cost FLOAT, OUT paths TEXT)
AS
$$
SELECT * FROM provsql.provenance_evaluate_tropical_paths(token, costs, labels);
$$ LANGUAGE sql STRICT PARALLEL SAFE STABLE;
/** @brief C implementation of @c sr_best_path */
CREATE FUNCTION provenance_evaluate_best_path(
token UUID, costs regclass, labels regclass,
OUT cost FLOAT, OUT path TEXT[])
AS 'provsql', 'provenance_evaluate_best_path'
LANGUAGE C PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as one least-cost derivation, its cost and
* its labels
*
* Inputs are read as nonnegative %float8 costs from @p costs (0 if absent)
* and as labels from @p labels (no label if absent). Returns the least
* cost, as @c sr_tropical over nonnegative costs, and the labels of one
* derivation of that cost, in the order of the query: on a path query, a
* shortest path. Among the derivations of least cost, the one returned has
* the fewest labels, and comes first in lexicographic order of its labels
* among those. With no derivation, the cost is infinite and the path
* @c NULL.
*/
CREATE FUNCTION sr_best_path(token ANYELEMENT, costs regclass,
labels regclass,
OUT cost FLOAT, OUT path TEXT[])
AS
$$
SELECT * FROM provsql.provenance_evaluate_best_path(token, costs, labels);
$$ LANGUAGE sql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the Viterbi (max-times) m-semiring
*
* Inputs are read as %float8 probability values in @f$[0,1]@f$.
* Returns the probability of the most likely derivation.
*/
CREATE FUNCTION sr_viterbi(token ANYELEMENT, token2value regclass)
RETURNS FLOAT AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'viterbi',
1::FLOAT
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the Łukasiewicz fuzzy m-semiring
*
* Inputs are read as %float8 graded-truth values in @f$[0,1]@f$.
* Addition is @f$\max@f$; multiplication is the Łukasiewicz t-norm
* @f$\max(a + b - 1, 0)@f$, which preserves crisp truth and avoids
* the near-zero collapse of long product chains.
*/
CREATE FUNCTION sr_lukasiewicz(token ANYELEMENT, token2value regclass)
RETURNS FLOAT AS
$$
BEGIN
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'lukasiewicz',
1::FLOAT
);
END
$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the min-max m-semiring on a user enum
*
* Inputs are read as values of a user-defined enum carrier; addition
* is enum-min, multiplication is enum-max. Bottom and top of the enum
* are derived from @c pg_enum.enumsortorder. The third argument is a
* value of the carrier enum, used only for type inference; its value is
* ignored, so @c NULL::type is enough.
*
* The security shape: alternative derivations combine to the least
* sensitive label, joins combine to the most sensitive label.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to enum values.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
*/
CREATE FUNCTION sr_minmax(token UUID, token2value regclass, element_one ANYENUM)
RETURNS ANYENUM AS
$$
BEGIN
IF token IS NULL OR token2value IS NULL THEN
RETURN NULL;
END IF;
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'minmax',
element_one
);
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the max-min m-semiring on a user enum
*
* Dual of :sqlfunc:`sr_minmax`: addition is enum-max, multiplication
* is enum-min. The fuzzy / availability / trust shape: alternatives
* combine to the most permissive label, joins combine to the strictest
* label. The third argument is a value of the carrier enum, used only
* for type inference; its value is ignored, so @c NULL::type is enough.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to enum values.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
*/
CREATE FUNCTION sr_maxmin(token UUID, token2value regclass, element_one ANYENUM)
RETURNS ANYENUM AS
$$
BEGIN
IF token IS NULL OR token2value IS NULL THEN
RETURN NULL;
END IF;
RETURN provsql.provenance_evaluate_compiled(
token,
token2value,
'maxmin',
element_one
);
END
$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over a subset semiring, as a bitmask (internal)
*
* Bit @f$i@f$ stands for the label of rank @f$i@f$ of the enum type of
* @p element_one; bit 63 for the unrestricted element of @c consent.
* @p semiring is @c subset, @c clearance or @c consent.
*/
CREATE OR REPLACE FUNCTION subset_evaluate(
token UUID, token2value regclass, semiring TEXT, element_one ANYENUM)
RETURNS bigint AS
'provsql', 'provenance_evaluate_compiled' LANGUAGE C PARALLEL SAFE STABLE;
/** @brief The labels of the enum type of @p element_one a bitmask holds (internal) */
CREATE OR REPLACE FUNCTION subset_labels(mask bigint, element_one ANYENUM)
RETURNS ANYARRAY AS
'provsql', 'subset_labels' LANGUAGE C IMMUTABLE PARALLEL SAFE;
/** @brief The settling level of a bitmask of clearance levels (internal) */
CREATE OR REPLACE FUNCTION subset_settling(mask bigint, element_one ANYENUM)
RETURNS ANYENUM AS
'provsql', 'subset_settling' LANGUAGE C IMMUTABLE PARALLEL SAFE;
/** @brief Membership of a label, given as text, in a subset annotation (internal)
*
* The enum type is that of the @c value column of @p token2value.
*/
CREATE OR REPLACE FUNCTION subset_member(
token UUID, token2value regclass, semiring TEXT, label TEXT)
RETURNS boolean AS
'provsql', 'subset_member' LANGUAGE C PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over the subsets of the labels of a user enum
*
* The powerset Boolean algebra: alternatives combine by union, joins by
* intersection, negation (@c EXCEPT, @c NOT @c EXISTS) by difference.
* A mapping value is a set of labels (an array of the enum), or a single
* label standing for itself. The result is the set of labels whose world
* contains the tuple. The third argument is a value of the enum, used
* only for type inference (@c NULL::type is enough). At most 63 labels.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to labels or arrays of labels.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
*/
CREATE OR REPLACE FUNCTION sr_subset(
token UUID, token2value regclass, element_one ANYENUM)
RETURNS ANYARRAY AS
$$
SELECT provsql.subset_labels(
provsql.subset_evaluate(token, token2value, 'subset', element_one),
element_one)
WHERE token IS NOT NULL AND token2value IS NOT NULL;
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance as the set of clearance levels a tuple is visible at
*
* The labels of the enum are clearance levels, in increasing order. A
* single level in the mapping makes a tuple visible at that level and every
* one above it; an array is an arbitrary set of levels (compartments). The
* result is the set of levels at which the query, run over the tuples
* visible at that level, returns the tuple: unlike :sqlfunc:`sr_minmax`,
* negation is exact, so a tuple can be visible at a level and not at a
* higher one.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to levels or arrays of levels.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
*/
CREATE OR REPLACE FUNCTION sr_clearance(
token UUID, token2value regclass, element_one ANYENUM)
RETURNS ANYARRAY AS
$$
SELECT provsql.subset_labels(
provsql.subset_evaluate(token, token2value, 'clearance', element_one),
element_one)
WHERE token IS NOT NULL AND token2value IS NOT NULL;
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @brief Whether a tuple is visible at a clearance level
*
* Membership of @p level in :sqlfunc:`sr_clearance`; the level is given as
* text, of the enum type of the @c value column of the mapping.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to levels or arrays of levels.
* @param level The clearance level.
*/
CREATE OR REPLACE FUNCTION visible_at(
token UUID, token2value regclass, level TEXT)
RETURNS boolean AS
$$
SELECT provsql.subset_member(token, token2value, 'clearance', level);
$$ LANGUAGE sql STRICT PARALLEL SAFE STABLE;
/** @brief The level from which the visibility of a tuple no longer changes
*
* The lowest clearance level @c settles_at such that the tuple is visible
* at every level from it to the top, or at none of them; @c visible_at_top
* says which. Meaningful for hierarchical levels.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to levels or arrays of levels.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
* @param settles_at The settling level.
* @param visible_at_top Whether the tuple is visible at the top level.
*/
CREATE OR REPLACE FUNCTION clearance_settling(
token UUID, token2value regclass, element_one ANYENUM,
OUT settles_at ANYENUM, OUT visible_at_top boolean) AS
$$
SELECT provsql.subset_settling(m, element_one),
enum_last(element_one) = ANY(provsql.subset_labels(m, element_one))
FROM provsql.subset_evaluate(token, token2value, 'clearance', element_one) m
WHERE token IS NOT NULL AND token2value IS NOT NULL;
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @brief Evaluate provenance over consented purposes
*
* The labels of the enum are purposes; the mapping gives the purposes each
* tuple is consented for (a purpose or an array of purposes). The result
* gives the purposes for which the query, run over the tuples consented for
* that purpose, returns the tuple, and whether the query over the whole
* database (@c unrestricted) returns it.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to purposes or arrays of purposes.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
* @param purposes The purposes for which the query returns the tuple.
* @param unrestricted Whether the query over the whole database returns it.
*/
CREATE OR REPLACE FUNCTION sr_consent(
token UUID, token2value regclass, element_one ANYENUM,
OUT purposes ANYARRAY, OUT unrestricted boolean) AS
$$
SELECT provsql.subset_labels(m, element_one), m < 0
FROM provsql.subset_evaluate(token, token2value, 'consent', element_one) m
WHERE token IS NOT NULL AND token2value IS NOT NULL;
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @brief Whether a tuple may be used for a purpose
*
* True when the query returns the tuple both over the whole database and
* over the tuples consented for @p purpose, given as text, of the enum type
* of the @c value column of the mapping.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to purposes or arrays of purposes.
* @param purpose The purpose.
*/
CREATE OR REPLACE FUNCTION consented_for(
token UUID, token2value regclass, purpose TEXT)
RETURNS boolean AS
$$
SELECT provsql.subset_member(token, token2value, 'consent', purpose);
$$ LANGUAGE sql STRICT PARALLEL SAFE STABLE;
/** @brief The purposes a tuple may be used for
*
* The purposes for which :sqlfunc:`consented_for` holds: empty when the
* query over the whole database does not return the tuple.
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to purposes or arrays of purposes.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
*/
CREATE OR REPLACE FUNCTION consent_purposes(
token UUID, token2value regclass, element_one ANYENUM)
RETURNS ANYARRAY AS
$$
SELECT CASE WHEN m < 0 THEN provsql.subset_labels(m, element_one)
ELSE provsql.subset_labels(0, element_one) END
FROM provsql.subset_evaluate(token, token2value, 'consent', element_one) m
WHERE token IS NOT NULL AND token2value IS NOT NULL;
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @brief The purposes for which consent changes the answer
*
* The purposes whose consented tuples give a different answer about the
* tuple than the whole database: the tuple is returned for the purpose but
* not over the whole database (only possible through a negation reading a
* tuple the purpose was not consented for), or the reverse (the purpose
* lacks consent for a tuple the answer needs).
*
* @param token Provenance token to evaluate.
* @param token2value Mapping from input gates to purposes or arrays of purposes.
* @param element_one A value of the carrier enum, used only for its type
* (@c NULL::type is enough).
*/
CREATE OR REPLACE FUNCTION consent_conflicts(
token UUID, token2value regclass, element_one ANYENUM)
RETURNS ANYARRAY AS
$$
SELECT CASE WHEN m < 0 THEN provsql.subset_labels(~m, element_one)
ELSE provsql.subset_labels(m, element_one) END
FROM provsql.subset_evaluate(token, token2value, 'consent', element_one) m
WHERE token IS NOT NULL AND token2value IS NOT NULL;
$$ LANGUAGE sql PARALLEL SAFE STABLE;
/** @} */
/** @defgroup choose_aggregate choose aggregate
* Choose one value among many, used in particular to code a mutually
* exclusive choice as an aggregate.
* @{
*/
/** @brief Transition function for the choose aggregate (keeps first non-NULL value) */
CREATE FUNCTION choose_function(state ANYELEMENT, data ANYELEMENT)
RETURNS ANYELEMENT AS
$$
BEGIN
IF state IS NULL THEN
RETURN data;
ELSE
RETURN state;
END IF;
END
$$ LANGUAGE plpgsql PARALLEL SAFE IMMUTABLE;
/** @brief Aggregate that returns an arbitrary non-NULL value from a group */
CREATE AGGREGATE choose(ANYELEMENT) (
SFUNC = choose_function,
STYPE = ANYELEMENT
);
/** @brief Transition function of @c array_collect */
CREATE FUNCTION array_collect_step(state ANYARRAY, data ANYNONARRAY)
RETURNS ANYARRAY AS
$$ SELECT array_append(state, data) $$
LANGUAGE sql PARALLEL SAFE IMMUTABLE;
/** @brief @c array_agg, but the empty array over no rows, not NULL
*
* This is the value of @c ARRAY(SELECT ...): ProvSQL rewrites such a
* subquery into this aggregate over the matching rows. */
CREATE AGGREGATE array_collect(ANYNONARRAY) (
SFUNC = array_collect_step,
STYPE = ANYARRAY,
INITCOND = '{}'
);
/** @brief Explodes a table column containing aggregated provenance into multiple rows.
*
* For each row in the input table, this function unnests the children of the
* specified aggregate token column and produces one output row per child.
* It reconstructs the corresponding value and provenance (`provsql`) for
* each resulting row.
*
* The original table is replaced by the transformed table.
*
* @param _tbl Name of the table to transform.
* @param agg_token Name of the column containing the aggregate to explode.
*/
CREATE OR REPLACE FUNCTION explode_table(_tbl text, agg_token text)
RETURNS void AS $$
DECLARE
_nsp text;
BEGIN
-- Resolve the schema actually holding _tbl so the rebuilt table is
-- recreated in place (the provsql helper functions are schema-qualified
-- so this works whatever the caller's search_path is).
SELECT n.nspname INTO _nsp
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.oid = _tbl::regclass;
EXECUTE format('
CREATE TABLE %1$I.temp_exploded AS
SELECT
%2$I.*,
provsql.get_extra(children[2]) AS new_t,
provsql.provenance_times(children[1], provsql) AS new_provsql
FROM %1$I.%2$I,
LATERAL (
SELECT provsql.get_children(sm) AS children
FROM UNNEST(provsql.agg_token_explode_children(%3$I)) AS sm
) AS sub', _nsp, _tbl, agg_token);
EXECUTE format('DROP TABLE %I.%I', _nsp, _tbl);
EXECUTE format('ALTER TABLE %I.temp_exploded DROP COLUMN %I, DROP COLUMN provsql', _nsp, agg_token);
EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_t TO %I', _nsp, agg_token);
EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_provsql TO provsql', _nsp);
EXECUTE format('ALTER TABLE %I.temp_exploded RENAME TO %I', _nsp, _tbl);
END;
$$ LANGUAGE plpgsql;
/** @} */
/**
* @brief The build of the ProvSQL library loaded, as text
*
* The commit the library was built from, as @c "git describe --tags --always
* --dirty" gives it (e.g., @c v1.12.0-431-g577cc730), or @c unknown for a
* build outside a git checkout. The extension's version is the same for
* every build of a development cycle; this tells them apart, for a bug report
* or a record of which build produced a result.
*/
CREATE OR REPLACE FUNCTION build_id()
RETURNS text
AS 'provsql','build_id' LANGUAGE C STABLE STRICT PARALLEL SAFE;
/**
* @brief Append @c provsql to this database's default search_path, if missing.
*
* ProvSQL's operators and functions live in the @c provsql schema and
* are resolved through @c search_path. When @c provsql is absent from
* the path some surfaces fail with a clear error (RV/agg_token
* arithmetic), but others can be silently misrouted by an implicit
* cross-domain cast. This helper makes the common case painless: it
* reads the current database-level search_path setting from
* @c pg_db_role_setting, appends @c provsql if not already present
* (never replacing or reordering the existing entries), and applies the
* result with @c ALTER @c DATABASE. It is idempotent and emits a
* @c NOTICE describing what it did.
*
* Only @b new sessions pick up the change; the calling session keeps its
* current path. Role-level settings (if any) take precedence over the
* database-level setting and are left untouched. The caller must be the
* database owner or a superuser (the privilege model of @c ALTER
* @c DATABASE). Returns the resulting search_path value.
*/
CREATE OR REPLACE FUNCTION setup_search_path()
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
db text := current_database();
cfg text[];
cur text; -- existing database-level search_path value
new_path text;
BEGIN
-- setrole = 0 selects the database-wide default, not a per-role override.
SELECT s.setconfig INTO cfg
FROM pg_db_role_setting s
JOIN pg_database d ON d.oid = s.setdatabase
WHERE d.datname = db AND s.setrole = 0;
IF cfg IS NOT NULL THEN
SELECT substr(e, length('search_path=') + 1) INTO cur
FROM unnest(cfg) AS e
WHERE e LIKE 'search_path=%';
END IF;
IF cur IS NULL THEN
-- No database-level search_path at all: install the documented
-- default with provsql appended.
new_path := '"$user", public, provsql';
EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
RAISE NOTICE 'ProvSQL: set search_path = % for database "%" (no previous database-level setting). Only new sessions are affected.',
new_path, db;
RETURN new_path;
END IF;
-- Already contains provsql as a path element? Idempotent no-op.
IF EXISTS (
SELECT 1 FROM unnest(string_to_array(cur, ',')) AS p
WHERE btrim(btrim(p), '"') = 'provsql')
THEN
RAISE NOTICE 'ProvSQL: search_path for database "%" already contains provsql (= %); no change.',
db, cur;
RETURN cur;
END IF;
new_path := cur || ', provsql';
EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
RAISE NOTICE 'ProvSQL: appended provsql to search_path for database "%" (now: %). Only new sessions are affected.',
db, new_path;
RETURN new_path;
END;
$$;
GRANT USAGE ON SCHEMA provsql TO PUBLIC;
SET search_path TO public;
-- Installation-time advisory: if provsql is not in the database's default
-- search_path, point the user at setup_search_path(). reset_val reflects
-- the configured session default (postgresql.conf / ALTER DATABASE / ALTER
-- ROLE), unaffected by the SET search_path statements this script ran.
-- CREATE EXTENSION raises client_min_messages to WARNING for the duration
-- of the script, so we lower it around the RAISE NOTICE. SET LOCAL only:
-- it unwinds by itself when CREATE EXTENSION's transaction ends. An
-- explicit save/restore here would capture the WARNING clamp (already in
-- force when this block runs) and restore *that* at session level,
-- leaving the whole installing session with NOTICEs suppressed.
DO $$
DECLARE
rp text;
has_provsql boolean;
BEGIN
SELECT reset_val INTO rp FROM pg_settings WHERE name = 'search_path';
SELECT bool_or(btrim(btrim(p), '"') = 'provsql')
INTO has_provsql
FROM unnest(string_to_array(coalesce(rp, ''), ',')) AS p;
IF NOT coalesce(has_provsql, false) THEN
SET LOCAL client_min_messages = notice;
RAISE NOTICE 'ProvSQL: schema "provsql" is not in your default search_path (currently: %).', rp;
RAISE NOTICE 'ProvSQL operators and functions are resolved through search_path. Run "SELECT provsql.setup_search_path();" to add it, or set it manually (e.g. ALTER DATABASE % SET search_path = "$user", public, provsql).', quote_ident(current_database());
END IF;
END;
$$;
-- Final constants-cache refresh. The planned SELECT statements earlier in
-- this script (reset_constants_cache itself, the zero/one create_gate calls)
-- make the installing session memoize the OID constants *mid-script*, while
-- objects defined later (notably the choose aggregate, used by the
-- scalar-subquery decorrelation) do not exist yet. Their optional lookups
-- then stay InvalidOid for the rest of the session, silently disabling the
-- corresponding rewrites (e.g. IN/NOT IN over a tracked relation would raise
-- "Subqueries ... not supported") until a new connection. Refreshing here,
-- after every object exists, repairs the installing session's cache.
SELECT provsql.reset_constants_cache();