ProvSQL SQL API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
provsql.sql
Go to the documentation of this file.
1/**
2 * @file
3 * @brief ProvSQL PL/pgSQL extension code
4 *
5 * This file contains the PL/pgSQL code of the ProvSQL extension. This
6 * extension requires the standard UUID-ossp extension.
7 */
8
9/**
10 * @brief <tt>provsql</tt> schema
11 *
12 * All types and functions introduced by ProvSQL are defined in the
13 * provsql schema, requiring prefixing them by <tt>provsql.</tt> or
14 * using PostgreSQL's <tt>search_path</tt> variable with a command such
15 * as \code{.sql}SET search_path TO public, provsql;\endcode
16 */
17CREATE SCHEMA provsql;
18
19SET search_path TO provsql;
20
21/**
22 * @brief Provenance circuit gate types
23 *
24 * Each gate in the provenance circuit has a type that determines
25 * its semantics during semiring evaluation.
26 */
27CREATE TYPE PROVENANCE_GATE AS
28 ENUM(
29 'input', -- Input (variable) gate of the circuit
30 'plus', -- Semiring plus
31 'times', -- Semiring times
32 'monus', -- M-Semiring monus
33 'project', -- Project gate (for where provenance)
34 'zero', -- Semiring zero
35 'one', -- Semiring one
36 'eq', -- Equijoin gate (for where provenance)
37 'agg', -- Aggregation operator (for aggregate provenance)
38 'semimod', -- Semimodule scalar multiplication (for aggregate provenance)
39 'cmp', -- Comparison of aggregate values (HAVING-clause provenance)
40 'delta', -- δ-semiring operator (see Amsterdamer, Deutch, Tannen, PODS 2011)
41 'value', -- Scalar value (for aggregate provenance)
42 'mulinput',-- Multivalued input (for Boolean provenance)
43 'update', -- Update operation
44 'rv', -- Continuous random-variable leaf
45 'arith', -- n-ary arithmetic gate over scalar-valued children
46 'mixture', -- Probabilistic mixture of two scalar RV roots with a Bernoulli weight
47 'assumed', -- Structural assumption marker over a single child: the
48 -- wrapped sub-circuit was computed under the
49 -- assumption named by the gate's extra label --
50 -- 'BOOLEAN' (e.g. the safe-query rewrite; the
51 -- default when the label is absent) or
52 -- 'absorptive' (cyclic recursion truncated at the
53 -- absorptive value fixpoint). Transparent for
54 -- evaluation semirings satisfying the assumption,
55 -- fatal error for the rest, rendered as an
56 -- explicit element in PROV-XML export.
57 'annotation', -- Transparent single-child wrapper carrying a
58 -- query-level annotation string in @c extra
59 -- (e.g. the inversion-free tractability
60 -- certificate / per-input order key). Identity
61 -- for EVERY evaluator; its UUID folds in @c extra
62 -- so distinct annotations over the same child are
63 -- distinct gates.
64 'conditioned', -- Conditioning marker: two children
65 -- [target, evidence]. Evaluated only in the
66 -- measure interpretation: probability_evaluate
67 -- returns P(target ∧ evidence) / P(evidence); the
68 -- RV / AGG_TOKEN evaluators return the restricted
69 -- distribution. For the UUID carrier it is a
70 -- TERMINAL gate (never a child of a semiring gate);
71 -- nested conditioning folds into a conjunction of
72 -- evidence. Refused by every general sr_* semiring
73 -- (normalization is not a semiring operation).
74 'mobius', -- Signed Möbius combination over child islands: one
75 -- INTEGER coefficient per child in @c extra (the
76 -- gate_arith precedent), probability_evaluate returns
77 -- Σ_i coeff_i · P(child_i). The one new primitive of
78 -- the safe-UCQ Möbius-inversion route, evaluated only
79 -- in the measure interpretation; refused by every
80 -- general sr_* semiring (a signed combination is not a
81 -- semiring operation).
82 'case', -- N-ary guarded selection over scalar (RV) children:
83 -- wires [guard_1, value_1, ..., guard_k, value_k,
84 -- default], first-match semantics (the value of the
85 -- first guard event that holds, else the default).
86 -- Backs a CASE expression over random variables (and
87 -- abs / clamp / ReLU as sugar). RV/measure-carrier;
88 -- refused by every general sr_* semiring.
89 'observe' -- Latent-variable observation (likelihood-weighting
90 -- evidence): one wire -> an observed bare gate_rv
91 -- leaf, the datum in extra. Contributes a
92 -- continuous density factor (the leaf's pdf at the
93 -- datum) rather than a Boolean truth value,
94 -- composing into an evidence circuit by gate_times
95 -- exactly like a conditioning event. Evaluated only
96 -- by the importance-sampling weight walk; refused by
97 -- every Boolean / semiring evaluator.
98 );
99
100/** @defgroup gate_manipulation Circuit gate manipulation
101 * Low-level functions for creating and querying provenance circuit gates.
102 * @{
103 */
104
105/**
106 * @brief Create a new gate in the provenance circuit
107 *
108 * @param token UUID identifying the new gate
109 * @param type gate type (see PROVENANCE_GATE)
110 * @param children optional array of child gate UUIDs
111 */
112CREATE OR REPLACE FUNCTION create_gate(
113 token UUID,
114 type PROVENANCE_GATE,
115 children UUID[] DEFAULT NULL)
116 RETURNS VOID AS
117 'provsql','create_gate' LANGUAGE C PARALLEL SAFE;
118/**
119 * @brief Return the gate type of a provenance token
120 *
121 * Returns @c 'input' for any token not yet materialized in the circuit,
122 * since input is the default semantics of an unmaterialized provenance token.
123 */
124CREATE OR REPLACE FUNCTION get_gate_type(
125 token UUID)
126 RETURNS PROVENANCE_GATE AS
127 'provsql','get_gate_type' LANGUAGE C IMMUTABLE PARALLEL SAFE;
128/** @brief Return the children of a provenance gate */
129CREATE OR REPLACE FUNCTION get_children(
130 token UUID)
131 RETURNS UUID[] AS
132 'provsql','get_children' LANGUAGE C IMMUTABLE PARALLEL SAFE;
133/**
134 * @brief Write the probability of an input gate, once
135 *
136 * A probability is a fact appended to the circuit, like the gate it
137 * belongs to: it can be written on a gate that has none, written again
138 * with the identical value (so setup scripts and notebook cells stay
139 * re-runnable), and is otherwise refused. A write made by a
140 * transaction that rolls back is cleared, leaving the circuit as the
141 * transaction found it.
142 *
143 * To give a tuple a *different* probability, mint a fresh input gate
144 * with @c provsql.replace_input and store it in the row's @c provsql
145 * column. The base table's token column is the place of truth, the
146 * same rule the data-modification triggers already follow, and the
147 * circuit keeps the old gate and everything derived from it.
148 *
149 * @param token UUID of the input gate
150 * @param p probability value in [0,1]
151 */
152CREATE OR REPLACE FUNCTION set_prob(
153 token UUID, p DOUBLE PRECISION)
154 RETURNS VOID AS
155 'provsql','set_prob' LANGUAGE C PARALLEL RESTRICTED;
156
157/**
158 * @brief Report whether a probability has been written on a gate
159 *
160 * @c get_prob returns the value an evaluation would use, so it answers
161 * 1 both for a gate written as certain and for one nobody has given a
162 * probability. This distinguishes them, which is what a client needs
163 * in order to offer "set" on the one and "replace" on the other.
164 */
165CREATE OR REPLACE FUNCTION probability_is_set(token UUID)
166 RETURNS BOOLEAN AS
167 'provsql','probability_is_set' LANGUAGE C STABLE PARALLEL SAFE;
168
169/**
170 * @brief Declare a just-minted leaf gate a replacement for a tracked row
171 *
172 * Internal. @c provsql.replace_input and @c provsql.replace_block call
173 * this so the @c UPDATE that stores the new token does not look, to
174 * @c provenance_guard, like a user pasting in an arbitrary UUID -- which
175 * would flip the table to @c OPAQUE. The declaration lasts for the
176 * transaction.
177 */
178CREATE OR REPLACE FUNCTION note_fresh_leaf(token UUID)
179 RETURNS VOID AS
180 'provsql','note_fresh_leaf' LANGUAGE C;
181
182/** @brief Whether @c token was minted as a replacement leaf by this
183 * transaction (see @c note_fresh_leaf). Internal. */
184CREATE OR REPLACE FUNCTION is_fresh_leaf(token UUID)
185 RETURNS BOOLEAN AS
186 'provsql','is_fresh_leaf' LANGUAGE C VOLATILE;
187
188/**
189 * @brief Mint a replacement input gate carrying a different probability
190 *
191 * Probabilities are written once, so a tuple's probability is changed
192 * the way its data is: by rewriting the row. This mints a fresh input
193 * gate with probability @c p and returns it, for the row's @c provsql
194 * column to carry:
195 *
196 * @code
197 * UPDATE s SET provsql = provsql.replace_input(provsql, 0.3) WHERE id = 42;
198 * @endcode
199 *
200 * The update is an ordinary heap write, so it has MVCC isolation, WAL,
201 * replication and @c pg_dump behind it, and the new gate and its
202 * probability roll back with it. The old gate and everything derived
203 * from it stay as they were: re-running a query over the base table
204 * builds new derived gates over the new token and so sees the new
205 * probability, while a table materialised earlier keeps the old tokens
206 * and the old probability -- a derived table reflects the base tables
207 * as they were when it was built, the same rule a @c DELETE under
208 * @c provsql.update_provenance already follows.
209 *
210 * @param old the token being replaced; must name an input gate
211 * @param p the new probability, in [0,1]
212 */
213CREATE OR REPLACE FUNCTION replace_input(old UUID, p DOUBLE PRECISION)
214 RETURNS UUID AS
215$$
216DECLARE
217 t UUID;
218 tp provsql.PROVENANCE_GATE;
219BEGIN
220 IF old IS NULL OR p IS NULL THEN
221 RAISE EXCEPTION 'replace_input: neither argument may be NULL';
222 END IF;
223 tp := provsql.get_gate_type(old);
224 IF tp = 'mulinput' THEN
225 RAISE EXCEPTION 'replace_input: % belongs to a repair_key block', old
226 USING HINT = 'A block''s values share one key gate and their masses '
227 'are meaningful together, so they are replaced together: '
228 'use provsql.replace_block().';
229 ELSIF tp = 'update' THEN
230 RAISE EXCEPTION 'replace_input: % is an update gate', old
231 USING HINT = 'Use provsql.replace_update() to give a recorded data '
232 'modification a different probability.';
233 ELSIF tp <> 'input' THEN
234 RAISE EXCEPTION 'replace_input: % is a gate of type %, not an input', old, tp
235 USING HINT = 'Only a leaf carries a probability of its own; a derived '
236 'gate''s is computed from its leaves.';
237 END IF;
238 t := public.uuid_generate_v4();
239 PERFORM provsql.create_gate(t, 'input');
240 PERFORM provsql.set_prob(t, p);
241 PERFORM provsql.note_fresh_leaf(t);
242 RETURN t;
243END
244$$ LANGUAGE plpgsql;
245
246/**
247 * @brief Replace a tracked row's input gate, rewriting the row
248 *
249 * The procedural form of @c replace_input: it mints the new gate and
250 * issues the @c UPDATE itself, which is what a client with only the
251 * token in hand (the Studio inspector, say) needs.
252 *
253 * @param _tbl the tracked table holding the row
254 * @param old the token the row carries
255 * @param p the new probability, in [0,1]
256 * @return the token the row now carries
257 */
258CREATE OR REPLACE FUNCTION replace_input(
259 _tbl REGCLASS, old UUID, p DOUBLE PRECISION)
260 RETURNS UUID AS
261$$
262DECLARE
263 t UUID;
264 n INT;
265BEGIN
266 t := provsql.replace_input(old, p);
267 EXECUTE format('UPDATE %s SET provsql = $1 WHERE provsql = $2', _tbl)
268 USING t, old;
269 GET DIAGNOSTICS n = ROW_COUNT;
270 IF n = 0 THEN
271 RAISE EXCEPTION 'replace_input: no row of % carries the token %', _tbl, old;
272 END IF;
273 RETURN t;
274END
275$$ LANGUAGE plpgsql;
276
277/**
278 * @brief Give a @c repair_key block a new set of probabilities
279 *
280 * The block-level counterpart of @c replace_input. A block's values
281 * share one key gate and their masses are meaningful together, so they
282 * are replaced together: this mints a new key gate and one new
283 * @c mulinput per row of the block, writes @c probs to them in the
284 * block's own order (the @c within_group index @c repair_key recorded),
285 * and rewrites the block's rows in @p _tbl to carry the new tokens.
286 *
287 * @param _tbl the tracked table holding the block's rows
288 * @param old_key the block's key gate -- the shared child of its rows'
289 * @c mulinput tokens, as reported by
290 * @c "get_children(provsql)[1]"
291 * @param probs one probability per row of the block, in block order;
292 * @c NULL leaves them unwritten, so the block evaluates
293 * at the uniform weight again
294 */
295CREATE OR REPLACE FUNCTION replace_block(
296 _tbl REGCLASS, old_key UUID, probs DOUBLE PRECISION[] DEFAULT NULL)
297 RETURNS VOID AS
298$$
299DECLARE
300 r RECORD;
301 n INT;
302 i INT := 0;
303 new_key UUID;
304 new_tok UUID;
305 was_active TEXT;
306BEGIN
307 IF provsql.get_gate_type(old_key) <> 'input' THEN
308 RAISE EXCEPTION 'replace_block: % is not a block key gate', old_key;
309 END IF;
310
311 -- The rewriter has no business in the bookkeeping below: the tokens of
312 -- _tbl are what this function is here to rewrite, not provenance to
313 -- carry into a temporary table. Restored before returning; a failure
314 -- aborts the transaction, which restores it too.
315 was_active := coalesce(current_setting('provsql.active', true), 'on');
316 PERFORM set_config('provsql.active', 'off', true);
317
318 EXECUTE format(
319 'CREATE TEMP TABLE provsql_replace_block_tmp ON COMMIT DROP AS
320 SELECT t.provsql AS old_token,
321 NULL::UUID AS new_token,
322 (provsql.get_infos(t.provsql)).info1 AS ord
323 FROM %s t
324 WHERE provsql.get_gate_type(t.provsql) = ''mulinput''
325 AND (provsql.get_children(t.provsql))[1] = %L', _tbl, old_key);
326
327 SELECT count(*) INTO n FROM provsql_replace_block_tmp;
328 IF n = 0 THEN
329 RAISE EXCEPTION 'replace_block: no row of % belongs to block %', _tbl, old_key;
330 END IF;
331 IF probs IS NOT NULL AND array_length(probs, 1) <> n THEN
332 RAISE EXCEPTION 'replace_block: block % has % rows but % probabilities were given',
333 old_key, n, array_length(probs, 1);
334 END IF;
335
336 new_key := public.uuid_generate_v4();
337 PERFORM provsql.create_gate(new_key, 'input');
338
339 FOR r IN SELECT old_token, ord FROM provsql_replace_block_tmp ORDER BY ord LOOP
340 i := i + 1;
341 new_tok := public.uuid_generate_v4();
342 PERFORM provsql.create_gate(new_tok, 'mulinput', ARRAY[new_key]);
343 PERFORM provsql.set_infos(new_tok, r.ord, n);
344 IF probs IS NOT NULL THEN
345 PERFORM provsql.set_prob(new_tok, probs[i]);
346 END IF;
347 PERFORM provsql.note_fresh_leaf(new_tok);
348 UPDATE provsql_replace_block_tmp SET new_token = new_tok
349 WHERE old_token = r.old_token;
350 END LOOP;
351
352 EXECUTE format(
353 'UPDATE %s t SET provsql = b.new_token
354 FROM provsql_replace_block_tmp b WHERE t.provsql = b.old_token', _tbl);
355
356 DROP TABLE provsql_replace_block_tmp;
357 PERFORM set_config('provsql.active', was_active, true);
358END
359$$ LANGUAGE plpgsql;
360/** @brief Get the probability associated with an input gate */
361CREATE OR REPLACE FUNCTION get_prob(
362 token UUID)
363 RETURNS DOUBLE PRECISION AS
364 'provsql','get_prob' LANGUAGE C STABLE PARALLEL SAFE;
365
366/**
367 * @brief Set additional INTEGER values on provenance circuit gate
368 *
369 * This function sets two INTEGER values associated to a circuit gate, used in
370 * different ways by different gate types:
371 * - for mulinput, info1 indicates the value of this multivalued variable
372 * - for eq, info1 and info2 indicate the attribute index of the
373 equijoin in, respectively, the first and second columns
374 * - for agg, info1 is the oid of the aggregate function and info2 the
375 oid of the aggregate result type
376 * - for cmp, info1 is the oid of the comparison operator
377 *
378 * @param token UUID of the circuit gate
379 * @param info1 first INTEGER value
380 * @param info2 second INTEGER value
381 */
382CREATE OR REPLACE FUNCTION set_infos(
383 token UUID, info1 INT, info2 INT DEFAULT NULL)
384 RETURNS VOID AS
385 'provsql','set_infos' LANGUAGE C PARALLEL SAFE;
386
387/** @brief Get the INTEGER info values associated with a circuit gate */
388CREATE OR REPLACE FUNCTION get_infos(
389 token UUID, OUT info1 INT, OUT info2 INT)
390 RETURNS RECORD AS
391 'provsql','get_infos' LANGUAGE C STABLE PARALLEL SAFE;
392
393/**
394 * @brief Wrap @p token in a fresh @c gate_assumed carrying @p assumption
395 * as its label, and return the wrapper's UUID.
396 *
397 * Public primitive callable from any rewrite or driver that needs to
398 * flag a sub-circuit as sound only under an evaluation assumption:
399 *
400 * - @c 'BOOLEAN' -- the sub-circuit only preserves the Boolean function
401 * of the lineage (e.g. the safe-query rewrite collapses derivation
402 * multiplicities); transparent for semirings admitting a homomorphism
403 * from Boolean functions.
404 * - @c 'absorptive' -- the sub-circuit was truncated at the absorptive
405 * value fixpoint (cyclic recursive query); transparent for absorptive
406 * semirings (probability, BOOLEAN, min-plus over nonnegative
407 * costs...), fatal for the rest (counting, why-provenance).
408 *
409 * Incompatible evaluators raise a @c CircuitException. Always kept as
410 * an explicit node in PROV-XML export.
411 *
412 * The wrapper UUID is content-derived via @c uuid_generate_v5 on the
413 * assumption and the child, so identical children always wrap to the
414 * same outer UUID per assumption. No-op (returns NULL) on a NULL
415 * input.
416 */
417CREATE OR REPLACE FUNCTION provenance_assume(token UUID, assumption TEXT)
418 RETURNS UUID AS
419$$
420DECLARE
421 wrapped UUID;
422BEGIN
423 IF token IS NULL THEN
424 RETURN NULL;
425 END IF;
426 IF assumption NOT IN ('BOOLEAN', 'absorptive') THEN
427 RAISE EXCEPTION 'provenance_assume: unknown assumption %', assumption;
428 END IF;
429 wrapped := public.uuid_generate_v5(uuid_ns_provsql(),
430 concat('assumed', assumption, token));
431 PERFORM create_gate(wrapped, 'assumed', ARRAY[token]);
432 PERFORM set_extra(wrapped, assumption);
433 RETURN wrapped;
434END
435$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
436 SECURITY DEFINER PARALLEL SAFE;
437
438/**
439 * @brief Wrap @p token in a Boolean-assumption marker (compatibility
440 * name; see @c provenance_assume).
441 *
442 * This is the entry point the safe-query (read-once) rewriter calls on
443 * every per-row root it produces, so the wrapper additionally carries
444 * @c PROVSQL_ROUTE_SQ_REWRITE in @c info1: the assumption kind alone does
445 * not identify the route (@c provenance_assume(t, 'BOOLEAN') is public),
446 * and the probability dispatcher reads the tag back to report
447 * @c sq-rewrite rather than the generic @c independent. Build an untagged
448 * Boolean-assumption wrapper with @c provenance_assume directly.
449 */
450CREATE OR REPLACE FUNCTION assume_boolean(token UUID) RETURNS UUID AS
451$$
452DECLARE
453 wrapped UUID;
454BEGIN
455 wrapped := provenance_assume(token, 'BOOLEAN');
456 IF wrapped IS NOT NULL THEN
457 -- 1 = PROVSQL_ROUTE_SQ_REWRITE (see provsql_route in src/provsql_utils.h)
458 PERFORM set_infos(wrapped, 1, 0);
459 END IF;
460 RETURN wrapped;
461END
462$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
463 SECURITY DEFINER PARALLEL SAFE;
464
465/**
466 * @brief Wrap @p token in a fresh transparent @c gate_annotation carrying
467 * @p extra, and return the wrapper's UUID.
468 *
469 * Unlike every other gate, the annotation wrapper's UUID folds in @p extra
470 * (not just the child): @c uuid_generate_v5 over @c concat('annotation',
471 * token, extra). This is deliberate -- two annotations over the same child
472 * with different @p extra must be distinct gates (e.g. the same input tuple
473 * carrying different per-occurrence order keys, or two queries attaching
474 * different certificates to a shared root). The wrapper is transparent
475 * (identity) for EVERY evaluator; @p extra is inert metadata read only by the
476 * code that placed it. No-op (returns NULL) on a NULL input.
477 */
478CREATE OR REPLACE FUNCTION annotate(token UUID, extra TEXT) RETURNS UUID AS
479$$
480DECLARE
481 annotated UUID;
482BEGIN
483 IF token IS NULL THEN
484 RETURN NULL;
485 END IF;
486 annotated := public.uuid_generate_v5(uuid_ns_provsql(),
487 concat('annotation', token, extra));
488 PERFORM create_gate(annotated, 'annotation', ARRAY[token]);
489 PERFORM set_extra(annotated, extra);
490 RETURN annotated;
491END
492$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
493 SECURITY DEFINER PARALLEL SAFE;
494
495/**
496 * @brief Peel every transparent @c gate_annotation wrapper off @p token,
497 * returning the first non-annotation gate underneath.
498 *
499 * The dual of @c annotate, for consumers keyed to gate *identity* rather
500 * than gate value: a provenance mapping matches input-gate UUIDs, and the
501 * reachability edge classifier matches token shapes, so both must see the
502 * wrapped gate, not the wrapper (e.g. the inversion-free certificate /
503 * order marker the planner attaches to a certified query's row roots).
504 * Identity on a token with no annotation wrapper; NULL on NULL.
505 */
506CREATE OR REPLACE FUNCTION strip_annotations(token UUID) RETURNS UUID AS
507$$
508WITH RECURSIVE peel(g) AS (
509 SELECT token
510 UNION ALL
511 SELECT (provsql.get_children(p.g))[1] FROM peel p
512 WHERE provsql.get_gate_type(p.g) = 'annotation'
513)
514SELECT g FROM peel WHERE provsql.get_gate_type(g) <> 'annotation' LIMIT 1;
515$$ LANGUAGE sql STABLE PARALLEL SAFE;
516
517/**
518 * @brief Condition a provenance token (a Boolean event) on another.
519 *
520 * Builds the terminal @c gate_conditioned that the measure evaluators read
521 * as @c "P(target ∧ evidence) / P(evidence)". This is the backing function
522 * of the binary @c | operator (@c "target | evidence", value-level
523 * conditioning of the UUID carrier).
525 * The gate stores three children @c [target, evidence, joint] with
526 * @c joint @c = @c times(target, @c evidence); evaluation is then the plain
527 * ratio @c P(joint)/P(evidence), and content-addressing makes a base tuple
528 * shared by @p target and @p evidence the same input gate in both circuits,
529 * so the conditional is exact and correlation-aware.
530 *
531 * Conventions:
532 * - Conditioning on a certain or absent event is a no-op: @c evidence NULL
533 * or @c gate_one() returns @p target unchanged (@c "P(X|true)=P(X)").
534 * - A @p target with no provenance defaults to the certain event 1, so
535 * @c "1 | c" is the well-defined certain-row posterior.
536 * - Nested conditioning folds (sequential Bayesian update):
537 * @c "(X | A) | B = X | (A ∧ B)" -- the gate never nests, it stays one
538 * level deep with the evidence accumulated by @c times.
539 *
540 * The result is TERMINAL: a conditioned token may not become a child of a
541 * @c plus / @c times / @c monus / @c agg gate (those constructors refuse
542 * it); the only operation it admits is more conditioning.
543 */
544CREATE OR REPLACE FUNCTION cond(target UUID, evidence UUID) RETURNS UUID AS
545$$
546DECLARE
547 tgt UUID;
548 ev UUID;
549 jnt UUID;
550 result UUID;
551 ch UUID[];
552BEGIN
553 -- P(X | true) = P(X): conditioning on a certain / absent event is inert.
554 IF evidence IS NULL OR evidence = gate_one() THEN
555 RETURN target;
556 END IF;
557
558 -- A row with no provenance defaults to the certain event 1.
559 tgt := coalesce(target, gate_one());
560
561 IF get_gate_type(tgt) = 'conditioned' THEN
562 -- Sequential update (X | A) | B = X | (A ∧ B): fold B into both the
563 -- evidence and the joint of the inner gate so the result stays a single
564 -- gate_conditioned over the ORIGINAL target.
565 ch := get_children(tgt);
566 tgt := ch[1]; -- original target X
567 ev := provenance_times(ch[2], evidence); -- A ∧ B
568 jnt := provenance_times(ch[3], evidence); -- (X ∧ A) ∧ B
569 ELSE
570 ev := evidence;
571 jnt := provenance_times(tgt, evidence); -- X ∧ C
572 END IF;
573
574 result := public.uuid_generate_v5(uuid_ns_provsql(),
575 concat('conditioned', tgt, ev, jnt));
576 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev, jnt]);
577 RETURN result;
578END
579$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
580 SECURITY DEFINER PARALLEL SAFE;
581
582/**
583 * @brief Binary @c | : value-level conditioning, @c "target | evidence".
584 *
585 * Carrier-parametric in its left operand; the UUID form builds the terminal
586 * @c gate_conditioned via @c cond. Does not collide with core PostgreSQL's
587 * INTEGER bitwise @c | (different argument types).
588 */
589CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=UUID, PROCEDURE=cond);
590
591/**
592 * @brief Placeholder for @c "X | (predicate)" on a UUID event.
593 *
594 * Lets the conditioning event be written as a natural Boolean combination of
595 * random_variable / aggregate comparisons (e.g. @c "event | (sensor > 3)")
596 * instead of a hand-built gate. Never executes: the ProvSQL planner hook
597 * converts the Boolean operand into a condition gate and emits @c cond.
598 */
599CREATE OR REPLACE FUNCTION cond_predicate(target UUID, predicate BOOLEAN)
600 RETURNS UUID AS
601$$
602BEGIN
603 RAISE EXCEPTION 'UUID | (predicate) must be rewritten by the ProvSQL '
604 'planner hook: the right operand must be a Boolean combination of '
605 'random_variable / aggregate comparisons (is provsql.active off?)';
606END
607$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
608
609CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=BOOLEAN, PROCEDURE=cond_predicate);
610
611/**
612 * @brief Placeholder for @c "(predicate) | (predicate)" on two events.
613 *
614 * Conditions one comparison event on another when both operands are written
615 * as comparisons rather than pre-built tokens (e.g.
616 * @c "probability((x >= 2000) | (x >= 1000))"): an @c random_variable /
617 * @c AGG_TOKEN comparison is statically @c BOOLEAN-typed, so neither the
618 * @c "UUID | UUID" (@c cond) nor the @c "UUID | BOOLEAN" (@c cond_predicate)
619 * operator resolves. Never executes: the ProvSQL planner hook lowers each
620 * Boolean operand to its event gate and emits @c cond(target, evidence), so
621 * the result carries the correlation-aware @c Pr(A ∧ B) / Pr(B). Returns
622 * @c UUID, so @c "A | B" is a first-class event token in every position
623 * (a @c probability(UUID) argument, a projected column, a further @c "|").
624 */
625CREATE OR REPLACE FUNCTION predicate_cond_predicate(target BOOLEAN, evidence BOOLEAN)
626 RETURNS UUID AS
627$$
628BEGIN
629 RAISE EXCEPTION '(predicate) | (predicate) must be rewritten by the ProvSQL '
630 'planner hook: both operands must be Boolean combinations of '
631 'random_variable / aggregate comparisons (is provsql.active off?)';
632END
633$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
634
635CREATE OPERATOR | (LEFTARG=BOOLEAN, RIGHTARG=BOOLEAN, PROCEDURE=predicate_cond_predicate);
636
637/**
638 * @brief Deterministic indicator gate for an ordinary (regular) comparison.
639 *
640 * The predicate-provenance of an ordinary comparison (both sides of regular
641 * type, e.g. @c "region = 'north'") is the deterministic indicator
642 * @c "χ(cond)": @c gate_one() when the comparison holds on the row,
643 * @c gate_zero() otherwise (Definition in the HAVING-provenance semantics).
644 * The planner emits this for a regular comparison appearing inside a MIXED
645 * conditioning predicate (one that also has a random_variable / aggregate
646 * comparison); @c cond is evaluated per row, so the indicator is the row's
647 * own truth value, combined by @c ⊗ / @c ⊕ with the probabilistic gates.
648 */
649CREATE OR REPLACE FUNCTION regular_indicator(cond BOOLEAN) RETURNS UUID AS
650$$
651 SELECT CASE WHEN cond THEN provsql.gate_one() ELSE provsql.gate_zero() END;
652$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
653
654/**
655 * @brief Whole-tuple output conditioning directive: @c "given(evidence)".
656 *
657 * Written as a term in the select list, @c given(c) conditions the OUTPUT
658 * provenance of the current query's rows on @p c:
659 *
660 * @code
661 * SELECT a, b, given((SELECT provenance() FROM tests
662 * WHERE patient_id = s.id AND result = 'positive'))
663 * FROM source s;
664 * -- visible columns: a, b (the given(...) term is stripped)
665 * -- per-row output provenance: provenance() | <that row's evidence>
666 * @endcode
667 *
668 * The query rewriter recognises the marker, STRIPS it from the visible
669 * projection, and wraps each output row's provenance expression in
670 * @c cond(row_provenance, c) -- deriving a new conditioned relation, never
671 * mutating any stored provenance. @p c is evaluated per output row and may
672 * correlate with the row's columns, so each tuple is conditioned on its own
673 * evidence. When the rewriter is inactive the call is a harmless identity
674 * (it returns @p evidence as an ordinary column).
675 *
676 * When @b executed rather than stripped -- i.e. nested inside an expression,
677 * the idiom @c "and_agg(given(Y = d))" that folds one observation per row
678 * into a latent-variable evidence circuit -- a point-equality @c "Y = d" on
679 * a bare random-variable leaf is turned into likelihood-weighting evidence
680 * (@c evidence_as_observation); any other evidence passes through unchanged.
681 */
682CREATE OR REPLACE FUNCTION given(evidence UUID) RETURNS UUID AS
684BEGIN
685 RETURN provsql.evidence_as_observation(evidence);
686END
687$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE
688 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
689
690/**
691 * @brief Prefix unary @c | : alias for @c given, @c "| evidence".
692 *
693 * Disambiguated from the binary @c | by the absence of a left operand
694 * (@c "a, | c" parses @c "| c" as the prefix form). PostgreSQL keeps
695 * prefix operators on every supported version (postfix operators were
696 * removed in PG14), so @c "| c" is safe across the CI matrix.
697 */
698CREATE OPERATOR | (RIGHTARG=UUID, PROCEDURE=given);
699
700/**
701 * @brief Conditioning-evidence from a predicate: @c "given(predicate)"
702 * (also the prefix @c "| (predicate)").
703 *
704 * Two uses of the same marker:
705 * - whole-tuple output conditioning written as a select-list term, the
706 * natural-predicate spelling of @c given (@c "SELECT a, given(sensor > 3)");
707 * - per-row evidence for a latent-variable posterior, folded with
708 * @c and_agg -- @c "and_agg(given(normal(mu,1) = x))" turns each row's
709 * observation into likelihood-weighting evidence.
710 *
711 * Never executes: the planner converts the Boolean operand into a condition
712 * gate and emits @c given(gate); a point-equality @c "Y = d" on a bare
713 * random-variable leaf then becomes an observation (see @c given(UUID) /
714 * @c evidence_as_observation).
715 */
716CREATE OR REPLACE FUNCTION given(predicate BOOLEAN) RETURNS UUID AS
717$$
718BEGIN
719 RAISE EXCEPTION 'given(predicate) / prefix | (predicate) must be rewritten '
720 'by the ProvSQL planner hook: the operand must be a Boolean combination '
721 'of random_variable / aggregate comparisons (is provsql.active off?)';
722END
723$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
724
725CREATE OPERATOR | (RIGHTARG=BOOLEAN, PROCEDURE=given);
726
727/**
728 * @brief Event negation: @c "! event" / @c "provenance_not(event)".
729 *
730 * The complement of a Boolean provenance event: @c "!x" holds in exactly the
731 * worlds where @p x does not. It is sugar for @c "monus(one, x)" -- an
732 * ordinary m-semiring expression (Boolean @c NOT, probability @c "1 - P(x)"),
733 * NOT a measure-only marker -- so it composes like any @c monus, and a
734 * conditioned / terminal token is refused as its child (so @c "!(x | c)"
735 * errors, as conditioning cannot be buried under further algebra).
736 *
737 * The motivating use is conditioning on the NON-occurrence of an arbitrary
738 * violation query @p W (a denial constraint), where @p W itself is built with
739 * ordinary idioms and needs no hand-rolled gates:
740 *
741 * @code
742 * -- W = "some pair of overlapping same-room bookings is present"
743 * WITH w AS (SELECT provenance() AS tok
744 * FROM bookings a JOIN bookings b
745 * ON a.id < b.id AND a.room = b.room
746 * AND a.lo < b.hi AND b.lo < a.hi
747 * GROUP BY ())
748 * SELECT probability_evaluate((SELECT provenance() FROM bookings WHERE id=1)
749 * | !w.tok) -- P(booking 1 | no overlap)
750 * FROM w;
751 * @endcode
752 *
753 * Named @c provenance_not, after the @c "provenance_times / _plus / _monus"
754 * family; the prefix @c ! operator is the ergonomic form (SQL's reserved
755 * @c NOT keyword cannot serve as a function name).
756 */
757CREATE OR REPLACE FUNCTION provenance_not(event UUID) RETURNS UUID AS
758$$
759 SELECT provsql.provenance_monus(provsql.gate_one(), event);
760$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
761 SET search_path=provsql,pg_temp,public;
762
763/**
764 * @brief Prefix unary @c ! : alias for @c provenance_not, @c "! event".
765 *
766 * Prefix operators are kept on every supported PostgreSQL version (postfix
767 * operators were removed in PG14), and core PG defines no prefix @c ! on
768 * @c UUID, so @c "! event" is safe across the CI matrix.
769 */
770CREATE OPERATOR ! (RIGHTARG=UUID, PROCEDURE=provenance_not);
771
772/**
773 * @brief Build a per-input order-key string for the inversion-free path.
774 *
775 * Emitted by the planner per certified atom: @c K-prefixed, length-prefixed
776 * @c "K<factor> <octet_length(root)>:<root><octet_length(sec)>:<sec>", parsed
777 * back at evaluation by @c safe_cert_key_parse. @p root / @p sec are the
778 * tuple's root- and secondary-class column values (TEXT-cast by the caller);
779 * the byte-length prefixes keep the values unambiguous for @em any column type,
780 * including TEXT containing spaces, colons or digits. @p factor is the atom's
781 * factor id (or -1 for the shared self-join guard). @c IMMUTABLE so the planner
782 * can fold it and the marker dedups by content-addressing.
783 */
784CREATE OR REPLACE FUNCTION inversion_free_key(root TEXT, sec TEXT, factor INT)
785 RETURNS TEXT AS
786$$ SELECT 'K' || factor::TEXT || ' '
787 || octet_length(root) || ':' || root
788 || octet_length(sec) || ':' || sec $$
789 LANGUAGE sql IMMUTABLE PARALLEL SAFE;
791/**
792 * @brief Set extra TEXT information on provenance circuit gate
793 *
794 * This function sets TEXT-encoded data associated to a circuit gate, used in
795 * different ways by different gate types:
796 * - for project, it is a TEXT-encoded ARRAY of two-element ARRAYs that
797 * indicate mappings between input attribute (first element) and output
798 * attribute (second element)
799 * - for value and agg, it is the TEXT-encoded (base for value, computed
800 * for agg) scalar value
801 *
802 * @param token UUID of the circuit gate
803 * @param data TEXT-encoded information
804 */
805CREATE OR REPLACE FUNCTION set_extra(
806 token UUID, data TEXT)
807 RETURNS VOID AS
808 'provsql','set_extra' LANGUAGE C PARALLEL SAFE STRICT;
809/** @brief Get the TEXT-encoded extra data associated with a circuit gate */
810CREATE OR REPLACE FUNCTION get_extra(token UUID)
811 RETURNS TEXT AS
812 'provsql','get_extra' LANGUAGE C STABLE PARALLEL SAFE RETURNS NULL ON NULL INPUT;
813
814/**
815 * @brief Return the total number of materialized gates in the provenance circuit
816 *
817 * Input gates for provenance-tracked table rows are created lazily on
818 * first reference; rows that have never appeared in a query result are
819 * not counted.
820 */
821CREATE OR REPLACE FUNCTION get_nb_gates() RETURNS BIGINT AS
822 'provsql', 'get_nb_gates' LANGUAGE C PARALLEL SAFE;
823
824/**
825 * @brief Report what does not add up in this database's circuit store
826 *
827 * The store lives in four files under the database's directory, outside
828 * PostgreSQL's WAL and buffer manager, so PostgreSQL's own crash recovery
829 * says nothing about it. Writes are ordered so that an interrupted one
830 * leaves a RECORD nothing points at rather than a pointer to a RECORD
831 * that is not there, and the rehash of the token table is done by
832 * renaming a complete new file over the old one; this function checks
833 * that those invariants hold.
834 *
835 * Every count is 0 for a healthy store. @c unclean_shutdown is true when
836 * a file was still marked open-for-writing when it was opened, which an
837 * immediate shutdown or a crash of the server leaves behind and is not by
838 * itself a problem. A non-zero @c dangling_indices, @c bad_wires or
839 * @c bad_extra means the files do not agree with each other -- typically
840 * a file-level backup taken while the server was running, or a base
841 * backup; @c provsql.circuit_cleanup() rebuilds the store from what is
842 * still reachable. @c unreferenced counts gate records the token table
843 * does not point at: a handful is normal (an interrupted write), a large
844 * number means the token table is missing entries.
845 */
846CREATE OR REPLACE FUNCTION check_store(
847 OUT unclean_shutdown BOOLEAN,
848 OUT nb_gates BIGINT,
849 OUT nb_tokens BIGINT,
850 OUT next_index BIGINT,
851 OUT dangling_indices BIGINT,
852 OUT unreferenced BIGINT,
853 OUT bad_wires BIGINT,
854 OUT bad_extra BIGINT)
855 RETURNS RECORD AS
856 'provsql', 'check_store' LANGUAGE C;
857
858/**
859 * @brief Rebuild this database's circuit store, keeping only what the
860 * tokens stored in the database reach
861 *
862 * The store only grows: a gate is never removed, because a rolled-back
863 * transaction leaves an orphan rather than an inconsistency, and because
864 * the same expression recomputed lands on the same content-addressed
865 * gate. This is the complement of that -- the one operation allowed to
866 * remove gates, run explicitly, the way @c VACUUM @c FULL is the
867 * complement of MVCC. It is also the repair tool for a store an
868 * interrupted write left inconsistent (see @c provsql.check_store).
870 * It takes the database exclusively: it holds the lock @c DROP
871 * @c DATABASE holds, so sessions connecting from then on wait, and it
872 * refuses to run while another session is already connected. That is
873 * unavoidable -- a query running alongside can adopt an orphan gate a
874 * moment before the sweep removes it, since gates are re-created
875 * idempotently and a backend's own cache answers "it exists" without
876 * asking the store at all.
877 *
878 * A root is every value of a @c UUID, @c AGG_TOKEN or @c random_variable
879 * column, and of arrays of those, in every table and materialised view of
880 * the database -- not only columns named @c provsql -- plus the semiring
881 * constants @c gate_zero and @c gate_one. A token that lives only
882 * outside the database is **not** a root: one kept in a notebook cell, a
883 * deep link, a file, or a @c TEXT / @c jsonb column. Content-addressed
884 * gates come back by re-running the query that built them; freshly minted
885 * ones do not.
886 *
887 * @param dry_run report what would be kept without writing anything; the
888 * wire and byte totals are then NULL, since the size of
889 * the rewrite is not known without doing it
890 * @param[out] gates_before gate records in the store beforehand
891 * @param[out] gates_after gate records the roots reach, and so kept
892 * @param[out] wires_before child wires beforehand
893 * @param[out] wires_after child wires kept (NULL on a dry run)
894 * @param[out] extra_bytes_before bytes of gate annotations beforehand
895 * @param[out] extra_bytes_after bytes of gate annotations kept (NULL on a
896 * dry run)
897 */
898CREATE OR REPLACE FUNCTION circuit_cleanup(
899 dry_run BOOLEAN DEFAULT false,
900 OUT gates_before BIGINT,
901 OUT gates_after BIGINT,
902 OUT wires_before BIGINT,
903 OUT wires_after BIGINT,
904 OUT extra_bytes_before BIGINT,
905 OUT extra_bytes_after BIGINT)
906 RETURNS RECORD AS
907 'provsql', 'circuit_cleanup' LANGUAGE C;
908
909/** @} */
910
911/** @defgroup table_management Provenance table management
912 * Functions for enabling, disabling, and configuring provenance
913 * tracking on user tables.
914 * @{
915 */
916
917
918/**
919 * @brief Trigger function for DELETE statement provenance tracking
920 *
921 * Records the deletion and applies monus to provenance tokens of
922 * deleted rows. This is the version for PostgreSQL < 14.
923 */
924CREATE OR REPLACE FUNCTION delete_statement_trigger()
925 RETURNS TRIGGER AS
926$$
927DECLARE
928 query_text TEXT;
929 delete_token UUID;
930 old_token UUID;
931 new_token UUID;
932 r RECORD;
933BEGIN
934 delete_token := public.uuid_generate_v4();
935
936 PERFORM create_gate(delete_token, 'input');
937
938 SELECT query
939 INTO query_text
940 FROM pg_stat_activity
941 WHERE pid = pg_backend_pid();
942
943 INSERT INTO delete_provenance (delete_token, query, deleted_by, deleted_at)
944 VALUES (delete_token, query_text, current_user, CURRENT_TIMESTAMP);
945
946 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
947
948 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
949 old_token := r.provsql;
950 new_token := provenance_monus(old_token, delete_token);
951
952 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
953 USING new_token, old_token;
954 END LOOP;
955
956 RETURN NULL;
957END
958$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
959
960
961/**
962 * @brief Per-relation provenance metadata used by the safe-query
963 * optimisation.
964 *
965 * One row per relation ProvSQL tracks. @c relid is stored as
966 * @c REGCLASS so a dump carries the relation's *name*: OIDs are not
967 * stable across databases, and @c pg_dump / @c pg_restore resolve a
968 * @c REGCLASS value back to whatever OID the relation has in the
969 * target database. @c kind is one of @c 'tid' / @c 'bid' /
970 * @c 'opaque' (see @c set_table_info); @c block_key lists the
971 * block-key column numbers of a BID relation; @c ancestors lists the
972 * base relations this one's atoms ultimately come from.
974 * Being a heap table, every change follows the transaction that made
975 * it: a rolled-back @c add_provenance leaves no RECORD, a rolled-back
976 * @c DROP @c TABLE keeps one, and a concurrent session sees a change
977 * only once it commits. Marked as a configuration table so
978 * @c pg_dump carries it.
979 */
980CREATE TABLE IF NOT EXISTS table_info(
981 relid REGCLASS PRIMARY KEY,
982 kind TEXT NOT NULL,
983 block_key int2[] NOT NULL DEFAULT ARRAY[]::int2[],
984 ancestors oid[] NOT NULL DEFAULT ARRAY[]::oid[]
985);
986SELECT pg_catalog.pg_extension_config_dump('table_info', '');
987
988/**
989 * @brief Row trigger keeping every backend's metadata cache honest.
990 *
991 * Each backend caches the metadata of the relations its queries touch
992 * and drops an entry when PostgreSQL invalidates that relation's
993 * relcache entry. This trigger issues that invalidation for the
994 * relation named by every inserted, updated, or deleted row, so a
995 * hand-written @c UPDATE on the table and the @c COPY a @c pg_restore
996 * performs are as visible to the caches as the setter functions are.
997 */
998CREATE OR REPLACE FUNCTION table_info_invalidate()
999 RETURNS trigger AS
1000 'provsql','provsql_table_info_invalidate' LANGUAGE C;
1001
1002DROP TRIGGER IF EXISTS table_info_invalidate ON table_info;
1003CREATE TRIGGER table_info_invalidate
1004 AFTER INSERT OR UPDATE OR DELETE ON table_info
1005 FOR EACH ROW EXECUTE PROCEDURE provsql.table_info_invalidate();
1006
1007/**
1008 * @brief Record per-relation provenance metadata used by the
1009 * safe-query optimisation.
1010 *
1011 * Upserts the @c (relid, kind, block_key) half of the relation's row
1012 * in @c provsql.table_info, preserving its @c ancestors. @p kind is
1013 * one of:
1014 * - @c 'tid' -- independent input leaves (post-@c add_provenance default)
1015 * - @c 'bid' -- block-correlated leaves; rows sharing the same value
1016 * of @p block_key are mutually exclusive. An empty
1017 * @p block_key means the whole table is one block.
1018 * - @c 'opaque' -- arbitrary correlations from a derived source
1019 * (CREATE TABLE AS SELECT, INSERT INTO SELECT,
1020 * UPDATE under provsql.update_provenance); the
1021 * safe-query rewriter must bail on these.
1022 *
1023 * @param relid pg_class OID of the relation.
1024 * @param kind One of @c 'tid' / @c 'bid' / @c 'opaque'.
1025 * @param block_key Block-key column numbers (only meaningful for
1026 * @c 'bid'; ignored otherwise but conventionally
1027 * passed empty).
1028 */
1029CREATE OR REPLACE FUNCTION set_table_info(
1030 relid OID, kind TEXT, block_key INT2[] DEFAULT ARRAY[]::INT2[])
1031 RETURNS VOID AS
1032 'provsql','set_table_info' LANGUAGE C SECURITY DEFINER;
1033
1034/** @brief Remove a relation's row from @c provsql.table_info.
1035 * No-op when missing. */
1036CREATE OR REPLACE FUNCTION remove_table_info(relid OID)
1037 RETURNS VOID AS
1038 'provsql','remove_table_info' LANGUAGE C SECURITY DEFINER;
1039
1040/**
1041 * @brief Read per-relation provenance metadata.
1042 *
1043 * Returns NULL if no RECORD exists. @c kind is one of @c 'tid' /
1044 * @c 'bid' / @c 'opaque'; @c block_key is the (possibly empty) array
1045 * of block-key column numbers, only meaningful when @c kind = @c 'bid'.
1046 * Used by the planner-time hierarchy detector to gate the safe-query
1047 * rewrite.
1048 */
1049CREATE OR REPLACE FUNCTION get_table_info(
1050 relid OID, OUT kind TEXT, OUT block_key INT2[])
1051 RETURNS RECORD AS
1052 'provsql','get_table_info' LANGUAGE C STABLE PARALLEL SAFE;
1053
1054/**
1055 * @brief Record the base-relation ancestor set of a tracked relation.
1056 *
1057 * Base tables created with @c add_provenance / @c repair_key carry
1058 * @c {self}; CTAS-derived tables inherit the union of their sources'
1059 * ancestor sets. The safe-query rewriter consults the registry to
1060 * enforce that joined FROM entries have disjoint base ancestors
1061 * before firing the read-once factoring.
1062 *
1063 * Preserves the relation's existing @c kind / @c block_key half on
1064 * update, and silently no-ops when no row exists for @p relid
1065 * (callers should run @c add_provenance / @c repair_key first). The
1066 * ancestor list is capped at 64 entries (clear error if exceeded).
1068 * @param relid pg_class OID of the relation.
1069 * @param ancestors Sorted, deduplicated base-relation OIDs.
1070 */
1071CREATE OR REPLACE FUNCTION set_ancestors(
1072 relid OID, ancestors OID[] DEFAULT ARRAY[]::OID[])
1073 RETURNS VOID AS
1074 'provsql','set_ancestors' LANGUAGE C SECURITY DEFINER;
1076/** @brief Clear the ancestor half of a per-relation RECORD (keeps kind/block_key).
1077 * No-op when missing. */
1078CREATE OR REPLACE FUNCTION remove_ancestors(relid OID)
1079 RETURNS VOID AS
1080 'provsql','remove_ancestors' LANGUAGE C SECURITY DEFINER;
1081
1082/**
1083 * @brief Copy per-relation metadata out of the legacy
1084 * @c provsql_table_info.mmap file into @c provsql.table_info.
1085 *
1086 * ProvSQL 1.13.0 moved this metadata from a fifth mmap file to a heap
1087 * table, so that it follows the transaction that writes it and is
1088 * carried by @c pg_dump. This function reads the legacy file, if the
1089 * database still has one, and inserts every RECORD it holds that the
1090 * heap table does not already have; it returns the number of rows
1091 * inserted, and 0 when there is no file to read. Idempotent, and a
1092 * no-op on a database that never had one.
1093 */
1094CREATE OR REPLACE FUNCTION migrate_table_info()
1095 RETURNS BIGINT AS
1096 'provsql','migrate_table_info' LANGUAGE C SECURITY DEFINER;
1097
1098/**
1099 * @brief Read the base-relation ancestor set of a tracked relation.
1100 *
1101 * Returns @c NULL when no ancestor RECORD exists for @p relid (or the
1102 * RECORD is empty -- both cases make the safe-query rewriter take
1103 * its conservative refuse path, so they collapse here).
1104 */
1105CREATE OR REPLACE FUNCTION get_ancestors(relid OID)
1106 RETURNS OID[] AS
1107 'provsql','get_ancestors' LANGUAGE C STABLE PARALLEL SAFE;
1108
1109/**
1110 * @brief BEFORE INSERT OR UPDATE OF provsql row trigger installed by
1111 * @c add_provenance.
1112 *
1113 * Two jobs:
1114 *
1115 * 1. Fill @c NEW.provsql with a fresh @c uuid_generate_v4 leaf when
1116 * the user did not supply one (a column DEFAULT would not do here:
1117 * it fires before the trigger sees the row, so we could not tell
1118 * "user omitted the column" from "user supplied a value").
1119 * 2. When the user does supply a non-NULL @c provsql on @c INSERT,
1120 * or changes it on @c UPDATE, flip the table's per-table
1121 * metadata to @c OPAQUE. The user is free to write whatever
1122 * UUIDs they want (cross-table reuse, compound tokens minted
1123 * via @c create_gate, ...); the cost is that the safe-query
1124 * rewriter then refuses to fire on this table, because TID
1125 * independence can no longer be assumed. The exception is a
1126 * leaf @c provsql.replace_input / @c replace_block minted in
1127 * this transaction: that one *is* an independent fresh leaf, so
1128 * the kind survives and the maintained mappings follow the
1129 * token to its replacement.
1130 */
1131CREATE OR REPLACE FUNCTION provenance_guard()
1132 RETURNS TRIGGER AS $$
1133DECLARE
1134 _m RECORD;
1135BEGIN
1136 IF TG_OP = 'INSERT' THEN
1137 IF NEW.provsql IS NULL THEN
1138 -- A genuine insert: mint a fresh atomic input variable. This is the
1139 -- one place a new input token is born, so it is also where any
1140 -- maintained mapping on this table is extended (keyed to that token).
1141 -- Data-modification re-insertions (INSERT ... SELECT * FROM OLD_TABLE)
1142 -- carry a supplied provsql and take the ELSE branch, so they are
1143 -- correctly skipped: the validity stays keyed to the original input,
1144 -- which is exactly the child a later monus/update gate wraps.
1145 NEW.provsql := public.uuid_generate_v4();
1146 FOR _m IN SELECT mapping, attribute
1147 FROM provsql.provenance_mapping_registry
1148 WHERE source = TG_RELID AND maintained
1149 LOOP
1150 EXECUTE format(
1151 'INSERT INTO %s(value, provenance) SELECT ($1).%I, $2',
1152 _m.mapping::REGCLASS, _m.attribute)
1153 USING NEW, NEW.provsql;
1154 END LOOP;
1155 ELSE
1156 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
1157 END IF;
1158 ELSIF TG_OP = 'UPDATE' THEN
1159 IF NEW.provsql IS DISTINCT FROM OLD.provsql THEN
1160 IF provsql.is_fresh_leaf(NEW.provsql) THEN
1161 -- A replacement leaf minted by provsql.replace_input /
1162 -- replace_block in this transaction: an independent fresh leaf by
1163 -- construction, so the table's kind survives. Carry the
1164 -- maintained mappings over from the token it replaces, the same
1165 -- job the INSERT branch does for a new row.
1166 FOR _m IN SELECT mapping, attribute
1167 FROM provsql.provenance_mapping_registry WHERE source = TG_RELID
1168 LOOP
1169 EXECUTE format(
1170 'INSERT INTO %1$s(value, provenance) '
1171 'SELECT value, $2 FROM %1$s WHERE provenance = $1',
1172 _m.mapping::REGCLASS)
1173 USING OLD.provsql, NEW.provsql;
1174 END LOOP;
1175 ELSE
1176 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
1177 END IF;
1178 END IF;
1179 END IF;
1180 RETURN NEW;
1181END;
1182$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
1183 SECURITY DEFINER;
1184
1185/**
1186 * @brief Enable provenance tracking on an existing table
1187 *
1188 * Adds a <tt>provsql</tt> UUID column to the table, an index for
1189 * fast UUID-keyed lookups, and a BEFORE INSERT/UPDATE row trigger
1190 * (@c provenance_guard) that mints a fresh @c uuid_generate_v4
1191 * leaf when the user omits the column on INSERT, or flips the
1192 * table's metadata to @c OPAQUE when the user supplies their own
1193 * value. Input gates for existing rows are created lazily when
1194 * first referenced by a query.
1195 *
1196 * @param _tbl the table to add provenance tracking to
1197 */
1198CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
1199 RETURNS VOID AS
1200$$
1201BEGIN
1202 -- Idempotence: a second add_provenance on an already-tracked table is
1203 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
1204 -- re-run freely.
1205 IF EXISTS (
1206 SELECT 1 FROM pg_attribute
1207 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
1208 ) THEN
1209 RAISE NOTICE 'table % already has provenance tracking', _tbl;
1210 RETURN;
1211 END IF;
1212 -- No DEFAULT: the guard trigger mints the UUID, so the trigger can
1213 -- distinguish "user omitted" (NULL) from "user supplied a value".
1214 -- No UNIQUE: we no longer rely on it to keep the table TID -- the
1215 -- guard does that semantically -- and a UNIQUE would reject the
1216 -- legitimate cross-table UUID copy that just flips the table to
1217 -- OPAQUE. We keep a plain index for fast UUID-keyed lookups.
1218 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
1219 EXECUTE format(
1220 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
1221 _tbl);
1222 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
1223 EXECUTE format(
1224 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
1225 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
1226 _tbl);
1227 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
1228 -- Seed the base-ancestor set to {self}: a base TID table's atoms
1229 -- come from itself and no other relation. CTAS-derived tables
1230 -- inherit unions of source ancestor sets; that is handled by the
1231 -- CTAS hook (a separate slice), not here.
1232 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
1233END
1234$$ LANGUAGE plpgsql SECURITY DEFINER;
1235
1236/**
1237 * @brief Remove provenance tracking from a table
1238 *
1239 * Drops the <tt>provsql</tt> column and associated triggers.
1240 *
1241 * @param _tbl the table to remove provenance tracking from
1242 */
1243CREATE OR REPLACE FUNCTION remove_provenance(_tbl REGCLASS)
1244 RETURNS VOID AS
1245$$
1246DECLARE
1247BEGIN
1248 PERFORM provsql.remove_table_info(_tbl::oid);
1249 -- Idempotence, mirroring add_provenance: removing provenance from a
1250 -- table that does not have it is a NOTICE-and-no-op, so setup scripts
1251 -- and notebook cells can be re-run freely. The metadata strip above
1252 -- still runs, so a table left half-tracked is cleaned up.
1253 IF NOT EXISTS (
1254 SELECT 1 FROM pg_attribute
1255 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
1256 ) THEN
1257 RAISE NOTICE 'table % does not have provenance tracking', _tbl;
1258 RETURN;
1259 END IF;
1260 -- Drop the BEFORE INSERT/UPDATE guard first: it has a column
1261 -- dependency on provsql (via the OF provsql clause), so the
1262 -- subsequent DROP COLUMN would otherwise raise.
1263 BEGIN
1264 EXECUTE format('DROP TRIGGER provenance_guard on %s', _tbl);
1265 EXCEPTION WHEN undefined_object THEN
1266 END;
1267 EXECUTE format('ALTER TABLE %s DROP COLUMN provsql', _tbl);
1268 BEGIN
1269 EXECUTE format('DROP TRIGGER add_gate on %s', _tbl);
1270 EXCEPTION WHEN undefined_object THEN
1271 END;
1272 BEGIN
1273 EXECUTE format('DROP TRIGGER insert_statement on %s', _tbl);
1274 EXECUTE format('DROP TRIGGER update_statement on %s', _tbl);
1275 EXECUTE format('DROP TRIGGER delete_statement on %s', _tbl);
1276 EXCEPTION WHEN undefined_object THEN
1277 END;
1278END
1279$$ LANGUAGE plpgsql;
1280
1281/**
1282 * @brief Set up provenance for a table with duplicate key values
1283 *
1284 * When a table has duplicate rows for a given key, this function
1285 * replaces simple input gates with multivalued input (mulinput) gates
1286 * that model a uniform distribution over duplicates. The uniform
1287 * weight is the default a row evaluates at, not a probability written
1288 * on it, so the usual next step -- @c "SELECT set_prob(provenance(),
1289 * p) FROM t" -- gives each row its real probability as a first write.
1290 *
1291 * @param _tbl the table to repair
1292 * @param key_att the key attribute(s) as a comma-separated string, or
1293 * empty string if the whole table is one group
1294 */
1295CREATE OR REPLACE FUNCTION repair_key(_tbl REGCLASS, key_att TEXT)
1296 RETURNS VOID AS
1297$$
1298DECLARE
1299 r RECORD;
1300 rows_query TEXT;
1301 block_key_cols INT2[];
1302BEGIN
1303 -- Resolve the (possibly comma-separated) key_att TEXT into the
1304 -- corresponding pg_attribute.attnum values for the safe-query
1305 -- metadata. Names are trimmed; quoting is not supported because
1306 -- repair_key has never accepted quoted identifiers in key_att.
1307 IF key_att = '' THEN
1308 block_key_cols := ARRAY[]::INT2[];
1309 ELSE
1310 SELECT array_agg(a.attnum ORDER BY t.ord)::INT2[]
1311 INTO block_key_cols
1312 FROM unnest(string_to[](key_att, ',')) WITH ORDINALITY AS t(name, ord)
1313 JOIN pg_attribute a
1314 ON a.attrelid = _tbl
1315 AND a.attname = trim(t.name)
1316 AND a.attnum > 0
1317 AND NOT a.attisdropped;
1318 IF block_key_cols IS NULL OR array_length(block_key_cols, 1) IS NULL THEN
1319 RAISE EXCEPTION 'repair_key: could not resolve key columns from "%"', key_att;
1320 END IF;
1321 IF array_length(block_key_cols, 1) > 16 THEN
1322 RAISE EXCEPTION 'repair_key: block key wider than 16 columns is not supported';
1323 END IF;
1324 END IF;
1325
1326 -- Same column shape as add_provenance: no UNIQUE, no DEFAULT past
1327 -- the initial backfill (the guard trigger added after the rename
1328 -- takes over both jobs once the column has been renamed to its
1329 -- final name). The DEFAULT is kept here only so the second pass
1330 -- below can read provsql_temp from the user-visible rows
1331 -- without a separate UPDATE.
1332 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql_temp UUID DEFAULT public.uuid_generate_v4()', _tbl);
1333
1334 -- Build a per-group mapping (key columns + a fresh key_token + the
1335 -- group size) once, then use it for both the create_gate(key_token,
1336 -- 'input') first pass and the per-row mulinput second pass. Going
1337 -- through a temp table avoids re-running uuid_generate_v4() (which
1338 -- would produce different UUIDs the second time). USING (%1$s) on
1339 -- the second pass handles the multi-column case uniformly.
1340 -- ON COMMIT DROP plus the explicit DROP TABLE at the end of this
1341 -- function leave the temp table cleaned up across transactions and
1342 -- across repeated calls in the same transaction.
1343 IF key_att = '' THEN
1344 EXECUTE format(
1345 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
1346 SELECT public.uuid_generate_v4() AS provsql_key_token,
1347 COUNT(*) AS provsql_group_size
1348 FROM %s', _tbl);
1349 rows_query := format(
1350 'SELECT t.provsql_temp,
1351 k.provsql_key_token AS key_token,
1352 ROW_NUMBER() OVER (ORDER BY t.ctid) AS within_group,
1353 k.provsql_group_size AS group_size
1354 FROM %s t CROSS JOIN provsql_repair_key_tmp k', _tbl);
1355 ELSE
1356 EXECUTE format(
1357 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
1358 SELECT %1$s,
1359 public.uuid_generate_v4() AS provsql_key_token,
1360 COUNT(*) AS provsql_group_size
1361 FROM %2$s
1362 GROUP BY %1$s', key_att, _tbl);
1363 rows_query := format(
1364 'SELECT t.provsql_temp,
1365 k.provsql_key_token AS key_token,
1366 ROW_NUMBER() OVER (PARTITION BY k.provsql_key_token
1367 ORDER BY t.ctid) AS within_group,
1368 k.provsql_group_size AS group_size
1369 FROM %2$s t
1370 JOIN provsql_repair_key_tmp k USING (%1$s)', key_att, _tbl);
1371 END IF;
1372
1373 -- Pass 1: one input gate per group key.
1374 FOR r IN SELECT provsql_key_token FROM provsql_repair_key_tmp LOOP
1375 PERFORM provsql.create_gate(r.provsql_key_token, 'input');
1376 END LOOP;
1377
1378 -- Pass 2: per row, attach a mulinput gate to its group's key token.
1379 -- The block size goes in info2 rather than the uniform 1/size going
1380 -- in the probability: a repaired row's probability is the user's to
1381 -- write (the documented "repair_key then set_prob(provenance(), p)"
1382 -- pattern), and probabilities are written once. A row nobody gives
1383 -- a probability evaluates at 1/size all the same -- see
1384 -- MMappedCircuit::getProb.
1385 FOR r IN EXECUTE rows_query LOOP
1386 PERFORM provsql.create_gate(r.provsql_temp, 'mulinput', ARRAY[r.key_token]);
1387 PERFORM provsql.set_infos(r.provsql_temp, r.within_group::INT,
1388 r.group_size::INT);
1389 END LOOP;
1390
1391 DROP TABLE provsql_repair_key_tmp;
1392
1393 EXECUTE format('ALTER TABLE %s ALTER COLUMN provsql_temp DROP DEFAULT', _tbl);
1394 EXECUTE format('ALTER TABLE %s RENAME COLUMN provsql_temp TO provsql', _tbl);
1395 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
1396 EXECUTE format(
1397 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
1398 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
1399 _tbl);
1400 PERFORM provsql.set_table_info(_tbl::oid, 'bid', block_key_cols);
1401 -- Base BID tables also have themselves as their sole ancestor. Same
1402 -- rationale as the @c add_provenance branch above.
1403 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
1404END
1405$$ LANGUAGE plpgsql;
1406
1407/**
1408 * @brief Event trigger that purges per-table provenance metadata when
1409 * a tracked relation is dropped outside of remove_provenance().
1411 * Plain DROP TABLE bypasses remove_provenance() and would otherwise
1412 * leave a stale row in provsql.table_info keyed by a now-recycled
1413 * OID, with confusing consequences for the safe-query rewriter the
1414 * next time the OID is reused. This trigger forwards every dropped
1415 * relation OID to provsql.remove_table_info(), which is a no-op for
1416 * relations that were not tracked. Both the deletion and the
1417 * registry cleanup below roll back with the DROP that triggered
1418 * them.
1419 */
1420CREATE OR REPLACE FUNCTION cleanup_table_info()
1421 RETURNS event_trigger AS
1422$$
1423DECLARE
1424 r RECORD;
1425BEGIN
1426 FOR r IN
1427 SELECT objid FROM pg_event_trigger_dropped_objects()
1428 WHERE object_type IN ('table', 'foreign table', 'materialized view')
1429 LOOP
1430 PERFORM provsql.remove_table_info(r.objid);
1431 -- Forget any maintained mapping whose source or mapping table is gone.
1432 DELETE FROM provsql.provenance_mapping_registry
1433 WHERE source = r.objid OR mapping = r.objid;
1434 END LOOP;
1435END
1436$$ LANGUAGE plpgsql;
1437
1438DROP EVENT TRIGGER IF EXISTS provsql_cleanup_table_info;
1439-- @c EXECUTE @c PROCEDURE (rather than the PG 11+ @c EXECUTE
1440-- @c FUNCTION alias) so the extension installs on PG 10 too.
1441CREATE EVENT TRIGGER provsql_cleanup_table_info ON sql_drop
1442 EXECUTE PROCEDURE provsql.cleanup_table_info();
1443
1444/**
1445 * @brief Registry of provenance mappings
1446 *
1447 * Each row records that mapping table @c mapping was built from the
1448 * @c attribute column of the provenance-tracked @c source table. When
1449 * @c maintained, every genuine insert into @c source also appends
1450 * @c (value, provenance) to it, so the mapping stays current; otherwise
1451 * the mapping is a snapshot and the row is here only so that a token
1452 * *replacement* carries the mapping over (see @c provenance_guard: a leaf
1453 * minted by @c replace_input names the same tuple, so its value is copied
1454 * to the new token in every mapping of the table, maintained or not).
1455 * Keyed on the mapping table, indexed on the source so the guard can look
1456 * up a table's mappings cheaply. Entries are removed when either table is
1457 * dropped (see @c cleanup_table_info).
1458 */
1459CREATE TABLE IF NOT EXISTS provsql.provenance_mapping_registry(
1460 mapping oid PRIMARY KEY,
1461 source oid NOT NULL,
1462 attribute name NOT NULL,
1463 maintained BOOLEAN NOT NULL DEFAULT false
1464);
1465ALTER TABLE provsql.provenance_mapping_registry
1466 ADD COLUMN IF NOT EXISTS maintained BOOLEAN NOT NULL DEFAULT false;
1467CREATE INDEX IF NOT EXISTS provenance_mapping_registry_source_idx
1468 ON provsql.provenance_mapping_registry(source);
1469
1470/**
1471 * @brief Create a provenance mapping table from an attribute
1472 *
1473 * Creates a new table mapping provenance tokens to values of a given
1474 * attribute, for use with semiring evaluation functions.
1475 * Idempotent: if the mapping table already exists, raises a NOTICE and
1476 * changes nothing (drop it first to rebuild).
1477 *
1478 * @param newtbl name of the mapping table to create
1479 * @param oldtbl source table with provenance tracking
1480 * @param att attribute whose values populate the mapping
1481 * @param preserve_case if true, quote the table name to preserve case
1482 * @param maintained if true, later inserts into @c oldtbl keep the mapping
1483 * current, and it stays correct after data modification
1484 * (deletes/updates rewrite a row's provsql, but the validity stays
1485 * keyed to the original input token). @c att must then be a plain
1486 * column name. When false (the default) the table is a one-off
1487 * snapshot; either way the mapping is recorded in
1488 * @c provenance_mapping_registry, so a row whose input gate is
1489 * replaced by @c provsql.replace_input keeps its value in it.
1490 */
1491CREATE OR REPLACE FUNCTION create_provenance_mapping(
1492 newtbl TEXT,
1493 oldtbl REGCLASS,
1494 att TEXT,
1495 preserve_case BOOL DEFAULT 'f',
1496 maintained BOOL DEFAULT false
1497) RETURNS VOID AS
1498$$
1499DECLARE
1500BEGIN
1501 -- Idempotence: when the mapping table already exists, leave it alone
1502 -- with a NOTICE (re-runnable setup scripts / notebook cells). Drop it
1503 -- first to rebuild a stale mapping.
1504 IF (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
1505 ELSE to_regclass(newtbl) END) IS NOT NULL THEN
1506 RAISE NOTICE 'mapping table % already exists', newtbl;
1507 RETURN;
1508 END IF;
1509 -- ON COMMIT DROP only fires at COMMIT: several mapping creations in
1510 -- one transaction (a notebook cell, a setup script run via psql -1)
1511 -- would otherwise collide on the leftover temp table. The to_regclass
1512 -- probe (rather than DROP IF EXISTS) keeps the first call NOTICE-free.
1513 IF to_regclass('pg_temp.tmp_provsql') IS NOT NULL THEN
1514 DROP TABLE tmp_provsql;
1515 END IF;
1516 EXECUTE format('CREATE TEMP TABLE tmp_provsql ON COMMIT DROP AS TABLE %s', oldtbl);
1517 ALTER TABLE tmp_provsql RENAME provsql TO provenance;
1518 -- The mapping is keyed by gate identity (input-token UUIDs), so peel any
1519 -- transparent annotation wrapper (e.g. the inversion-free certificate a
1520 -- certified query attaches to its row roots) off the captured tokens.
1521 UPDATE tmp_provsql SET provenance = provsql.strip_annotations(provenance)
1522 WHERE provsql.get_gate_type(provenance) = 'annotation';
1523 IF preserve_case THEN
1524 EXECUTE format('CREATE TABLE %I AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
1525 EXECUTE format('CREATE INDEX ON %I(provenance)', newtbl);
1526 ELSE
1527 EXECUTE format('CREATE TABLE %s AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
1528 EXECUTE format('CREATE INDEX ON %s(provenance)', newtbl);
1529 END IF;
1530 -- Register the mapping. When maintained, genuine inserts into oldtbl
1531 -- keep it current (see provenance_guard); keyed to the input token, so
1532 -- it survives the provsql rewrites that data modification performs.
1533 -- A snapshot mapping is registered too, so that replacing a row's input
1534 -- gate (provsql.replace_input) carries the row's value over to the new
1535 -- token: the tuple is the same one, only its token moved.
1536 INSERT INTO provsql.provenance_mapping_registry(mapping, source, attribute, maintained)
1537 VALUES (
1538 (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
1539 ELSE to_regclass(newtbl) END)::oid,
1540 oldtbl::oid, att, maintained)
1541 ON CONFLICT (mapping)
1542 DO UPDATE SET source = EXCLUDED.source, attribute = EXCLUDED.attribute,
1543 maintained = EXCLUDED.maintained;
1544END
1545$$ LANGUAGE plpgsql;
1546
1547/** @} */
1548
1549/** @defgroup internal_constants Internal constants
1550 * UUID namespace and identity element functions used for
1551 * deterministic gate generation.
1552 * @{
1553 */
1554
1555/** @brief Return the ProvSQL UUID namespace (used for deterministic gate UUIDs) */
1556CREATE OR REPLACE FUNCTION uuid_ns_provsql() RETURNS UUID AS
1557$$
1558 -- uuid_generate_v5(uuid_ns_url(),'http://pierre.senellart.com/software/provsql/')
1559 SELECT '920d4f02-8718-5319-9532-d4ab83a64489'::UUID
1560$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1561
1562/** @brief Return the UUID of the semiring zero gate */
1563CREATE OR REPLACE FUNCTION gate_zero() RETURNS UUID AS
1564$$
1565 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'zero');
1566$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1568/** @brief Return the UUID of the semiring one gate */
1569CREATE OR REPLACE FUNCTION gate_one() RETURNS UUID AS
1570$$
1571 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'one');
1572$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1573
1574/** @brief Return the epsilon threshold used for probability comparisons */
1575CREATE OR REPLACE FUNCTION epsilon() RETURNS DOUBLE PRECISION AS
1576$$
1577 SELECT CAST(0.001 AS DOUBLE PRECISION)
1578$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1579
1580/** @} */
1581
1582/** @defgroup semiring_operations Semiring operations
1583 * Functions that build provenance circuit gates for semiring operations.
1584 * These are called internally by the query rewriter.
1585 *
1586 * They are declared @c IMMUTABLE: each derives its gate UUID
1587 * deterministically from its arguments (a @c uuid5 content address) and
1588 * the @c create_gate write at that address is idempotent, so the token a
1589 * call returns is a pure function of its inputs. The marking matters for
1590 * parallelism: PL/pgSQL runs a non-volatile function's inner SPI
1591 * read-only, so the per-row builders the rewriter injects into a scan do
1592 * not call @c CommandCounterIncrement -- which would raise "cannot start
1593 * commands during a parallel operation" once the enclosing statement has
1594 * gone parallel. A @c VOLATILE builder both blocks that parallel plan and
1595 * loses the query-wide speed-up.
1596 * @{
1597 */
1598
1599/**
1600 * @brief Create a times (product) gate from multiple provenance tokens
1601 *
1602 * Filters out NULL and one-gates; returns gate_one() if all tokens
1603 * are trivial, or a single token if only one remains.
1604 *
1605 * Before creating an ordinary gate, the *times-canonical* address of
1606 * the surviving multiset -- @c uuid5('times-canonical{sorted tokens}')
1607 * -- is probed: the reachability rewriter pre-creates there, for
1608 * self-join conjunctions of reachability tokens, a certified
1609 * equivalent (the all-members-reachable circuit; see
1610 * @c plant_reach_cover). Ordinary creation never writes under that
1611 * recipe, so a hit is always a deliberate plant; the ordinary
1612 * order-dependent recipe is used otherwise, so ordinary
1613 * times gates (and their formula rendering) are untouched.
1614 */
1615CREATE OR REPLACE FUNCTION provenance_times(VARIADIC tokens UUID[])
1616 RETURNS UUID AS
1617$$
1618DECLARE
1619 times_token UUID;
1620 filtered_tokens UUID[];
1621 canonical UUID;
1622BEGIN
1623 -- A NULL element reads as the ⊗-neutral 1: it is the token slot of an
1624 -- untracked source (a join against an untracked table), which is
1625 -- certain. Contrast provenance_plus / provenance_monus, where NULL
1626 -- reads as the ⊕- / ⊖-right-neutral 0: each combinator maps NULL to
1627 -- its own neutral element. Nothing may therefore hand a NULL to ⊗
1628 -- meaning "false"; a comparison with a NULL operand goes through
1629 -- provenance_cmp, which returns gate_zero for it.
1630 SELECT array_agg(t) FROM unnest(tokens) t WHERE t IS NOT NULL AND t <> gate_one() INTO filtered_tokens;
1631
1632 -- Dispatch on the FILTERED count: a single survivor short-circuits
1633 -- to that token directly (no useless single-child times gate); zero
1634 -- survivors collapse to the identity. Using array_length(tokens, 1)
1635 -- here would miss the [one, cmp] → [cmp] case, leaving the cmp wrapped
1636 -- in a one-child times when its only sibling was gate_one().
1637 CASE coalesce(array_length(filtered_tokens, 1), 0)
1638 WHEN 0 THEN
1639 times_token:=gate_one();
1640 WHEN 1 THEN
1641 times_token:=filtered_tokens[1];
1642 ELSE
1643 -- Computed separately from the filtering aggregate above: an
1644 -- ORDER BY aggregate there would make the planner feed *both*
1645 -- aggregates sorted input, scrambling the stored children order.
1646 SELECT uuid_generate_v5(uuid_ns_provsql(),
1647 concat('times-canonical', array_agg(t ORDER BY t)))
1648 FROM unnest(filtered_tokens) t
1649 INTO canonical;
1650 IF get_gate_type(canonical) = 'times' THEN
1651 -- A deliberate pre-creation at the canonical address: same
1652 -- children, same product.
1653 times_token := canonical;
1654 ELSE
1655 times_token := uuid_generate_v5(uuid_ns_provsql(),concat('times',filtered_tokens));
1656
1657 PERFORM create_gate(times_token, 'times', ARRAY_AGG(t)) FROM UNNEST(filtered_tokens) AS t WHERE t IS NOT NULL;
1658 END IF;
1659 END CASE;
1660
1661 RETURN times_token;
1662END
1663$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1664
1665/**
1666 * @brief Create a monus (difference) gate from two provenance tokens
1667 *
1668 * Implements m-semiring monus. Returns token1 if token2 is NULL
1669 * (used for LEFT OUTER JOIN semantics in the EXCEPT rewriting).
1670 */
1671CREATE OR REPLACE FUNCTION provenance_monus(token1 UUID, token2 UUID)
1672 RETURNS UUID AS
1673$$
1674DECLARE
1675 monus_token UUID;
1676BEGIN
1677 IF token1 IS NULL THEN
1678 RAISE EXCEPTION USING MESSAGE='provenance_monus is called with first argument NULL';
1679 END IF;
1680
1681 IF token2 IS NULL THEN
1682 -- The ⊖-right-neutral 0: a NULL second argument is the no-match case
1683 -- of the difference operator's LEFT OUTER JOIN (nothing to subtract),
1684 -- so X ⊖ NULL = X ⊖ 0 = X. Note this is NOT the NULL ≡ 1 reading of
1685 -- provenance_times; each combinator maps NULL to its own neutral.
1686 RETURN token1;
1687 END IF;
1688
1689 IF token1 = token2 THEN
1690 -- X-X=0
1691 monus_token:=gate_zero();
1692 ELSIF token1 = gate_zero() THEN
1693 -- 0-X=0
1694 monus_token:=gate_zero();
1695 ELSIF token2 = gate_zero() THEN
1696 -- X-0=X
1697 monus_token:=token1;
1698 ELSE
1699 monus_token:=uuid_generate_v5(uuid_ns_provsql(),concat('monus',token1,token2));
1700 PERFORM create_gate(monus_token, 'monus', ARRAY[token1::UUID, token2::UUID]);
1701 END IF;
1702
1703 RETURN monus_token;
1704END
1705$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1706
1707/**
1708 * @brief Create a project gate for where-provenance tracking
1709 *
1710 * Records the mapping between input and output attribute positions.
1711 *
1712 * @param token child provenance token
1713 * @param positions array encoding attribute position mappings
1714 */
1715CREATE OR REPLACE FUNCTION provenance_project(token UUID, VARIADIC positions INT[])
1716 RETURNS UUID AS
1717$$
1718DECLARE
1719 project_token UUID;
1720 rec RECORD;
1721BEGIN
1722 project_token:=uuid_generate_v5(uuid_ns_provsql(),concat('project', token, positions));
1723 PERFORM create_gate(project_token, 'project', ARRAY[token]);
1724 PERFORM set_extra(project_token, ARRAY_AGG(pair)::TEXT)
1725 FROM (
1726 SELECT ARRAY[(CASE WHEN info=0 THEN NULL ELSE info END), idx] AS pair
1727 FROM unnest(positions) WITH ORDINALITY AS a(info, idx)
1728 ORDER BY idx
1729 ) t;
1730
1731 RETURN project_token;
1732END
1733$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1734
1735/**
1736 * @brief Create an equijoin gate for where-provenance tracking
1737 *
1738 * @param token child provenance token
1739 * @param pos1 attribute index in the first relation
1740 * @param pos2 attribute index in the second relation
1741 */
1742CREATE OR REPLACE FUNCTION provenance_eq(token UUID, pos1 INT, pos2 INT)
1743 RETURNS UUID AS
1744$$
1745DECLARE
1746 eq_token UUID;
1747 rec RECORD;
1748BEGIN
1749 eq_token:=uuid_generate_v5(uuid_ns_provsql(),concat('eq',token,pos1,',',pos2));
1750
1751 PERFORM create_gate(eq_token, 'eq', ARRAY[token::UUID]);
1752 PERFORM set_infos(eq_token, pos1, pos2);
1753 RETURN eq_token;
1755$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1756
1757/**
1758 * @brief Create a plus (sum) gate from an array of provenance tokens
1759 *
1760 * Filters out NULL and zero-gates; returns gate_zero() if all tokens
1761 * are trivial, or a single token if only one remains. Before creating
1762 * a gate, probes the *canonical* address of the multiset -- a dedicated
1763 * v5 recipe namespace over the sorted tokens (plus is commutative), in
1764 * which this function never creates anything, so a gate found there is
1765 * always a deliberate pre-creation computing the same sum. That is the
1766 * bounded-hop reachability route's hook: it plants, at the canonical
1767 * address of a vertex's per-length tokens, a certified gate over its
1768 * native within-bound circuit, keeping the natural hop-discarding query
1769 * on the linear evaluation route. Absent a canonical gate, the
1770 * ordinary order-dependent recipe is used, so ordinary plus
1771 * gates (and their formula rendering) are untouched.
1773CREATE OR REPLACE FUNCTION provenance_plus(tokens UUID[])
1774 RETURNS UUID AS
1775$$
1776DECLARE
1777 c INTEGER;
1778 plus_token UUID;
1779 filtered_tokens UUID[];
1780 canonical UUID;
1781BEGIN
1782 -- A NULL element reads as the ⊕-neutral 0: it stands for a row absent
1783 -- from the disjunction (a null-padded antijoin row whose token array
1784 -- slot is NULL), not for an untracked source. Contrast provenance_times,
1785 -- where NULL reads as the ⊗-neutral 1 (untracked source): each
1786 -- combinator maps NULL to its own neutral element.
1787 SELECT array_agg(t) FROM unnest(tokens) t
1788 WHERE t IS NOT NULL AND t <> gate_zero()
1789 INTO filtered_tokens;
1790
1791 c:=array_length(filtered_tokens, 1);
1792
1793 IF c = 0 THEN
1794 plus_token := gate_zero();
1795 ELSIF c = 1 THEN
1796 plus_token := filtered_tokens[1];
1797 ELSE
1798 -- Computed separately from the filtering aggregate above: an ORDER
1799 -- BY aggregate there would make the planner feed *both* aggregates
1800 -- sorted input, scrambling the stored (aggregation-order) children.
1801 SELECT uuid_generate_v5(uuid_ns_provsql(),
1802 concat('plus-canonical', array_agg(t ORDER BY t)))
1803 FROM unnest(filtered_tokens) t
1804 INTO canonical;
1805 IF get_gate_type(canonical) = 'plus' THEN
1806 -- A deliberate pre-creation at the canonical address: same
1807 -- children, same sum.
1808 plus_token := canonical;
1809 ELSE
1810 plus_token := uuid_generate_v5(
1811 uuid_ns_provsql(),
1812 concat('plus', filtered_tokens));
1813
1814 PERFORM create_gate(plus_token, 'plus', filtered_tokens);
1815 END IF;
1816 END IF;
1817
1818 RETURN plus_token;
1819END
1820$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1821
1823 * @brief Driver for provenance over recursive queries (WITH RECURSIVE).
1824 *
1825 * Invoked by the planner hook (@c lower_recursive_cte in @c provsql.c) when it
1826 * lowers a recursive CTE whose body touches provenance-tracked relations. The
1827 * hook deparses the CTE body to SQL and calls this function, which runs naive
1828 * bottom-up (fixpoint) evaluation: each round re-evaluates the body
1829 * @c base @c UNION @c recursive over a tracked working table until the
1830 * provenance tokens stop changing. Every round goes through ProvSQL's normal
1831 * rewriting, so the recursive join yields @c times gates, the untracked base
1832 * branch yields @c gate_one, and the @c UNION yields the @c plus merge of
1833 * alternative derivations -- no provenance is plumbed by hand here. The result
1834 * is left in a tracked temp table named @p work_name, which the hook then scans
1835 * in place of the CTE.
1836 *
1837 * The working tables (@p work_name and a scratch @c _new) are created once and
1838 * reused across rounds (TRUNCATE + INSERT), so the round count never
1839 * accumulates relation locks. Because content-addressed gate UUIDs make
1840 * structurally identical sub-circuits share, the fixpoint test is an exact
1841 * relational @c EXCEPT and the circuit stays the shared (polynomial) form.
1842 *
1843 * Scope: UNION (set) recursion. On *acyclic* input the structural fixpoint is
1844 * reached and the resulting circuit is the universal provenance, sound for any
1845 * semiring. On *cyclic* input the circuit never stabilises structurally; when
1846 * the session's provenance class (@c provsql.provenance) is @c 'absorptive' or
1847 * @c 'BOOLEAN' we instead stop at the value-fixpoint bound (number of
1848 * derivable tuples) -- every minimal, tuple-repetition-free derivation is then
1849 * covered, and the longer ones are absorbed in any absorptive semiring (after
1850 * Deutch, Milo, Roy & Tannen, ICDT 2014) -- and wrap the resulting tokens in
1851 * the @c 'absorptive' assumption marker, so that non-absorptive semiring
1852 * evaluations (counting, why-provenance: genuinely infinite on cyclic data)
1853 * refuse them while probability, Boolean, formula-as-circuit and min-plus
1854 * evaluations proceed. Under the general classes, cyclic input trips the
1855 * @p max_iter guard.
1856 *
1857 * This function has no @c SET @c search_path on purpose: @p body_sql is the
1858 * caller's deparsed query and must resolve relation names in the caller's path.
1859 *
1860 * @param body_sql the recursive CTE body, e.g.
1861 * @c 'SELECT 1 UNION SELECT e.dst FROM edge e JOIN reach r ON e.src=r.node'
1862 * @param work_name the working relation name @p body_sql references (the CTE name)
1863 * @param colnames comma-separated user columns, e.g. @c 'node'
1864 * @param coldef column definitions for the working table, e.g. @c 'node INTEGER'
1865 * @param max_iter safety bound on fixpoint rounds (non-termination guard)
1866 */
1867CREATE OR REPLACE FUNCTION eval_recursive(
1868 body_sql TEXT,
1869 work_name TEXT,
1870 colnames TEXT,
1871 coldef TEXT,
1872 max_iter INT DEFAULT 1000)
1873 RETURNS VOID AS
1874$$
1875DECLARE
1876 changed BOOLEAN; -- circuit changed structurally this round
1877 set_stable BOOLEAN; -- user-column tuple set unchanged this round
1878 iters INT := 0;
1879 new_count INT; -- rows in _new this round (INSERT ROW_COUNT)
1880 -- Under an absorptive semiring the provenance *value* converges on cyclic
1881 -- data even though the circuit keeps growing structurally. A minimal
1882 -- derivation cannot repeat a tuple, so it has depth <= (number of derivable
1883 -- tuples); after that many naive rounds the value equals the least fixpoint,
1884 -- and the surplus (longer, cyclic) derivations are absorbed at evaluation
1885 -- time. We learn that bound from the tuple-set fixpoint, stop there, and
1886 -- mark the resulting tokens with the 'absorptive' assumption so evaluation
1887 -- under a non-absorptive semiring refuses rather than silently returning a
1888 -- truncated value.
1889 absorptive_mode BOOLEAN :=
1890 coalesce(current_setting('provsql.provenance', true), 'semiring')
1891 IN ('absorptive', 'BOOLEAN');
1892 truncated BOOLEAN := false; -- exited at the value fixpoint (cyclic data)
1893 ntuples INT := NULL; -- the bound above, set once the tuple set stabilises
1894BEGIN
1895 EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
1896 DROP TABLE IF EXISTS _new;
1897
1898 -- Tracked working table (carries provsql), initially empty, plus a scratch
1899 -- table of the same shape; both reused across rounds.
1900 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID)', work_name, coldef);
1901 EXECUTE format('CREATE TEMP TABLE _new (LIKE %I)', work_name);
1902
1903 LOOP
1904 iters := iters + 1;
1905 -- Hard safety bound (also catches genuinely unbounded recursion, e.g. an
1906 -- unbounded counter, where even the tuple set never stabilises).
1907 IF iters > max_iter THEN
1908 RAISE EXCEPTION 'eval_recursive: no fixpoint after % rounds (cyclic data?)', max_iter;
1909 END IF;
1910
1911 -- One round of naive evaluation: re-run the CTE body over the current
1912 -- working table. INSERT targets a tracked table, so ProvSQL fills provsql.
1913 -- Take the row count from the INSERT itself (counting _new directly would be
1914 -- an aggregate over a provenance-tracked table -> an AGG_TOKEN).
1915 EXECUTE 'TRUNCATE _new';
1916 EXECUTE format('INSERT INTO _new(%s) %s', colnames, body_sql);
1917 GET DIAGNOSTICS new_count = ROW_COUNT;
1918
1919 -- Exact structural fixpoint test (content-addressed tokens => set equality).
1920 EXECUTE format(
1921 'SELECT EXISTS((TABLE _new EXCEPT TABLE %1$I) UNION ALL (TABLE %1$I EXCEPT TABLE _new))',
1922 work_name) INTO changed;
1923
1924 -- In an absorptive class, learn the round bound from the tuple-set
1925 -- fixpoint (the set always stabilises after finitely many rounds, even on
1926 -- cyclic data).
1927 IF absorptive_mode AND ntuples IS NULL THEN
1928 EXECUTE format(
1929 'SELECT NOT EXISTS('
1930 || '(SELECT %2$s FROM _new EXCEPT SELECT %2$s FROM %1$I) UNION ALL '
1931 || '(SELECT %2$s FROM %1$I EXCEPT SELECT %2$s FROM _new))',
1932 work_name, colnames) INTO set_stable;
1933 IF set_stable THEN
1934 ntuples := new_count;
1935 END IF;
1936 END IF;
1938 -- Copy _new into the working table (tracked -> tracked carries the tokens).
1939 EXECUTE format('TRUNCATE %I', work_name);
1940 EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM _new', work_name, colnames);
1941
1942 -- Structural fixpoint: done (acyclic / fully converged) -- sound for any
1943 -- semiring.
1944 EXIT WHEN NOT changed;
1946 -- Absorptive class on cyclic data: once the value-fixpoint bound is
1947 -- reached (plus one confirming round, so that acyclic circuits whose
1948 -- token depth lags the tuple-set saturation still exit through the
1949 -- structural test above, untagged) we stop, even though the circuit
1950 -- is not structurally stable.
1951 IF absorptive_mode AND ntuples IS NOT NULL AND iters >= ntuples + 1 THEN
1952 truncated := true;
1953 EXIT;
1954 END IF;
1955 END LOOP;
1957 -- Tokens of a truncated (cyclic) fixpoint are sound only under absorptive
1958 -- evaluation: RECORD that in the circuit itself.
1959 IF truncated THEN
1960 EXECUTE format(
1961 'UPDATE %I SET provsql = provsql.provenance_assume(provsql, ''absorptive'')',
1962 work_name);
1963 END IF;
1964END
1965$$ LANGUAGE plpgsql SET client_min_messages = warning;
1966
1967/**
1968 * @brief Create a comparison gate for HAVING clause provenance
1969 *
1970 * @param left_token provenance token for the left operand
1971 * @param comparison_op OID of the comparison operator
1972 * @param right_token provenance token for the right operand
1973 */
1974CREATE OR REPLACE FUNCTION provenance_cmp(
1975 left_token UUID,
1976 comparison_op OID,
1977 right_token UUID
1978)
1979RETURNS UUID AS
1980$$
1981DECLARE
1982 cmp_token UUID;
1983BEGIN
1984 -- A comparison with a NULL operand (a NULL random_variable cell, or an
1985 -- aggregate that is NULL on the instance) is unknown under SQL's 3VL in
1986 -- every possible world: the row is annotated zero. The function must
1987 -- not be STRICT: a NULL result would read as the neutral token
1988 -- (provenance_times drops it), silently turning "unknown" into
1989 -- "certainly true".
1990 IF left_token IS NULL OR right_token IS NULL OR comparison_op IS NULL THEN
1991 RETURN gate_zero();
1992 END IF;
1993 -- deterministic v5 namespace id
1994 cmp_token := public.uuid_generate_v5(
1995 uuid_ns_provsql(),
1996 concat('cmp', left_token::TEXT, comparison_op::TEXT, right_token::TEXT)
1997 );
1998 -- wire it up in the circuit
1999 PERFORM create_gate(cmp_token, 'cmp', ARRAY[left_token, right_token]);
2000 PERFORM set_infos(cmp_token, comparison_op::INTEGER);
2001 RETURN cmp_token;
2002END
2003$$ LANGUAGE plpgsql
2004 SET search_path=provsql,pg_temp,public
2005 SECURITY DEFINER
2006 IMMUTABLE
2007 PARALLEL SAFE;
2008
2009/**
2010 * @brief The factors of a row annotation an aggregate comparison does not
2011 * subsume.
2012 *
2013 * A lifted comparison entails the existence of the group it ranges over, so it
2014 * supersedes that group's @c gate_delta instead of multiplying with it. This
2015 * reports which factors of @p tokens survive that supersede: a bare δ over the
2016 * compared group disappears, a @c times keeps its other factors, and anything
2017 * else -- an earlier comparison on the same group, an input -- is kept whole.
2018 */
2019CREATE FUNCTION cmp_surviving_factors(tokens UUID[], cmp UUID)
2020 RETURNS UUID[] AS
2021 'provsql', 'cmp_surviving_factors' LANGUAGE C PARALLEL SAFE STABLE;
2022
2024 * @brief Combine a lifted aggregate comparison with the row annotation it
2025 * supersedes only part of.
2026 *
2027 * @param cmp Gate of the lifted comparison.
2028 * @param tokens Row-annotation factors at the level owning the comparison.
2029 * @return @c cmp multiplied with whatever of @p tokens it does not subsume.
2030 */
2031CREATE OR REPLACE FUNCTION provenance_cmp_times(cmp UUID, tokens UUID[])
2032 RETURNS UUID AS
2033$$
2034DECLARE
2035 kept UUID[];
2036BEGIN
2037 kept := provsql.cmp_surviving_factors(tokens, cmp);
2038 IF kept IS NULL OR array_length(kept, 1) IS NULL THEN
2039 RETURN cmp;
2040 END IF;
2041 RETURN provsql.provenance_times(VARIADIC kept || cmp);
2042END
2043$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
2044
2045/**
2046 * @brief Create an arithmetic gate over scalar-valued provenance children
2048 * Builds a deterministic @c gate_arith from an operator tag and an
2049 * ordered list of children. The tag is one of the @c provsql_arith_op
2050 * ENUM values declared in @c src/provsql_utils.h
2051 * (@c PLUS=0, @c TIMES=1, @c MINUS=2, @c DIV=3, @c NEG=4) and is
2052 * stored in the gate's @c info1 field. Children must be UUIDs of
2053 * scalar-producing gates (@c gate_rv, @c gate_value, or another
2054 * @c gate_arith). The token UUID is derived deterministically from
2055 * @p op and @p children so identical sub-expressions share their gate.
2056 *
2057 * @param op Operator tag (@c provsql_arith_op).
2058 * @param children Ordered list of child gate UUIDs.
2059 * @return UUID of the (possibly pre-existing) @c gate_arith.
2060 */
2061CREATE OR REPLACE FUNCTION provenance_arith(
2062 op INTEGER,
2063 children UUID[]
2064)
2065RETURNS UUID AS
2066$$
2067DECLARE
2068 arith_token UUID;
2069BEGIN
2070 arith_token := public.uuid_generate_v5(
2071 uuid_ns_provsql(),
2072 concat('arith', op::TEXT, children::TEXT)
2073 );
2074 PERFORM create_gate(arith_token, 'arith', children);
2075 PERFORM set_infos(arith_token, op);
2076 RETURN arith_token;
2077END
2078$$ LANGUAGE plpgsql
2079 SET search_path=provsql,pg_temp,public
2080 SECURITY DEFINER
2081 IMMUTABLE
2082 PARALLEL SAFE
2083 STRICT;
2084
2085/**
2086 * @brief Create a guarded-selection gate over scalar (RV) children.
2088 * Builds a deterministic @c gate_case from the flattened wire list
2089 * @c [guard_1, value_1, ..., guard_k, value_k, default] (odd length): the
2090 * value of the first guard event that holds, else the default (first-match
2091 * semantics). Each guard is a Boolean event token (a @c gate_cmp or Boolean
2092 * combination); each value and the default are scalar-producing gates
2093 * (@c gate_rv, @c gate_value, @c gate_arith, another @c gate_case, ...). The
2094 * token UUID is derived deterministically from @p children so identical
2095 * @c CASE expressions share their gate.
2096 *
2097 * @param children Flattened guard/value wires ending with the default
2098 * (@c array_length must be odd and @c >= 1).
2099 * @return UUID of the (possibly pre-existing) @c gate_case.
2100 */
2101CREATE OR REPLACE FUNCTION provenance_case(
2102 children UUID[]
2104RETURNS UUID AS
2105$$
2106DECLARE
2107 case_token UUID;
2108BEGIN
2109 IF array_length(children, 1) IS NULL OR array_length(children, 1) % 2 = 0 THEN
2110 RAISE EXCEPTION 'provenance_case expects an odd number of children '
2111 '(guard/value pairs followed by a default), got %',
2112 coalesce(array_length(children, 1), 0);
2113 END IF;
2114 case_token := public.uuid_generate_v5(
2115 uuid_ns_provsql(),
2116 concat('case', children::TEXT)
2117 );
2118 PERFORM create_gate(case_token, 'case', children);
2119 RETURN case_token;
2120END
2121$$ LANGUAGE plpgsql
2122 SET search_path=provsql,pg_temp,public
2123 SECURITY DEFINER
2124 IMMUTABLE
2125 PARALLEL SAFE
2126 STRICT;
2128/** @} */
2129
2130/** @defgroup semiring_evaluation Semiring evaluation
2131 * Functions for evaluating provenance circuits over semirings,
2132 * both user-defined (via function references) and compiled (built-in).
2133 * @{
2135
2136/**
2137 * @brief Evaluate provenance using a compiled (built-in) semiring
2138 *
2139 * This C function handles semiring evaluation entirely in C++ for
2140 * better performance. The semiring is specified by name.
2141 *
2142 * @param token provenance token to evaluate
2143 * @param token2value mapping table from tokens to semiring values
2144 * @param semiring name of the compiled semiring (e.g., "formula", "counting")
2145 * @param element_one identity element of the semiring
2146 */
2147CREATE OR REPLACE FUNCTION provenance_evaluate_compiled(
2148 token UUID,
2149 token2value REGCLASS,
2150 semiring TEXT,
2151 element_one ANYELEMENT)
2152RETURNS ANYELEMENT AS
2153 'provsql', 'provenance_evaluate_compiled' LANGUAGE C PARALLEL SAFE STABLE;
2155
2156/**
2157 * @brief Evaluate provenance over a user-defined semiring (PL/pgSQL version)
2158 *
2159 * Recursively walks the provenance circuit and evaluates each gate
2160 * using the provided semiring operations. This is the generic version
2161 * that accepts semiring operations as function references.
2162 *
2163 * @param token provenance token to evaluate
2164 * @param token2value mapping table from tokens to semiring values
2165 * @param element_one identity element of the semiring
2166 * @param value_type OID of the semiring value type
2167 * @param plus_function semiring addition (aggregate)
2168 * @param times_function semiring multiplication (aggregate)
2169 * @param monus_function semiring monus (binary), or NULL
2170 * @param delta_function δ-semiring operator, or NULL
2171 */
2172CREATE OR REPLACE FUNCTION provenance_evaluate(
2173 token UUID,
2174 token2value REGCLASS,
2175 element_one ANYELEMENT,
2176 value_type REGTYPE,
2177 plus_function REGPROC,
2178 times_function REGPROC,
2179 monus_function REGPROC,
2180 delta_function REGPROC)
2181 RETURNS ANYELEMENT AS
2182$$
2183DECLARE
2184 gate_type PROVENANCE_GATE;
2185 result ALIAS FOR $0;
2186 children UUID[];
2187-- cmp_value ANYELEMENT;
2188-- temp_result ANYELEMENT;
2189 value_text TEXT;
2190BEGIN
2191 SELECT get_gate_type(token) INTO gate_type;
2192
2193 IF gate_type IS NULL THEN
2194 RETURN NULL;
2195
2196 ELSIF gate_type = 'input' THEN
2197 EXECUTE format('SELECT value FROM %s WHERE provenance=%L', token2value, token)
2198 INTO result;
2199 IF result IS NULL THEN
2200 result := element_one;
2201 END IF;
2202 ELSIF gate_type = 'mulinput' THEN
2203 SELECT concat('{',(get_children(token))[1]::TEXT,'=',(get_infos(token)).info1,'}')
2204 INTO result;
2205 ELSIF gate_type='update' THEN
2206 EXECUTE format('SELECT value FROM %s WHERE provenance=%L',token2value,token) INTO result;
2207 IF result IS NULL THEN
2208 result:=element_one;
2209 END IF;
2210 ELSIF gate_type = 'plus' THEN
2211 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
2212 plus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
2213 INTO result;
2214
2215 ELSIF gate_type = 'times' THEN
2216 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
2217 times_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
2218 INTO result;
2219
2220 ELSIF gate_type = 'monus' THEN
2221 IF monus_function IS NULL THEN
2222 RAISE EXCEPTION USING MESSAGE='Provenance with negation evaluated over a semiring without monus function';
2223 ELSE
2224 EXECUTE format('SELECT %s(a1,a2) FROM (SELECT provsql.provenance_evaluate(c[1],%L,%L::%s,%L,%L,%L,%L,%L) AS a1, ' ||
2225 'provsql.provenance_evaluate(c[2],%L,%L::%s,%L,%L,%L,%L,%L) AS a2 FROM get_children(%L) c) tmp',
2226 monus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function,
2227 token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
2228 INTO result;
2229 END IF;
2230
2231 ELSIF gate_type = 'eq' THEN
2232 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2233 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2234 INTO result;
2235
2236/* elsif gate_type = 'cmp' then
2237
2238 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2239 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2240 INTO temp_result;
2241
2242 EXECUTE format('SELECT get_extra((get_children(%L))[2])', token)
2243 INTO cmp_value;
2244
2245 IF temp_result::TEXT = cmp_value::TEXT THEN
2246 SELECT concat('{',temp_result::TEXT,'=',cmp_value::TEXT,'}')
2247 INTO result;
2248 ELSE
2249 RETURN gate_zero()
2250 */
2251
2252
2254 ELSIF gate_type = 'delta' THEN
2255 IF delta_function IS NULL THEN
2256 RAISE EXCEPTION USING MESSAGE='Provenance with aggregation evaluated over a semiring without delta function';
2257 ELSE
2258 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',
2259 delta_function, token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2260 INTO result;
2261 END IF;
2262
2263 ELSIF gate_type = 'zero' THEN
2264 EXECUTE format('SELECT %I(a) FROM (SELECT %L::%I AS a WHERE FALSE) temp', plus_function, element_one, value_type)
2265 INTO result;
2266
2267 ELSIF gate_type = 'one' THEN
2268 EXECUTE format('SELECT %L::%I', element_one, value_type)
2269 INTO result;
2270
2271 ELSIF gate_type = 'project' THEN
2272 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2273 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2274 INTO result;
2276 ELSIF gate_type = 'annotation' THEN
2277 -- Transparent single-child wrapper (carries the inversion-free certificate
2278 -- / per-input order keys in extra, inert for every semiring): evaluate
2279 -- through to the child, like 'project'.
2280 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2281 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2282 INTO result;
2283
2284 ELSE
2285 RAISE EXCEPTION USING MESSAGE='provenance_evaluate cannot be called on formulas using ' || gate_type || ' gates; use compiled semirings instead';
2286 END IF;
2287
2288 RETURN result;
2289END
2290$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
2291
2293/**
2294 * @brief Evaluate provenance over a user-defined semiring (C version)
2296 * Optimized C implementation of provenance_evaluate. Infers the
2297 * value type from element_one. Monus and delta functions are optional.
2298 *
2299 * @param token provenance token to evaluate
2300 * @param token2value mapping table from tokens to semiring values
2301 * @param element_one identity element of the semiring
2302 * @param plus_function semiring addition (aggregate)
2303 * @param times_function semiring multiplication (aggregate)
2304 * @param monus_function semiring monus, or NULL if not needed
2305 * @param delta_function δ-semiring operator, or NULL if not needed
2306 */
2307CREATE OR REPLACE FUNCTION provenance_evaluate(
2308 token UUID,
2309 token2value REGCLASS,
2310 element_one ANYELEMENT,
2311 plus_function REGPROC,
2312 times_function REGPROC,
2313 monus_function REGPROC = NULL,
2314 delta_function REGPROC = NULL)
2315 RETURNS ANYELEMENT AS
2316 'provsql','provenance_evaluate' LANGUAGE C STABLE;
2317
2318/** @} */
2319
2320/** @defgroup circuit_introspection Circuit introspection
2321 * Functions for examining the structure of provenance circuits,
2322 * used by visualization and where-provenance features.
2323 * @{
2324 */
2325
2326/** @brief Row type for sub_circuit_with_desc results */
2327CREATE TYPE GATE_WITH_DESC AS (f UUID, t UUID, gate_type PROVENANCE_GATE, desc_str CHARACTER VARYING, infos INTEGER[], extra TEXT);
2328
2329/**
2330 * @brief Return the sub-circuit reachable from a token, with descriptions
2331 *
2332 * Recursively traverses the provenance circuit from the given token and
2333 * returns all edges together with input gate descriptions from the
2334 * mapping table.
2335 *
2336 * @param token root provenance token
2337 * @param token2desc mapping table providing descriptions for input gates
2338 */
2339CREATE OR REPLACE FUNCTION sub_circuit_with_desc(
2340 token UUID,
2341 token2desc REGCLASS) RETURNS SETOF GATE_WITH_DESC AS
2342$$
2343BEGIN
2344 RETURN QUERY EXECUTE
2345 'WITH RECURSIVE transitive_closure(f,t,gate_type) AS (
2346 SELECT $1,t,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) AS t
2347 UNION ALL
2348 SELECT p1.t,u,provsql.get_gate_type(p1.t) FROM transitive_closure p1, unnest(provsql.get_children(p1.t)) AS u)
2349 SELECT *, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
2350 SELECT f::UUID,t::UUID,gate_type,NULL FROM transitive_closure
2351 UNION ALL
2352 SELECT p2.provenance::UUID as f, NULL::UUID, ''input'', CAST (p2.value AS varchar) FROM transitive_closure p1 JOIN ' || token2desc || ' AS p2
2353 ON p2.provenance=t
2354 UNION ALL
2355 SELECT provenance::UUID as f, NULL::UUID, ''input'', CAST (value AS varchar) FROM ' || token2desc || ' WHERE provenance=$1
2356 ) t'
2357 USING token LOOP;
2358 RETURN;
2359END
2360$$ LANGUAGE plpgsql PARALLEL SAFE;
2361
2362/**
2363 * @brief Identify which table and how many columns a provenance token belongs to
2364 *
2365 * Searches all provenance-tracked tables for a row matching the given
2366 * token and returns the table name and column count.
2368 * @param token provenance token to look up
2369 * @param table_name (OUT) the table containing this token
2370 * @param nb_columns (OUT) number of non-provenance columns in that table
2372CREATE OR REPLACE FUNCTION identify_token(
2373 token UUID, OUT table_name REGCLASS, OUT nb_columns INTEGER) AS
2374$$
2375DECLARE
2376 t RECORD;
2377 result RECORD;
2378BEGIN
2379 table_name:=NULL;
2380 nb_columns:=-1;
2381 FOR t IN
2382 SELECT relname,
2383 (SELECT count(*) FROM pg_attribute a2 WHERE a2.attrelid=a1.attrelid AND attnum>0 AND atttypid<>0)-1 c
2384 FROM pg_attribute a1 JOIN pg_type ON atttypid=pg_type.oid
2385 JOIN pg_class ON attrelid=pg_class.oid
2386 JOIN pg_namespace ON relnamespace=pg_namespace.oid
2387 WHERE typname='UUID' AND relkind='r'
2388 AND nspname<>'provsql'
2389 AND attname='provsql'
2390 LOOP
2391 EXECUTE format('SELECT * FROM %I WHERE provsql=%L',t.relname,token) INTO result;
2392 -- Test result.provsql rather than the whole RECORD: "RECORD IS NOT NULL"
2393 -- is true only when every field is non-null, so a matched row that has any
2394 -- NULL data column would be wrongly skipped. The provsql column is the
2395 -- (non-null) token we matched on, so it is set iff a row was found.
2396 IF result.provsql IS NOT NULL THEN
2397 table_name:=t.relname;
2398 nb_columns:=t.c;
2399 EXIT;
2400 END IF;
2401 END LOOP;
2402END
2403$$ LANGUAGE plpgsql STRICT;
2404
2405/**
2406 * @brief Return the sub-circuit for where-provenance computation
2408 * Similar to sub_circuit_with_desc but resolves input gates to their
2409 * source table and column count for where-provenance evaluation.
2410 */
2411CREATE OR REPLACE FUNCTION sub_circuit_for_where(token UUID)
2412 RETURNS TABLE(f UUID, t UUID, gate_type PROVENANCE_GATE, table_name REGCLASS, nb_columns INTEGER, infos INTEGER[], extra TEXT) AS
2413$$
2414 WITH RECURSIVE transitive_closure(f,t,idx,gate_type) AS (
2415 SELECT $1,t,id,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) WITH ORDINALITY AS a(t,id)
2416 UNION ALL
2417 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)
2418 ) SELECT f, t, gate_type, table_name, nb_columns, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
2419 -- One row per distinct (parent, child, child-position) edge. The
2420 -- recursive closure (UNION ALL) re-emits a gate's outgoing edges once per
2421 -- path that reaches it, so a *shared* non-input gate would otherwise be
2422 -- reported with duplicate edges; DISTINCT on the (f,t,idx) triple
2423 -- collapses those while keeping genuine repeated children (same f,t,
2424 -- different idx, e.g. a self-product). Without this, a shared
2425 -- single-child gate (notably an inversion-free order-marker annotation)
2426 -- gets its child wired k times in the where-circuit -> the locator sets
2427 -- are duplicated k-fold.
2428 SELECT DISTINCT f, t::UUID, idx, gate_type, NULL::REGCLASS AS table_name, NULL::INTEGER AS nb_columns FROM transitive_closure
2429 UNION ALL
2430 SELECT DISTINCT t, NULL::UUID, NULL::INT, 'input'::PROVENANCE_GATE, (id).table_name, (id).nb_columns FROM transitive_closure JOIN (SELECT t AS prov, provsql.identify_token(t) as id FROM transitive_closure WHERE t NOT IN (SELECT f FROM transitive_closure)) temp ON t=prov
2431 UNION ALL
2432 SELECT DISTINCT $1, NULL::UUID, NULL::INT, 'input'::PROVENANCE_GATE, (id).table_name, (id).nb_columns FROM (SELECT provsql.identify_token($1) AS id WHERE $1 NOT IN (SELECT f FROM transitive_closure)) temp
2433 ) t
2434 -- order each parent's edges by child position so the where-circuit's TIMES
2435 -- concatenation reproduces the column order (input rows have idx NULL).
2436 ORDER BY f, idx
2437$$
2438LANGUAGE sql;
2440/**
2441 * @brief BFS expansion of a provenance circuit, capped at @p max_depth
2442 *
2443 * Returns one row per (parent, child) edge in the BFS-bounded subgraph
2444 * rooted at @p root, plus one row for the root with <tt>parent</tt> and
2445 * <tt>child_pos</tt> NULL. Provenance circuits are DAGs, so a child gate
2446 * may have several parents within the bound; each such edge is reported
2447 * as a separate row, so callers must deduplicate on <tt>node</tt> if they
2448 * need a one-row-per-node view.
2449 *
2450 * <tt>depth</tt> is the node's longest-path distance from @p root
2451 * within the depth bound (the standard circuit-depth notion), so for
2452 * an edge (parent, child) it is the case that
2453 * <tt>child.depth &gt;= parent.depth + 1</tt>, except at the
2454 * <tt>max_depth</tt> truncation frontier. A node at
2455 * <tt>depth = max_depth</tt> is not
2456 * expanded; callers can detect a partial expansion by comparing
2457 * <tt>provsql.get_children</tt> length against the number of outgoing
2458 * edges reported.
2459 *
2460 * <tt>info1</tt> and <tt>info2</tt> are the INTEGER values stored on
2461 * the gate by <tt>provsql.set_infos</tt>, formatted as TEXT; their
2462 * meaning is gate-type-specific (see <tt>provsql.set_infos</tt>).
2463 *
2464 * @param root root provenance token
2465 * @param max_depth maximum BFS depth (default 8)
2466 */
2467CREATE OR REPLACE FUNCTION circuit_subgraph(root UUID, max_depth INT DEFAULT 8)
2468 RETURNS TABLE(node UUID, parent UUID, child_pos INT, gate_type TEXT, info1 TEXT, info2 TEXT, depth INT) AS
2469$$
2470 WITH RECURSIVE bfs(node, parent, child_pos, depth) AS (
2471 SELECT root, NULL::UUID, NULL::INT, 0
2472 UNION ALL
2473 SELECT c.t, b.node, c.idx::INT, b.depth + 1
2474 FROM bfs b
2475 CROSS JOIN LATERAL unnest(provsql.get_children(b.node))
2476 WITH ORDINALITY AS c(t, idx)
2477 WHERE b.depth < max_depth
2478 ),
2479 -- Each node's canonical depth is its longest-path distance from the
2480 -- root (the standard circuit-depth notion: the longest chain of
2481 -- gates separating the node from the output). The recursive CTE
2482 -- enumerates paths up to @c max_depth, so MAX over those is the
2483 -- longest path of length at most @c max_depth.
2484 node_depth AS (
2485 SELECT node, MAX(depth) AS depth FROM bfs GROUP BY node
2486 ),
2487 -- All distinct (parent, child, child_pos) triples seen during the BFS.
2488 -- A child reached from k parents within the bound contributes k rows.
2489 -- Self-joins (times(x, x)) contribute one row per child position.
2490 edges AS (
2491 SELECT DISTINCT parent, node AS child, child_pos
2492 FROM bfs WHERE parent IS NOT NULL
2493 )
2494 SELECT
2495 d.node,
2496 e.parent,
2497 e.child_pos,
2498 provsql.get_gate_type(d.node)::TEXT,
2499 i.info1::TEXT,
2500 i.info2::TEXT,
2501 d.depth
2502 FROM node_depth d
2503 LEFT JOIN edges e ON e.child = d.node
2504 LEFT JOIN LATERAL provsql.get_infos(d.node) i ON TRUE
2505 ORDER BY d.depth, d.node, e.parent;
2506$$ LANGUAGE sql STABLE PARALLEL SAFE;
2507
2508/**
2509 * @brief BFS subgraph of the IN-MEMORY simplified circuit rooted at @p root.
2510 *
2511 * Same row shape as @ref circuit_subgraph plus an inline @c extra
2512 * column, but built from the @c GenericCircuit returned by
2513 * @c getGenericCircuit -- i.e. AFTER @c provsql.simplify_on_load
2514 * passes (RangeCheck, ...) have rewritten any decidable @c gate_cmp
2515 * into Bernoulli @c gate_input / @c gate_zero / @c gate_one leaves.
2516 * Lets a renderer show the user what the evaluator actually sees,
2517 * without mutating the persisted DAG.
2518 *
2519 * Returns @c jsonb (an array of objects) rather than @c SETOF RECORD
2520 * to keep the C++ implementation free of SRF / @c FuncCallContext
2521 * boilerplate; callers either consume the array directly or expand
2522 * it via @c jsonb_array_elements.
2523 *
2524 * @param root Root provenance token.
2525 * @param max_depth Maximum BFS depth (default 8).
2526 */
2527CREATE OR REPLACE FUNCTION simplified_circuit_subgraph(
2528 root UUID, max_depth INT DEFAULT 8) RETURNS jsonb
2529 AS 'provsql','simplified_circuit_subgraph'
2530 LANGUAGE C STABLE PARALLEL SAFE;
2531
2532/**
2533 * @brief Empirical histogram of a scalar sub-circuit
2534 *
2535 * Returns a jsonb array of @c {bin_lo, bin_hi, count} objects covering
2536 * the observed @c [min, max] range of @p bins equal-width samples from
2537 * the sub-circuit rooted at @p token. Sample count is taken from
2538 * @c provsql.rv_mc_samples; pinning @c provsql.monte_carlo_seed makes
2539 * the result reproducible.
2540 *
2541 * Accepted root gate types are the scalar ones: @c gate_value (Dirac
2542 * at the constant, single bin), @c gate_rv (sampled from the leaf's
2543 * distribution), and @c gate_arith (sampled by recursing through the
2544 * arithmetic DAG, with shared @c gate_rv leaves correctly correlated
2545 * within an iteration). Any other gate type raises.
2546 *
2547 * @param token Root provenance token of a scalar sub-circuit.
2548 * @param bins Number of equal-width histogram bins (default 30).
2549 * @param prov Conditioning event (defaults to @c gate_one() = no
2550 * conditioning). When non-trivial, the histogram is
2551 * over the conditional distribution recovered by
2552 * rejection sampling on the joint circuit with @p token.
2553 */
2554CREATE OR REPLACE FUNCTION rv_histogram(
2555 token UUID, bins INT DEFAULT 30, prov UUID DEFAULT gate_one())
2556 RETURNS jsonb
2557 AS 'provsql','rv_histogram'
2558 LANGUAGE C VOLATILE PARALLEL SAFE;
2559
2560/**
2561 * @brief Sample the closed-form PDF and CDF of a (possibly truncated)
2562 * scalar distribution.
2563 *
2564 * Returns @c {"pdf": [{x, p}, ...], "cdf": [{x, p}, ...]} with @p samples
2565 * evenly-spaced points spanning the distribution's natural display
2566 * range (intersected with the conditioning event's interval when
2567 * @c prov is non-trivial). Used by ProvSQL Studio's Distribution
2568 * profile panel to overlay the analytical curve on the empirical
2569 * histogram from :sqlfunc:`rv_histogram` -- the simplifier's
2570 * analytical wins (e.g. @c c·Exp(λ) folding to @c Exp(λ/c)) become
2571 * visible as a smooth curve riding over the MC-sampled bars.
2572 *
2573 * Returns @c NULL when the root sub-circuit is not a closed-form
2574 * shape (V1: only bare @c gate_rv of Normal / Uniform / Exponential
2575 * / INTEGER-Erlang). The frontend reads @c NULL as "skip overlay"
2576 * without erroring, so the caller can dispatch this in parallel with
2577 * @c rv_histogram regardless of the underlying shape.
2578 *
2579 * @param token Scalar gate token (random_variable's UUID).
2580 * @param samples Number of (x, p) points; must be >= 2.
2581 * @param prov Conditioning event (defaults to @c gate_one() = no
2582 * conditioning). When non-trivial, the curves are
2583 * over the truncated distribution.
2584 */
2585CREATE OR REPLACE FUNCTION rv_analytical_curves(
2586 token UUID, samples INT DEFAULT 100, prov UUID DEFAULT gate_one())
2587 RETURNS jsonb
2588 AS 'provsql','rv_analytical_curves'
2589 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2590
2591/**
2592 * @brief Draw conditional Monte Carlo samples from a scalar gate.
2593 *
2594 * Returns up to @c n samples of the scalar value at @c token; when
2595 * @c prov is not the trivial @c gate_one() event, draws are accepted
2596 * only on iterations where @c prov evaluates true (rejection
2597 * sampling). Shared @c gate_rv leaves between @c token and @c prov
2598 * are loaded into a single joint circuit so the indicator's draw
2599 * and the value's draw share their per-iteration state.
2600 *
2601 * @param token Scalar sub-circuit root.
2602 * @param n Number of accepted samples to attempt.
2603 * @param prov Conditioning event (defaults to @c gate_one() = no
2604 * conditioning).
2605 *
2606 * Emits a @c NOTICE when the conditional acceptance rate yields fewer
2607 * than @c n samples within the @c provsql.rv_mc_samples budget so the
2608 * caller can choose to widen the budget.
2609 */
2610CREATE OR REPLACE FUNCTION rv_sample(
2611 token UUID, n INTEGER, prov UUID DEFAULT gate_one())
2612 RETURNS SETOF float8
2613 AS 'provsql','rv_sample'
2614 LANGUAGE C VOLATILE PARALLEL SAFE;
2615
2617 * @brief Resolve an input gate UUID back to its source row
2618 *
2619 * Searches every provenance-tracked relation for a row whose
2620 * <tt>provsql</tt> column equals @p UUID and returns the relation's
2621 * REGCLASS together with the row encoded as JSONB. Returns zero
2622 * rows when @p UUID is not the provenance token of any tracked row,
2623 * including when it identifies an internal gate (<tt>plus</tt>,
2624 * <tt>times</tt>, ...) rather than an input.
2625 *
2626 * Ordinarily exactly one row is returned, but if the same UUID
2627 * happens to appear as a <tt>provsql</tt> value in several tracked
2628 * tables, all matches are returned.
2629 *
2630 * @param UUID token to resolve
2631 */
2632CREATE OR REPLACE FUNCTION resolve_input(UUID UUID)
2633 RETURNS TABLE(relation REGCLASS, row_data JSONB) AS
2634$$
2635DECLARE
2636 t RECORD;
2637 rel REGCLASS;
2638 rd JSONB;
2639 -- ProvSQL's rewriter unconditionally appends a provsql column to the
2640 -- targetlist of any SELECT reading from a tracked relation; capture and
2641 -- discard it here rather than disabling the rewriter for the whole call.
2642 ign UUID;
2643BEGIN
2644 FOR t IN
2645 SELECT c.oid::REGCLASS AS regc
2646 FROM pg_attribute a
2647 JOIN pg_class c ON a.attrelid = c.oid
2648 JOIN pg_namespace ns ON c.relnamespace = ns.oid
2649 JOIN pg_type ty ON a.atttypid = ty.oid
2650 WHERE a.attname = 'provsql'
2651 AND ty.typname = 'UUID'
2652 AND c.relkind = 'r'
2653 AND ns.nspname <> 'provsql'
2654 AND a.attnum > 0
2655 LOOP
2656 FOR rel, rd, ign IN
2657 EXECUTE format(
2658 'SELECT %L::REGCLASS, to_jsonb(t) - ''provsql'', t.provsql FROM %s AS t WHERE provsql = $1',
2659 t.regc, t.regc)
2660 USING UUID
2661 LOOP
2662 relation := rel;
2663 row_data := rd;
2664 RETURN NEXT;
2665 END LOOP;
2666 END LOOP;
2668$$ LANGUAGE plpgsql STABLE;
2669
2670/** @} */
2671
2672/** @defgroup agg_token_type Type for the result of aggregate queries
2673 *
2674 * Custom type <tt>AGG_TOKEN</tt> for a provenance semimodule value, to
2675 * be used in attributes that are computed as a result of aggregation.
2676 * As for provenance tokens, this is simply a UUID, but this UUID is
2677 * displayed in a specific way (as the result of the aggregation
2678 * followed by a "(*)") to help with readability.
2679 *
2680 * The TEXT output is controlled by the
2681 * <tt>provsql.aggtoken_text_as_uuid</tt> GUC. By default it is off and
2682 * the cell renders as <tt>"value (*)"</tt>. When set to on (typical
2683 * for UI layers such as ProvSQL Studio), the cell renders as the
2684 * underlying UUID instead, so the caller can click through to the
2685 * provenance circuit; the value side is then recovered via
2686 * <tt>provsql.agg_token_value_text(UUID)</tt>.
2687 *
2688 * @{
2689 */
2690
2691CREATE TYPE AGG_TOKEN;
2692
2693/** @brief Input function for the AGG_TOKEN type (parses TEXT representation) */
2694CREATE OR REPLACE FUNCTION agg_token_in(CSTRING)
2695 RETURNS AGG_TOKEN
2696 AS 'provsql','agg_token_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2697
2698/**
2699 * @brief Output function for the AGG_TOKEN type
2700 *
2701 * Default: produces the human-friendly @c "value (*)" form, where
2702 * @c value is the running aggregate state.
2703 *
2704 * When the @c provsql.aggtoken_text_as_uuid GUC is on, returns the
2705 * underlying provenance UUID instead. UI layers (notably ProvSQL
2706 * Studio) flip this on per session so aggregate cells expose the
2707 * circuit root UUID for click-through; the @c "value (*)" display
2708 * string is recovered via @c provsql.agg_token_value_text(UUID).
2709 *
2710 * Marked STABLE rather than IMMUTABLE because the chosen output
2711 * shape now depends on a GUC that the same session can flip at
2712 * runtime.
2713 */
2714CREATE OR REPLACE FUNCTION agg_token_out(AGG_TOKEN)
2715 RETURNS CSTRING
2716 AS 'provsql','agg_token_out' LANGUAGE C STABLE STRICT PARALLEL SAFE;
2717
2718/** @brief Cast an AGG_TOKEN to its TEXT representation */
2719CREATE OR REPLACE FUNCTION agg_token_cast(AGG_TOKEN)
2720 RETURNS TEXT
2721 AS 'provsql','agg_token_cast' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2722
2723CREATE TYPE AGG_TOKEN (
2724 internallength = 117,
2725 input = agg_token_in,
2726 output = agg_token_out,
2727 alignment = char
2728);
2729
2730/** @brief Extract the UUID from an AGG_TOKEN (implicit cast to UUID) */
2731CREATE OR REPLACE FUNCTION agg_token_uuid(aggtok AGG_TOKEN)
2732 RETURNS UUID AS
2733$$
2734BEGIN
2735 RETURN agg_token_cast(aggtok)::UUID;
2736END
2737$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE PARALLEL SAFE;
2738
2739/** @brief Implicit PostgreSQL cast from AGG_TOKEN to UUID (delegates to agg_token_uuid()) */
2740CREATE CAST (AGG_TOKEN AS UUID) WITH FUNCTION agg_token_uuid(AGG_TOKEN) AS IMPLICIT;
2742/**
2743 * @brief Deterministic truth of a Boolean guard sub-circuit over aggregate
2744 * comparisons, evaluated in the actual world (all input tuples present).
2745 *
2746 * Guards are the shapes @c having_Expr_to_provenance_cmp mints: @c cmp gates
2747 * over aggregate-valued children (comparison-operator OID in @c info1),
2748 * @c times / @c plus combinations (AND / OR, with negation pushed into the
2749 * comparison operators), and the @c one / @c zero indicators of regular
2750 * (aggregate-free) conditions. Uses Kleene three-valued logic: returns
2751 * @c NULL on any other gate shape, or when an operand's deterministic value
2752 * cannot be resolved.
2753 */
2754CREATE OR REPLACE FUNCTION agg_guard_holds(token UUID)
2755 RETURNS BOOLEAN AS
2756$$
2757DECLARE
2758 gt PROVENANCE_GATE := get_gate_type(token);
2759 ch UUID[];
2760 opname TEXT;
2761 l NUMERIC;
2762 r NUMERIC;
2763 all_true BOOLEAN;
2764 any_true BOOLEAN;
2765 any_null BOOLEAN;
2766BEGIN
2767 IF gt = 'one' THEN
2768 RETURN true;
2769 ELSIF gt = 'zero' THEN
2770 RETURN false;
2771 ELSIF gt IN ('times', 'plus') THEN
2772 SELECT bool_and(h), bool_or(h), bool_or(h IS NULL)
2773 INTO all_true, any_true, any_null
2774 FROM (SELECT provsql.agg_guard_holds(c) AS h
2775 FROM unnest(get_children(token)) AS c) AS s;
2776 IF gt = 'times' THEN
2777 -- AND: false dominates unknown (bool_and skips NULL inputs, so it is
2778 -- false exactly when some child is false).
2779 RETURN CASE WHEN NOT all_true THEN false
2780 WHEN any_null THEN NULL
2781 ELSE true END;
2782 ELSE
2783 -- OR: true dominates unknown.
2784 RETURN CASE WHEN any_true THEN true
2785 WHEN any_null THEN NULL
2786 ELSE false END;
2787 END IF;
2788 ELSIF gt = 'cmp' THEN
2789 ch := get_children(token);
2790 l := agg_gate_value(ch[1]);
2791 r := agg_gate_value(ch[2]);
2792 IF l IS NULL OR r IS NULL THEN
2793 RETURN NULL;
2794 END IF;
2795 SELECT oprname INTO opname
2796 FROM pg_catalog.pg_operator WHERE oid = (get_infos(token)).info1;
2797 RETURN CASE opname
2798 WHEN '<' THEN l < r
2799 WHEN '<=' THEN l <= r
2800 WHEN '=' THEN l = r
2801 WHEN '<>' THEN l <> r
2802 WHEN '>=' THEN l >= r
2803 WHEN '>' THEN l > r
2804 END;
2805 END IF;
2806 RETURN NULL;
2807END
2808$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2809 SET search_path=provsql,pg_temp,public;
2810
2811/**
2812 * @brief Deterministic (actual-world) scalar value of an aggregate-carrying
2813 * gate.
2815 * Resolves the value an aggregate expression takes on the actual data -- the
2816 * value an @c AGG_TOKEN display cell carries: @c agg / @c arith gates RECORD
2817 * it in @c extra (set by aggregate evaluation and @c agg_arith_make), a
2818 * @c value gate carries its constant, a @c semimod wraps a value gate, a
2819 * @c conditioned gate has its target's value, and a @c case gate selects the
2820 * first branch whose guard holds in the actual world (per
2821 * @c agg_guard_holds), else the default. Returns @c NULL when the gate is
2822 * not aggregate-carrying or the value cannot be resolved (e.g. a
2823 * non-NUMERIC aggregate).
2824 */
2825CREATE OR REPLACE FUNCTION agg_gate_value(token UUID)
2826 RETURNS NUMERIC AS
2827$$
2828DECLARE
2829 gt PROVENANCE_GATE := get_gate_type(token);
2830 ch UUID[];
2831 n INTEGER;
2832 holds BOOLEAN;
2833BEGIN
2834 IF gt IN ('agg', 'arith', 'value') THEN
2835 BEGIN
2836 RETURN get_extra(token)::NUMERIC;
2837 EXCEPTION WHEN others THEN
2838 RETURN NULL; -- non-NUMERIC aggregate (e.g. min over TEXT)
2839 END;
2840 ELSIF gt = 'semimod' THEN
2841 RETURN agg_gate_value((get_children(token))[2]);
2842 ELSIF gt = 'conditioned' THEN
2843 RETURN agg_gate_value((get_children(token))[1]);
2844 ELSIF gt = 'case' THEN
2845 ch := get_children(token);
2846 n := array_length(ch, 1);
2847 FOR i IN 1 .. (n - 1) / 2 LOOP
2848 holds := agg_guard_holds(ch[2 * i - 1]);
2849 IF holds IS NULL THEN
2850 RETURN NULL;
2851 ELSIF holds THEN
2852 RETURN agg_gate_value(ch[2 * i]);
2853 END IF;
2854 END LOOP;
2855 RETURN agg_gate_value(ch[n]);
2856 END IF;
2857 RETURN NULL;
2858END
2859$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2860 SET search_path=provsql,pg_temp,public;
2861
2862/**
2863 * @brief Recover the @c "value (*)" display string for an aggregation gate
2864 *
2865 * Companion helper to the @c provsql.aggtoken_text_as_uuid GUC. With
2866 * the GUC on, an @c AGG_TOKEN cell prints as the underlying provenance
2867 * UUID, which is convenient for tooling that wants to click through to
2868 * the circuit but loses the human-readable aggregate value. This
2869 * function takes such a UUID and returns the original @c "value (*)"
2870 * string by reading the gate's @c extra (set by aggregate evaluation
2871 * for @c agg gates, and by @c agg_arith_make for the @c arith gates
2872 * that AGG_TOKEN arithmetic mints); for the other aggregate-carrying
2873 * gates (@c case, @c conditioned, @c semimod, @c value) the value is
2874 * resolved through the circuit by @c agg_gate_value. Returns @c NULL
2875 * if @p token does not resolve to an aggregate-carrying gate.
2876 *
2877 * @param token UUID of an @c agg gate (typically obtained from an
2878 * @c AGG_TOKEN cell when @c aggtoken_text_as_uuid is on,
2879 * or via a manual UUID cast otherwise).
2880 */
2881CREATE OR REPLACE FUNCTION agg_token_value_text(token UUID)
2882 RETURNS TEXT AS
2883$$
2884 SELECT CASE
2885 -- agg gates: extra is set by aggregate evaluation; arith gates
2886 -- (AGG_TOKEN arithmetic): extra is recorded by agg_arith_make.
2887 WHEN provsql.get_gate_type(token) IN ('agg', 'arith')
2888 THEN provsql.get_extra(token) || ' (*)'
2889 -- other aggregate-carrying gates: resolve the actual-world value
2890 -- through the circuit.
2891 WHEN provsql.get_gate_type(token) IN ('case', 'conditioned', 'semimod', 'value')
2892 THEN provsql.agg_gate_value(token)::TEXT || ' (*)'
2893 ELSE NULL
2894 END;
2895$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE;
2896
2897/** @brief Cast an AGG_TOKEN to NUMERIC (extracts the aggregate value, loses provenance) */
2898CREATE OR REPLACE FUNCTION agg_token_to_numeric(AGG_TOKEN)
2899 RETURNS NUMERIC
2900 AS 'provsql','agg_token_to_numeric' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2902/** @brief Cast an AGG_TOKEN to double precision (extracts the aggregate value, loses provenance) */
2903CREATE OR REPLACE FUNCTION agg_token_to_float8(AGG_TOKEN)
2904 RETURNS double precision
2905 AS 'provsql','agg_token_to_float8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2906
2907/** @brief Cast an AGG_TOKEN to INTEGER (extracts the aggregate value, loses provenance) */
2908CREATE OR REPLACE FUNCTION agg_token_to_int4(AGG_TOKEN)
2909 RETURNS INTEGER
2910 AS 'provsql','agg_token_to_int4' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2911
2912/** @brief Cast an AGG_TOKEN to bigint (extracts the aggregate value, loses provenance) */
2913CREATE OR REPLACE FUNCTION agg_token_to_int8(AGG_TOKEN)
2914 RETURNS bigint
2915 AS 'provsql','agg_token_to_int8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2916
2917/** @brief Cast an AGG_TOKEN to TEXT (extracts the aggregate value, loses provenance) */
2918CREATE OR REPLACE FUNCTION agg_token_to_text(AGG_TOKEN)
2919 RETURNS TEXT
2920 AS 'provsql','agg_token_to_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2922/** @brief Assignment cast from AGG_TOKEN to NUMERIC (extracts the scalar
2923 * value, dropping provenance). ASSIGNMENT, not IMPLICIT: provenance-
2924 * preserving arithmetic on aggregates is provided by the native
2925 * AGG_TOKEN operators below, so an implicit NUMERIC coercion would only
2926 * silently steal `s + 1` away from them (and reroute it differently
2927 * depending on whether provsql is in search_path). Write `s::NUMERIC`
2928 * to opt into the lossy scalar. */
2929CREATE CAST (AGG_TOKEN AS NUMERIC) WITH FUNCTION agg_token_to_numeric(AGG_TOKEN) AS ASSIGNMENT;
2930
2931-- ---------------------------------------------------------------------
2932-- Arithmetic on aggregates (AGG_TOKEN)
2933--
2934-- Mirrors the random_variable arithmetic surface: the operators build a
2935-- `gate_arith` over the operand provenance UUIDs (via provenance_arith,
2936-- info1 = PROVSQL_ARITH_*), so the arithmetic is recorded symbolically
2937-- in the circuit and can be resolved when a comparison (gate_cmp) over
2938-- the result is evaluated. Unlike random_variable (a bare UUID), an
2939-- AGG_TOKEN also carries a running scalar value, so each operator
2940-- additionally computes the resulting value and bundles it back with the
2941-- new gate.
2942-- ---------------------------------------------------------------------
2943
2944/** @brief Running value of an AGG_TOKEN as NUMERIC, without the
2945 * provenance-loss warning the public cast emits (internal use). */
2946CREATE OR REPLACE FUNCTION agg_token_value(AGG_TOKEN)
2947 RETURNS NUMERIC
2948 AS 'provsql','agg_token_value' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2949
2950/** @brief Bundle a provenance gate UUID with a running value into an
2951 * AGG_TOKEN (inverse of the agg_token_uuid / agg_token_value
2952 * accessors). */
2953CREATE OR REPLACE FUNCTION agg_token_make(tok UUID, val NUMERIC)
2954 RETURNS AGG_TOKEN AS
2955$$
2956 SELECT format('( %s , %s )', tok::TEXT, val::TEXT)::provsql.AGG_TOKEN;
2957$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
2958 SET search_path=provsql,pg_temp,public;
2959
2960/** @brief Lift a scalar NUMERIC constant into a gate_value leaf and
2961 * return its UUID, so it can be a child of a gate_arith (the agg-side
2962 * analogue of as_random for random_variable). */
2963CREATE OR REPLACE FUNCTION agg_value_gate(v NUMERIC)
2964 RETURNS UUID AS
2965$$
2966DECLARE
2967 token UUID := public.uuid_generate_v5(
2968 provsql.uuid_ns_provsql(), concat('value', v::TEXT));
2969BEGIN
2970 PERFORM provsql.create_gate(token, 'value');
2971 PERFORM provsql.set_extra(token, v::TEXT);
2972 RETURN token;
2973END
2974$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE
2975 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
2976
2977/** @brief Mint (or reuse) the gate_arith for an AGG_TOKEN arithmetic
2978 * result and return the AGG_TOKEN carrying it.
2979 *
2980 * Also records the computed scalar in the gate's @c extra -- exactly
2981 * what aggregate evaluation does for @c agg gates -- so
2982 * @c agg_token_value_text can recover the @c "value (*)" display from
2983 * the bare UUID (as ProvSQL Studio does for result cells under
2984 * @c provsql.aggtoken_text_as_uuid). The gate UUID is deterministic in
2985 * (op, children), so re-recording the (identical) value is idempotent. */
2986CREATE OR REPLACE FUNCTION agg_arith_make(op INT, children UUID[], val NUMERIC)
2987 RETURNS AGG_TOKEN AS
2988$$
2989DECLARE
2990 token UUID := provsql.provenance_arith(op, children);
2991BEGIN
2992 PERFORM provsql.set_extra(token, val::TEXT);
2993 RETURN provsql.agg_token_make(token, val);
2994END
2995$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
2996 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
2997
2998-- AGG_TOKEN <op> AGG_TOKEN --------------------------------------------
2999/** @brief AGG_TOKEN + AGG_TOKEN (gate_arith PLUS). */
3000CREATE OR REPLACE FUNCTION agg_token_plus(a AGG_TOKEN, b AGG_TOKEN)
3001 RETURNS AGG_TOKEN AS
3002$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, (b)::UUID],
3003 provsql.agg_token_value(a) + provsql.agg_token_value(b)); $$
3004 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3005
3006/** @brief AGG_TOKEN - AGG_TOKEN (gate_arith MINUS). */
3007CREATE OR REPLACE FUNCTION agg_token_minus(a AGG_TOKEN, b AGG_TOKEN)
3008 RETURNS AGG_TOKEN AS
3009$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, (b)::UUID],
3010 provsql.agg_token_value(a) - provsql.agg_token_value(b)); $$
3011 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3012
3013/** @brief AGG_TOKEN * AGG_TOKEN (gate_arith TIMES). */
3014CREATE OR REPLACE FUNCTION agg_token_times(a AGG_TOKEN, b AGG_TOKEN)
3015 RETURNS AGG_TOKEN AS
3016$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, (b)::UUID],
3017 provsql.agg_token_value(a) * provsql.agg_token_value(b)); $$
3018 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3020/** @brief AGG_TOKEN / AGG_TOKEN (gate_arith DIV). */
3021CREATE OR REPLACE FUNCTION agg_token_div(a AGG_TOKEN, b AGG_TOKEN)
3022 RETURNS AGG_TOKEN AS
3023$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, (b)::UUID],
3024 provsql.agg_token_value(a) / provsql.agg_token_value(b)); $$
3025 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3026
3027/** @brief Unary -AGG_TOKEN (gate_arith NEG). */
3028CREATE OR REPLACE FUNCTION agg_token_neg(a AGG_TOKEN)
3029 RETURNS AGG_TOKEN AS
3030$$ SELECT provsql.agg_arith_make(4, ARRAY[(a)::UUID],
3031 - provsql.agg_token_value(a)); $$
3032 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3033
3034-- AGG_TOKEN <op> NUMERIC ----------------------------------------------
3035/** @brief AGG_TOKEN + NUMERIC (gate_arith PLUS, constant lifted to a value gate). */
3036CREATE OR REPLACE FUNCTION agg_token_plus_numeric(a AGG_TOKEN, b NUMERIC)
3037 RETURNS AGG_TOKEN AS
3038$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3039 provsql.agg_token_value(a) + b); $$
3040 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3041
3042/** @brief AGG_TOKEN - NUMERIC. */
3043CREATE OR REPLACE FUNCTION agg_token_minus_numeric(a AGG_TOKEN, b NUMERIC)
3044 RETURNS AGG_TOKEN AS
3045$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3046 provsql.agg_token_value(a) - b); $$
3047 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3048
3049/** @brief AGG_TOKEN * NUMERIC. */
3050CREATE OR REPLACE FUNCTION agg_token_times_numeric(a AGG_TOKEN, b NUMERIC)
3051 RETURNS AGG_TOKEN AS
3052$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3053 provsql.agg_token_value(a) * b); $$
3054 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3055
3056/** @brief AGG_TOKEN / NUMERIC. */
3057CREATE OR REPLACE FUNCTION agg_token_div_numeric(a AGG_TOKEN, b NUMERIC)
3058 RETURNS AGG_TOKEN AS
3059$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3060 provsql.agg_token_value(a) / b); $$
3061 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3062
3063-- NUMERIC <op> AGG_TOKEN ----------------------------------------------
3064/** @brief NUMERIC + AGG_TOKEN. */
3065CREATE OR REPLACE FUNCTION numeric_plus_agg_token(a NUMERIC, b AGG_TOKEN)
3066 RETURNS AGG_TOKEN AS
3067$$ SELECT provsql.agg_arith_make(0, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3068 a + provsql.agg_token_value(b)); $$
3069 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3070
3071/** @brief NUMERIC - AGG_TOKEN. */
3072CREATE OR REPLACE FUNCTION numeric_minus_agg_token(a NUMERIC, b AGG_TOKEN)
3073 RETURNS AGG_TOKEN AS
3074$$ SELECT provsql.agg_arith_make(2, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3075 a - provsql.agg_token_value(b)); $$
3076 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3077
3078/** @brief NUMERIC * AGG_TOKEN. */
3079CREATE OR REPLACE FUNCTION numeric_times_agg_token(a NUMERIC, b AGG_TOKEN)
3080 RETURNS AGG_TOKEN AS
3081$$ SELECT provsql.agg_arith_make(1, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3082 a * provsql.agg_token_value(b)); $$
3083 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3084
3085/** @brief NUMERIC / AGG_TOKEN. */
3086CREATE OR REPLACE FUNCTION numeric_div_agg_token(a NUMERIC, b AGG_TOKEN)
3087 RETURNS AGG_TOKEN AS
3088$$ SELECT provsql.agg_arith_make(3, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3089 a / provsql.agg_token_value(b)); $$
3090 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3091
3092-- Operator declarations -----------------------------------------------
3093CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_plus, COMMUTATOR = +);
3094CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_minus);
3095CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_times, COMMUTATOR = *);
3096CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_div);
3097CREATE OPERATOR - (RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_neg);
3098
3099CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_plus_numeric, COMMUTATOR = +);
3100CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_minus_numeric);
3101CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_times_numeric, COMMUTATOR = *);
3102CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_div_numeric);
3103
3104CREATE OPERATOR + (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_plus_agg_token, COMMUTATOR = +);
3105CREATE OPERATOR - (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_minus_agg_token);
3106CREATE OPERATOR * (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_times_agg_token, COMMUTATOR = *);
3107CREATE OPERATOR / (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_div_agg_token);
3108
3109/** @brief Assignment cast from AGG_TOKEN to double precision */
3110CREATE CAST (AGG_TOKEN AS double precision) WITH FUNCTION agg_token_to_float8(AGG_TOKEN) AS ASSIGNMENT;
3111/** @brief Assignment cast from AGG_TOKEN to INTEGER */
3112CREATE CAST (AGG_TOKEN AS INTEGER) WITH FUNCTION agg_token_to_int4(AGG_TOKEN) AS ASSIGNMENT;
3113/** @brief Assignment cast from AGG_TOKEN to bigint */
3114CREATE CAST (AGG_TOKEN AS bigint) WITH FUNCTION agg_token_to_int8(AGG_TOKEN) AS ASSIGNMENT;
3115/** @brief Assignment cast from AGG_TOKEN to TEXT (extracts value, not UUID) */
3116CREATE CAST (AGG_TOKEN AS TEXT) WITH FUNCTION agg_token_to_text(AGG_TOKEN) AS ASSIGNMENT;
3117
3118/**
3119 * @brief Condition a discrete aggregate's distribution on an event:
3120 * @c "SUM(x) | C".
3121 *
3122 * Mirrors @c random_variable_cond for the @c AGG_TOKEN carrier: returns a
3123 * conditioned @c AGG_TOKEN that flows onward, its provenance token wrapped in
3124 * the composable two-child @c gate_conditioned @c [agg_target, condition]
3125 * while its running value is preserved. The moment / support dispatchers
3126 * unpack it (@c agg_conditioned_target + @c rv_conditioned_prov) and route
3127 * through the existing @c agg_raw_moment with the condition conjoined into the
3128 * @c prov argument, so @c expected(SUM(x)|C) / @c variance(SUM(x)|C) report
3129 * the conditional aggregate distribution. Nested conditioning folds.
3130 */
3131CREATE OR REPLACE FUNCTION agg_token_cond(a AGG_TOKEN, cond UUID)
3132 RETURNS AGG_TOKEN AS
3133$$
3134DECLARE
3135 tok UUID;
3136 ev UUID;
3137 result UUID;
3138 ch UUID[];
3139BEGIN
3140 IF cond IS NULL OR cond = gate_one() THEN
3141 RETURN a;
3142 END IF;
3143
3144 tok := (a)::UUID;
3145 IF get_gate_type(tok) = 'conditioned'
3146 AND array_length(get_children(tok), 1) = 2 THEN
3147 ch := get_children(tok);
3148 tok := ch[1];
3149 ev := provenance_times(ch[2], cond);
3150 ELSE
3151 ev := cond;
3152 END IF;
3153
3154 result := public.uuid_generate_v5(uuid_ns_provsql(),
3155 concat('conditioned', tok, ev));
3156 PERFORM create_gate(result, 'conditioned', ARRAY[tok, ev]);
3157 RETURN agg_token_make(result, agg_token_value(a));
3158END
3159$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
3160 SECURITY DEFINER PARALLEL SAFE;
3161
3162CREATE OPERATOR | (
3163 LEFTARG = AGG_TOKEN,
3164 RIGHTARG = UUID,
3165 PROCEDURE = agg_token_cond
3166);
3167
3168/**
3169 * @brief Placeholder for @c "SUM(x) | (predicate)" on an AGG_TOKEN.
3170 *
3171 * Lets the conditioning event be a natural Boolean predicate (e.g.
3172 * @c "SUM(x) | (SUM(x) > 5)") instead of a hand-built gate. Never executes:
3173 * the planner converts the Boolean operand into a condition gate and emits
3174 * @c agg_token_cond.
3175 */
3176CREATE OR REPLACE FUNCTION agg_token_cond_predicate(
3177 a AGG_TOKEN, predicate BOOLEAN) RETURNS AGG_TOKEN AS
3178$$
3179BEGIN
3180 RAISE EXCEPTION 'AGG_TOKEN | (predicate) must be rewritten by the ProvSQL '
3181 'planner hook: the right operand must be a Boolean combination of '
3182 'aggregate / random_variable comparisons (is provsql.active off?)';
3183END
3184$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
3185
3186CREATE OPERATOR | (
3187 LEFTARG = AGG_TOKEN,
3188 RIGHTARG = BOOLEAN,
3189 PROCEDURE = agg_token_cond_predicate
3191
3192/**
3193 * @brief Unpack the target of a conditioned @c AGG_TOKEN.
3194 *
3195 * For a @c "SUM(x) | C" whose provenance token is the two-child
3196 * @c gate_conditioned @c [agg_target, condition] returns the AGG_TOKEN over
3197 * @c agg_target (same running value); for any other AGG_TOKEN returns it
3198 * unchanged. The conditioning event itself is recovered separately via
3199 * @c rv_conditioned_prov on the token's UUID.
3200 */
3201CREATE OR REPLACE FUNCTION agg_conditioned_target(a AGG_TOKEN)
3202 RETURNS AGG_TOKEN AS
3203$$
3204 SELECT CASE
3205 WHEN provsql.get_gate_type((a)::UUID) = 'conditioned'
3206 AND array_length(provsql.get_children((a)::UUID), 1) = 2
3207 THEN provsql.agg_token_make(
3208 (provsql.get_children((a)::UUID))[1], provsql.agg_token_value(a))
3209 ELSE a
3210 END;
3211$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3212
3213/**
3214 * @brief Placeholder comparison of AGG_TOKEN with NUMERIC
3216 * This function is never actually called; it exists so the SQL parser
3217 * accepts comparison operators between AGG_TOKEN and NUMERIC values.
3218 * The ProvSQL query rewriter replaces these comparisons at plan time.
3219 */
3220CREATE OR REPLACE FUNCTION agg_token_comp_numeric(a AGG_TOKEN, b NUMERIC)
3221RETURNS BOOLEAN
3222LANGUAGE plpgsql
3223IMMUTABLE STRICT PARALLEL SAFE
3224AS $$
3225BEGIN
3226 RAISE EXCEPTION 'Comparison AGG_TOKEN-NUMERIC not implemented, should be replaced by ProvSQL behavior';
3227END;
3228$$;
3229
3230/**
3231 * @brief Placeholder comparison of NUMERIC with AGG_TOKEN
3233 * Symmetric to agg_token_comp_numeric; never actually called.
3234 * The ProvSQL query rewriter replaces these comparisons at plan time.
3235 */
3236CREATE OR REPLACE FUNCTION numeric_comp_agg_token(a NUMERIC, b AGG_TOKEN)
3237RETURNS BOOLEAN
3238LANGUAGE plpgsql
3239IMMUTABLE STRICT PARALLEL SAFE
3240AS $$
3241BEGIN
3242 RAISE EXCEPTION 'Comparison NUMERIC-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
3243END;
3244$$;
3245
3246/** @brief SQL operator AGG_TOKEN < NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3247CREATE OPERATOR < (
3248 LEFTARG = AGG_TOKEN,
3249 RIGHTARG = NUMERIC,
3250 PROCEDURE = agg_token_comp_numeric,
3251 COMMUTATOR = >,
3252 NEGATOR = >=
3253);
3254/** @brief SQL operator NUMERIC < AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3255CREATE OPERATOR < (
3256 LEFTARG = NUMERIC,
3257 RIGHTARG = AGG_TOKEN,
3258 PROCEDURE = numeric_comp_agg_token,
3259 COMMUTATOR = >,
3260 NEGATOR = >=
3262
3263/** @brief SQL operator AGG_TOKEN <= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3264CREATE OPERATOR <= (
3265 LEFTARG = AGG_TOKEN,
3266 RIGHTARG = NUMERIC,
3267 PROCEDURE = agg_token_comp_numeric,
3268 COMMUTATOR = >=,
3269 NEGATOR = >
3270);
3271/** @brief SQL operator NUMERIC <= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3272CREATE OPERATOR <= (
3273 LEFTARG = NUMERIC,
3274 RIGHTARG = AGG_TOKEN,
3275 PROCEDURE = numeric_comp_agg_token,
3276 COMMUTATOR = >=,
3277 NEGATOR = >
3278);
3279
3280/** @brief SQL operator AGG_TOKEN = NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3281CREATE OPERATOR = (
3282 LEFTARG = AGG_TOKEN,
3283 RIGHTARG = NUMERIC,
3284 PROCEDURE = agg_token_comp_numeric,
3285 COMMUTATOR = =,
3286 NEGATOR = <>
3287);
3288/** @brief SQL operator NUMERIC = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3289CREATE OPERATOR = (
3290 LEFTARG = NUMERIC,
3291 RIGHTARG = AGG_TOKEN,
3292 PROCEDURE = numeric_comp_agg_token,
3293 COMMUTATOR = =,
3294 NEGATOR = <>
3295);
3297/** @brief SQL operator AGG_TOKEN <> NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3298CREATE OPERATOR <> (
3299 LEFTARG = AGG_TOKEN,
3300 RIGHTARG = NUMERIC,
3301 PROCEDURE = agg_token_comp_numeric,
3302 COMMUTATOR = <>,
3303 NEGATOR = =
3305/** @brief SQL operator NUMERIC <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3306CREATE OPERATOR <> (
3307 LEFTARG = NUMERIC,
3308 RIGHTARG = AGG_TOKEN,
3309 PROCEDURE = numeric_comp_agg_token,
3310 COMMUTATOR = <>,
3311 NEGATOR = =
3313
3314/** @brief SQL operator AGG_TOKEN >= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3315CREATE OPERATOR >= (
3316 LEFTARG = AGG_TOKEN,
3317 RIGHTARG = NUMERIC,
3318 PROCEDURE = agg_token_comp_numeric,
3319 COMMUTATOR = <=,
3320 NEGATOR = <
3321);
3322/** @brief SQL operator NUMERIC >= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3323CREATE OPERATOR >= (
3324 LEFTARG = NUMERIC,
3325 RIGHTARG = AGG_TOKEN,
3326 PROCEDURE = numeric_comp_agg_token,
3327 COMMUTATOR = <=,
3328 NEGATOR = <
3329);
3330
3331/** @brief SQL operator AGG_TOKEN > NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3332CREATE OPERATOR > (
3333 LEFTARG = AGG_TOKEN,
3334 RIGHTARG = NUMERIC,
3335 PROCEDURE = agg_token_comp_numeric,
3336 COMMUTATOR = <,
3337 NEGATOR = <=
3338);
3339/** @brief SQL operator NUMERIC > AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3340CREATE OPERATOR > (
3341 LEFTARG = NUMERIC,
3342 RIGHTARG = AGG_TOKEN,
3343 PROCEDURE = numeric_comp_agg_token,
3344 COMMUTATOR = <,
3345 NEGATOR = <=
3346);
3347
3348/**
3349 * @brief Placeholder comparison of two AGG_TOKEN values (the diagonal)
3350 *
3351 * Never actually called; lets the parser accept AGG_TOKEN \<op\> AGG_TOKEN
3352 * (e.g. sum(x) > sum(y) on materialised tokens), which the ProvSQL
3353 * rewriter lowers to a gate_cmp at plan time. Declaring this diagonal
3354 * also disambiguates `s = s2` (otherwise "operator is not unique",
3355 * because both AGG_TOKEN -> UUID and AGG_TOKEN -> NUMERIC casts apply).
3356 */
3357CREATE OR REPLACE FUNCTION agg_token_comp_agg_token(a AGG_TOKEN, b AGG_TOKEN)
3358RETURNS BOOLEAN
3359LANGUAGE plpgsql
3360IMMUTABLE STRICT PARALLEL SAFE
3361AS $$
3362BEGIN
3363 RAISE EXCEPTION 'Comparison AGG_TOKEN-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
3364END;
3365$$;
3366
3367/** @brief SQL operator AGG_TOKEN < AGG_TOKEN (placeholder rewritten at plan time) */
3368CREATE OPERATOR < (
3369 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
3370 COMMUTATOR = >, NEGATOR = >=
3371);
3372/** @brief SQL operator AGG_TOKEN <= AGG_TOKEN (placeholder rewritten at plan time) */
3373CREATE OPERATOR <= (
3374 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
3375 COMMUTATOR = >=, NEGATOR = >
3376);
3377/** @brief SQL operator AGG_TOKEN > AGG_TOKEN (placeholder rewritten at plan time) */
3378CREATE OPERATOR > (
3379 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
3380 COMMUTATOR = <, NEGATOR = <=
3381);
3382/** @brief SQL operator AGG_TOKEN >= AGG_TOKEN (placeholder rewritten at plan time) */
3383CREATE OPERATOR >= (
3384 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
3385 COMMUTATOR = <=, NEGATOR = <
3386);
3387/** @brief SQL operator AGG_TOKEN = AGG_TOKEN (placeholder rewritten at plan time) */
3388CREATE OPERATOR = (
3389 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
3390 COMMUTATOR = =, NEGATOR = <>
3391);
3392/** @brief SQL operator AGG_TOKEN <> AGG_TOKEN (placeholder rewritten at plan time) */
3393CREATE OPERATOR <> (
3394 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
3395 COMMUTATOR = <>, NEGATOR = =
3396);
3397
3398/**
3399 * @brief Placeholder comparison of AGG_TOKEN with TEXT
3400 *
3401 * This function is never actually called; it exists so the SQL parser
3402 * accepts comparison operators between AGG_TOKEN and TEXT values.
3403 * The ProvSQL query rewriter replaces these comparisons at plan time.
3404 */
3405CREATE OR REPLACE FUNCTION agg_token_comp_text(a AGG_TOKEN, b TEXT)
3406RETURNS BOOLEAN
3407LANGUAGE plpgsql
3408IMMUTABLE STRICT PARALLEL SAFE
3409AS $$
3410BEGIN
3411 RAISE EXCEPTION 'Comparison AGG_TOKEN-TEXT not implemented, should be replaced by ProvSQL behavior';
3412END;
3413$$;
3414
3415/**
3416 * @brief Placeholder comparison of TEXT with AGG_TOKEN
3417 *
3418 * Symmetric to agg_token_comp_text; never actually called.
3419 * The ProvSQL query rewriter replaces these comparisons at plan time.
3420 */
3421CREATE OR REPLACE FUNCTION text_comp_agg_token(a TEXT, b AGG_TOKEN)
3422RETURNS BOOLEAN
3423LANGUAGE plpgsql
3424IMMUTABLE STRICT PARALLEL SAFE
3425AS $$
3426BEGIN
3427 RAISE EXCEPTION 'Comparison TEXT-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
3428END;
3429$$;
3430
3431/** @brief SQL operator AGG_TOKEN = TEXT (placeholder rewritten by ProvSQL at plan time) */
3432CREATE OPERATOR = (
3433 LEFTARG = AGG_TOKEN,
3434 RIGHTARG = TEXT,
3435 PROCEDURE = agg_token_comp_text,
3436 COMMUTATOR = =,
3437 NEGATOR = <>
3438);
3439/** @brief SQL operator TEXT = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3440CREATE OPERATOR = (
3441 LEFTARG = TEXT,
3442 RIGHTARG = AGG_TOKEN,
3443 PROCEDURE = text_comp_agg_token,
3444 COMMUTATOR = =,
3445 NEGATOR = <>
3446);
3447
3448/** @brief SQL operator AGG_TOKEN <> TEXT (placeholder rewritten by ProvSQL at plan time) */
3449CREATE OPERATOR <> (
3450 LEFTARG = AGG_TOKEN,
3451 RIGHTARG = TEXT,
3452 PROCEDURE = agg_token_comp_text,
3453 COMMUTATOR = <>,
3454 NEGATOR = =
3455);
3456/** @brief SQL operator TEXT <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3457CREATE OPERATOR <> (
3458 LEFTARG = TEXT,
3459 RIGHTARG = AGG_TOKEN,
3460 PROCEDURE = text_comp_agg_token,
3461 COMMUTATOR = <>,
3462 NEGATOR = =
3463);
3464
3465/** @} */
3466
3467/** @defgroup random_variable_type Type for continuous random variables
3468 *
3469 * Custom type <tt>random_variable</tt>: a thin wrapper around a
3470 * provenance gate UUID, used to expose continuous probabilistic
3471 * c-tables in SQL. The UUID indexes either a <tt>gate_rv</tt>
3472 * (an actual distribution) or a <tt>gate_value</tt> (a
3473 * zero-variance constant produced by <tt>provsql.as_random</tt>).
3474 * Binary-coercible with <tt>UUID</tt> (same 16-byte layout), so an
3475 * <tt>rv</tt>-typed expression flows directly into any function
3476 * expecting a UUID at zero runtime cost.
3478 * Constructors live in this group: <tt>provsql.normal(μ, σ)</tt>,
3479 * <tt>provsql.uniform(a, b)</tt>, <tt>provsql.exponential(λ)</tt>,
3480 * <tt>provsql.erlang(k, λ)</tt>, <tt>provsql.gamma(k, λ)</tt>,
3481 * <tt>provsql.chi_squared(k)</tt>, <tt>provsql.lognormal(μ, σ)</tt>,
3482 * <tt>provsql.weibull(k, λ)</tt>, <tt>provsql.pareto(xₘ, α)</tt>,
3483 * <tt>provsql.beta(α, β)</tt>,
3484 * the discrete count constructors (<tt>provsql.poisson(λ)</tt>,
3485 * <tt>provsql.binomial(n, p)</tt>, <tt>provsql.geometric(p)</tt>,
3486 * <tt>provsql.hypergeometric(N, K, n)</tt>,
3487 * <tt>provsql.negative_binomial(r, p)</tt>, all lowering to
3488 * @c categorical via @c categorical_from_log_pmf),
3489 * and <tt>provsql.as_random(c)</tt>.
3490 * Operator overloads
3491 * (<tt>+ - * /</tt> and the six comparators) are defined further
3492 * below, alongside direct <tt>rv_cmp_*</tt> UUID constructors for
3493 * callers that want a <tt>gate_cmp</tt> token without going through
3494 * the planner hook.
3495 * @{
3496 */
3497
3498CREATE TYPE random_variable;
3499
3500/** @brief Input function for the random_variable type */
3501CREATE OR REPLACE FUNCTION random_variable_in(CSTRING)
3502 RETURNS random_variable
3503 AS 'provsql','random_variable_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3504
3505/** @brief Output function for the random_variable type */
3506CREATE OR REPLACE FUNCTION random_variable_out(random_variable)
3507 RETURNS CSTRING
3508 AS 'provsql','random_variable_out' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3509
3510CREATE TYPE random_variable (
3511 internallength = 16,
3512 input = random_variable_in,
3513 output = random_variable_out,
3514 alignment = char
3515);
3516
3517/** @brief Build a random_variable from a UUID (internal). */
3518CREATE OR REPLACE FUNCTION random_variable_make(tok UUID)
3519 RETURNS random_variable
3520 AS 'provsql','random_variable_make' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3522/** @brief Binary-coercible cast random_variable -> UUID.
3523 * A random_variable is byte-for-byte a pg_uuid_t (alignment char,
3524 * length 16), so WITHOUT FUNCTION lets PostgreSQL reinterpret the
3525 * bytes at zero runtime cost. The cast is ASSIGNMENT (not IMPLICIT):
3526 * an implicit cross-domain cast would silently reroute a comparison
3527 * such as `v < w` to `UUID < UUID` (raw byte comparison) whenever
3528 * `provsql` is not in search_path, since operators are resolved
3529 * through search_path but casts are not. Demoting to ASSIGNMENT
3530 * turns that silent wrong result into a clean parse error. Passing a
3531 * random_variable to a UUID-taking function now needs an explicit
3532 * `v::UUID` (function resolution never applies assignment casts). */
3533CREATE CAST (random_variable AS UUID) WITHOUT FUNCTION AS ASSIGNMENT;
3534CREATE CAST (UUID AS random_variable) WITHOUT FUNCTION;
3535
3536/**
3537 * @brief Coerce an @c AGG_TOKEN to a @c random_variable (its circuit token).
3538 *
3539 * An aggregate over probabilistic tuples IS a random variable: its
3540 * @c AGG_TOKEN carries the provenance circuit of the aggregate distribution.
3541 * Exposing that as a @c random_variable lets a comparison / conditioning
3542 * predicate mix the two -- e.g. conditioning a latent leaf on a count,
3543 * @c "R | (poisson(lambda) = C)" with @c C a @c count(*) AGG_TOKEN -- resolve
3544 * to the ordinary @c random_variable comparison operators (which the planner
3545 * hook rewrites into a @c gate_cmp). IMPLICIT so the mixed comparison
3546 * type-checks without an explicit cast; the polymorphic dispatchers keep
3547 * their exact @c AGG_TOKEN overloads (an exact match beats the cast).
3548 */
3549CREATE OR REPLACE FUNCTION agg_token_to_random_variable(a AGG_TOKEN)
3550 RETURNS random_variable AS
3551$$ SELECT provsql.random_variable_make(provsql.agg_token_uuid($1)); $$
3552 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3553CREATE CAST (AGG_TOKEN AS random_variable)
3554 WITH FUNCTION agg_token_to_random_variable(AGG_TOKEN) AS IMPLICIT;
3555
3556/**
3557 * @brief Internal: true iff @p x is a finite (non-NaN, non-±∞) float8.
3558 *
3559 * PostgreSQL's <tt>isnan</tt> is defined for <tt>NUMERIC</tt> only,
3560 * not for <tt>double precision</tt>; we use the inequality form,
3561 * which works because PG defines <tt>NaN = NaN</tt> as <tt>TRUE</tt>
3562 * for floats (so <tt>NaN <> 'NaN'::float8</tt> is <tt>FALSE</tt>).
3563 */
3564CREATE OR REPLACE FUNCTION is_finite_float8(x double precision)
3565 RETURNS BOOL AS
3566$$
3567 SELECT $1 <> 'NaN'::float8 AND $1 <> 'Infinity'::float8 AND $1 <> '-Infinity'::float8;
3568$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3569
3570/*
3571 * Latent (token-valued) distribution parameters.
3572 *
3573 * A distribution parameter may be a scalar provenance token -- another
3574 * random_variable (or an AGG_TOKEN cast to UUID) -- rather than a
3575 * concrete double. The parameter is then a random variable itself,
3576 * making the leaf a compound (hierarchical) distribution: e.g.
3577 * normal(M, 1) with M ~ normal(0, 10). The token constructors below
3578 * wire such parameters as children of the gate_rv, encoding each wired
3579 * slot as "$i" in the extra TEXT (a literal slot keeps its decimal TEXT,
3580 * so an all-literal call is byte-identical to the plain NUMERIC
3581 * constructor). Only the Monte Carlo sampler resolves the wires (per
3582 * iteration); every analytic path recognises the wired form and falls
3583 * through to MC.
3584 */
3586/**
3587 * @brief Internal: build a two-parameter latent @c gate_rv.
3588 *
3589 * Each parameter is supplied as EITHER a token (@p pN_tok, a scalar
3590 * gate @c UUID) OR a literal (@p pN_lit); exactly one is non-NULL per
3591 * parameter. Token parameters are appended to the gate's wire vector
3592 * in order and referenced as @c "$i" in the @c extra TEXT; literal
3593 * parameters keep their decimal TEXT. Not @c STRICT: the NULLs are the
3594 * literal-vs-token sentinels.
3595 */
3596CREATE OR REPLACE FUNCTION rv_parametric2(
3597 family TEXT,
3598 p1_tok UUID, p1_lit double precision,
3599 p2_tok UUID, p2_lit double precision)
3600 RETURNS random_variable AS
3601$$
3602DECLARE
3603 token UUID;
3604 wires UUID[] := ARRAY[]::UUID[];
3605 s1 TEXT;
3606 s2 TEXT;
3607BEGIN
3608 IF p1_tok IS NOT NULL THEN
3609 wires := wires || p1_tok;
3610 s1 := '$' || (array_length(wires, 1) - 1);
3611 ELSE
3612 IF NOT provsql.is_finite_float8(p1_lit) THEN
3613 RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
3614 family, p1_lit;
3615 END IF;
3616 s1 := p1_lit::TEXT;
3617 END IF;
3618 IF p2_tok IS NOT NULL THEN
3619 wires := wires || p2_tok;
3620 s2 := '$' || (array_length(wires, 1) - 1);
3621 ELSE
3622 IF NOT provsql.is_finite_float8(p2_lit) THEN
3623 RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
3624 family, p2_lit;
3625 END IF;
3626 s2 := p2_lit::TEXT;
3627 END IF;
3628 token := public.uuid_generate_v4();
3629 PERFORM provsql.create_gate(token, 'rv', wires);
3630 PERFORM provsql.set_extra(token, family || ':' || s1 || ',' || s2);
3631 RETURN provsql.random_variable_make(token);
3632END
3633$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE;
3635/**
3636 * @brief Internal: build a one-parameter latent @c gate_rv (rate/scale).
3637 */
3638CREATE OR REPLACE FUNCTION rv_parametric1(family TEXT, p_tok UUID)
3639 RETURNS random_variable AS
3640$$
3641DECLARE
3642 token UUID;
3643BEGIN
3644 token := public.uuid_generate_v4();
3645 PERFORM provsql.create_gate(token, 'rv', ARRAY[p_tok]);
3646 PERFORM provsql.set_extra(token, family || ':$0');
3647 RETURN provsql.random_variable_make(token);
3648END
3649$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3650
3651/*
3652 * Token-accepting constructor overloads. For each NUMERIC family the
3653 * three mixed-arity forms (token, literal), (literal, token), (token,
3654 * token) let any parameter be a random_variable; an AGG_TOKEN parameter
3655 * is passed as @c (agg)::UUID::random_variable. The all-literal call
3656 * still resolves to the plain NUMERIC constructor (an exact match beats
3657 * the implicit NUMERIC->random_variable cast), so the literal fast path
3658 * is unchanged. STRICT: a NULL parameter yields a NULL random_variable.
3659 */
3660
3661-- normal(mu, sigma)
3662CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma double precision)
3663 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::UUID, NULL, NULL, $2); $$
3664 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3665CREATE OR REPLACE FUNCTION normal(mu double precision, sigma random_variable)
3666 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', NULL, $1, ($2)::UUID, NULL); $$
3667 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3668CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma random_variable)
3669 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3670 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3671
3672-- logistic(mu, s)
3673CREATE OR REPLACE FUNCTION logistic(mu random_variable, s double precision)
3674 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::UUID, NULL, NULL, $2); $$
3675 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3676CREATE OR REPLACE FUNCTION logistic(mu double precision, s random_variable)
3677 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', NULL, $1, ($2)::UUID, NULL); $$
3678 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3679CREATE OR REPLACE FUNCTION logistic(mu random_variable, s random_variable)
3680 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3681 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3682
3683-- uniform(a, b)
3684CREATE OR REPLACE FUNCTION uniform(a random_variable, b double precision)
3685 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::UUID, NULL, NULL, $2); $$
3686 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3687CREATE OR REPLACE FUNCTION uniform(a double precision, b random_variable)
3688 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', NULL, $1, ($2)::UUID, NULL); $$
3689 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3690CREATE OR REPLACE FUNCTION uniform(a random_variable, b random_variable)
3691 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3692 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3693
3694-- exponential(lambda)
3695CREATE OR REPLACE FUNCTION exponential(lambda random_variable)
3696 RETURNS random_variable AS $$ SELECT provsql.rv_parametric1('exponential', ($1)::UUID); $$
3697 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3698
3699-- gamma(k, lambda)
3700CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda double precision)
3701 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::UUID, NULL, NULL, $2); $$
3702 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3703CREATE OR REPLACE FUNCTION gamma(k double precision, lambda random_variable)
3704 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', NULL, $1, ($2)::UUID, NULL); $$
3705 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3706CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda random_variable)
3707 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3708 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3709
3710-- lognormal(mu, sigma)
3711CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma double precision)
3712 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::UUID, NULL, NULL, $2); $$
3713 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3714CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma random_variable)
3715 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', NULL, $1, ($2)::UUID, NULL); $$
3716 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3717CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma random_variable)
3718 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3719 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3720
3721-- weibull(k, lambda)
3722CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda double precision)
3723 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::UUID, NULL, NULL, $2); $$
3724 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3725CREATE OR REPLACE FUNCTION weibull(k double precision, lambda random_variable)
3726 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', NULL, $1, ($2)::UUID, NULL); $$
3727 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3728CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda random_variable)
3729 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3730 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3731
3732-- pareto(xm, alpha)
3733CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha double precision)
3734 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::UUID, NULL, NULL, $2); $$
3735 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3736CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha random_variable)
3737 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', NULL, $1, ($2)::UUID, NULL); $$
3738 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3739CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha random_variable)
3740 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3741 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3742
3743-- beta(alpha, beta)
3744CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta double precision)
3745 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::UUID, NULL, NULL, $2); $$
3746 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3747CREATE OR REPLACE FUNCTION beta(alpha double precision, beta random_variable)
3748 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', NULL, $1, ($2)::UUID, NULL); $$
3749 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3750CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta random_variable)
3751 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3752 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3753
3754-- inverse_gamma(alpha, beta)
3755CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta double precision)
3756 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::UUID, NULL, NULL, $2); $$
3757 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3758CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta random_variable)
3759 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', NULL, $1, ($2)::UUID, NULL); $$
3760 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3761CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta random_variable)
3762 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3763 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3764
3765-- inverse_gaussian(mu, lambda)
3766CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda double precision)
3767 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::UUID, NULL, NULL, $2); $$
3768 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3769CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda random_variable)
3770 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', NULL, $1, ($2)::UUID, NULL); $$
3771 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3772CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda random_variable)
3773 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3774 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3775
3776/**
3777 * @brief Construct a normal-distribution random variable
3778 *
3779 * Creates a fresh <tt>gate_rv</tt> with @c "normal:μ,σ" stored in
3780 * the gate's @c extra field, and returns a <tt>random_variable</tt>
3781 * pointing at it.
3782 *
3783 * Validation:
3784 * - @p mu and @p sigma must be finite (no @c NaN, no @c ±Infinity).
3785 * - @p sigma must be non-negative.
3786 * - When @p sigma is zero the distribution degenerates to the Dirac
3787 * at @p mu; the call is silently routed through @c as_random(mu),
3788 * producing a @c gate_value rather than a zero-variance @c gate_rv.
3789 * This keeps the sampler / moment / boundcheck paths free of σ=0
3790 * special cases and lets <tt>normal(x, 0)</tt> share its gate with
3791 * <tt>as_random(x)</tt>.
3792 *
3793 * @warning The <tt>VOLATILE</tt> marking is load-bearing and must
3794 * not be weakened. Each call mints a fresh <tt>uuid_generate_v4</tt>
3795 * token because two calls to <tt>normal(0, 1)</tt> are *independent*
3796 * random variables; if PostgreSQL were allowed to fold the function
3797 * (which it would under <tt>STABLE</tt> / <tt>IMMUTABLE</tt>), two
3798 * calls in the same query would share a UUID and collapse into a
3799 * single dependent RV, silently breaking the c-table semantics.
3800 * Same warning applies to @c uniform and @c exponential below.
3801 *
3802 * @sa <a href="https://en.wikipedia.org/wiki/Normal_distribution">Wikipedia: Normal distribution</a>
3803 */
3804CREATE OR REPLACE FUNCTION normal(mu double precision, sigma double precision)
3805 RETURNS random_variable AS
3806$$
3807DECLARE
3808 token UUID;
3809BEGIN
3810 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
3811 RAISE EXCEPTION 'provsql.normal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
3812 END IF;
3813 IF sigma < 0 THEN
3814 RAISE EXCEPTION 'provsql.normal: sigma must be non-negative (got %)', sigma;
3815 END IF;
3816 IF sigma = 0 THEN
3817 RETURN provsql.as_random(mu);
3818 END IF;
3819 token := public.uuid_generate_v4();
3820 PERFORM provsql.create_gate(token, 'rv');
3821 PERFORM provsql.set_extra(token, 'normal:' || mu || ',' || sigma);
3822 RETURN provsql.random_variable_make(token);
3823END
3824$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3825
3826/**
3827 * @brief Construct a logistic-distribution random variable Logistic(μ, s)
3828 *
3829 * The location-scale family whose CDF is the logistic sigmoid; a threshold
3830 * event over a Logistic(0, 1) noise realises the logit link exactly
3831 * (@c P(eps < score) = 1/(1 + exp(-score))), the natural link for a
3832 * log-odds / latent-utility selection model.
3833 *
3834 * Validation:
3835 * - @p mu and @p s must be finite.
3836 * - @p s (the scale) must be non-negative; <tt>s = 0</tt> is the Dirac at
3837 * @p mu, routed through @c as_random(mu) as with @c normal's sigma = 0.
3838 *
3839 * @param mu location (the mean and median).
3840 * @param s scale (> 0); the variance is @f$\pi^2 s^2 / 3@f$.
3841 * @return a @c random_variable token for Logistic(μ, s).
3842 *
3843 * @sa <a href="https://en.wikipedia.org/wiki/Logistic_distribution">Wikipedia: Logistic distribution</a>
3844 */
3845CREATE OR REPLACE FUNCTION logistic(mu double precision, s double precision)
3846 RETURNS random_variable AS
3847$$
3848DECLARE
3849 token UUID;
3850BEGIN
3851 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(s) THEN
3852 RAISE EXCEPTION 'provsql.logistic: parameters must be finite (got mu=%, s=%)', mu, s;
3853 END IF;
3854 IF s < 0 THEN
3855 RAISE EXCEPTION 'provsql.logistic: scale s must be non-negative (got %)', s;
3856 END IF;
3857 IF s = 0 THEN
3858 RETURN provsql.as_random(mu);
3859 END IF;
3860 token := public.uuid_generate_v4();
3861 PERFORM provsql.create_gate(token, 'rv');
3862 PERFORM provsql.set_extra(token, 'logistic:' || mu || ',' || s);
3863 RETURN provsql.random_variable_make(token);
3864END
3865$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3866
3867/**
3868 * @brief Construct a uniform-distribution random variable on [a, b]
3869 *
3870 * Validation:
3871 * - @p a and @p b must be finite.
3872 * - @p a must be ≤ @p b (reversed bounds are rejected).
3873 * - When <tt>a = b</tt> the distribution is the Dirac at @p a; the
3874 * call is silently routed through @c as_random(a) for the same
3875 * reason as @c normal with @p sigma = 0.
3876 *
3877 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3878 * @ref normal.
3879 *
3880 * @sa <a href="https://en.wikipedia.org/wiki/Continuous_uniform_distribution">Wikipedia: Continuous uniform distribution</a>
3881 */
3882CREATE OR REPLACE FUNCTION uniform(a double precision, b double precision)
3883 RETURNS random_variable AS
3884$$
3885DECLARE
3886 token UUID;
3887BEGIN
3888 IF NOT provsql.is_finite_float8(a) OR NOT provsql.is_finite_float8(b) THEN
3889 RAISE EXCEPTION 'provsql.uniform: bounds must be finite (got a=%, b=%)', a, b;
3890 END IF;
3891 IF a > b THEN
3892 RAISE EXCEPTION 'provsql.uniform: a must be <= b (got a=%, b=%)', a, b;
3893 END IF;
3894 IF a = b THEN
3895 RETURN provsql.as_random(a);
3896 END IF;
3897 token := public.uuid_generate_v4();
3898 PERFORM provsql.create_gate(token, 'rv');
3899 PERFORM provsql.set_extra(token, 'uniform:' || a || ',' || b);
3900 RETURN provsql.random_variable_make(token);
3901END
3902$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3903
3904/**
3905 * @brief Construct an exponential-distribution random variable with rate λ
3906 *
3907 * Validation:
3908 * - @p lambda must be finite and strictly positive. No degenerate
3909 * form exists for the exponential distribution, so there is no
3910 * silent route through @c as_random.
3911 *
3912 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3913 * @ref normal.
3914 *
3915 * @sa <a href="https://en.wikipedia.org/wiki/Exponential_distribution">Wikipedia: Exponential distribution</a>
3916 */
3917CREATE OR REPLACE FUNCTION exponential(lambda double precision)
3918 RETURNS random_variable AS
3919$$
3920DECLARE
3921 token UUID;
3922BEGIN
3923 IF NOT provsql.is_finite_float8(lambda) THEN
3924 RAISE EXCEPTION 'provsql.exponential: lambda must be finite (got %)', lambda;
3925 END IF;
3926 IF lambda <= 0 THEN
3927 RAISE EXCEPTION 'provsql.exponential: lambda must be strictly positive (got %)', lambda;
3928 END IF;
3929 token := public.uuid_generate_v4();
3930 PERFORM provsql.create_gate(token, 'rv');
3931 PERFORM provsql.set_extra(token, 'exponential:' || lambda);
3932 RETURN provsql.random_variable_make(token);
3933END
3934$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3936/**
3937 * @brief Construct an Erlang-distribution random variable, sum of
3938 * @p k i.i.d. exponentials with shared rate @p lambda
3939 *
3940 * The Erlang distribution is the sum of @p k independent
3941 * <tt>Exp(λ)</tt> random variables (equivalently the gamma with
3942 * INTEGER shape). It is the natural closure of i.i.d.
3943 * exponentials under addition, and is materialised here as a single
3944 * <tt>gate_rv</tt> so the analytic CDF and closed-form moments fire
3945 * directly (rather than the sampler having to draw and sum @p k
3946 * exponential leaves per Monte-Carlo iteration).
3947 *
3948 * Validation:
3949 * - @p k must be ≥ 1. The degenerate @c k=1 case is silently routed
3950 * through @c exponential so <tt>erlang(1, λ)</tt> shares its gate
3951 * with <tt>exponential(λ)</tt>.
3952 * - @p lambda must be finite and strictly positive.
3953 *
3954 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3955 * @ref normal.
3956 *
3957 * @sa <a href="https://en.wikipedia.org/wiki/Erlang_distribution">Wikipedia: Erlang distribution</a>
3958 */
3959CREATE OR REPLACE FUNCTION erlang(k INTEGER, lambda double precision)
3960 RETURNS random_variable AS
3961$$
3962DECLARE
3963 token UUID;
3964BEGIN
3965 IF k < 1 THEN
3966 RAISE EXCEPTION 'provsql.erlang: k must be >= 1 (got %)', k;
3967 END IF;
3968 IF NOT provsql.is_finite_float8(lambda) THEN
3969 RAISE EXCEPTION 'provsql.erlang: lambda must be finite (got %)', lambda;
3970 END IF;
3971 IF lambda <= 0 THEN
3972 RAISE EXCEPTION 'provsql.erlang: lambda must be strictly positive (got %)', lambda;
3973 END IF;
3974 IF k = 1 THEN
3975 RETURN provsql.exponential(lambda);
3976 END IF;
3977 token := public.uuid_generate_v4();
3978 PERFORM provsql.create_gate(token, 'rv');
3979 PERFORM provsql.set_extra(token, 'erlang:' || k || ',' || lambda);
3980 RETURN provsql.random_variable_make(token);
3981END
3982$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3983
3984/**
3985 * @brief Construct a gamma-distribution random variable with shape @p k
3986 * (any positive real) and rate @p lambda
3987 *
3988 * The gamma distribution generalises Erlang to non-INTEGER shape; its
3989 * CDF is the regularised lower incomplete gamma, evaluated in closed
3990 * form by the analytic passes. Sums of independent gammas with the
3991 * same rate fold to a single gamma in the simplifier.
3992 *
3993 * Validation:
3994 * - @p k must be finite and strictly positive. An INTEGER @p k (in
3995 * @c INTEGER range) is silently routed through @c erlang -- the gamma
3996 * with INTEGER shape *is* Erlang -- so <tt>gamma(2, λ)</tt> shares
3997 * its gate encoding and closure interplay with <tt>erlang(2, λ)</tt>.
3998 * - @p lambda must be finite and strictly positive.
3999 *
4000 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4001 * @ref normal.
4002 *
4003 * @sa <a href="https://en.wikipedia.org/wiki/Gamma_distribution">Wikipedia: Gamma distribution</a>
4004 */
4005CREATE OR REPLACE FUNCTION gamma(k double precision, lambda double precision)
4006 RETURNS random_variable AS
4008DECLARE
4009 token UUID;
4010BEGIN
4011 IF NOT provsql.is_finite_float8(k) THEN
4012 RAISE EXCEPTION 'provsql.gamma: k must be finite (got %)', k;
4013 END IF;
4014 IF k <= 0 THEN
4015 RAISE EXCEPTION 'provsql.gamma: k must be strictly positive (got %)', k;
4016 END IF;
4017 IF NOT provsql.is_finite_float8(lambda) THEN
4018 RAISE EXCEPTION 'provsql.gamma: lambda must be finite (got %)', lambda;
4019 END IF;
4020 IF lambda <= 0 THEN
4021 RAISE EXCEPTION 'provsql.gamma: lambda must be strictly positive (got %)', lambda;
4022 END IF;
4023 IF k = floor(k) AND k <= 2147483647 THEN
4024 RETURN provsql.erlang(k::INTEGER, lambda);
4025 END IF;
4026 token := public.uuid_generate_v4();
4027 PERFORM provsql.create_gate(token, 'rv');
4028 PERFORM provsql.set_extra(token, 'gamma:' || k || ',' || lambda);
4029 RETURN provsql.random_variable_make(token);
4030END
4031$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4032
4033/**
4034 * @brief Construct a chi-squared random variable with @p k degrees of
4035 * freedom: syntactic sugar for <tt>gamma(k/2, 1/2)</tt>
4036 *
4037 * @p k is accepted as @c double @c precision so fractional degrees of
4038 * freedom work; it must be finite and strictly positive. Even degrees
4039 * of freedom route through @c erlang via @c gamma's INTEGER-shape rule.
4040 *
4041 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4042 * @ref normal.
4044 * @sa <a href="https://en.wikipedia.org/wiki/Chi-squared_distribution">Wikipedia: Chi-squared distribution</a>
4045 */
4046CREATE OR REPLACE FUNCTION chi_squared(k double precision)
4047 RETURNS random_variable AS
4048$$
4049BEGIN
4050 IF NOT provsql.is_finite_float8(k) THEN
4051 RAISE EXCEPTION 'provsql.chi_squared: k must be finite (got %)', k;
4052 END IF;
4053 IF k <= 0 THEN
4054 RAISE EXCEPTION 'provsql.chi_squared: k must be strictly positive (got %)', k;
4055 END IF;
4056 RETURN provsql.gamma(k / 2, 0.5);
4057END
4058$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4059
4060/**
4061 * @brief Construct a log-normal random variable: @c exp of a
4062 * Normal(@p mu, @p sigma), parameterised by the underlying
4063 * normal (so its median is <tt>exp(mu)</tt> and its mean
4064 * <tt>exp(mu + sigma^2/2)</tt>)
4065 *
4066 * The multiplicative counterpart of @c normal: products of independent
4067 * lognormals fold to a lognormal in the simplifier, and the
4068 * <tt>exp(normal(...))</tt> / <tt>ln(lognormal(...))</tt> bridges fold
4069 * in both directions, so log-scale models stay closed-form.
4070 *
4071 * Validation mirrors @c normal: both parameters must be finite,
4072 * @p sigma non-negative; the degenerate @c sigma = 0 case is silently
4073 * routed through @c as_random (a Dirac at <tt>exp(mu)</tt>).
4074 *
4075 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4076 * @ref normal.
4077 *
4078 * @sa <a href="https://en.wikipedia.org/wiki/Log-normal_distribution">Wikipedia: Log-normal distribution</a>
4079 */
4080CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma double precision)
4081 RETURNS random_variable AS
4082$$
4083DECLARE
4084 token UUID;
4085BEGIN
4086 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
4087 RAISE EXCEPTION 'provsql.lognormal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
4088 END IF;
4089 IF sigma < 0 THEN
4090 RAISE EXCEPTION 'provsql.lognormal: sigma must be non-negative (got %)', sigma;
4091 END IF;
4092 IF sigma = 0 THEN
4093 RETURN provsql.as_random(exp(mu));
4094 END IF;
4095 token := public.uuid_generate_v4();
4096 PERFORM provsql.create_gate(token, 'rv');
4097 PERFORM provsql.set_extra(token, 'lognormal:' || mu || ',' || sigma);
4098 RETURN provsql.random_variable_make(token);
4099END
4100$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4101
4102/**
4103 * @brief Construct a Weibull random variable with shape @p k and
4104 * scale @p lambda
4106 * @p lambda is the SCALE (the 63.2% quantile), not a rate: @c k = 1 is
4107 * the exponential with rate <tt>1/lambda</tt>, and that case is
4108 * silently routed through @c exponential to share its gate. The shape
4109 * tunes the hazard: @c k < 1 infant mortality, @c k > 1 wear-out.
4110 * Quantiles are exact, truncated moments are closed-form (via the
4111 * regularised incomplete gamma), and the min of i.i.d. Weibulls has a
4112 * closed-form mean (min-stability).
4114 * Validation: both parameters must be finite and strictly positive.
4115 *
4116 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4117 * @ref normal.
4118 *
4119 * @sa <a href="https://en.wikipedia.org/wiki/Weibull_distribution">Wikipedia: Weibull distribution</a>
4120 */
4121CREATE OR REPLACE FUNCTION weibull(k double precision, lambda double precision)
4122 RETURNS random_variable AS
4123$$
4124DECLARE
4125 token UUID;
4126BEGIN
4127 IF NOT provsql.is_finite_float8(k) OR NOT provsql.is_finite_float8(lambda) THEN
4128 RAISE EXCEPTION 'provsql.weibull: parameters must be finite (got k=%, lambda=%)', k, lambda;
4129 END IF;
4130 IF k <= 0 OR lambda <= 0 THEN
4131 RAISE EXCEPTION 'provsql.weibull: parameters must be strictly positive (got k=%, lambda=%)', k, lambda;
4132 END IF;
4133 IF k = 1 THEN
4134 RETURN provsql.exponential(1 / lambda);
4135 END IF;
4136 token := public.uuid_generate_v4();
4137 PERFORM provsql.create_gate(token, 'rv');
4138 PERFORM provsql.set_extra(token, 'weibull:' || k || ',' || lambda);
4139 RETURN provsql.random_variable_make(token);
4140END
4141$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4142
4143/**
4144 * @brief Construct a Pareto random variable with scale (minimum)
4145 * @p xm and shape @p alpha
4147 * The canonical heavy-tailed power law. Raw moments are @b infinite
4148 * for <tt>alpha <= k</tt> and reported as <tt>Infinity</tt> (the mean
4149 * for <tt>alpha <= 1</tt>, the variance for <tt>alpha <= 2</tt>)
4150 * rather than estimated; quantiles, truncated moments, conditional
4151 * sampling (self-similarity: <tt>X | X > a</tt> is Pareto(a, alpha)),
4152 * and Pareto-vs-Pareto comparisons are all exact.
4153 *
4154 * Validation: both parameters must be finite and strictly positive.
4155 *
4156 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4157 * @ref normal.
4158 *
4159 * @sa <a href="https://en.wikipedia.org/wiki/Pareto_distribution">Wikipedia: Pareto distribution</a>
4160 */
4161CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha double precision)
4162 RETURNS random_variable AS
4163$$
4164DECLARE
4165 token UUID;
4166BEGIN
4167 IF NOT provsql.is_finite_float8(xm) OR NOT provsql.is_finite_float8(alpha) THEN
4168 RAISE EXCEPTION 'provsql.pareto: parameters must be finite (got xm=%, alpha=%)', xm, alpha;
4169 END IF;
4170 IF xm <= 0 OR alpha <= 0 THEN
4171 RAISE EXCEPTION 'provsql.pareto: parameters must be strictly positive (got xm=%, alpha=%)', xm, alpha;
4172 END IF;
4173 token := public.uuid_generate_v4();
4174 PERFORM provsql.create_gate(token, 'rv');
4175 PERFORM provsql.set_extra(token, 'pareto:' || xm || ',' || alpha);
4176 RETURN provsql.random_variable_make(token);
4177END
4178$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4179
4180/**
4181 * @brief Construct an inverse-gamma random variable with shape
4182 * @p alpha and scale @p beta
4183 *
4184 * The distribution of <tt>1/Y</tt> for <tt>Y ~ gamma(alpha, beta)</tt>
4185 * (the conjugate prior for a Gaussian variance). Its CDF is the
4186 * regularised upper incomplete gamma, evaluated in closed form by the
4187 * analytic passes; raw moments are @b infinite for <tt>alpha <= k</tt>
4188 * and reported as <tt>Infinity</tt> (the mean for <tt>alpha <= 1</tt>,
4189 * the variance for <tt>alpha <= 2</tt>) rather than estimated. Positive
4190 * scalings rescale @p beta in the simplifier.
4191 *
4192 * Validation: both parameters must be finite and strictly positive.
4193 *
4194 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4195 * @ref normal.
4196 *
4197 * @sa <a href="https://en.wikipedia.org/wiki/Inverse-gamma_distribution">Wikipedia: Inverse-gamma distribution</a>
4198 */
4199CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta double precision)
4200 RETURNS random_variable AS
4201$$
4202DECLARE
4203 token UUID;
4204BEGIN
4205 IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
4206 RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
4207 END IF;
4208 IF alpha <= 0 OR beta <= 0 THEN
4209 RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
4210 END IF;
4211 token := public.uuid_generate_v4();
4212 PERFORM provsql.create_gate(token, 'rv');
4213 PERFORM provsql.set_extra(token, 'inverse_gamma:' || alpha || ',' || beta);
4214 RETURN provsql.random_variable_make(token);
4215END
4216$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4217
4218/**
4219 * @brief Construct an inverse-Gaussian (Wald) random variable with mean
4220 * @p mu and shape @p lambda
4221 *
4222 * The first-passage time of Brownian motion with drift: a positive,
4223 * right-skewed family. Its CDF has a closed form in the standard normal
4224 * @c Phi, so comparisons and quantiles are analytic; all raw moments are
4225 * finite. Positive scalings map <tt>c·IG(mu, lambda)</tt> to
4226 * <tt>IG(c·mu, c·lambda)</tt>, and a sum of independent inverse
4227 * Gaussians sharing the ratio <tt>lambda/mu²</tt> folds to a single
4228 * inverse Gaussian in the simplifier. @ref wald is an alias.
4229 *
4230 * Validation: both parameters must be finite and strictly positive.
4231 *
4232 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4233 * @ref normal.
4234 *
4235 * @sa <a href="https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution">Wikipedia: Inverse Gaussian distribution</a>
4236 */
4237CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda double precision)
4238 RETURNS random_variable AS
4239$$
4240DECLARE
4241 token UUID;
4242BEGIN
4243 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(lambda) THEN
4244 RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be finite (got mu=%, lambda=%)', mu, lambda;
4245 END IF;
4246 IF mu <= 0 OR lambda <= 0 THEN
4247 RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be strictly positive (got mu=%, lambda=%)', mu, lambda;
4248 END IF;
4249 token := public.uuid_generate_v4();
4250 PERFORM provsql.create_gate(token, 'rv');
4251 PERFORM provsql.set_extra(token, 'inverse_gaussian:' || mu || ',' || lambda);
4252 RETURN provsql.random_variable_make(token);
4253END
4254$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4255
4256/**
4257 * @brief Wald distribution: alias for @ref inverse_gaussian.
4258 *
4259 * @sa <a href="https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution">Wikipedia: Inverse Gaussian distribution</a>
4260 */
4261CREATE OR REPLACE FUNCTION wald(mu double precision, lambda double precision)
4262 RETURNS random_variable AS
4264 SELECT provsql.inverse_gaussian(mu, lambda);
4265$$ LANGUAGE sql VOLATILE PARALLEL SAFE;
4266
4267/**
4268 * @brief Build a discrete (categorical) random variable from outcomes
4269 * and UNNORMALISED log-masses
4270 *
4271 * The shared back end of the discrete count constructors (@c poisson,
4272 * @c binomial, @c geometric, @c hypergeometric,
4273 * @c negative_binomial), and directly usable for any custom discrete
4274 * pmf: the log-masses are shifted by their maximum (so only relative
4275 * magnitudes matter and no @c exp underflows), outcomes whose relative
4276 * mass is below <tt>1e-15</tt> are dropped, and the rest is
4277 * renormalised before being handed to @c categorical. Working in log
4278 * space keeps arbitrarily large parameters stable (e.g. a
4279 * <tt>Poisson(1000)</tt> pmf whose linear-space recurrence would
4280 * underflow at @c exp(-1000)).
4281 *
4282 * @param outcomes outcome values, same length as @p log_pmf
4283 * @param log_pmf natural logs of the (unnormalised) masses
4284 */
4285CREATE OR REPLACE FUNCTION categorical_from_log_pmf(
4286 outcomes double precision[], log_pmf double precision[])
4287 RETURNS random_variable AS
4288$$
4289DECLARE
4290 n INT := array_length(outcomes, 1);
4291 max_lp double precision := '-Infinity';
4292 kept_o double precision[] := '{}';
4293 kept_p double precision[] := '{}';
4294 total double precision := 0;
4295 v double precision;
4296 i INT;
4297BEGIN
4298 IF n IS NULL OR n = 0 OR n <> coalesce(array_length(log_pmf, 1), 0) THEN
4299 RAISE EXCEPTION 'provsql.categorical_from_log_pmf: outcomes and log_pmf must be non-empty arrays of the same length';
4300 END IF;
4301 FOR i IN 1..n LOOP
4302 IF log_pmf[i] > max_lp THEN max_lp := log_pmf[i]; END IF;
4303 END LOOP;
4304 IF max_lp = '-Infinity' THEN
4305 RAISE EXCEPTION 'provsql.categorical_from_log_pmf: all masses are zero';
4306 END IF;
4307 FOR i IN 1..n LOOP
4308 v := exp(log_pmf[i] - max_lp);
4309 IF v >= 1e-15 THEN
4310 kept_o := array_append(kept_o, outcomes[i]);
4311 kept_p := array_append(kept_p, v);
4312 total := total + v;
4313 END IF;
4314 END LOOP;
4315 FOR i IN 1..array_length(kept_p, 1) LOOP
4316 kept_p[i] := kept_p[i] / total;
4317 END LOOP;
4318 RETURN provsql.categorical(kept_p, kept_o);
4319END
4320$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4321
4323 * @brief Construct a Poisson random variable with mean @p lambda, as a
4324 * truncated categorical
4325 *
4326 * The pmf is enumerated over <tt>[max(0, λ-12√λ), λ+12√λ+30]</tt> (the
4327 * omitted tails carry ~1e-30 of mass) by the log-space recurrence
4328 * <tt>ln p(k+1) = ln p(k) + ln λ - ln(k+1)</tt> and handed to
4329 * @c categorical_from_log_pmf, so moments, quantiles, and (in)equality
4330 * comparisons are exact over the enumerated support. @c lambda = 0 is
4331 * a Dirac at @c 0 (routed through @c as_random); supports up to 10000
4332 * outcomes (λ up to ~170000), beyond which it raises -- approximate
4333 * huge means by @c normal(λ, √λ) instead.
4334 *
4335 * @sa <a href="https://en.wikipedia.org/wiki/Poisson_distribution">Wikipedia: Poisson distribution</a>
4337CREATE OR REPLACE FUNCTION poisson(lambda double precision)
4338 RETURNS random_variable AS
4339$$
4340DECLARE
4341 lo INT;
4342 hi INT;
4343 outcomes double precision[] := '{}';
4344 lps double precision[] := '{}';
4345 lp double precision := 0;
4346 k INT;
4347BEGIN
4348 IF NOT provsql.is_finite_float8(lambda) OR lambda < 0 THEN
4349 RAISE EXCEPTION 'provsql.poisson: lambda must be finite and non-negative (got %)', lambda;
4350 END IF;
4351 IF lambda = 0 THEN
4352 RETURN provsql.as_random(0);
4353 END IF;
4354 lo := greatest(0, floor(lambda - 12 * sqrt(lambda)))::INT;
4355 hi := ceil(lambda + 12 * sqrt(lambda))::INT + 30;
4356 IF hi - lo + 1 > 10000 THEN
4357 RAISE EXCEPTION 'provsql.poisson: support window of % outcomes exceeds 10000; approximate with normal(%, sqrt(%))', hi - lo + 1, lambda, lambda;
4358 END IF;
4359 -- ln p(0) = -λ; walk the recurrence, keeping only the window.
4360 lp := -lambda;
4361 FOR k IN 1..hi LOOP
4362 lp := lp + ln(lambda) - ln(k::double precision);
4363 IF k >= lo THEN
4364 outcomes := array_append(outcomes, k::double precision);
4365 lps := array_append(lps, lp);
4366 END IF;
4367 END LOOP;
4368 IF lo = 0 THEN
4369 outcomes := array_prepend(0::double precision, outcomes);
4370 lps := array_prepend(-lambda, lps);
4371 END IF;
4372 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4373END
4374$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4375
4377 * @brief Poisson with a LATENT rate: @c poisson(random_variable).
4378 *
4379 * A latent (token-valued) rate cannot be enumerated into a categorical at
4380 * construction, so this builds a parametric @c gate_rv leaf (family
4381 * @c "poisson") wiring the rate, exactly like the continuous latent
4382 * constructors. Only the Monte Carlo sampler resolves the rate (per draw,
4383 * then draws a Poisson); @c observe weights by the Poisson pmf; the mean is
4384 * exact (E[Poisson(Λ)] = E[Λ], affine). Unblocks discrete-likelihood
4385 * posteriors such as @c "R | (poisson(120*R) = observed_count)".
4386 */
4387CREATE OR REPLACE FUNCTION poisson(lambda random_variable)
4388 RETURNS random_variable AS
4389$$ SELECT provsql.rv_parametric1('poisson', ($1)::UUID); $$
4390 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4391
4392/**
4393 * @brief Construct a Beta(α, β) random variable on the unit interval
4394 *
4395 * The conjugate prior of Bernoulli / binomial success probabilities:
4396 * closed-form moments, CDF through the regularised incomplete beta,
4397 * quantiles through the generic CDF bisection over the finite
4398 * @c [0, 1] support, and closed-form truncated moments (interval
4399 * conditioning). <tt>Beta(1, 1)</tt> IS <tt>Uniform(0, 1)</tt> and is
4400 * silently routed through @c uniform to share its richer closed forms.
4401 *
4402 * Validation: both shapes must be finite and strictly positive.
4403 *
4404 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4405 * @ref normal.
4406 *
4407 * @sa <a href="https://en.wikipedia.org/wiki/Beta_distribution">Wikipedia: Beta distribution</a>
4408 */
4409CREATE OR REPLACE FUNCTION beta(alpha double precision, beta double precision)
4410 RETURNS random_variable AS
4411$$
4412DECLARE
4413 token UUID;
4414BEGIN
4415 IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
4416 RAISE EXCEPTION 'provsql.beta: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
4417 END IF;
4418 IF alpha <= 0 OR beta <= 0 THEN
4419 RAISE EXCEPTION 'provsql.beta: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
4420 END IF;
4421 IF alpha = 1 AND beta = 1 THEN
4422 RETURN provsql.uniform(0, 1);
4423 END IF;
4424 token := public.uuid_generate_v4();
4425 PERFORM provsql.create_gate(token, 'rv');
4426 PERFORM provsql.set_extra(token, 'beta:' || alpha || ',' || beta);
4427 RETURN provsql.random_variable_make(token);
4428END
4429$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4430
4431/**
4432 * @brief Construct a Binomial(n, p) random variable (number of
4433 * successes in @p n independent trials), as a categorical
4434 *
4435 * Enumerated over <tt>{0..n}</tt> by the log-space recurrence
4436 * <tt>ln p(k+1) = ln p(k) + ln((n-k)/(k+1)) + ln(p/(1-p))</tt>
4437 * (outcomes below 1e-15 relative mass are dropped). @c p = 0 /
4438 * @c p = 1 are Diracs at @c 0 / @c n; @c n is capped at 10000.
4439 *
4440 * @sa <a href="https://en.wikipedia.org/wiki/Binomial_distribution">Wikipedia: Binomial distribution</a>
4441 */
4442CREATE OR REPLACE FUNCTION binomial(n INTEGER, p double precision)
4443 RETURNS random_variable AS
4444$$
4445DECLARE
4446 outcomes double precision[] := '{}';
4447 lps double precision[] := '{}';
4448 lp double precision;
4449 k INT;
4450BEGIN
4451 IF n IS NULL OR n < 0 THEN
4452 RAISE EXCEPTION 'provsql.binomial: n must be non-negative (got %)', n;
4453 END IF;
4454 IF NOT provsql.is_finite_float8(p) OR p < 0 OR p > 1 THEN
4455 RAISE EXCEPTION 'provsql.binomial: p must be in [0, 1] (got %)', p;
4456 END IF;
4457 IF n > 10000 THEN
4458 RAISE EXCEPTION 'provsql.binomial: n = % exceeds 10000; approximate with normal(n*p, sqrt(n*p*(1-p)))', n;
4459 END IF;
4460 IF n = 0 OR p = 0 THEN
4461 RETURN provsql.as_random(0);
4462 END IF;
4463 IF p = 1 THEN
4464 RETURN provsql.as_random(n);
4465 END IF;
4466 lp := n * ln(1 - p); -- ln p(0)
4467 outcomes := array_append(outcomes, 0::double precision);
4468 lps := array_append(lps, lp);
4469 FOR k IN 0..(n - 1) LOOP
4470 lp := lp + ln((n - k)::double precision / (k + 1)) + ln(p / (1 - p));
4471 outcomes := array_append(outcomes, (k + 1)::double precision);
4472 lps := array_append(lps, lp);
4473 END LOOP;
4474 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4475END
4476$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4477
4478/**
4479 * @brief Binomial with a fixed trial count and a LATENT success
4480 * probability: @c binomial(INTEGER, random_variable).
4481 *
4482 * @p n is a literal trial count; @p p is a latent (token-valued) success
4483 * probability (e.g. @c "40.0 / N" for a latent population size @c N).
4484 * Builds a parametric @c gate_rv leaf (family @c "binomial", @c extra
4485 * @c "binomial:n,$0") the Monte Carlo sampler resolves per draw; @c observe
4486 * weights by the Binomial pmf. Unblocks capture-recapture-style posteriors
4487 * such as @c "N | (binomial(50, 40.0/N) = recaptured_count)".
4488 */
4489CREATE OR REPLACE FUNCTION binomial(n INTEGER, p random_variable)
4490 RETURNS random_variable AS
4491$$ SELECT provsql.rv_parametric2('binomial', NULL, $1::double precision,
4492 ($2)::UUID, NULL); $$
4493 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4494
4495/**
4496 * @brief Construct a Geometric(p) random variable -- the number of
4497 * TRIALS up to and including the first success (support
4498 * starting at 1; subtract 1 for the failures convention)
4499 *
4500 * <tt>P(X = k) = (1-p)^{k-1} p</tt>, enumerated up to the 1e-15
4501 * relative-mass tail and renormalised. @c p = 1 is a Dirac at @c 1.
4502 *
4503 * @sa <a href="https://en.wikipedia.org/wiki/Geometric_distribution">Wikipedia: Geometric distribution</a>
4504 */
4505CREATE OR REPLACE FUNCTION geometric(p double precision)
4506 RETURNS random_variable AS
4507$$
4508DECLARE
4509 k_max INT;
4510 outcomes double precision[] := '{}';
4511 lps double precision[] := '{}';
4512 k INT;
4513BEGIN
4514 IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
4515 RAISE EXCEPTION 'provsql.geometric: p must be in (0, 1] (got %)', p;
4516 END IF;
4517 IF p = 1 THEN
4518 RETURN provsql.as_random(1);
4519 END IF;
4520 k_max := 1 + ceil(ln(1e-15) / ln(1 - p))::INT;
4521 IF k_max > 10000 THEN
4522 RAISE EXCEPTION 'provsql.geometric: support window of % outcomes exceeds 10000 (p = % is too small); approximate with exponential(%)', k_max, p, p;
4523 END IF;
4524 FOR k IN 1..k_max LOOP
4525 outcomes := array_append(outcomes, k::double precision);
4526 lps := array_append(lps, (k - 1) * ln(1 - p) + ln(p));
4527 END LOOP;
4528 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4529END
4530$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4531
4532/**
4533 * @brief Geometric with a LATENT success probability: @c geometric(random_variable).
4534 *
4535 * A latent (token-valued) @p p cannot be enumerated at construction, so this
4536 * builds a parametric @c gate_rv leaf (family @c "geometric") wiring the
4537 * probability, resolved per draw by the sampler. @c observe weights by the
4538 * geometric pmf; unblocks a Beta-Geometric conjugate posterior.
4539 */
4540CREATE OR REPLACE FUNCTION geometric(p random_variable)
4541 RETURNS random_variable AS
4542$$ SELECT provsql.rv_parametric1('geometric', ($1)::UUID); $$
4543 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4544
4545/**
4546 * @brief Construct a Hypergeometric(N, K, n) random variable: the
4547 * number of marked items among @p n draws WITHOUT replacement
4548 * from a population of @p pop_n items of which @p k_marked are
4549 * marked
4550 *
4551 * The exact finite support <tt>[max(0, n-(N-K)), min(n, K)]</tt> is
4552 * enumerated by the pmf ratio recurrence (in log space, so large
4553 * populations cannot overflow) and normalised -- exact "sampling
4554 * without replacement" probabilities with no combinatorial functions
4555 * needed.
4556 *
4557 * @sa <a href="https://en.wikipedia.org/wiki/Hypergeometric_distribution">Wikipedia: Hypergeometric distribution</a>
4558 */
4559CREATE OR REPLACE FUNCTION hypergeometric(pop_n INTEGER, k_marked INTEGER, n INTEGER)
4560 RETURNS random_variable AS
4561$$
4562DECLARE
4563 lo INT;
4564 hi INT;
4565 outcomes double precision[] := '{}';
4566 lps double precision[] := '{}';
4567 lp double precision := 0; -- relative log-mass; normalised later
4568 k INT;
4569BEGIN
4570 IF pop_n IS NULL OR k_marked IS NULL OR n IS NULL
4571 OR pop_n < 0 OR k_marked < 0 OR n < 0
4572 OR k_marked > pop_n OR n > pop_n THEN
4573 RAISE EXCEPTION 'provsql.hypergeometric: need 0 <= k_marked, n <= pop_n (got pop_n=%, k_marked=%, n=%)', pop_n, k_marked, n;
4574 END IF;
4575 lo := greatest(0, n - (pop_n - k_marked));
4576 hi := least(n, k_marked);
4577 IF hi - lo + 1 > 10000 THEN
4578 RAISE EXCEPTION 'provsql.hypergeometric: support window of % outcomes exceeds 10000', hi - lo + 1;
4579 END IF;
4580 outcomes := array_append(outcomes, lo::double precision);
4581 lps := array_append(lps, lp);
4582 FOR k IN lo..(hi - 1) LOOP
4583 -- pmf(k+1)/pmf(k) = (K-k)(n-k) / ((k+1)(N-K-n+k+1))
4584 lp := lp + ln((k_marked - k)::double precision * (n - k))
4585 - ln((k + 1)::double precision * (pop_n - k_marked - n + k + 1));
4586 outcomes := array_append(outcomes, (k + 1)::double precision);
4587 lps := array_append(lps, lp);
4588 END LOOP;
4589 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4590END
4591$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4592
4593/**
4594 * @brief Construct a negative-binomial random variable: the number of
4595 * FAILURES before the @p r-th success (support starting at 0),
4596 * with real @p r > 0 allowed (the Polya / overdispersed-count
4597 * parameterisation, the Poisson-Gamma mixture)
4598 *
4599 * <tt>P(X = k) = C(k+r-1, k) p^r (1-p)^k</tt>, enumerated by the
4600 * log-space recurrence
4601 * <tt>ln p(k+1) = ln p(k) + ln((k+r)/(k+1)) + ln(1-p)</tt> up to the
4602 * 1e-15 relative-mass tail. @c p = 1 is a Dirac at @c 0.
4603 *
4604 * @sa <a href="https://en.wikipedia.org/wiki/Negative_binomial_distribution">Wikipedia: Negative binomial distribution</a>
4605 */
4606CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p double precision)
4607 RETURNS random_variable AS
4608$$
4609DECLARE
4610 outcomes double precision[] := '{}';
4611 lps double precision[] := '{}';
4612 lp double precision;
4613 max_lp double precision;
4614 mean double precision;
4615 k INT := 0;
4616BEGIN
4617 IF NOT provsql.is_finite_float8(r) OR r <= 0 THEN
4618 RAISE EXCEPTION 'provsql.negative_binomial: r must be finite and strictly positive (got %)', r;
4619 END IF;
4620 IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
4621 RAISE EXCEPTION 'provsql.negative_binomial: p must be in (0, 1] (got %)', p;
4622 END IF;
4623 IF p = 1 THEN
4624 RETURN provsql.as_random(0);
4625 END IF;
4626 mean := r * (1 - p) / p;
4627 lp := r * ln(p); -- ln p(0)
4628 max_lp := lp;
4629 outcomes := array_append(outcomes, 0::double precision);
4630 lps := array_append(lps, lp);
4631 LOOP
4632 lp := lp + ln((k + r) / (k + 1)) + ln(1 - p);
4633 k := k + 1;
4634 IF lp > max_lp THEN max_lp := lp; END IF;
4635 outcomes := array_append(outcomes, k::double precision);
4636 lps := array_append(lps, lp);
4637 EXIT WHEN k > mean AND lp < max_lp + ln(1e-15);
4638 IF k >= 10000 THEN
4639 RAISE EXCEPTION 'provsql.negative_binomial: support window exceeds 10000 outcomes (r=%, p=%)', r, p;
4640 END IF;
4641 END LOOP;
4642 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4643END
4644$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4645
4646/*
4647 * NegativeBinomial with a LATENT parameter: the count r (number of successes)
4648 * stays a plain number while the success probability p is a random_variable,
4649 * built as a parametric gate_rv leaf (family "negative_binomial") -- the
4650 * Beta-NegativeBinomial conjugate shape. The all-random and latent-r forms
4651 * are provided for uniformity with the continuous constructors.
4652 */
4653CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p random_variable)
4654 RETURNS random_variable AS
4655$$ SELECT provsql.rv_parametric2('negative_binomial', NULL, $1, ($2)::UUID, NULL); $$
4656 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4657CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p double precision)
4658 RETURNS random_variable AS
4659$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::UUID, NULL, NULL, $2); $$
4660 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4661CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p random_variable)
4662 RETURNS random_variable AS
4663$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4664 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4666/**
4667 * @brief Catalog of the registered continuous-distribution families.
4668 *
4669 * One row per @c gate_rv family known to this build of the extension:
4670 * @c name is the on-disk token (the part before the colon in the gate's
4671 * @c extra encoding), @c nparams the parameter count, @c param_names the
4672 * conventional parameter symbols in @c extra order (e.g.
4673 * <tt>{μ, σ}</tt>), and @c label a short display glyph (e.g. @c "N",
4674 * @c "Γ"). UI clients (ProvSQL Studio's circuit inspector) read this to
4675 * render families they were not hard-coded for, so a newly added family
4676 * shows up without a client release.
4677 */
4678CREATE OR REPLACE FUNCTION rv_families()
4679 RETURNS TABLE(name TEXT, nparams INT, param_names TEXT[], label TEXT) AS
4680 'provsql','rv_families' LANGUAGE C STABLE PARALLEL SAFE;
4682/**
4683 * @brief Construct a probabilistic-mixture random variable.
4684 *
4685 * Returns a @c random_variable whose distribution is a Bernoulli
4686 * mixture of two scalar RV roots: with probability <tt>P(p = true)</tt>
4687 * the mixture samples @p x, with the complementary probability it
4688 * samples @p y. The mixing token @p p is a @c gate_input Bernoulli
4689 * whose probability has been pinned with @c set_prob, and the same
4690 * @p p can be shared with other branches of the circuit -- the
4691 * Monte-Carlo sampler's per-iteration cache couples every reference
4692 * to the same draw, so users can build joint conditional structures
4693 * (e.g. <tt>mixture(p, X1, Y1) + mixture(p, X2, Y2)</tt> samples
4694 * X1 + X2 with prob π and Y1 + Y2 with prob 1-π).
4695 *
4696 * @p x and @p y may be any scalar RV root: a base @c gate_rv
4697 * (@c normal / @c uniform / @c exponential / @c erlang), a
4698 * @c gate_value Dirac (@c as_random), a @c gate_arith expression, or
4699 * another @c mixture. N-ary mixtures are built by composition --
4700 * <tt>mixture(p1, A, mixture(p2, B, C))</tt> realises a 3-component
4701 * mixture with effective weights <tt>π1, (1-π1)·π2, (1-π1)·(1-π2)</tt>.
4702 *
4703 * Validation:
4704 * - @p p must point to a Boolean gate (@c input, @c mulinput,
4705 * @c update, @c plus, @c times, @c monus, @c project, @c eq,
4706 * @c cmp, @c zero, @c one). Compound Boolean gates derive their
4707 * probability from their atoms via the active probability-evaluation
4708 * method; a bare @c gate_input's probability is whatever @c set_prob
4709 * pinned (@c set_prob is responsible for keeping it in [0, 1]).
4710 * - @p x and @p y must be scalar RV roots; aggregate / Boolean roots
4711 * are rejected at construction.
4712 *
4713 * Two calls to @c mixture with the same @c (p, x, y) operands collapse
4714 * to the same @c gate_mixture node by v5-hash, exactly like
4715 * @c arith(PLUS, X, Y). Draw independence is controlled by @p p:
4716 * sharing @p p couples branch selection across consumers via the
4717 * sampler's @c bool_cache_; minting independent Bernoullis (e.g. via
4718 * the @c mixture(p_value, …) overload) decouples them.
4719 *
4720 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
4721 */
4722CREATE OR REPLACE FUNCTION mixture(
4723 p UUID, x random_variable, y random_variable)
4724 RETURNS random_variable AS
4725$$
4726DECLARE
4727 token UUID;
4728 p_kind provsql.PROVENANCE_GATE;
4729 x_uuid UUID;
4730 y_uuid UUID;
4731 x_kind provsql.PROVENANCE_GATE;
4732 y_kind provsql.PROVENANCE_GATE;
4733BEGIN
4734 p_kind := provsql.get_gate_type(p);
4735 IF p_kind NOT IN ('input','mulinput','update',
4736 'plus','times','monus',
4737 'project','eq','cmp',
4738 'zero','one') THEN
4739 RAISE EXCEPTION 'provsql.mixture: p must be a Boolean gate '
4740 '(input/mulinput/update/plus/times/monus/project/eq/cmp/zero/one), got %', p_kind;
4741 END IF;
4742
4743 x_uuid := (x)::UUID;
4744 y_uuid := (y)::UUID;
4745 x_kind := provsql.get_gate_type(x_uuid);
4746 y_kind := provsql.get_gate_type(y_uuid);
4747 IF x_kind NOT IN ('rv','value','arith','mixture') THEN
4748 RAISE EXCEPTION 'provsql.mixture: x must be a scalar RV root (rv / value / arith / mixture), got %', x_kind;
4749 END IF;
4750 IF y_kind NOT IN ('rv','value','arith','mixture') THEN
4751 RAISE EXCEPTION 'provsql.mixture: y must be a scalar RV root (rv / value / arith / mixture), got %', y_kind;
4752 END IF;
4753
4754 token := public.uuid_generate_v5(
4755 provsql.uuid_ns_provsql(),
4756 concat('mixture', p, x_uuid, y_uuid));
4757 PERFORM provsql.create_gate(token, 'mixture', ARRAY[p, x_uuid, y_uuid]);
4758 RETURN provsql.random_variable_make(token);
4759END
4760$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
4761
4762/**
4763 * @brief Ad-hoc mixture constructor that mints a fresh anonymous
4764 * @c gate_input Bernoulli with probability @p p_value.
4765 *
4766 * Sugar over the @c mixture(UUID, x, y) form: when the caller doesn't
4767 * care about reusing the Bernoulli token elsewhere in the circuit
4768 * (which is the common case &ndash; "give me a 0.3 / 0.7 weighted GMM,
4769 * I don't need to share the coin"), this overload creates the
4770 * underlying @c gate_input on the fly with a fresh
4771 * @c uuid_generate_v4() token, pins @p p_value via @c set_prob, and
4772 * threads everything into the UUID-keyed constructor.
4773 *
4774 * Each call mints a NEW Bernoulli, so two calls to
4775 * <tt>mixture(0.5, X, Y)</tt> are *independent* mixtures whose branch
4776 * selections are uncorrelated. When coupling is desired (e.g. two
4777 * mixtures sharing a coin), use the @c mixture(UUID, x, y) form with a
4778 * user-managed @c gate_input token.
4779 *
4780 * @warning <tt>VOLATILE</tt> is load-bearing for the same reason as
4781 * @ref normal and the other RV constructors -- folding under
4782 * @c STABLE / @c IMMUTABLE would collapse two independent draws into
4783 * one shared gate.
4784 *
4785 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
4786 */
4787CREATE OR REPLACE FUNCTION mixture(
4788 p_value double precision,
4789 x random_variable,
4790 y random_variable)
4791 RETURNS random_variable AS
4792$$
4793DECLARE
4794 p_token UUID;
4795BEGIN
4796 IF p_value IS NULL OR p_value <> p_value OR p_value < 0 OR p_value > 1 THEN
4797 RAISE EXCEPTION 'provsql.mixture: probability must be in [0,1] (got %)', p_value;
4798 END IF;
4799 p_token := public.uuid_generate_v4();
4800 PERFORM provsql.create_gate(p_token, 'input');
4801 PERFORM provsql.set_prob(p_token, p_value);
4802 RETURN provsql.mixture(p_token, x, y);
4803END
4804$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4805
4806/**
4807 * @brief Categorical-RV constructor over explicit (probabilities,
4808 * values) arrays.
4809 *
4810 * Builds a categorical-form @c gate_mixture directly: a fresh
4811 * @c gate_input "key" anchor and one @c gate_mulinput per outcome with
4812 * positive mass, all sharing the key. The wires
4813 * <tt>[key, mul_1, ..., mul_n]</tt> are what downstream evaluators
4814 * (@c Expectation, @c MonteCarloSampler, @c AnalyticEvaluator,
4815 * @c RangeCheck) recognise via @c isCategoricalMixture and treat as a
4816 * scalar RV with the categorical distribution @p probs over
4817 * @p outcomes.
4818 *
4819 * Validation:
4820 * - @p probs and @p outcomes must be non-null, same length, length &ge; 1.
4821 * - Each @c probs[i] must be finite, in <tt>[0, 1]</tt>, and the array
4822 * must sum to 1 within @c 1e-9.
4823 * - Each @c outcomes[i] must be finite.
4824 *
4825 * Each call mints a fresh key gate and a fresh set of mulinputs, so
4826 * two calls to @c categorical with the same arrays are *independent*
4827 * categorical RVs. The marking is @c VOLATILE accordingly.
4828 *
4829 * Degenerate case: a categorical with exactly one positive-mass
4830 * outcome reduces to @c as_random(v) at construction (the block would
4831 * just be a single mulinput, which is operationally a Dirac point
4832 * mass). Two such calls share the @c gate_value UUID via the v5
4833 * convention @c as_random already uses.
4834 *
4835 * @sa @c mixture for the Bernoulli-weighted choice constructor.
4836 * @sa <a href="https://en.wikipedia.org/wiki/Categorical_distribution">Wikipedia: Categorical distribution</a>
4837 */
4838CREATE OR REPLACE FUNCTION categorical(
4839 probs double precision[],
4840 outcomes double precision[])
4841 RETURNS random_variable AS
4842$$
4843DECLARE
4844 n INTEGER;
4845 p_sum double precision := 0.0;
4846 i INTEGER;
4847 key_token UUID;
4848 mix_token UUID;
4849 mul_token UUID;
4850 mul_tokens UUID[] := ARRAY[]::UUID[];
4851 mix_wires UUID[];
4852 pi_i double precision;
4853 vi_i double precision;
4854BEGIN
4855 IF probs IS NULL OR outcomes IS NULL THEN
4856 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must be non-null';
4857 END IF;
4858 n := array_length(probs, 1);
4859 IF n IS NULL OR n < 1 THEN
4860 RAISE EXCEPTION 'provsql.categorical: probs must be non-empty';
4861 END IF;
4862 IF array_length(outcomes, 1) <> n THEN
4863 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must have the same length (got % and %)',
4864 n, array_length(outcomes, 1);
4865 END IF;
4866
4867 FOR i IN 1..n LOOP
4868 pi_i := probs[i];
4869 vi_i := outcomes[i];
4870 -- PostgreSQL diverges from IEEE 754: NaN = NaN is TRUE there, so
4871 -- the canonical x <> x NaN test doesn't fire. Compare against the
4872 -- literal 'NaN'::float8 instead, and reject ±Infinity for outcomes
4873 -- explicitly.
4874 IF pi_i IS NULL OR pi_i = 'NaN'::float8 OR pi_i < 0 OR pi_i > 1 THEN
4875 RAISE EXCEPTION 'provsql.categorical: probs[%] must be in [0,1] (got %)', i, pi_i;
4876 END IF;
4877 IF vi_i IS NULL OR vi_i = 'NaN'::float8
4878 OR vi_i = 'Infinity'::float8 OR vi_i = '-Infinity'::float8 THEN
4879 RAISE EXCEPTION 'provsql.categorical: outcomes[%] must be finite (got %)', i, vi_i;
4880 END IF;
4881 p_sum := p_sum + pi_i;
4882 END LOOP;
4883 IF abs(p_sum - 1.0) > 1e-9 THEN
4884 RAISE EXCEPTION 'provsql.categorical: probs must sum to 1 within 1e-9 (got %)', p_sum;
4885 END IF;
4886
4887 -- Degenerate case: exactly one positive-mass outcome (the rest are
4888 -- zero). The "categorical" is then a Dirac point mass; skip the
4889 -- block-allocation entirely and return @c as_random(v), which yields
4890 -- a shared, v5-keyed gate_value -- exactly what downstream
4891 -- evaluators (rv_moment, AnalyticEvaluator, rv_support) treat
4892 -- specially. Saves a key gate and a mulinput per call, and lets
4893 -- two calls to @c categorical({1.0}, {v}) collide on the same
4894 -- gate_value UUID instead of producing distinct anonymous blocks.
4895 DECLARE
4896 nb_positive INTEGER := 0;
4897 only_idx INTEGER := 0;
4898 BEGIN
4899 FOR i IN 1..n LOOP
4900 IF probs[i] > 0.0 THEN
4901 nb_positive := nb_positive + 1;
4902 only_idx := i;
4903 END IF;
4904 END LOOP;
4905 IF nb_positive = 1 THEN
4906 RETURN provsql.as_random(outcomes[only_idx]);
4907 END IF;
4908 END;
4909
4910 -- Mint the block's key anchor. Probability 1.0 matches the
4911 -- joint-table convention: the categorical mass lives on the
4912 -- mulinputs, the key just identifies the block.
4913 key_token := public.uuid_generate_v4();
4914 PERFORM provsql.create_gate(key_token, 'input');
4915 PERFORM provsql.set_prob(key_token, 1.0);
4916
4917 -- One mulinput per positive-probability outcome. Zero-probability
4918 -- entries contribute no mass and are skipped: the gate_mixture's
4919 -- wire vector is otherwise polluted with no-op leaves.
4920 FOR i IN 1..n LOOP
4921 pi_i := probs[i];
4922 IF pi_i <= 0.0 THEN CONTINUE; END IF;
4923 mul_token := public.uuid_generate_v4();
4924 PERFORM provsql.create_gate(mul_token, 'mulinput', ARRAY[key_token]);
4925 PERFORM provsql.set_prob(mul_token, pi_i);
4926 PERFORM provsql.set_infos(mul_token, (i - 1));
4927 PERFORM provsql.set_extra(mul_token, outcomes[i]::TEXT);
4928 mul_tokens := mul_tokens || mul_token;
4929 END LOOP;
4930
4931 mix_wires := ARRAY[key_token] || mul_tokens;
4932 mix_token := public.uuid_generate_v4();
4933 PERFORM provsql.create_gate(mix_token, 'mixture', mix_wires);
4934 RETURN provsql.random_variable_make(mix_token);
4935END
4936$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4937
4938/**
4939 * @brief Gaussian-mixture-model (GMM) constructor.
4940 *
4941 * Packages the common fitted-density pattern -- a categorical choice
4942 * among Normal components -- into one call:
4943 *
4944 * @code
4945 * provsql.gmm(weights => ARRAY[0.3, 0.5, 0.2],
4946 * means => ARRAY[120.0, 380.0, 1200.0],
4947 * stddevs => ARRAY[40.0, 90.0, 250.0])
4948 * @endcode
4949 *
4950 * No new gate: the mixture decomposes into a stick-breaking cascade of
4951 * Bernoulli @c gate_mixture nodes over @c gate_rv Normal leaves
4952 * (component @c i is selected with conditional probability
4953 * @c w_i / (w_i + ... + w_n), so the joint selection probabilities are
4954 * exactly @p weights), which every evaluator already handles: moments
4955 * are closed-form through the mixture recursion, sampling is exact,
4956 * and comparisons ride the existing mixture machinery. Zero-weight
4957 * components are skipped; a single positive-weight component returns
4958 * its Normal directly (no mixture node).
4959 *
4960 * Validation mirrors @c categorical: same-length non-empty arrays,
4961 * weights finite in <tt>[0, 1]</tt> summing to 1 within @c 1e-9; the
4962 * component parameters are validated by @c provsql.normal (finite
4963 * @c mu, non-negative @c sigma; @c sigma @c = @c 0 degenerates to a
4964 * Dirac component).
4965 *
4966 * @sa @c mixture, @c categorical, @c normal
4967 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_model">Wikipedia: Mixture model</a>
4968 */
4969CREATE OR REPLACE FUNCTION gmm(
4970 weights double precision[],
4971 means double precision[],
4972 stddevs double precision[])
4973 RETURNS random_variable AS
4974$$
4975DECLARE
4976 n INTEGER;
4977 w_sum double precision := 0.0;
4978 i INTEGER;
4979 acc random_variable := NULL;
4980 remaining double precision := 0.0;
4981BEGIN
4982 IF weights IS NULL OR means IS NULL OR stddevs IS NULL THEN
4983 RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must be non-null';
4984 END IF;
4985 n := array_length(weights, 1);
4986 IF n IS NULL OR n < 1 THEN
4987 RAISE EXCEPTION 'provsql.gmm: weights must be non-empty';
4988 END IF;
4989 IF array_length(means, 1) <> n OR array_length(stddevs, 1) <> n THEN
4990 RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must have the same length (got %, %, %)',
4991 n, array_length(means, 1), array_length(stddevs, 1);
4992 END IF;
4993 FOR i IN 1..n LOOP
4994 IF weights[i] IS NULL OR weights[i] = 'NaN'::float8
4995 OR weights[i] < 0 OR weights[i] > 1 THEN
4996 RAISE EXCEPTION 'provsql.gmm: weights[%] must be in [0,1] (got %)',
4997 i, weights[i];
4998 END IF;
4999 w_sum := w_sum + weights[i];
5000 END LOOP;
5001 IF abs(w_sum - 1.0) > 1e-9 THEN
5002 RAISE EXCEPTION 'provsql.gmm: weights must sum to 1 within 1e-9 (got %)', w_sum;
5003 END IF;
5004
5005 -- Stick-breaking, built back to front: acc holds the mixture of
5006 -- components i+1..n, and prepending component i selects it with
5007 -- conditional probability w_i / (w_i + ... + w_n).
5008 FOR i IN REVERSE n..1 LOOP
5009 IF weights[i] <= 0.0 THEN
5010 CONTINUE;
5011 END IF;
5012 IF acc IS NULL THEN
5013 acc := provsql.normal(means[i], stddevs[i]);
5014 remaining := weights[i];
5015 ELSE
5016 remaining := remaining + weights[i];
5017 acc := provsql.mixture(least(1.0, weights[i] / remaining),
5018 provsql.normal(means[i], stddevs[i]), acc);
5019 END IF;
5020 END LOOP;
5021 RETURN acc;
5022END
5023$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5024
5025/**
5026 * @brief Empirical-samples constructor: the ecdf of a sample bundle as
5027 * a @c random_variable.
5028 *
5029 * Loads a Monte Carlo / MCMC / bootstrap sample array as the discrete
5030 * distribution putting mass @c 1/n on each draw (duplicates merge, so a
5031 * value drawn @c k times carries @c k/n) -- the standard empirical
5032 * distribution. Reduces entirely to @ref categorical, so the whole
5033 * exact discrete surface applies: moments are the sample moments,
5034 * comparisons against constants are decided analytically ("fraction of
5035 * samples below c"), and quantiles are the exact empirical quantiles.
5036 *
5037 * @code
5038 * -- Bulk load via array_agg over a sample table
5039 * INSERT INTO model_posteriors
5040 * SELECT param, provsql.empirical_samples(array_agg(value))
5041 * FROM mcmc_chain GROUP BY param;
5042 * @endcode
5043 *
5044 * At most 10000 distinct values (the categorical block cap): thin the
5045 * chain or bin the samples (e.g. with @c width_bucket) beyond that.
5046 *
5047 * @sa @ref categorical, @ref empirical_cdf
5048 * @sa <a href="https://en.wikipedia.org/wiki/Empirical_distribution_function">Wikipedia: Empirical distribution function</a>
5050CREATE OR REPLACE FUNCTION empirical_samples(samples double precision[])
5051 RETURNS random_variable AS
5052$$
5053DECLARE
5054 n INTEGER;
5055 sorted double precision[];
5056 outcomes double precision[] := '{}';
5057 probs double precision[] := '{}';
5058 v double precision;
5059 prev double precision;
5060 run INTEGER := 0;
5061 started BOOLEAN := false;
5062BEGIN
5063 n := array_length(samples, 1);
5064 IF n IS NULL OR n < 1 THEN
5065 RAISE EXCEPTION 'provsql.empirical_samples: samples must be non-empty';
5066 END IF;
5067 sorted := ARRAY(SELECT s FROM unnest(samples) AS s ORDER BY 1);
5068 FOREACH v IN ARRAY sorted LOOP
5069 IF v IS NULL OR v = 'NaN'::float8
5070 OR v = 'Infinity'::float8 OR v = '-Infinity'::float8 THEN
5071 RAISE EXCEPTION
5072 'provsql.empirical_samples: samples must be finite (got %)', v;
5073 END IF;
5074 IF started AND v = prev THEN
5075 run := run + 1;
5076 ELSE
5077 IF started THEN
5078 outcomes := outcomes || prev;
5079 probs := probs || (run::double precision / n);
5080 END IF;
5081 prev := v;
5082 run := 1;
5083 started := true;
5084 END IF;
5085 END LOOP;
5086 outcomes := outcomes || prev;
5087 probs := probs || (run::double precision / n);
5088 IF array_length(outcomes, 1) > 10000 THEN
5089 RAISE EXCEPTION
5090 'provsql.empirical_samples: at most 10000 distinct values are '
5091 'supported (got %); thin the chain or bin the samples (e.g. with '
5092 'width_bucket)', array_length(outcomes, 1);
5093 END IF;
5094 RETURN provsql.categorical(probs, outcomes);
5095END
5096$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5097
5099 * @brief Empirical-CDF constructor: a piecewise-linear CDF table as a
5100 * @c random_variable.
5101 *
5102 * Loads a tabulated CDF -- simulation output percentile tables, risk
5103 * models, expert-elicited forecasts -- as the distribution whose CDF is
5104 * @c cdf[i] at @c grid[i], linear in between: mass
5105 * @c cdf[i+1] @c - @c cdf[i] spread uniformly over
5106 * <tt>(grid[i], grid[i+1])</tt>, plus (when @c cdf[1] @c > @c 0) an
5107 * atom of mass @c cdf[1] at @c grid[1] for the probability at or below
5108 * the grid start. Packaged, like @ref gmm, as a stick-breaking cascade
5109 * of Bernoulli @ref mixture nodes over @ref uniform components (and the
5110 * optional @ref as_random atom), so moments and sampling are exact
5111 * through the existing mixture machinery; comparisons ride Monte Carlo.
5112 *
5113 * @code
5114 * provsql.empirical_cdf(
5115 * grid => ARRAY[0.0, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0],
5116 * cdf => ARRAY[0.32, 0.51, 0.67, 0.82, 0.94, 0.99, 1.0])
5117 * @endcode
5118 *
5119 * Validation: same-length arrays of at least two entries, @p grid
5120 * strictly increasing and finite, @p cdf non-decreasing within
5121 * <tt>[0, 1]</tt> and ending at @c 1 within @c 1e-9.
5123 * @sa @ref gmm, @ref empirical_samples
5124 * @sa <a href="https://en.wikipedia.org/wiki/Cumulative_distribution_function">Wikipedia: Cumulative distribution function</a>
5125 */
5126CREATE OR REPLACE FUNCTION empirical_cdf(grid double precision[],
5127 cdf double precision[])
5128 RETURNS random_variable AS
5129$$
5130DECLARE
5131 n INTEGER;
5132 i INTEGER;
5133 acc random_variable := NULL;
5134 remaining double precision := 0.0;
5135 w double precision;
5136 comp random_variable;
5137BEGIN
5138 n := array_length(grid, 1);
5139 IF n IS NULL OR n < 2 THEN
5140 RAISE EXCEPTION 'provsql.empirical_cdf: grid must have at least two entries';
5141 END IF;
5142 IF array_length(cdf, 1) <> n THEN
5143 RAISE EXCEPTION 'provsql.empirical_cdf: grid and cdf must have the same length (got % and %)',
5144 n, array_length(cdf, 1);
5145 END IF;
5146 IF n > 10000 THEN
5147 RAISE EXCEPTION 'provsql.empirical_cdf: at most 10000 grid points are supported (got %)', n;
5148 END IF;
5149 FOR i IN 1..n LOOP
5150 IF grid[i] IS NULL OR grid[i] = 'NaN'::float8
5151 OR grid[i] = 'Infinity'::float8 OR grid[i] = '-Infinity'::float8 THEN
5152 RAISE EXCEPTION 'provsql.empirical_cdf: grid[%] must be finite (got %)', i, grid[i];
5153 END IF;
5154 IF i > 1 AND NOT grid[i] > grid[i-1] THEN
5155 RAISE EXCEPTION 'provsql.empirical_cdf: grid must be strictly increasing (grid[%] = %, grid[%] = %)',
5156 i-1, grid[i-1], i, grid[i];
5157 END IF;
5158 IF cdf[i] IS NULL OR cdf[i] = 'NaN'::float8 OR cdf[i] < 0 OR cdf[i] > 1 THEN
5159 RAISE EXCEPTION 'provsql.empirical_cdf: cdf[%] must be in [0,1] (got %)', i, cdf[i];
5160 END IF;
5161 IF i > 1 AND cdf[i] < cdf[i-1] THEN
5162 RAISE EXCEPTION 'provsql.empirical_cdf: cdf must be non-decreasing (cdf[%] = %, cdf[%] = %)',
5163 i-1, cdf[i-1], i, cdf[i];
5164 END IF;
5165 END LOOP;
5166 IF abs(cdf[n] - 1.0) > 1e-9 THEN
5167 RAISE EXCEPTION 'provsql.empirical_cdf: cdf must end at 1 within 1e-9 (got %)', cdf[n];
5168 END IF;
5169
5170 -- Stick-breaking cascade, back to front: component i = 1 is the atom
5171 -- at the grid start (mass cdf[1]); component i >= 2 is
5172 -- uniform(grid[i-1], grid[i]) with mass cdf[i] - cdf[i-1].
5173 FOR i IN REVERSE n..1 LOOP
5174 w := CASE WHEN i = 1 THEN cdf[1] ELSE cdf[i] - cdf[i-1] END;
5175 IF w <= 0.0 THEN
5176 CONTINUE;
5177 END IF;
5178 comp := CASE WHEN i = 1 THEN provsql.as_random(grid[1])
5179 ELSE provsql.uniform(grid[i-1], grid[i]) END;
5180 IF acc IS NULL THEN
5181 acc := comp;
5182 remaining := w;
5183 ELSE
5184 remaining := remaining + w;
5185 acc := provsql.mixture(least(1.0, w / remaining), comp, acc);
5186 END IF;
5187 END LOOP;
5188 RETURN acc;
5189END
5190$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5191
5192/**
5193 * @brief Lift a deterministic constant into a random_variable
5194 *
5195 * Creates a <tt>gate_value</tt> carrying the constant's TEXT form so
5196 * that comparisons against a <tt>random_variable</tt> column produce
5197 * the same circuit shape regardless of whether the operand is an
5198 * actual RV or a literal constant.
5199 *
5200 * Marked <tt>IMMUTABLE</tt>: the gate UUID is derived deterministically
5201 * from the constant via the same v5 convention as <tt>provenance_semimod</tt>'s
5202 * inline value gate (<tt>concat('value', CAST(c AS VARCHAR))</tt>), so
5203 * <tt>as_random(2)</tt> always resolves to the same gate, and any other
5204 * code path that already creates a value gate for the same constant
5205 * (e.g. <tt>provenance_semimod</tt>) shares the UUID.
5206 * <tt>create_gate</tt> is idempotent on already-mapped tokens, so
5207 * repeat invocations are harmless.
5208 *
5209 * @sa <a href="https://en.wikipedia.org/wiki/Degenerate_distribution">Wikipedia: Degenerate distribution (Dirac point mass)</a>
5210 */
5211CREATE OR REPLACE FUNCTION as_random(c double precision)
5212 RETURNS random_variable AS
5213$$
5214DECLARE
5215 -- Canonicalise -0.0 to +0.0: IEEE 754 defines x + 0.0 = +0.0 for
5216 -- both signed zeros, and is identity for finite, NaN, and ±Infinity.
5217 -- Without this, as_random(-0.0) and as_random(+0.0) would produce
5218 -- different gate UUIDs (their CAST AS VARCHAR TEXT representations
5219 -- differ: '-0' vs '0') even though they denote the same constant.
5220 c_canon double precision := c + 0.0;
5221 c_text varchar := CAST(c_canon AS VARCHAR);
5222 token UUID := public.uuid_generate_v5(
5223 provsql.uuid_ns_provsql(), concat('value', c_text));
5224BEGIN
5225 PERFORM provsql.create_gate(token, 'value');
5226 PERFORM provsql.set_extra(token, c_text);
5227 RETURN provsql.random_variable_make(token);
5228END
5229$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
5230
5231/**
5232 * @brief Implicit cast double precision -> random_variable (lifts a
5233 * scalar literal to a constant RV).
5234 *
5235 * Lets users write <tt>WHERE reading > 2.5::float8</tt> instead of
5236 * <tt>WHERE reading > provsql.as_random(2.5)</tt>; the planner-hook
5237 * rewriter then sees a uniform <tt>random_variable</tt> on both sides.
5238 * Sibling casts below cover @c INTEGER and @c NUMERIC literals so
5239 * plain <tt>WHERE reading > 2</tt> and <tt>WHERE reading > 2.5</tt>
5240 * also work; PostgreSQL's operator resolution does not chain casts
5241 * across more than one step, so each NUMERIC-source type needs its
5242 * own direct cast.
5244CREATE CAST (double precision AS random_variable)
5245 WITH FUNCTION as_random(double precision) AS IMPLICIT;
5246
5247/** @brief @c as_random for @c INTEGER (delegates to the @c float8 form). */
5248CREATE OR REPLACE FUNCTION as_random(c INTEGER)
5249 RETURNS random_variable AS
5250$$ SELECT provsql.as_random(c::double precision); $$
5251LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
5252
5253/** @brief @c as_random for @c NUMERIC (delegates to the @c float8 form). */
5254CREATE OR REPLACE FUNCTION as_random(c NUMERIC)
5255 RETURNS random_variable AS
5256$$ SELECT provsql.as_random(c::double precision); $$
5257LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
5258
5259/** @brief Implicit cast INTEGER -> random_variable. */
5260CREATE CAST (INTEGER AS random_variable)
5261 WITH FUNCTION as_random(INTEGER) AS IMPLICIT;
5262
5263/** @brief Implicit cast NUMERIC -> random_variable. */
5264CREATE CAST (NUMERIC AS random_variable)
5265 WITH FUNCTION as_random(NUMERIC) AS IMPLICIT;
5266
5267/**
5268 * @name Arithmetic and comparison on random_variable
5269 *
5270 * Each binary operator below is declared on @c (random_variable,
5271 * random_variable) only; mixed shapes such as <tt>rv + 2</tt> or
5272 * <tt>2.5 > rv</tt> resolve through the implicit casts from
5273 * @c INTEGER / @c NUMERIC / @c double @c precision to
5274 * @c random_variable declared above. This avoids the resolution
5275 * ambiguity that would arise if both <tt>(rv, NUMERIC)</tt> and
5276 * <tt>(rv, rv)</tt> overloads were declared while implicit casts also
5277 * existed.
5278 *
5279 * Arithmetic operators build a @c gate_arith via @c provenance_arith
5280 * and return a new @c random_variable wrapping its UUID.
5282 * Comparison operators are placeholders that return @c BOOLEAN and
5283 * raise if executed -- the @c BOOLEAN return type is required so that
5284 * PostgreSQL accepts <tt>WHERE rv > 2</tt> at parse-analyze. The
5285 * planner hook intercepts every such @c OpExpr (matched by
5286 * @c opfuncid against @c constants_t::OID_FUNCTION_RV_CMP) and rewrites
5287 * it into a @c provenance_cmp call whose UUID is conjoined into the
5288 * tuple's @c provsql column via @c provenance_times. Code that needs
5289 * a @c gate_cmp UUID directly (without going through the planner hook)
5290 * uses the @c rv_cmp_* family below, which call @c provenance_cmp
5291 * with the matching float8-comparator OID.
5292 *
5293 * @{
5294 */
5295
5296/** @brief @c random_variable + @c random_variable (gate_arith PLUS). */
5297CREATE OR REPLACE FUNCTION random_variable_plus(
5298 a random_variable, b random_variable)
5299 RETURNS random_variable AS
5300$$
5301 SELECT provsql.random_variable_make(
5302 provsql.provenance_arith(
5303 0, -- PROVSQL_ARITH_PLUS
5304 ARRAY[(a)::UUID,
5305 (b)::UUID]));
5306$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5307
5308/** @brief @c random_variable - @c random_variable (gate_arith MINUS). */
5309CREATE OR REPLACE FUNCTION random_variable_minus(
5310 a random_variable, b random_variable)
5311 RETURNS random_variable AS
5312$$
5313 SELECT provsql.random_variable_make(
5314 provsql.provenance_arith(
5315 2, -- PROVSQL_ARITH_MINUS
5316 ARRAY[(a)::UUID,
5317 (b)::UUID]));
5318$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5319
5320/** @brief @c random_variable * @c random_variable (gate_arith TIMES). */
5321CREATE OR REPLACE FUNCTION random_variable_times(
5322 a random_variable, b random_variable)
5323 RETURNS random_variable AS
5324$$
5325 SELECT provsql.random_variable_make(
5326 provsql.provenance_arith(
5327 1, -- PROVSQL_ARITH_TIMES
5328 ARRAY[(a)::UUID,
5329 (b)::UUID]));
5330$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5331
5332/** @brief @c random_variable / @c random_variable (gate_arith DIV). */
5333CREATE OR REPLACE FUNCTION random_variable_div(
5334 a random_variable, b random_variable)
5335 RETURNS random_variable AS
5336$$
5337 SELECT provsql.random_variable_make(
5338 provsql.provenance_arith(
5339 3, -- PROVSQL_ARITH_DIV
5340 ARRAY[(a)::UUID,
5341 (b)::UUID]));
5342$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5343
5344/** @brief Unary @c -random_variable (gate_arith NEG). */
5345CREATE OR REPLACE FUNCTION random_variable_neg(a random_variable)
5346 RETURNS random_variable AS
5347$$
5348 SELECT provsql.random_variable_make(
5349 provsql.provenance_arith(
5350 4, -- PROVSQL_ARITH_NEG
5351 ARRAY[(a)::UUID]));
5352$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5353
5354/**
5355 * @brief @c random_variable ^ @c random_variable (gate_arith POW).
5356 *
5357 * Real-valued branch only: evaluation raises if a negative base is
5358 * drawn together with a non-INTEGER exponent (write
5359 * <tt>pow(greatest(x, 0), p)</tt> for the non-negative branch).
5360 */
5361CREATE OR REPLACE FUNCTION random_variable_pow(
5362 a random_variable, b random_variable)
5363 RETURNS random_variable AS
5364$$
5365 SELECT provsql.random_variable_make(
5366 provsql.provenance_arith(
5367 7, -- PROVSQL_ARITH_POW
5368 ARRAY[(a)::UUID,
5369 (b)::UUID]));
5370$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5371
5372/**
5373 * @brief Natural logarithm of a @c random_variable (gate_arith LN).
5374 *
5375 * Defined on @c [0, +Infinity): evaluation raises if a negative value
5376 * is drawn (restrict the argument's support); a draw of exactly @c 0
5377 * yields @c -Infinity.
5378 */
5379CREATE OR REPLACE FUNCTION ln(a random_variable)
5380 RETURNS random_variable AS
5381$$
5382 SELECT provsql.random_variable_make(
5383 provsql.provenance_arith(
5384 8, -- PROVSQL_ARITH_LN
5385 ARRAY[(a)::UUID]));
5386$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5387
5388/** @brief @c e^x for a @c random_variable (gate_arith EXP). Total. */
5389CREATE OR REPLACE FUNCTION exp(a random_variable)
5390 RETURNS random_variable AS
5391$$
5392 SELECT provsql.random_variable_make(
5393 provsql.provenance_arith(
5394 9, -- PROVSQL_ARITH_EXP
5395 ARRAY[(a)::UUID]));
5396$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5397
5398/**
5399 * @brief @c pow / @c power spellings of the @c ^ operator, mirroring
5400 * PostgreSQL's NUMERIC surface. Scalar exponents resolve
5401 * through the implicit NUMERIC-to-rv casts:
5402 * <tt>pow(x, 0.5)</tt> is <tt>x ^ 0.5</tt>.
5403 */
5404CREATE OR REPLACE FUNCTION pow(a random_variable, b random_variable)
5405 RETURNS random_variable AS
5406$$
5407 SELECT provsql.random_variable_pow(a, b);
5408$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5409
5410CREATE OR REPLACE FUNCTION power(a random_variable, b random_variable)
5411 RETURNS random_variable AS
5412$$
5413 SELECT provsql.random_variable_pow(a, b);
5414$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5415
5416/**
5417 * @brief Square root of a @c random_variable: sugar for
5418 * <tt>x ^ 0.5</tt> (no gate or opcode of its own). Evaluation
5419 * raises on a negative draw, like any non-INTEGER exponent.
5420 */
5421CREATE OR REPLACE FUNCTION sqrt(a random_variable)
5422 RETURNS random_variable AS
5423$$
5424 SELECT provsql.random_variable_pow(a, provsql.as_random(0.5));
5425$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5426
5427/**
5428 * @brief Internal helper: float8-comparator OID for a given symbol.
5429 *
5430 * Wraps the @c '&lt;sym&gt;(double precision,double precision)'::regoperator
5431 * lookup so the per-comparator functions read uniformly. Marked
5432 * @c IMMUTABLE because the resolved OID is fixed at catalog level
5433 * (the float8 comparators are core PG and never re-installed).
5434 */
5435CREATE OR REPLACE FUNCTION random_variable_cmp_oid(sym TEXT)
5436 RETURNS oid AS
5437$$
5438 SELECT (sym || '(double precision,double precision)')::regoperator::oid;
5439$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5440
5441/* The six @c random_variable_{lt,le,eq,ne,ge,gt} functions below are
5442 * BOOLEAN placeholders -- they exist only so the @c (rv, rv) operators
5443 * can be declared at all (PostgreSQL needs a procedure to bind to the
5444 * operator definition, and a procedure returning anything but @c BOOLEAN
5445 * would be rejected by parse-analyze in a WHERE position). They MUST
5446 * NOT be invoked directly: the planner hook in @c src/provsql.c
5447 * intercepts every @c OpExpr whose @c opfuncid matches one of these and
5448 * rewrites it into a @c provenance_cmp() call against the row's
5449 * provenance. If the executor ever reaches one of these, it means the
5450 * planner hook was bypassed (e.g. @c provsql.active was off), in which
5451 * case raising is the right behaviour. */
5452
5453/** @brief Placeholder body shared by every <tt>random_variable_*</tt>
5454 * comparison procedure. Raises with a uniform message. */
5455CREATE OR REPLACE FUNCTION random_variable_cmp_placeholder(
5456 a random_variable, b random_variable)
5457 RETURNS BOOLEAN AS
5458$$
5459BEGIN
5460 RAISE EXCEPTION 'random_variable comparison must be rewritten by the '
5461 'ProvSQL planner hook (is provsql.active off?)';
5462END
5463$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
5464
5465CREATE OR REPLACE FUNCTION random_variable_lt(
5466 a random_variable, b random_variable) RETURNS BOOLEAN AS
5467$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5468LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5469
5470CREATE OR REPLACE FUNCTION random_variable_le(
5471 a random_variable, b random_variable) RETURNS BOOLEAN AS
5472$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5473LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5474
5475CREATE OR REPLACE FUNCTION random_variable_eq(
5476 a random_variable, b random_variable) RETURNS BOOLEAN AS
5477$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5478LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5479
5480CREATE OR REPLACE FUNCTION random_variable_ne(
5481 a random_variable, b random_variable) RETURNS BOOLEAN AS
5482$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5483LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5484
5485CREATE OR REPLACE FUNCTION random_variable_ge(
5486 a random_variable, b random_variable) RETURNS BOOLEAN AS
5487$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5488LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5489
5490CREATE OR REPLACE FUNCTION random_variable_gt(
5491 a random_variable, b random_variable) RETURNS BOOLEAN AS
5492$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5493LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5494
5495/* Direct UUID constructors -- used by tests and any caller that wants
5496 * a @c gate_cmp without going through the planner hook (e.g. building
5497 * a circuit fragment in a SELECT list). Each delegates to
5498 * @c provenance_cmp with the matching float8-comparator OID. */
5499
5500/** @brief Build a @c gate_cmp for <tt>a &lt; b</tt> and return its UUID. */
5501CREATE OR REPLACE FUNCTION rv_cmp_lt(
5502 a random_variable, b random_variable) RETURNS UUID AS
5503$$
5504 SELECT provsql.provenance_cmp(
5505 (a)::UUID,
5506 provsql.random_variable_cmp_oid('<'),
5507 (b)::UUID);
5508$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5509
5510/** @brief Build a @c gate_cmp for <tt>a &le; b</tt> and return its UUID. */
5511CREATE OR REPLACE FUNCTION rv_cmp_le(
5512 a random_variable, b random_variable) RETURNS UUID AS
5513$$
5514 SELECT provsql.provenance_cmp(
5515 (a)::UUID,
5516 provsql.random_variable_cmp_oid('<='),
5517 (b)::UUID);
5518$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5519
5520/** @brief Build a @c gate_cmp for <tt>a = b</tt> and return its UUID. */
5521CREATE OR REPLACE FUNCTION rv_cmp_eq(
5522 a random_variable, b random_variable) RETURNS UUID AS
5523$$
5524 SELECT provsql.provenance_cmp(
5525 (a)::UUID,
5526 provsql.random_variable_cmp_oid('='),
5527 (b)::UUID);
5528$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5529
5530/** @brief Build a @c gate_cmp for <tt>a &lt;&gt; b</tt> and return its UUID. */
5531CREATE OR REPLACE FUNCTION rv_cmp_ne(
5532 a random_variable, b random_variable) RETURNS UUID AS
5533$$
5534 SELECT provsql.provenance_cmp(
5535 (a)::UUID,
5536 provsql.random_variable_cmp_oid('<>'),
5537 (b)::UUID);
5538$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5539
5540/** @brief Build a @c gate_cmp for <tt>a &ge; b</tt> and return its UUID. */
5541CREATE OR REPLACE FUNCTION rv_cmp_ge(
5542 a random_variable, b random_variable) RETURNS UUID AS
5543$$
5544 SELECT provsql.provenance_cmp(
5545 (a)::UUID,
5546 provsql.random_variable_cmp_oid('>='),
5547 (b)::UUID);
5548$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5549
5550/** @brief Build a @c gate_cmp for <tt>a &gt; b</tt> and return its UUID. */
5551CREATE OR REPLACE FUNCTION rv_cmp_gt(
5552 a random_variable, b random_variable) RETURNS UUID AS
5553$$
5554 SELECT provsql.provenance_cmp(
5555 (a)::UUID,
5556 provsql.random_variable_cmp_oid('>'),
5557 (b)::UUID);
5558$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5559
5560CREATE OPERATOR + (
5561 LEFTARG = random_variable,
5562 RIGHTARG = random_variable,
5563 PROCEDURE = random_variable_plus,
5564 COMMUTATOR = +
5565);
5566
5567CREATE OPERATOR - (
5568 LEFTARG = random_variable,
5569 RIGHTARG = random_variable,
5570 PROCEDURE = random_variable_minus
5571);
5572
5573CREATE OPERATOR * (
5574 LEFTARG = random_variable,
5575 RIGHTARG = random_variable,
5576 PROCEDURE = random_variable_times,
5577 COMMUTATOR = *
5578);
5579
5580CREATE OPERATOR / (
5581 LEFTARG = random_variable,
5582 RIGHTARG = random_variable,
5583 PROCEDURE = random_variable_div
5584);
5585
5586/** @brief Prefix unary minus on @c random_variable. */
5587CREATE OPERATOR - (
5588 RIGHTARG = random_variable,
5589 PROCEDURE = random_variable_neg
5590);
5591
5592CREATE OPERATOR ^ (
5593 LEFTARG = random_variable,
5594 RIGHTARG = random_variable,
5595 PROCEDURE = random_variable_pow
5596);
5597
5598CREATE OPERATOR < (
5599 LEFTARG = random_variable,
5600 RIGHTARG = random_variable,
5601 PROCEDURE = random_variable_lt,
5602 COMMUTATOR = >,
5603 NEGATOR = >=
5604);
5605
5606CREATE OPERATOR <= (
5607 LEFTARG = random_variable,
5608 RIGHTARG = random_variable,
5609 PROCEDURE = random_variable_le,
5610 COMMUTATOR = >=,
5611 NEGATOR = >
5612);
5613
5614CREATE OPERATOR = (
5615 LEFTARG = random_variable,
5616 RIGHTARG = random_variable,
5617 PROCEDURE = random_variable_eq,
5618 COMMUTATOR = =,
5619 NEGATOR = <>
5620);
5621
5622CREATE OPERATOR <> (
5623 LEFTARG = random_variable,
5624 RIGHTARG = random_variable,
5625 PROCEDURE = random_variable_ne,
5626 COMMUTATOR = <>,
5627 NEGATOR = =
5628);
5629
5630CREATE OPERATOR >= (
5631 LEFTARG = random_variable,
5632 RIGHTARG = random_variable,
5633 PROCEDURE = random_variable_ge,
5634 COMMUTATOR = <=,
5635 NEGATOR = <
5636);
5637
5638CREATE OPERATOR > (
5639 LEFTARG = random_variable,
5640 RIGHTARG = random_variable,
5641 PROCEDURE = random_variable_gt,
5642 COMMUTATOR = <,
5643 NEGATOR = <=
5644);
5645
5646/**
5647 * @brief btree comparison support for @c random_variable -- always an error.
5648 *
5649 * A @c random_variable is a distribution, not a scalar, so it has no total
5650 * order: sorting (@c ORDER @c BY), de-duplicating (@c DISTINCT), grouping, and
5651 * the built-in @c GREATEST / @c LEAST all reduce to this btree comparison
5652 * proc, which raises a clear diagnostic rather than a placeholder message.
5653 *
5654 * The proc exists only so a DEFAULT btree operator class can be declared for
5655 * @c random_variable -- which is what lets PostgreSQL's @c GREATEST / @c LEAST
5656 * grammar parse over random variables so the planner hook can lift it into a
5657 * @c gate_arith @c MAX / @c MIN order statistic. When the hook is active the
5658 * @c GREATEST / @c LEAST node is rewritten before it ever calls this proc.
5659 */
5660CREATE OR REPLACE FUNCTION random_variable_btree_cmp(
5661 a random_variable, b random_variable) RETURNS INTEGER AS
5662$$
5663BEGIN
5664 RAISE EXCEPTION 'comparison or ordering of random_variable values is '
5665 'meaningless: a random_variable is a distribution, not a scalar'
5666 USING HINT =
5667 'Compare them as a probabilistic event -- in a WHERE / JOIN clause or '
5668 'with probability(x > y); take order statistics with provsql.greatest / '
5669 'provsql.least (or the min / max aggregates); summarise numerically with '
5670 'expected / variance / support.';
5671END
5672$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
5673
5674-- DEFAULT btree operator class over the (planner-hook-lifted) comparison
5675-- operators. Its only purpose is to make GREATEST / LEAST over random_variable
5676-- parse; every actual comparison it would drive (ORDER BY, DISTINCT, an
5677-- un-rewritten GREATEST) funnels through random_variable_btree_cmp above and
5678-- raises the "meaningless" diagnostic.
5679CREATE OPERATOR CLASS random_variable_ops
5680 DEFAULT FOR TYPE random_variable USING btree AS
5681 OPERATOR 1 <,
5682 OPERATOR 2 <=,
5683 OPERATOR 3 =,
5684 OPERATOR 4 >=,
5685 OPERATOR 5 >,
5686 FUNCTION 1 random_variable_btree_cmp(random_variable, random_variable);
5687
5688/**
5689 * @brief Condition a random variable on an event: @c "X | C".
5690 *
5691 * Returns a conditioned distribution that flows onward like any other
5692 * @c random_variable: it can be stored, re-conditioned, and queried with
5693 * @c expected / @c variance / @c moment / @c support, which then report the
5694 * conditional distribution. @p cond is a Boolean-event provenance token,
5695 * typically a comparison over the variable itself (@c "X | rv_cmp_gt(X,
5696 * as_random(3))" -- a truncation) or any external event.
5697 *
5698 * Unlike the UUID carrier's terminal @c cond, the random-variable form is a
5699 * composable two-child @c gate_conditioned @c [target, condition]: the moment
5700 * / support dispatchers unpack it and route through the existing conditional
5701 * evaluator (@c rv_moment over the joint of the target and the condition).
5702 * Nested conditioning folds: @c "(X|A)|B = X|(A∧B)".
5703 */
5704CREATE OR REPLACE FUNCTION random_variable_cond(rv random_variable, cond UUID)
5705 RETURNS random_variable AS
5706$$
5707DECLARE
5708 tgt UUID;
5709 ev UUID;
5710 result UUID;
5711 ch UUID[];
5712BEGIN
5713 IF cond IS NULL OR cond = gate_one() THEN
5714 RETURN rv;
5715 END IF;
5716
5717 -- A point-equality "Y = c" on a bare random-variable leaf is an
5718 -- OBSERVATION, not a truncation: rewrite it to the internal likelihood-
5719 -- weighting evidence (its density / mass at c). This is what lets
5720 -- "X | (normal(mu,1) = 8)" (a continuous point event, measure-zero as a
5721 -- Boolean selection) condition as the disintegration rather than fold to
5722 -- an infeasible event.
5723 cond := provsql.evidence_as_observation(cond);
5724
5725 tgt := (rv)::UUID;
5726 IF get_gate_type(tgt) = 'conditioned'
5727 AND array_length(get_children(tgt), 1) = 2 THEN
5728 -- Fold (X|A)|B = X|(A∧B): the rv-carrier conditioned gate is the
5729 -- two-child [target, condition] shape; accumulate the new event.
5730 ch := get_children(tgt);
5731 tgt := ch[1];
5732 ev := provenance_times(ch[2], cond);
5733 ELSE
5734 ev := cond;
5735 END IF;
5736
5737 result := public.uuid_generate_v5(uuid_ns_provsql(),
5738 concat('conditioned', tgt, ev));
5739 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev]);
5740 RETURN (result)::random_variable;
5741END
5742$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
5743 SECURITY DEFINER PARALLEL SAFE;
5745CREATE OPERATOR | (
5746 LEFTARG = random_variable,
5747 RIGHTARG = UUID,
5748 PROCEDURE = random_variable_cond
5749);
5750
5751/**
5752 * @brief Placeholder for @c "X | (predicate)" -- conditioning a random
5753 * variable on a Boolean comparison written naturally.
5754 *
5755 * Lets one write @c "X | (X > 3)" instead of
5756 * @c "X | rv_cmp_gt(X, as_random(3))". Never executes: the ProvSQL planner
5757 * hook rewrites the Boolean operand (a combination of random_variable
5758 * comparisons) into the corresponding condition gate and emits
5759 * @c random_variable_cond. Reaching it at runtime means the rewriter was
5760 * inactive or the predicate was not a random_variable comparison.
5761 */
5762CREATE OR REPLACE FUNCTION random_variable_cond_predicate(
5763 rv random_variable, predicate BOOLEAN) RETURNS random_variable AS
5764$$
5765BEGIN
5766 RAISE EXCEPTION 'random_variable | (predicate) must be rewritten by the '
5767 'ProvSQL planner hook: the right operand must be a Boolean combination '
5768 'of random_variable comparisons (is provsql.active off?)';
5769END
5770$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
5771
5772CREATE OPERATOR | (
5773 LEFTARG = random_variable,
5774 RIGHTARG = BOOLEAN,
5775 PROCEDURE = random_variable_cond_predicate
5777
5779 * @brief Unpack the target of a random-variable conditioning gate.
5780 *
5781 * For a two-child @c gate_conditioned @c [target, condition] (the @c "X | C"
5782 * shape) returns @p target; for any other token returns it unchanged. Used
5783 * by the moment / support dispatchers to route a conditioned distribution
5784 * through the existing conditional evaluator.
5785 */
5786CREATE OR REPLACE FUNCTION rv_conditioned_target(token UUID) RETURNS UUID AS
5787$$
5788 SELECT CASE
5789 WHEN provsql.get_gate_type(token) = 'conditioned'
5790 AND array_length(provsql.get_children(token), 1) = 2
5791 THEN (provsql.get_children(token))[1]
5792 ELSE token
5793 END;
5794$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
5795
5796/**
5797 * @brief Combine a conditioning gate's event with an explicit @p prov.
5798 *
5799 * For a two-child @c gate_conditioned @c [target, condition] returns
5800 * @c "condition ∧ prov"; otherwise returns @p prov unchanged. Lets a stored
5801 * @c "X | C" be queried as @c expected(X|C) (prov defaulting to one) or have
5802 * an extra condition conjoined as @c expected(X|C, extra_prov).
5803 */
5804CREATE OR REPLACE FUNCTION rv_conditioned_prov(token UUID, prov UUID)
5805 RETURNS UUID AS
5806$$
5807 SELECT CASE
5808 WHEN provsql.get_gate_type(token) = 'conditioned'
5809 AND array_length(provsql.get_children(token), 1) = 2
5810 THEN provsql.provenance_times((provsql.get_children(token))[2], prov)
5811 ELSE prov
5812 END;
5813$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
5814
5816 * Latent-variable posterior inference.
5817 *
5818 * Likelihood weighting (self-normalised importance sampling): bind an
5819 * observed datum to a latent-dependent random-variable leaf with observe,
5820 * conjoin the per-observation evidence with and_agg into a single evidence
5821 * token, and pass it as the prov conditioning argument of any moment /
5822 * quantile / sample readout. Latents are drawn from the prior (the
5823 * existing forward recursion) and each draw is weighted by the observed
5824 * leaves' densities at the data; the readouts then report the posterior.
5825 * It is the continuous generalisation of the rejection-based conditioning:
5826 * a Boolean event in the evidence contributes a 0/1 weight, an observe
5827 * contributes a pdf weight -- same evidence conjunction, same
5828 * "P(query AND evidence)/P(evidence)" normaliser, now weighted.
5829 */
5830
5831/**
5832 * @brief Internal: rewrite a point-equality conditioning event into an
5833 * observation. If @p ev is a @c gate_cmp with the @c "=" operator,
5834 * one side a bare @c gate_rv leaf and the other a constant, return
5835 * @c observe(leaf, const); otherwise return @p ev unchanged.
5836 *
5837 * This is the bridge that makes the natural equality form the surface for
5838 * likelihood-weighting conditioning: @c "X | (Y = c)" and @c "given(Y = c)"
5839 * both produce a @c gate_cmp, which this turns into density evidence. A
5840 * point event on a bare leaf is only meaningful as an observation (a
5841 * continuous @c "Y = c" is measure-zero as a Boolean selection), so the
5842 * rewrite is unambiguous. Non-equality / non-leaf events pass through as
5843 * ordinary Boolean conditioning.
5844 */
5845CREATE OR REPLACE FUNCTION evidence_as_observation(ev UUID) RETURNS UUID AS
5846$$
5847DECLARE
5848 ch UUID[];
5849 i1 INTEGER;
5850 leaf UUID;
5851 datum_gate UUID;
5852BEGIN
5853 IF ev IS NULL OR provsql.get_gate_type(ev) <> 'cmp' THEN
5854 RETURN ev;
5855 END IF;
5856 ch := provsql.get_children(ev);
5857 IF array_length(ch, 1) <> 2 THEN
5858 RETURN ev;
5859 END IF;
5860 -- The cmp stores the comparison OPERATOR's OID in info1; match on its name
5861 -- '=' the same way the C-side cmpOpFromOid does (get_opname), rather than a
5862 -- fixed operator OID (which varies per install / carrier type).
5863 SELECT info1 INTO i1 FROM provsql.get_infos(ev);
5864 IF (SELECT oprname FROM pg_catalog.pg_operator WHERE oid = i1) IS DISTINCT FROM '=' THEN
5865 RETURN ev; -- not an equality
5866 END IF;
5867 IF provsql.get_gate_type(ch[1]) = 'rv'
5868 AND provsql.get_gate_type(ch[2]) = 'value' THEN
5869 leaf := ch[1]; datum_gate := ch[2];
5870 ELSIF provsql.get_gate_type(ch[2]) = 'rv'
5871 AND provsql.get_gate_type(ch[1]) = 'value' THEN
5872 leaf := ch[2]; datum_gate := ch[1];
5873 ELSE
5874 RETURN ev; -- not a bare-leaf-vs-constant point event
5875 END IF;
5876 RETURN provsql.observe((leaf)::random_variable,
5877 provsql.get_extra(datum_gate)::double precision);
5878END
5879$$ LANGUAGE plpgsql VOLATILE
5880 SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
5883 * @brief Internal: bind an observed datum to a random-variable leaf --
5884 * the likelihood-weighting evidence behind @c "X | (Y = d)".
5885 *
5886 * @p x MUST be a bare @c gate_rv leaf (typically a latent-parameterised
5887 * one, e.g. @c normal(mu, 1) sharing a latent @c mu across rows).
5888 * Returns an @b evidence UUID -- a @c gate_observe wrapping the leaf with
5889 * the datum in @c extra -- that composes with other evidence through
5890 * @c and_agg (a @c gate_times conjunction) and is consumed by the
5891 * importance-sampling weight walk, contributing the factor @c f_X(d).
5892 *
5893 * Internal: the user-facing surface is the equality form @c "X | (Y = d)"
5894 * (single conditioning) and @c "given(Y = d)" (per-row evidence for
5895 * @c and_agg), both of which route here through @c evidence_as_observation.
5896 *
5897 * A fresh gate is minted per call (each observation is a distinct
5898 * evidence atom, so a repeated @c (leaf, datum) contributes its density
5899 * factor once per row -- and each is a separate Shapley atom). Observing
5900 * a derived quantity (@c observe(X+Y, d)) is out of scope: it needs a
5901 * change-of-variables density; a non-leaf argument is refused.
5902 */
5903CREATE OR REPLACE FUNCTION observe(x random_variable, datum double precision)
5904 RETURNS UUID AS
5905$$
5906DECLARE
5907 leaf UUID := (x)::UUID;
5908 result UUID;
5909BEGIN
5910 IF provsql.get_gate_type(leaf) <> 'rv' THEN
5911 RAISE EXCEPTION 'provsql.observe: the argument must be a bare '
5912 'random-variable leaf (a gate_rv), got a % gate', provsql.get_gate_type(leaf)
5913 USING HINT = 'observe binds a datum to a single distribution leaf; '
5914 'observing a derived quantity (a sum, product, or comparison) needs '
5915 'a change-of-variables density and is out of scope.';
5916 END IF;
5917 IF NOT provsql.is_finite_float8(datum) THEN
5918 RAISE EXCEPTION 'provsql.observe: datum must be finite (got %)', datum;
5919 END IF;
5920 result := public.uuid_generate_v4();
5921 PERFORM provsql.create_gate(result, 'observe', ARRAY[leaf]);
5922 PERFORM provsql.set_extra(result, datum::TEXT);
5923 RETURN result;
5924END
5925$$ LANGUAGE plpgsql VOLATILE
5926 SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
5928/**
5929 * @brief Conjunction state function for @c and_agg (evidence @c gate_times).
5930 *
5931 * Not @c STRICT: @c provenance_times maps a @c NULL operand to the times
5932 * neutral, so an empty group leaves the state @c NULL (no evidence) and a
5933 * first row seeds it with that row's evidence.
5934 */
5935CREATE OR REPLACE FUNCTION and_agg_sfunc(state UUID, ev UUID)
5936 RETURNS UUID AS
5937$$
5938 SELECT provsql.provenance_times(state, ev);
5939$$ LANGUAGE sql PARALLEL SAFE;
5940
5941/**
5942 * @brief Conjoin per-row evidence tokens into one evidence circuit.
5943 *
5944 * The evidence-conjunction counterpart used to fold one @c observe (or any
5945 * Boolean conditioning event) per row into a single @c gate_times root, to
5946 * be passed as the @c prov argument of the moment / quantile / sample
5947 * readouts. An empty group yields @c NULL (no evidence).
5948 */
5949CREATE AGGREGATE and_agg(UUID) (
5950 SFUNC = and_agg_sfunc,
5951 STYPE = UUID
5952);
5953
5954/**
5955 * @brief Marginal likelihood @c P(data) of an evidence circuit.
5957 * The mean raw importance weight over @c provsql.rv_mc_samples prior draws
5958 * -- the same quantity rejection conditioning computes as @c P(C), now the
5959 * product of the observations' densities. @p evidence is an @c and_agg
5960 * conjunction of @c observe tokens (and/or Boolean events).
5961 */
5962CREATE OR REPLACE FUNCTION evidence(evidence UUID)
5963 RETURNS double precision
5964 AS 'provsql','rv_evidence' LANGUAGE C STRICT PARALLEL SAFE;
5965
5966/**
5967 * @brief The @c observe atoms of an evidence circuit.
5968 *
5969 * Collects every @c gate_observe leaf reachable through the @c gate_times
5970 * conjunction spine (the shape @c and_agg builds -- a possibly left-nested
5971 * tree, since @c provenance_times does not flatten). Used by
5972 * @c shapley_observe to recover the flat observation set regardless of the
5973 * conjunction's nesting.
5974 */
5975CREATE OR REPLACE FUNCTION observe_atoms(evidence UUID)
5976 RETURNS UUID[] AS
5977$$
5978 WITH RECURSIVE walk(tok) AS (
5979 SELECT evidence
5980 UNION
5981 SELECT c
5982 FROM walk, LATERAL unnest(provsql.get_children(walk.tok)) AS c
5983 WHERE provsql.get_gate_type(walk.tok) = 'times'
5984 )
5985 SELECT array_agg(tok ORDER BY tok)
5986 FROM walk
5987 WHERE provsql.get_gate_type(tok) = 'observe';
5988$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
5989
5990/**
5991 * @brief Shapley attribution of each observation to a posterior moment.
5992 *
5993 * "Which observation most shifted my posterior?" Because the importance
5994 * weight is a product of per-observation density factors, dropping an
5995 * observation is dropping one factor: the classical Shapley value of each
5996 * @c gate_observe atom over the coalitional value function
5997 * @c "v(S) = payoff(target | observations in S)" is the attribution, a
5998 * byproduct of the same likelihood-weighting machinery (see the
5999 * explainable-inference angle in the continuous-distributions notes).
6001 * @p target is the latent (its @c UUID); @p evidence is the @c and_agg
6002 * conjunction of @c observe atoms; @p payoff is @c 'expected' or
6003 * @c 'variance'. Returns each observation atom with its Shapley value; the
6004 * values sum to @c "payoff(target | all data) - payoff(target)" (Shapley
6005 * efficiency: the total shift from prior to posterior).
6006 *
6007 * Exact enumeration over the @c 2^n observation subsets, so it is capped at
6008 * @c n = 12 observations (sampling-based attribution for larger sets is
6009 * future work); pin @c provsql.monte_carlo_seed so the coalitional value
6010 * functions share common random numbers (lower-variance differences).
6011 */
6012CREATE OR REPLACE FUNCTION shapley_observe(
6013 target UUID, evidence UUID, payoff TEXT DEFAULT 'expected')
6014 RETURNS TABLE(observation UUID, value double precision) AS
6015$$
6016DECLARE
6017 atoms UUID[];
6018 n INT;
6019 nmasks INT;
6020 pv double precision[];
6021 popc INT[];
6022 fact double precision[];
6023 mask INT;
6024 i INT;
6025 j INT;
6026 cnt INT;
6027 subset UUID[];
6028 ev_s UUID;
6029 sh double precision;
6030 bit INT;
6031 s_size INT;
6032BEGIN
6033 IF payoff NOT IN ('expected', 'variance') THEN
6034 RAISE EXCEPTION 'provsql.shapley_observe: payoff must be ''expected'' or '
6035 '''variance'' (got %)', payoff;
6036 END IF;
6037 atoms := provsql.observe_atoms(evidence);
6038 n := coalesce(array_length(atoms, 1), 0);
6039 IF n = 0 THEN
6040 RAISE EXCEPTION 'provsql.shapley_observe: evidence contains no observe() '
6041 'atoms (got a % gate)', provsql.get_gate_type(evidence);
6042 END IF;
6043 IF n > 12 THEN
6044 RAISE EXCEPTION 'provsql.shapley_observe: exact attribution over % '
6045 'observations is exponential; capped at 12 (sampling-based '
6046 'attribution is future work)', n;
6047 END IF;
6048
6049 -- factorials 0!..n! (fact[k+1] = k!)
6050 fact := ARRAY[1::double precision];
6051 FOR i IN 1..n LOOP fact := fact || (fact[i] * i); END LOOP;
6052
6053 nmasks := (1 << n);
6054 pv := array_fill(NULL::double precision, ARRAY[nmasks]);
6055 popc := array_fill(0, ARRAY[nmasks]);
6056
6057 -- Payoff value function for every subset of observations.
6058 FOR mask IN 0 .. nmasks - 1 LOOP
6059 subset := ARRAY[]::UUID[];
6060 cnt := 0;
6061 FOR i IN 0 .. n - 1 LOOP
6062 IF (mask >> i) & 1 = 1 THEN
6063 subset := subset || atoms[i + 1];
6064 cnt := cnt + 1;
6065 END IF;
6066 END LOOP;
6067 popc[mask + 1] := cnt;
6068 IF cnt = 0 THEN
6069 ev_s := provsql.gate_one(); -- prior (no evidence)
6070 ELSE
6071 ev_s := provsql.provenance_times(VARIADIC subset);
6072 END IF;
6073 IF payoff = 'expected' THEN
6074 pv[mask + 1] := provsql.rv_moment(target, 1, false, ev_s);
6075 ELSE
6076 pv[mask + 1] := provsql.rv_moment(target, 2, true, ev_s);
6077 END IF;
6078 END LOOP;
6079
6080 -- Shapley value of each observation atom.
6081 FOR i IN 0 .. n - 1 LOOP
6082 sh := 0;
6083 bit := (1 << i);
6084 FOR mask IN 0 .. nmasks - 1 LOOP
6085 IF (mask >> i) & 1 = 0 THEN -- subsets S not containing i
6086 s_size := popc[mask + 1];
6087 -- weight |S|! (n-|S|-1)! / n!
6088 sh := sh + (fact[s_size + 1] * fact[n - s_size] / fact[n + 1])
6089 * (pv[(mask | bit) + 1] - pv[mask + 1]);
6090 END IF;
6091 END LOOP;
6092 observation := atoms[i + 1];
6093 value := sh;
6094 RETURN NEXT;
6095 END LOOP;
6096END
6097$$ LANGUAGE plpgsql VOLATILE
6098 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6099
6100/**
6101 * @name Order statistics over random_variable
6102 *
6103 * Same-row @c greatest / @c least over @c random_variable arguments: the
6104 * order-statistic counterpart of the element-wise @c "+ - * /" operators.
6105 * They lower to a single @c gate_arith with the @c MAX / @c MIN opcode over
6106 * the argument circuits, the same n-ary shape the @c max / @c min aggregates
6107 * build. Evaluation is Monte-Carlo-correct out of the box (@c std::max /
6108 * @c std::min over the jointly-sampled children, so shared base RVs stay
6109 * coupled); closed forms for i.i.d. families come from the analytic
6110 * order-statistic pass.
6111 *
6112 * PostgreSQL's built-in @c GREATEST / @c LEAST are dedicated syntax (a
6113 * @c MinMaxExpr requiring a btree comparison), not overloadable functions, so
6114 * the surface is the schema-qualified @c provsql.greatest(...) /
6115 * @c provsql.least(...). @c NULL arguments are ignored, matching the built-in
6116 * (an all-@c NULL / empty call returns @c NULL).
6117 * @{
6118 */
6119
6120-- "greatest" / "least" are col_name keywords, so the CREATE FUNCTION name
6121-- must be quoted; callers reach them qualified as provsql.greatest(...).
6122-- Idempotence: max / min ignore repeats, so identical children (same gate)
6123-- are de-duplicated -- greatest(x, x, y) == greatest(x, y) -- and a single
6124-- surviving child collapses to itself -- greatest(x) == x. DISTINCT also sorts
6125-- the children, so the argument order does not matter for gate sharing. (Two
6126-- independent draws of the same distribution are distinct gates and are NOT
6127-- de-duplicated.)
6128CREATE OR REPLACE FUNCTION "greatest"(VARIADIC args random_variable[])
6129 RETURNS random_variable AS
6130$$
6131DECLARE
6132 children UUID[];
6133BEGIN
6134 IF args IS NULL THEN
6135 RETURN NULL;
6136 END IF;
6137 SELECT array_agg(DISTINCT (a)::UUID) INTO children
6138 FROM unnest(args) a WHERE a IS NOT NULL;
6139 IF children IS NULL OR array_length(children, 1) IS NULL THEN
6140 RETURN NULL;
6141 END IF;
6142 IF array_length(children, 1) = 1 THEN
6143 RETURN provsql.random_variable_make(children[1]);
6144 END IF;
6145 RETURN provsql.random_variable_make(
6146 provsql.provenance_arith(5, children)); -- 5 = PROVSQL_ARITH_MAX
6147END
6148$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6149
6150CREATE OR REPLACE FUNCTION "least"(VARIADIC args random_variable[])
6151 RETURNS random_variable AS
6152$$
6153DECLARE
6154 children UUID[];
6155BEGIN
6156 IF args IS NULL THEN
6157 RETURN NULL;
6158 END IF;
6159 SELECT array_agg(DISTINCT (a)::UUID) INTO children
6160 FROM unnest(args) a WHERE a IS NOT NULL;
6161 IF children IS NULL OR array_length(children, 1) IS NULL THEN
6162 RETURN NULL;
6163 END IF;
6164 IF array_length(children, 1) = 1 THEN
6165 RETURN provsql.random_variable_make(children[1]);
6166 END IF;
6167 RETURN provsql.random_variable_make(
6168 provsql.provenance_arith(6, children)); -- 6 = PROVSQL_ARITH_MIN
6169END
6170$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6171
6172/**
6173 * @brief Build a @c random_variable from a guarded-selection @c gate_case.
6174 *
6175 * Thin @c random_variable wrapper over @c provenance_case (defined with the
6176 * other gate builders, since it is UUID-only), the target of the planner-hook
6177 * @c CASE-over-RV rewrite: the hook flattens the branches into
6178 * @c [guard_1, value_1, ..., default] and emits this call so an RV-typed
6179 * @c CASE surfaces as a first-class @c random_variable.
6180 */
6181CREATE OR REPLACE FUNCTION rv_case(
6182 children UUID[]
6183)
6184RETURNS random_variable AS
6186 SELECT provsql.random_variable_make(provsql.provenance_case(children));
6187$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6188
6189/**
6190 * @brief Build an @c AGG_TOKEN from a guarded-selection @c gate_case.
6191 *
6192 * The aggregate-carrier analogue of @c rv_case: a thin @c AGG_TOKEN wrapper
6193 * over the carrier-agnostic @c provenance_case, the target of the planner-hook
6194 * lowering of a searched @c CASE whose guards are aggregate comparisons and
6195 * whose branches are aggregates. The branches (and default) are already
6196 * flattened into @c [guard_1, value_1, ..., default] UUIDs. The display cell
6197 * carries the actual-world CASE value -- the branch selected on the actual
6198 * data, resolved by @c agg_gate_value, exactly as a bare aggregate's cell
6199 * carries its actual-world value. The probabilistic result is produced by
6200 * the measure evaluators (``expected`` / ``probability`` / possible-worlds /
6201 * Monte Carlo) from the gate, not the token's cell.
6202 */
6203CREATE OR REPLACE FUNCTION agg_case(
6204 children UUID[]
6205)
6206RETURNS AGG_TOKEN AS
6207$$
6208 SELECT provsql.agg_token_make(t, coalesce(provsql.agg_gate_value(t), 0))
6209 FROM (SELECT provsql.provenance_case(children) AS t) AS s;
6210$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6211
6212/** @} */
6213
6214/**
6215 * @name Aggregates over random_variable
6216 *
6217 * An overload of the standard
6218 * @c sum aggregate that takes a @c random_variable per row and returns
6219 * the @c random_variable representing the (provenance-weighted) sum.
6220 * Lives in the @c provsql schema so a @c sum(random_variable) call
6221 * resolves to it without colliding with the built-in NUMERIC @c sum
6222 * overloads in @c pg_catalog.
6223 *
6224 * Direct calls outside a provenance-tracked query treat each row's
6225 * contribution unconditionally (no per-row Boolean selector). When
6226 * the planner hook sees a @c provsql.sum @c Aggref over a
6227 * provenance-tracked query, it wraps the per-row argument @c x in
6228 * <tt>provsql.mixture(prov_token, x, provsql.as_random(0))</tt> so the
6229 * aggregate's effective semantics become
6230 * @f$\mathrm{SUM}(x) = \sum_i \mathbf{1}\{\varphi_i\} \cdot X_i@f$,
6231 * the natural extension of semimodule-provenance to RV-valued M.
6232 *
6233 * The internal state is the array of UUIDs of the per-row mixtures.
6234 * The final function builds a single @c gate_arith @c PLUS over them
6235 * (or returns @c as_random(0) for an empty group, the additive
6236 * identity). Sharing on @c provenance_arith's v5 hash means two
6237 * @c sum invocations over the same set of rows collide on the same
6238 * gate.
6239 *
6240 * @{
6241 */
6242
6244 * @brief Per-row helper: wrap an RV in @c mixture(prov, rv, as_random(0)).
6245 *
6246 * Internal helper used by the planner-hook rewriter to lift a
6247 * @c sum(random_variable) argument into its provenance-aware form.
6248 * Encodes one row's contribution to the SUM as a Bernoulli mixture
6249 * over the row's provenance: with probability @c P(prov) the mixture
6250 * samples @c rv, otherwise it samples the additive identity
6251 * @c as_random(0). Exposed as a regular SQL function so the planner
6252 * can construct a @c FuncExpr by name without needing to disambiguate
6253 * @c mixture / @c as_random overloads at OID-lookup time.
6254 */
6255CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
6256 prov UUID, rv random_variable)
6257 RETURNS random_variable AS
6258$$
6259 SELECT provsql.mixture(prov, rv, provsql.as_random(0::double precision));
6260$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6261
6262/**
6263 * @brief Identity-parameterised per-row wrap for an RV-returning aggregate.
6264 *
6265 * Generalises the two-argument @ref rv_aggregate_semimod. The else-branch
6266 * (a row's contribution when its provenance is false) is
6267 * @c as_random(@p identity) instead of the additive @c as_random(0). The
6268 * planner-hook rewrite bakes each aggregate's own identity element into the
6269 * wrap -- @c 1 for @c product, @f$-\infty@f$ / @f$+\infty@f$ for @c max /
6270 * @c min -- so the aggregate's final function is a plain fold over the
6271 * per-row mixtures with no gate inspection. @c sum keeps the two-argument
6272 * form (@c identity @c = @c 0).
6273 */
6274CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
6275 prov UUID, rv random_variable, identity double precision)
6276 RETURNS random_variable AS
6277$$
6278 SELECT provsql.mixture(prov, rv, provsql.as_random(identity));
6279$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6280
6281/**
6282 * @brief Per-row denominator wrap for @c avg(random_variable): the
6283 * provenance indicator @f$\mathbf{1}\{\varphi\}@f$.
6284 *
6285 * The row contributes @c 1 to the running count when present and @c 0 when
6286 * absent, so @c sum over these wraps is the provenance-weighted count
6287 * @f$\sum_i \mathbf{1}\{\varphi_i\}@f$. The planner-hook rewrites
6288 * @c avg(x) into @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
6289 * @c sum(rv_aggregate_indicator(prov)) -- the "@c AVG @c = @c SUM @c /
6290 * @c COUNT" identity lifted into the @c random_variable algebra -- so
6291 * @c avg rides entirely on @c sum's fold and never inspects a gate.
6292 */
6293CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov UUID)
6294 RETURNS random_variable AS
6295$$
6296 SELECT provsql.rv_aggregate_semimod(prov, provsql.as_random(1::double precision));
6297$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6298
6299/**
6300 * @brief Value-aware presence indicator: NULL when the row's aggregated
6301 * value is NULL.
6302 *
6303 * SQL aggregates skip NULL inputs, so a NULL @c random_variable cell must
6304 * not count in @c avg's denominator: the wrap yields NULL (which the
6305 * @c sum fold skips) exactly when the value is NULL, and the plain
6306 * one-argument indicator otherwise. The planner-hook @c avg rewrite
6307 * emits this form; the one-argument indicator remains for the internal
6308 * public-form defaults.
6309 */
6310CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov UUID, rv random_variable)
6311 RETURNS random_variable AS
6312$$
6313 SELECT CASE WHEN rv IS NULL THEN NULL
6314 ELSE provsql.rv_aggregate_indicator(prov) END;
6315$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6316
6317/**
6318 * @brief State-transition function for @c sum(random_variable).
6319 *
6320 * Appends the input RV's UUID to the running array. NULL inputs are
6321 * skipped (matching standard SUM semantics). The aggregate's INITCOND
6322 * is @c '{}' so the FINALFUNC always runs and can tell an empty group
6323 * (state @c '{}') apart from a group whose every input was NULL -- both
6324 * of which SQL reports as @c NULL.
6325 */
6326CREATE OR REPLACE FUNCTION sum_rv_sfunc(
6327 state UUID[], rv random_variable)
6328 RETURNS UUID[] AS
6329$$
6330 SELECT CASE
6331 WHEN rv IS NULL THEN state
6332 ELSE array_append(state, (rv)::UUID)
6333 END;
6334$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6335
6336/**
6337 * @brief Final function for @c sum(random_variable): build a
6338 * @c gate_arith PLUS root.
6339 *
6340 * Empty group (@c state = @c '{}'): return @c NULL, as SQL's @c sum
6341 * does over zero rows -- the same answer the @c AGG_TOKEN path gives
6342 * for @c sum over an empty aggregation.
6343 *
6344 * Singleton group: return the single child directly without minting a
6345 * useless single-child @c gate_arith.
6346 *
6347 * Otherwise: build @c gate_arith(PLUS, state) via @c provenance_arith.
6348 */
6349CREATE OR REPLACE FUNCTION sum_rv_ffunc(state UUID[])
6350 RETURNS random_variable AS
6352DECLARE
6353 arith_token UUID;
6354BEGIN
6355 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6356 RETURN NULL;
6357 END IF;
6358 IF array_length(state, 1) = 1 THEN
6359 RETURN provsql.random_variable_make(state[1]);
6360 END IF;
6361 arith_token := provsql.provenance_arith(0, state); -- 0 = PROVSQL_ARITH_PLUS
6362 RETURN provsql.random_variable_make(arith_token);
6363END
6364$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6365
6366CREATE AGGREGATE sum(random_variable) (
6367 SFUNC = sum_rv_sfunc,
6368 STYPE = UUID[],
6369 INITCOND = '{}',
6370 FINALFUNC = sum_rv_ffunc
6371);
6372
6373/**
6374 * @brief Numerator final function for the @c avg rewrite: @c sum,
6375 * @c NULL on an empty group.
6376 *
6377 * Behaviourally identical to @ref sum_rv_ffunc; kept as a separate
6378 * catalog entry because the @c avg rewrite names it explicitly. The
6379 * planner-hook @c avg rewrite emits
6380 * @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
6381 * @c sum(rv_aggregate_indicator(prov)); @c random_variable_div is
6382 * @c STRICT, so an empty group propagates the numerator's @c NULL and
6383 * @c avg is @c NULL -- the standard SQL @c AVG convention -- while a
6384 * non-empty group behaves exactly like @c sum.
6385 */
6386CREATE OR REPLACE FUNCTION rv_sum_or_null_ffunc(state UUID[])
6387 RETURNS random_variable AS
6388$$
6389BEGIN
6390 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6391 RETURN NULL;
6392 END IF;
6393 IF array_length(state, 1) = 1 THEN
6394 RETURN provsql.random_variable_make(state[1]);
6395 END IF;
6396 RETURN provsql.random_variable_make(
6397 provsql.provenance_arith(0, state)); -- 0 = PROVSQL_ARITH_PLUS
6399$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6400
6401CREATE AGGREGATE rv_sum_or_null(random_variable) (
6402 SFUNC = sum_rv_sfunc,
6403 STYPE = UUID[],
6404 INITCOND = '{}',
6405 FINALFUNC = rv_sum_or_null_ffunc
6406);
6407
6408/**
6409 * @brief Final function for @c avg(random_variable).
6411 * @c avg lifts the "@c AVG @c = @c SUM @c / @c COUNT" identity into the
6412 * @c random_variable algebra:
6413 * @f[
6414 * \mathrm{AVG}(x) \;=\; \frac{\sum_i \mathbf{1}\{\varphi_i\} \cdot X_i}
6415 * {\sum_i \mathbf{1}\{\varphi_i\}}.
6416 * @f]
6417 * In a provenance-tracked query the planner-hook rewrites @c avg(x) into
6418 * @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
6419 * @c sum(rv_aggregate_indicator(prov)) (see
6420 * @c make_rv_aggregate_expression), so both the numerator and the
6421 * provenance-weighted count denominator are built by @c sum's fold and no
6422 * gate is inspected. This FFUNC is therefore reached only on an
6423 * @em untracked call, where every row is unconditionally present: the
6424 * numerator is @c sum over the raw per-row RVs and the denominator is the
6425 * plain row count @c n (each row contributing @c as_random(1)).
6426 *
6427 * Empty group: returns @c NULL, matching standard SQL @c AVG (and unlike
6428 * @c sum, whose empty group is the additive identity @c as_random(0)):
6429 * the caller cannot otherwise disambiguate "0 rows" from "rows summing
6430 * to 0".
6431 */
6432CREATE OR REPLACE FUNCTION avg_rv_ffunc(state UUID[])
6433 RETURNS random_variable AS
6434$$
6435DECLARE
6436 n INTEGER;
6437 i INTEGER;
6438 num_token UUID;
6439 denom_token UUID;
6440 denom_state UUID[] := '{}';
6441 one_uuid UUID;
6442BEGIN
6443 IF state IS NULL THEN
6444 RETURN NULL;
6445 END IF;
6446 n := array_length(state, 1);
6447 IF n IS NULL THEN
6448 RETURN NULL;
6449 END IF;
6450
6451 one_uuid := (provsql.as_random(1::double precision))::UUID;
6452 FOR i IN 1..n LOOP
6453 denom_state := array_append(denom_state, one_uuid);
6454 END LOOP;
6455
6456 IF n = 1 THEN
6457 num_token := state[1];
6458 denom_token := denom_state[1];
6459 ELSE
6460 num_token := provsql.provenance_arith(0, state); -- 0 = PLUS
6461 denom_token := provsql.provenance_arith(0, denom_state); -- 0 = PLUS
6462 END IF;
6463
6464 RETURN provsql.random_variable_make(
6465 provsql.provenance_arith(
6466 3, -- 3 = PROVSQL_ARITH_DIV
6467 ARRAY[num_token, denom_token]));
6468END
6469$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6470
6471CREATE AGGREGATE avg(random_variable) (
6472 SFUNC = sum_rv_sfunc,
6473 STYPE = UUID[],
6474 INITCOND = '{}',
6475 FINALFUNC = avg_rv_ffunc
6476);
6477
6478/**
6479 * @brief Final function for @c product(random_variable): fold a
6480 * @c gate_arith TIMES root over the per-row contributions.
6481 *
6482 * Multiplicative analogue of @c sum(random_variable):
6483 * @f[
6484 * \mathrm{PRODUCT}(x) \;=\; \prod_i \big(\mathbf{1}\{\varphi_i\} \cdot X_i
6485 * + \mathbf{1}\{\neg\varphi_i\} \cdot 1\big)
6486 * \;=\; \prod_{i : \varphi_i} X_i.
6487 * @f]
6488 * Each per-row contribution already carries the multiplicative identity
6489 * as its absent-row value: a provenance-tracked query wraps the argument
6490 * as @c mixture(prov_i, X_i, as_random(1)) (identity baked in by the
6491 * three-argument @ref rv_aggregate_semimod), and an untracked call passes
6492 * the raw RV through. So the FFUNC is a plain fold with no gate
6493 * inspection: @c gate_arith(TIMES, state).
6494 *
6495 * Reuses @c sum_rv_sfunc as the state-transition function. Empty group:
6496 * @c NULL, by symmetry with @c sum / @c avg / @c min / @c max, which take
6497 * it from their standard-SQL counterparts. The multiplicative identity
6498 * @c as_random(1) stays the absent-row value inside the fold, where it
6499 * belongs; a group containing no row has no product to report.
6500 * Singleton group: the single child directly, without a one-child TIMES
6501 * root.
6502 */
6503CREATE OR REPLACE FUNCTION product_rv_ffunc(state UUID[])
6504 RETURNS random_variable AS
6505$$
6506BEGIN
6507 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6508 RETURN NULL;
6509 END IF;
6510 IF array_length(state, 1) = 1 THEN
6511 RETURN provsql.random_variable_make(state[1]);
6512 END IF;
6513 RETURN provsql.random_variable_make(
6514 provsql.provenance_arith(1, state)); -- 1 = PROVSQL_ARITH_TIMES
6515END
6516$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6517
6518CREATE AGGREGATE product(random_variable) (
6519 SFUNC = sum_rv_sfunc,
6520 STYPE = UUID[],
6521 INITCOND = '{}',
6522 FINALFUNC = product_rv_ffunc
6523);
6525/**
6526 * @brief Final function for @c max(random_variable) / @c min(random_variable):
6527 * fold a @c gate_arith MAX / MIN root over the per-row contributions.
6528 *
6529 * The order-statistic analogues of @c sum / @c product:
6530 * @f[
6531 * \mathrm{MAX}(x) = \max_{i : \varphi_i} X_i, \qquad
6532 * \mathrm{MIN}(x) = \min_{i : \varphi_i} X_i.
6533 * @f]
6534 * A row absent in a world (its provenance @f$\varphi_i@f$ false) must not
6535 * perturb the extremum, so it contributes the order-statistic identity
6536 * @f$\mp\infty@f$. That identity is baked into each per-row contribution
6537 * upstream: a provenance-tracked query wraps the argument as
6538 * @c mixture(prov_i, X_i, as_random(∓∞)) (via the three-argument
6539 * @ref rv_aggregate_semimod), and an untracked call passes the raw RV
6540 * through. So the FFUNC is a plain fold with no gate inspection:
6541 * @c gate_arith(@p op, state).
6542 *
6543 * Empty group: @c NULL, as SQL's @c min / @c max report over zero rows.
6544 * @p identity belongs to the catalog signature and describes the per-row
6545 * absent contribution baked in upstream; the empty group does not consult
6546 * it, since @f$\mp\infty@f$ is an artefact of the fold rather than a value
6547 * the group actually contains.
6548 * Singleton group: the single child directly.
6549 */
6550CREATE OR REPLACE FUNCTION extremum_rv_ffunc(
6551 state UUID[], op INTEGER, identity double precision)
6552 RETURNS random_variable AS
6553$$
6554BEGIN
6555 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6556 RETURN NULL;
6557 END IF;
6558 IF array_length(state, 1) = 1 THEN
6559 RETURN provsql.random_variable_make(state[1]);
6560 END IF;
6561 RETURN provsql.random_variable_make(
6562 provsql.provenance_arith(op, state));
6563END
6564$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6565
6566CREATE OR REPLACE FUNCTION max_rv_ffunc(state UUID[])
6567 RETURNS random_variable AS
6568$$
6569 -- 5 = PROVSQL_ARITH_MAX; empty-group / row-absent identity -inf.
6570 SELECT provsql.extremum_rv_ffunc(state, 5, '-Infinity'::double precision);
6571$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6572
6573CREATE OR REPLACE FUNCTION min_rv_ffunc(state UUID[])
6574 RETURNS random_variable AS
6575$$
6576 -- 6 = PROVSQL_ARITH_MIN; empty-group / row-absent identity +inf.
6577 SELECT provsql.extremum_rv_ffunc(state, 6, 'Infinity'::double precision);
6578$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6579
6580CREATE AGGREGATE max(random_variable) (
6581 SFUNC = sum_rv_sfunc,
6582 STYPE = UUID[],
6583 INITCOND = '{}',
6584 FINALFUNC = max_rv_ffunc
6585);
6586
6587CREATE AGGREGATE min(random_variable) (
6588 SFUNC = sum_rv_sfunc,
6589 STYPE = UUID[],
6590 INITCOND = '{}',
6591 FINALFUNC = min_rv_ffunc
6592);
6593
6594-- ---------------------------------------------------------------------
6595-- SQL-standard statistic aggregates over random_variable rows:
6596-- covar_pop / covar_samp / corr (two-argument), stddev_pop / stddev_samp
6597-- (one-argument), and the ordered-set percentile_cont.
6598--
6599-- Row presence is carried by a per-row 0/1 indicator RV: the public
6600-- aggregates use the certain indicator as_random(1) (every row present),
6601-- and a provenance-tracked query is rewritten by the planner hook
6602-- (make_rv_aggregate_expression) to the rv_*_impl aggregates whose extra
6603-- leading argument is rv_aggregate_indicator(prov), so a row absent in a
6604-- world drops out of every sum, the count, and the percentile member set.
6605-- The moment statistics are built from indicator-weighted power sums with
6606-- existing gate_arith opcodes (e.g. covar_pop = SXY/N - (SX/N)(SY/N)); a
6607-- world where the statistic is undefined (N = 0, or N = 1 for the sample
6608-- forms) evaluates to NaN, the established undefined-world convention the
6609-- moment estimators skip. percentile_cont is the one gate the arithmetic
6610-- cannot express: it mints the PROVSQL_ARITH_PERCENTILE gate_arith
6611-- (interleaved [ind_1, x_1, ...] wires, fraction in extra) that the Monte
6612-- Carlo sampler evaluates by sorting each draw's present values and
6613-- interpolating.
6614-- ---------------------------------------------------------------------
6615
6616/** @brief State transition for the one-argument RV statistic aggregates
6617 * (@c stddev_pop / @c stddev_samp): append the certain indicator and the
6618 * row's RV as a pair. NULL rows are skipped (standard SQL). */
6619CREATE OR REPLACE FUNCTION rv_stat1_sfunc(state UUID[], x random_variable)
6620 RETURNS UUID[] AS
6621$$
6622 SELECT CASE
6623 WHEN x IS NULL THEN state
6624 ELSE state || ARRAY[(provsql.as_random(1::double precision))::UUID,
6625 (x)::UUID]
6626 END;
6627$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6628
6629/** @brief State transition for the two-argument RV statistic aggregates
6630 * (@c covar_pop / @c covar_samp / @c corr): append the certain indicator
6631 * and the row's RV pair as a triple. Rows with either side NULL are
6632 * skipped (standard SQL covariance semantics). */
6633CREATE OR REPLACE FUNCTION rv_stat2_sfunc(
6634 state UUID[], x random_variable, y random_variable)
6635 RETURNS UUID[] AS
6636$$
6637 SELECT CASE
6638 WHEN x IS NULL OR y IS NULL THEN state
6639 ELSE state || ARRAY[(provsql.as_random(1::double precision))::UUID,
6640 (x)::UUID, (y)::UUID]
6641 END;
6642$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6643
6644/** @brief Indicator-carrying state transition for the one-argument
6645 * @c rv_*_impl statistic aggregates: the planner-hook rewrite passes the
6646 * row's provenance indicator @c rv_aggregate_indicator(prov) as @p ind. */
6647CREATE OR REPLACE FUNCTION rv_stat1_impl_sfunc(
6648 state UUID[], ind random_variable, x random_variable)
6649 RETURNS UUID[] AS
6650$$
6651 SELECT CASE
6652 WHEN x IS NULL THEN state
6653 ELSE state || ARRAY[coalesce((ind)::UUID,
6654 (provsql.as_random(1::double precision))::UUID),
6655 (x)::UUID]
6656 END;
6657$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6658
6659/** @brief Indicator-carrying state transition for the two-argument
6660 * @c rv_*_impl statistic aggregates. */
6661CREATE OR REPLACE FUNCTION rv_stat2_impl_sfunc(
6662 state UUID[], ind random_variable, x random_variable, y random_variable)
6663 RETURNS UUID[] AS
6664$$
6665 SELECT CASE
6666 WHEN x IS NULL OR y IS NULL THEN state
6667 ELSE state || ARRAY[coalesce((ind)::UUID,
6668 (provsql.as_random(1::double precision))::UUID),
6669 (x)::UUID, (y)::UUID]
6670 END;
6671$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6672
6673/**
6674 * @brief Mint the indicator-weighted power-sum gates shared by the
6675 * covariance / stddev final functions.
6676 *
6677 * @p state is the flat interleaved aggregate state -- pairs
6678 * @c [ind, x, ...] (@p stride 2) or triples @c [ind, x, y, ...]
6679 * (@p stride 3). Emits @c gate_arith tokens for
6680 * @f$N = \sum_i \mathbf{1}_i@f$, @f$SX = \sum_i \mathbf{1}_i x_i@f$,
6681 * @f$SXX = \sum_i \mathbf{1}_i x_i^2@f$ and, at stride 3, @f$SY@f$,
6682 * @f$SXY@f$, @f$SYY@f$. The per-row indicator gate is shared between
6683 * @f$N@f$ and every product it weighs, so the Monte Carlo per-iteration
6684 * cache keeps the row's presence coupled across all the sums (and a
6685 * repeated child @c [ind, x, x] reuses the same draw of @c x, giving
6686 * @f$x^2@f$, not two independent draws).
6687 */
6688CREATE OR REPLACE FUNCTION rv_stat_sum_tokens(
6689 state UUID[], stride INTEGER,
6690 OUT n_tok UUID, OUT sx_tok UUID, OUT sxx_tok UUID,
6691 OUT sy_tok UUID, OUT sxy_tok UUID, OUT syy_tok UUID)
6692AS
6693$$
6694DECLARE
6695 nrows INTEGER := coalesce(array_length(state, 1), 0) / stride;
6696 inds UUID[] := '{}';
6697 xs UUID[] := '{}';
6698 xxs UUID[] := '{}';
6699 ys UUID[] := '{}';
6700 xys UUID[] := '{}';
6701 yys UUID[] := '{}';
6702 ind UUID;
6703 x UUID;
6704 y UUID;
6705BEGIN
6706 FOR i IN 1..nrows LOOP
6707 ind := state[(i-1) * stride + 1];
6708 x := state[(i-1) * stride + 2];
6709 inds := array_append(inds, ind);
6710 xs := array_append(xs, provenance_arith(1, ARRAY[ind, x]));
6711 xxs := array_append(xxs, provenance_arith(1, ARRAY[ind, x, x]));
6712 IF stride = 3 THEN
6713 y := state[(i-1) * stride + 3];
6714 ys := array_append(ys, provenance_arith(1, ARRAY[ind, y]));
6715 xys := array_append(xys, provenance_arith(1, ARRAY[ind, x, y]));
6716 yys := array_append(yys, provenance_arith(1, ARRAY[ind, y, y]));
6717 END IF;
6718 END LOOP;
6719 n_tok := provenance_arith(0, inds);
6720 sx_tok := provenance_arith(0, xs);
6721 sxx_tok := provenance_arith(0, xxs);
6722 IF stride = 3 THEN
6723 sy_tok := provenance_arith(0, ys);
6724 sxy_tok := provenance_arith(0, xys);
6725 syy_tok := provenance_arith(0, yys);
6726 END IF;
6727END
6728$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6729 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6730
6731/** @brief Population-variance gate @f$SXX/N - (SX/N)^2@f$ from the
6732 * power-sum tokens. */
6733CREATE OR REPLACE FUNCTION rv_stat_var_pop_token(
6734 n_tok UUID, s_tok UUID, ss_tok UUID)
6735 RETURNS UUID AS
6736$$
6737 SELECT provsql.provenance_arith(2, ARRAY[
6738 provsql.provenance_arith(3, ARRAY[ss_tok, n_tok]),
6739 provsql.provenance_arith(1, ARRAY[
6740 provsql.provenance_arith(3, ARRAY[s_tok, n_tok]),
6741 provsql.provenance_arith(3, ARRAY[s_tok, n_tok])])]);
6742$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6743
6744/** @brief Sample-variance gate @f$(SXX - SX^2/N) / (N - 1)@f$ from the
6745 * power-sum tokens (NaN in a world with @f$N \le 1@f$, the undefined-world
6746 * convention). */
6747CREATE OR REPLACE FUNCTION rv_stat_var_samp_token(
6748 n_tok UUID, s_tok UUID, ss_tok UUID)
6749 RETURNS UUID AS
6750$$
6751 SELECT provsql.provenance_arith(3, ARRAY[
6752 provsql.provenance_arith(2, ARRAY[
6753 ss_tok,
6754 provsql.provenance_arith(3, ARRAY[
6755 provsql.provenance_arith(1, ARRAY[s_tok, s_tok]), n_tok])]),
6756 provsql.provenance_arith(2, ARRAY[
6757 n_tok, (provsql.as_random(1::double precision))::UUID])]);
6758$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6759
6760/** @brief @f$\sqrt{\max(v, 0)}@f$ gate over a variance token: the max-clamp
6761 * removes the tiny negative values float error can produce (variance is
6762 * mathematically non-negative), so the POW domain guard never fires. */
6763CREATE OR REPLACE FUNCTION rv_stat_sqrt_token(v_tok UUID)
6764 RETURNS UUID AS
6765$$
6766 SELECT provsql.provenance_arith(7, ARRAY[
6767 provsql.provenance_arith(5, ARRAY[
6768 v_tok, (provsql.as_random(0::double precision))::UUID]),
6769 (provsql.as_random(0.5::double precision))::UUID]);
6770$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6771
6772/** @brief Population-covariance gate @f$SXY/N - (SX/N)(SY/N)@f$ from the
6773 * power-sum tokens. */
6774CREATE OR REPLACE FUNCTION rv_stat_covar_pop_token(
6775 n_tok UUID, sx_tok UUID, sy_tok UUID, sxy_tok UUID)
6776 RETURNS UUID AS
6777$$
6778 SELECT provsql.provenance_arith(2, ARRAY[
6779 provsql.provenance_arith(3, ARRAY[sxy_tok, n_tok]),
6780 provsql.provenance_arith(1, ARRAY[
6781 provsql.provenance_arith(3, ARRAY[sx_tok, n_tok]),
6782 provsql.provenance_arith(3, ARRAY[sy_tok, n_tok])])]);
6783$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6784
6785/** @brief Final function for @c covar_pop(random_variable, random_variable). */
6786CREATE OR REPLACE FUNCTION covar_pop_rv_ffunc(state UUID[])
6787 RETURNS random_variable AS
6788$$
6789DECLARE
6790 t RECORD;
6791BEGIN
6792 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6793 RETURN NULL;
6794 END IF;
6795 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
6796 RETURN random_variable_make(
6797 rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok));
6798END
6799$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6800 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6801
6802/** @brief Final function for @c covar_samp(random_variable, random_variable):
6803 * @f$(SXY - SX\,SY/N) / (N-1)@f$. */
6804CREATE OR REPLACE FUNCTION covar_samp_rv_ffunc(state UUID[])
6805 RETURNS random_variable AS
6806$$
6807DECLARE
6808 t RECORD;
6809BEGIN
6810 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6811 RETURN NULL;
6812 END IF;
6813 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
6814 RETURN random_variable_make(
6815 provenance_arith(3, ARRAY[
6816 provenance_arith(2, ARRAY[
6817 t.sxy_tok,
6818 provenance_arith(3, ARRAY[
6819 provenance_arith(1, ARRAY[t.sx_tok, t.sy_tok]), t.n_tok])]),
6820 provenance_arith(2, ARRAY[
6821 t.n_tok, (as_random(1::double precision))::UUID])]));
6822END
6823$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6824 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6825
6826/** @brief Final function for @c corr(random_variable, random_variable):
6827 * @f$\mathrm{covar\_pop} / \sqrt{\max(v_x v_y, 0)}@f$ (a zero-variance
6828 * world divides to @f$\pm\infty@f$ / NaN, the undefined-world convention,
6829 * matching SQL's NULL for a zero-stddev input). */
6830CREATE OR REPLACE FUNCTION corr_rv_ffunc(state UUID[])
6831 RETURNS random_variable AS
6832$$
6833DECLARE
6834 t RECORD;
6835 vx UUID;
6836 vy UUID;
6837BEGIN
6838 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6839 RETURN NULL;
6840 END IF;
6841 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
6842 vx := rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok);
6843 vy := rv_stat_var_pop_token(t.n_tok, t.sy_tok, t.syy_tok);
6844 RETURN random_variable_make(
6845 provenance_arith(3, ARRAY[
6846 rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok),
6847 rv_stat_sqrt_token(provenance_arith(1, ARRAY[vx, vy]))]));
6848END
6849$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6850 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6851
6852/** @brief Final function for @c stddev_pop(random_variable). */
6853CREATE OR REPLACE FUNCTION stddev_pop_rv_ffunc(state UUID[])
6854 RETURNS random_variable AS
6855$$
6856DECLARE
6857 t RECORD;
6858BEGIN
6859 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6860 RETURN NULL;
6861 END IF;
6862 SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
6863 RETURN random_variable_make(
6864 rv_stat_sqrt_token(
6865 rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok)));
6866END
6867$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6868 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6869
6870/** @brief Final function for @c stddev_samp(random_variable). */
6871CREATE OR REPLACE FUNCTION stddev_samp_rv_ffunc(state UUID[])
6872 RETURNS random_variable AS
6873$$
6874DECLARE
6875 t RECORD;
6876BEGIN
6877 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6878 RETURN NULL;
6879 END IF;
6880 SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
6881 RETURN random_variable_make(
6882 rv_stat_sqrt_token(
6883 rv_stat_var_samp_token(t.n_tok, t.sx_tok, t.sxx_tok)));
6884END
6885$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6886 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6887
6888CREATE AGGREGATE covar_pop(random_variable, random_variable) (
6889 SFUNC = rv_stat2_sfunc,
6890 STYPE = UUID[],
6891 INITCOND = '{}',
6892 FINALFUNC = covar_pop_rv_ffunc
6893);
6894
6895CREATE AGGREGATE covar_samp(random_variable, random_variable) (
6896 SFUNC = rv_stat2_sfunc,
6897 STYPE = UUID[],
6898 INITCOND = '{}',
6899 FINALFUNC = covar_samp_rv_ffunc
6900);
6901
6902CREATE AGGREGATE corr(random_variable, random_variable) (
6903 SFUNC = rv_stat2_sfunc,
6904 STYPE = UUID[],
6905 INITCOND = '{}',
6906 FINALFUNC = corr_rv_ffunc
6907);
6908
6909CREATE AGGREGATE stddev_pop(random_variable) (
6910 SFUNC = rv_stat1_sfunc,
6911 STYPE = UUID[],
6912 INITCOND = '{}',
6913 FINALFUNC = stddev_pop_rv_ffunc
6914);
6915
6916CREATE AGGREGATE stddev_samp(random_variable) (
6917 SFUNC = rv_stat1_sfunc,
6918 STYPE = UUID[],
6919 INITCOND = '{}',
6920 FINALFUNC = stddev_samp_rv_ffunc
6921);
6922
6923-- The indicator-carrying rewrite targets (planner hook only; never called
6924-- directly by users).
6925
6926CREATE AGGREGATE rv_covar_pop_impl(
6927 random_variable, random_variable, random_variable) (
6928 SFUNC = rv_stat2_impl_sfunc,
6929 STYPE = UUID[],
6930 INITCOND = '{}',
6931 FINALFUNC = covar_pop_rv_ffunc
6932);
6933
6934CREATE AGGREGATE rv_covar_samp_impl(
6935 random_variable, random_variable, random_variable) (
6936 SFUNC = rv_stat2_impl_sfunc,
6937 STYPE = UUID[],
6938 INITCOND = '{}',
6939 FINALFUNC = covar_samp_rv_ffunc
6940);
6941
6942CREATE AGGREGATE rv_corr_impl(
6943 random_variable, random_variable, random_variable) (
6944 SFUNC = rv_stat2_impl_sfunc,
6945 STYPE = UUID[],
6946 INITCOND = '{}',
6947 FINALFUNC = corr_rv_ffunc
6948);
6949
6950CREATE AGGREGATE rv_stddev_pop_impl(random_variable, random_variable) (
6951 SFUNC = rv_stat1_impl_sfunc,
6952 STYPE = UUID[],
6953 INITCOND = '{}',
6954 FINALFUNC = stddev_pop_rv_ffunc
6955);
6956
6957CREATE AGGREGATE rv_stddev_samp_impl(random_variable, random_variable) (
6958 SFUNC = rv_stat1_impl_sfunc,
6959 STYPE = UUID[],
6960 INITCOND = '{}',
6961 FINALFUNC = stddev_samp_rv_ffunc
6962);
6963
6964/**
6965 * @brief Mint the @c PROVSQL_ARITH_PERCENTILE gate: the continuous
6966 * percentile (SQL @c percentile_cont) over a group of RV rows.
6967 *
6968 * @p pairs is the interleaved wire list @c [ind_1, x_1, ..., ind_n, x_n]
6969 * (each @p ind_i a 0/1 presence-indicator RV). The @p fraction is
6970 * TEXT-encoded in the gate's @c extra and participates in the token UUID
6971 * (two percentiles of the same group at different fractions are distinct
6972 * gates). Per Monte Carlo draw, the sampler collects the values whose
6973 * indicator draws 1, sorts them, and linearly interpolates at the
6974 * fraction; a draw with no present row is NaN (undefined world).
6975 */
6976CREATE OR REPLACE FUNCTION rv_percentile_make(fraction double precision,
6977 pairs UUID[])
6978 RETURNS random_variable AS
6979$$
6980DECLARE
6981 token UUID;
6982BEGIN
6983 IF fraction IS NULL THEN
6984 RETURN NULL;
6985 END IF;
6986 IF fraction < 0 OR fraction > 1 THEN
6987 RAISE EXCEPTION
6988 'percentile_cont: fraction must be between 0 and 1 (got %)', fraction;
6989 END IF;
6990 token := public.uuid_generate_v5(
6991 uuid_ns_provsql(),
6992 concat('arith', '10', pairs::TEXT, fraction::TEXT));
6993 PERFORM create_gate(token, 'arith', pairs);
6994 PERFORM set_infos(token, 10); -- 10 = PROVSQL_ARITH_PERCENTILE
6995 PERFORM set_extra(token, fraction::TEXT);
6996 RETURN random_variable_make(token);
6997END
6998$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6999 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7000
7001/** @brief State transition for the public ordered-set
7002 * @c percentile_cont(float8) WITHIN GROUP (ORDER BY random_variable):
7003 * append the certain indicator and the row's RV. Only reachable on
7004 * untracked input (a provenance-tracked query is rewritten to
7005 * @c rv_percentile_impl before planning), where the sort over
7006 * @c random_variable raises the ordering-is-meaningless diagnostic
7007 * first -- so in practice this runs only for empty input. */
7008CREATE OR REPLACE FUNCTION percentile_cont_rv_sfunc(
7009 state UUID[], x random_variable)
7010 RETURNS UUID[] AS
7011$$
7012 SELECT provsql.rv_stat1_sfunc(state, x);
7013$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7014
7015/** @brief Final function for the public ordered-set @c percentile_cont:
7016 * receives the direct @p fraction argument after the state. */
7017CREATE OR REPLACE FUNCTION percentile_cont_rv_ffunc(
7018 state UUID[], fraction double precision)
7019 RETURNS random_variable AS
7020$$
7021 SELECT CASE
7022 WHEN state IS NULL OR array_length(state, 1) IS NULL THEN NULL
7023 ELSE provsql.rv_percentile_make(fraction, state)
7024 END;
7025$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7026
7027CREATE AGGREGATE percentile_cont(double precision ORDER BY random_variable) (
7028 SFUNC = percentile_cont_rv_sfunc,
7029 STYPE = UUID[],
7030 INITCOND = '{}',
7031 FINALFUNC = percentile_cont_rv_ffunc
7032);
7033
7034/** @brief Transition state for @c rv_percentile_impl: the fraction (from
7035 * the first row) plus the interleaved indicator/value token pairs. */
7036CREATE TYPE rv_percentile_state AS (
7037 fraction double precision,
7038 tokens UUID[]
7039);
7040
7041/** @brief State transition for @c rv_percentile_impl, the planner-hook
7042 * rewrite target of a provenance-tracked @c percentile_cont: stashes the
7043 * (group-constant) fraction and appends the indicator/value pair. */
7044CREATE OR REPLACE FUNCTION rv_percentile_impl_sfunc(
7045 state rv_percentile_state, fraction double precision,
7046 ind random_variable, x random_variable)
7047 RETURNS rv_percentile_state AS
7048$$
7049 SELECT ROW(
7050 coalesce((state).fraction, fraction),
7051 CASE
7052 WHEN x IS NULL THEN (state).tokens
7053 ELSE (state).tokens ||
7054 ARRAY[coalesce((ind)::UUID,
7055 (provsql.as_random(1::double precision))::UUID),
7056 (x)::UUID]
7057 END)::provsql.rv_percentile_state;
7058$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7059
7060/** @brief Final function for @c rv_percentile_impl. */
7061CREATE OR REPLACE FUNCTION rv_percentile_impl_ffunc(state rv_percentile_state)
7062 RETURNS random_variable AS
7063$$
7064 SELECT CASE
7065 WHEN state IS NULL OR array_length((state).tokens, 1) IS NULL THEN NULL
7066 ELSE provsql.rv_percentile_make((state).fraction, (state).tokens)
7067 END;
7068$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7069
7070CREATE AGGREGATE rv_percentile_impl(
7071 double precision, random_variable, random_variable) (
7072 SFUNC = rv_percentile_impl_sfunc,
7073 STYPE = rv_percentile_state,
7074 INITCOND = '(,"{}")',
7075 FINALFUNC = rv_percentile_impl_ffunc
7076);
7077
7078/** @} */
7079
7080/** @} */
7081
7082/** @} */
7083
7084/** @defgroup aggregate_provenance Aggregate provenance
7085 * Functions for building and evaluating aggregate (GROUP BY) provenance,
7086 * including the δ-semiring operator and semimodule multiplication.
7087 * @{
7088 */
7089
7090/**
7091 * @brief Create a δ-semiring gate wrapping a provenance token
7092 *
7093 * Used internally for aggregate provenance. Returns the token unchanged
7094 * if it is gate_zero() or gate_one(), and gate_one() if the token is NULL.
7095 */
7096CREATE OR REPLACE FUNCTION provenance_delta
7097 (token UUID)
7098 RETURNS UUID AS
7099$$
7100DECLARE
7101 delta_token UUID;
7102BEGIN
7103 -- NULL token ≡ 1 (untracked source), and δ(1) = 1. Tested first: the
7104 -- equality comparisons below are not NULL-safe.
7105 IF token IS NULL THEN
7106 return gate_one();
7107 END IF;
7108
7109 IF token = gate_zero() OR token = gate_one() THEN
7110 return token;
7111 END IF;
7112
7113 delta_token:=uuid_generate_v5(uuid_ns_provsql(),concat('delta',token));
7114
7115 PERFORM create_gate(delta_token,'delta',ARRAY[token::UUID]);
7116
7117 RETURN delta_token;
7118END
7119$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
7120
7121/**
7122 * @brief Build an aggregate provenance gate from grouped tokens
7123 *
7124 * Called internally by the query rewriter for GROUP BY queries.
7125 * Creates an agg gate linking all contributing tokens and records
7126 * the aggregate function OID and the computed scalar value.
7127 *
7128 * @param aggfnoid OID of the SQL aggregate function
7129 * @param aggtype OID of the aggregate result type
7130 * @param val computed aggregate value
7131 * @param tokens array of provenance tokens being aggregated
7132 * @param is_scalar true for a scalar (no GROUP BY) aggregation, whose
7133 * output row exists even when no tuple is present; stored in the
7134 * high bit of info2
7135 */
7136CREATE OR REPLACE FUNCTION provenance_aggregate(
7137 aggfnoid INTEGER,
7138 aggtype INTEGER,
7139 val ANYELEMENT,
7140 tokens UUID[],
7141 is_scalar BOOLEAN DEFAULT false)
7142 RETURNS AGG_TOKEN AS
7143$$
7144DECLARE
7145 c INTEGER;
7146 agg_tok UUID;
7147 agg_val varchar;
7148BEGIN
7149 -- Drop the NULL placeholders array_agg keeps for rows that did not produce a
7150 -- semimod gate (provenance_semimod returns NULL for a NULL aggregated value),
7151 -- so a NULL input never participates in the aggregate.
7152 tokens := array_remove(tokens, NULL);
7153 c:=COALESCE(array_length(tokens, 1), 0);
7154
7155 agg_val = CAST(val as VARCHAR);
7156
7157 IF c = 0 THEN
7158 agg_tok := gate_zero();
7159 ELSE
7160 -- aggfnoid must be part of the UUID: SUM(id) and AVG(id) over the
7161 -- same children would otherwise collapse to a single gate, and
7162 -- their concurrent set_infos calls would overwrite each other's
7163 -- aggregation operator (resulting in the wrong agg_kind being
7164 -- read by provsql_having under cross-backend contention). The
7165 -- scalar-aggregation flag must likewise be hashed: a scalar and a
7166 -- grouped aggregate over identical children carry different info2 and
7167 -- must stay distinct gates, else the concurrent set_infos calls would
7168 -- clobber the flag. The flag is stored in the high bit of info2 (the
7169 -- low 31 bits keep the result-type OID); aggtype itself is passed clean
7170 -- so the AGG_TOKEN->scalar cast still finds a valid type.
7171 agg_tok := uuid_generate_v5(
7172 uuid_ns_provsql(),
7173 concat('agg',aggfnoid,tokens,CASE WHEN is_scalar THEN 'S' ELSE '' END));
7174 PERFORM create_gate(agg_tok, 'agg', tokens);
7175 PERFORM set_infos(agg_tok, aggfnoid,
7176 CASE WHEN is_scalar THEN aggtype | (-2147483648) ELSE aggtype END);
7177 PERFORM set_extra(agg_tok, agg_val);
7178 END IF;
7179
7180 RETURN '( '||agg_tok||' , '||agg_val||' )';
7181END
7182$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE;
7183
7184/**
7185 * @brief Create a semimodule scalar multiplication gate
7186 *
7187 * Pairs a scalar value with a provenance token, used internally by
7188 * the query rewriter for aggregate provenance.
7189 *
7190 * @param val the scalar value
7191 * @param token the provenance token to multiply
7192 */
7193CREATE OR REPLACE FUNCTION provenance_semimod(val ANYELEMENT, token UUID)
7194 RETURNS UUID AS
7195$$
7196DECLARE
7197 semimod_token UUID;
7198 value_token UUID;
7199BEGIN
7200 -- A NULL value means this row does not participate in the aggregate (SQL
7201 -- aggregates ignore NULL inputs; only count(*) counts rows unconditionally,
7202 -- and it passes a constant 1 here). Produce no semimod gate so the row is
7203 -- skipped when provenance_aggregate builds the agg gate.
7204 IF val IS NULL THEN
7205 RETURN NULL;
7206 END IF;
7207
7208 SELECT uuid_generate_v5(uuid_ns_provsql(),concat('value',CAST(val AS VARCHAR)))
7209 INTO value_token;
7210 SELECT uuid_generate_v5(uuid_ns_provsql(),concat('semimod',value_token,token))
7211 INTO semimod_token;
7212
7213 --create value gates
7214 PERFORM create_gate(value_token,'value');
7215 PERFORM set_extra(value_token, CAST(val AS VARCHAR));
7216
7217 --create semimod gate
7218 PERFORM create_gate(semimod_token,'semimod',ARRAY[token::UUID,value_token]);
7219
7220 RETURN semimod_token;
7221END
7222$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE;
7223
7224/** @} */
7225
7226/** @defgroup probability Probability and Shapley values
7227 * Functions for computing probabilities, expected values, and
7228 * game-theoretic contribution measures (Shapley/Banzhaf values)
7229 * from provenance circuits.
7230 * @{
7231 */
7232
7233/**
7234 * @brief Compute the probability of a provenance token
7235 *
7236 * Compiles the provenance circuit to d-DNNF and evaluates the
7237 * probability. The compilation method can be selected explicitly.
7238 *
7239 * @ref probability() "probability" is a shorter alias bound to the same C symbol, so
7240 * @c probability(token) is exactly @c probability_evaluate(token); it is
7241 * usually preferable, and additionally carries a @c (BOOLEAN) predicate
7242 * overload (e.g. @c probability(x @c > @c y)).
7243 *
7244 * @param token provenance token to evaluate
7245 * @param method knowledge compilation method (NULL for default)
7246 * @param arguments additional arguments for the method
7247 */
7248CREATE OR REPLACE FUNCTION probability_evaluate(
7249 token UUID,
7250 method TEXT = NULL,
7251 arguments TEXT = NULL)
7252 RETURNS DOUBLE PRECISION AS
7253 'provsql','probability_evaluate' LANGUAGE C STABLE;
7254
7255/**
7256 * @brief Short alias of @ref probability_evaluate.
7257 *
7258 * Bound to the same C symbol as @ref probability_evaluate, so
7259 * @c probability(token) is exactly @c probability_evaluate(token).
7260 * Provided to match the concise polymorphic surface of @ref expected,
7261 * @ref variance, and @ref support "support": callers are not forced to
7262 * spell out @c probability_evaluate.
7263 *
7264 * @param token provenance token to evaluate
7265 * @param method knowledge compilation method (NULL for default)
7266 * @param arguments additional arguments for the method
7267 */
7268CREATE OR REPLACE FUNCTION probability(
7269 token UUID,
7270 method TEXT = NULL,
7271 arguments TEXT = NULL)
7272 RETURNS DOUBLE PRECISION AS
7273 'provsql','probability_evaluate' LANGUAGE C STABLE;
7274
7275/**
7276 * @brief Probability of a Boolean event over random variables.
7277 *
7278 * The @c (BOOLEAN) overload of @c probability lets a query ask for the
7279 * probability of an event with the natural infix grammar, e.g.
7280 * @c probability(x @c > @c y @c AND @c x @c < @c z). When the argument
7281 * carries a probabilistic (random_variable / aggregate) comparison, the
7282 * planner hook intercepts the call and rewrites it into
7283 * @c probability_evaluate over the argument's event token (a @c gate_cmp /
7284 * Boolean combination); the body below is then never reached.
7285 *
7286 * When the argument is a purely deterministic Boolean (no probabilistic
7287 * comparison) the hook leaves the call alone and the body runs, so the
7288 * probability of a definite event is simply @c 1 when it holds and @c 0 when
7289 * it does not (@c NULL propagates). This makes @c probability total over
7290 * Booleans -- @c probability(1 @c > @c 0) is @c 1, @c probability(region @c =
7291 * @c 'north') is a per-row @c 0/1 -- and it works even with
7292 * @c provsql.active off. @c NOT strict so a default-NULL @c method does not
7293 * short-circuit the cast.
7294 *
7295 * The predicate surface deliberately lives only on the short @c probability
7296 * name, not on @c probability_evaluate: a Boolean overload of the latter
7297 * would make @c probability_evaluate('<UUID-as-TEXT>') ambiguous (an unknown
7298 * literal matches both the @c UUID and the @c BOOLEAN overload), breaking
7299 * existing string-literal callers. @c probability is new, so it carries the
7300 * predicate overload without that hazard.
7301 */
7302CREATE OR REPLACE FUNCTION probability(
7303 predicate BOOLEAN,
7304 method TEXT = NULL,
7305 arguments TEXT = NULL)
7306 RETURNS DOUBLE PRECISION AS
7307$$
7308 SELECT predicate::INTEGER::double precision;
7309$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7310
7311/**
7312 * @brief Cheap certified probability interval of a DNF-shaped circuit.
7313 *
7314 * Returns @c [lower,upper] with @c lower <= probability_evaluate(token) <=
7315 * @c upper, computed without compiling the circuit (the Olteanu-Huang d-tree
7316 * leaf bound). Errors when @p token is not a monotone DNF over input leaves.
7317 */
7318CREATE OR REPLACE FUNCTION probability_bounds(
7319 token UUID,
7320 OUT lower DOUBLE PRECISION,
7321 OUT upper DOUBLE PRECISION) AS
7322 'provsql','probability_bounds' LANGUAGE C STABLE;
7323
7324/**
7325 * @brief Compute the expected value of a probabilistic scalar
7326 *
7327 * Computes E[input | prov] for either an @c AGG_TOKEN (discrete
7328 * SUM/MIN/MAX aggregation over Boolean-input gate_agg circuits, with
7329 * @c prov as the Boolean conditioning event) or a @c random_variable
7330 * (continuous distribution, traversed by the analytical / MC
7331 * evaluator from @c Expectation.cpp).
7332 *
7333 * Implementation: thin wrapper over @c moment(input, 1, prov, method,
7334 * arguments). Both branches converge on the same machinery; the
7335 * AGG_TOKEN side computes E[X] as the @f$k=1@f$ instance of the
7336 * @f$n^k@f$-tuple enumeration in @c agg_raw_moment, the
7337 * random_variable side calls @c compute_expectation through
7338 * @c rv_moment.
7339 *
7340 * @param input aggregate expression or random variable to compute E[·] of
7341 * @param prov provenance condition (defaults to gate_one(), i.e., unconditional)
7342 * @param method knowledge compilation method (AGG_TOKEN path only)
7343 * @param arguments additional arguments for the method (AGG_TOKEN path only)
7344 */
7345CREATE OR REPLACE FUNCTION expected(
7346 input ANYELEMENT,
7347 prov UUID = gate_one(),
7348 method TEXT = NULL,
7349 arguments TEXT = NULL)
7350 RETURNS DOUBLE PRECISION AS $$
7351 SELECT moment(input, 1, prov, method, arguments);
7352$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7353
7354/**
7355 * @brief Internal: shared C entry point for variance / moment / central_moment.
7356 *
7357 * The @c expected() SQL function reaches the Expectation evaluator
7358 * through @c provenance_evaluate_compiled(..., 'expectation', ...).
7359 * The variance / raw-moment / central-moment SQL functions need an
7360 * extra @p k INTEGER argument that does not fit that dispatcher's
7361 * signature, so they go through this dedicated entry point. Returns
7362 * E[X^k] when @p central is FALSE, or E[(X - E[X])^k] when TRUE.
7363 */
7364CREATE OR REPLACE FUNCTION rv_moment(
7365 token UUID, k INTEGER, central BOOLEAN,
7366 prov UUID DEFAULT gate_one())
7367 RETURNS double precision
7368 AS 'provsql','rv_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7369
7370/** @brief Exact E[AVG^k | COUNT >= 1] over independent rows (the joint
7371 * (sum, count) fold); NULL when the shape is out of scope (shared
7372 * leaves, compound contributors), signalling @c agg_raw_moment's avg
7373 * arm to fall back to the Monte-Carlo scalar path. */
7374CREATE OR REPLACE FUNCTION agg_avg_moment_exact(token UUID, k INTEGER)
7375 RETURNS double precision
7376 AS 'provsql','agg_avg_moment_exact' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7377
7378/** @brief Collapsed (Rao-Blackwellised) raw moment E[C^k] of a correlated
7379 * COUNT / SUM whose per-row selection events are coupled through a single
7380 * shared continuous latent: 1-D quadrature over the latent, closed-form
7381 * per-row CDF given it (O(G·n), exact up to the grid). NULL when the
7382 * circuit does not match the shared-latent pattern (caller falls back to
7383 * the exact n^k enumeration). k in {1, 2}. */
7384CREATE OR REPLACE FUNCTION agg_collapsed_moment(token UUID, k INTEGER)
7385 RETURNS double precision
7386 AS 'provsql','agg_collapsed_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7387
7388/** @brief Both collapsed raw moments {E[C], E[C^2]} of a correlated COUNT / SUM
7389 * from a single circuit load and plan build; NULL when the shared-latent
7390 * pattern does not match. @c variance() uses this so a mean+variance readout
7391 * traverses the circuit once rather than calling @c agg_collapsed_moment twice
7392 * (the load and O(n) plan build dominate once the grid loop is arithmetic). */
7393CREATE OR REPLACE FUNCTION agg_collapsed_moments(token UUID)
7394 RETURNS double precision[]
7395 AS 'provsql','agg_collapsed_moments' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7396
7397/**
7398 * @brief Boolean event "this aggregate-carrying gate's value is defined
7399 * (non-NULL) in the world".
7400 *
7401 * Backs the conditional-on-defined convention of the aggregate moment
7402 * readouts: @c sum / @c count (and constants) have a value in every
7403 * world -- the empty group is the real value @c 0 -- so their defined
7404 * event is @c gate_one(); @c min / @c max / @c avg (and any other
7405 * aggregate) are @c NULL on an empty group, so their defined event is
7406 * "some contributing row is present", the OR of the semimod children's
7407 * row tokens; a @c case gate's value is defined iff its first-match
7408 * selected branch's value is (the same region walk as the moment
7409 * evaluator, conjoined per branch). Anything else (an @c arith
7410 * composite, whose AGG_TOKEN running value is total) counts as always
7411 * defined.
7412 */
7413CREATE OR REPLACE FUNCTION agg_defined_event(token UUID)
7414 RETURNS UUID AS $$
7415DECLARE
7416 gt PROVENANCE_GATE := get_gate_type(token);
7417 fname varchar;
7418 toks UUID[];
7419 wires UUID[];
7420 nw INTEGER;
7421 m INTEGER;
7422 i INTEGER;
7423 running_neg UUID := gate_one();
7424 parts UUID[] := '{}';
7425BEGIN
7426 IF gt = 'agg' THEN
7427 SELECT proname INTO fname
7428 FROM pg_proc WHERE oid = (get_infos(token)).info1;
7429 IF fname IN ('sum', 'count') THEN
7430 RETURN gate_one();
7431 END IF;
7432 SELECT array_agg((get_children(c))[1]) INTO toks
7433 FROM unnest(get_children(token)) AS c;
7434 IF toks IS NULL THEN
7435 RETURN gate_zero(); -- structurally empty aggregate: never defined
7436 END IF;
7437 RETURN provenance_plus(toks);
7438 ELSIF gt = 'case' THEN
7439 wires := get_children(token);
7440 nw := array_length(wires, 1);
7441 m := (nw - 1) / 2;
7442 FOR i IN 1..m LOOP
7443 parts := parts || provenance_times(
7444 running_neg, wires[2 * i - 1],
7445 agg_defined_event(wires[2 * i]));
7446 running_neg := provenance_times(running_neg,
7447 provenance_not(wires[2 * i - 1]));
7448 END LOOP;
7449 parts := parts || provenance_times(running_neg,
7450 agg_defined_event(wires[nw]));
7451 RETURN provenance_plus(parts);
7452 END IF;
7453 -- value / arith / anything else: a value exists in every world.
7454 RETURN gate_one();
7455END
7456$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
7457 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7458
7459/**
7460 * @brief Compute the raw moment E[X^k | prov] of an AGG_TOKEN aggregate
7461 *
7462 * Sister of @c expected() for the AGG_TOKEN side of the polymorphic
7463 * @c moment / @c variance / @c central_moment dispatch. Supports the
7464 * same aggregation functions as @c expected: SUM (which COUNT
7465 * normalises to at the gate level via @c Aggregation.cpp:322), MIN,
7466 * MAX, and AVG (exact over independent / laminar rows via the joint
7467 * (sum, count) distribution, Monte-Carlo scalar fallback otherwise).
7468 * MIN / MAX / AVG are NULL on an empty group, so their moments are
7469 * CONDITIONAL on the aggregate being defined -- NULL only when it never
7470 * is; SUM / COUNT treat the empty world as the real value 0.
7471 *
7472 * Strategy:
7473 * - <b>SUM</b>: with X = Σᵢ Iᵢ·vᵢ (Iᵢ the per-row inclusion indicator,
7474 * vᵢ the row's value), expanding X^k and taking expectation gives
7475 * @f$E[X^k] = \sum_{(i_1,\ldots,i_k) \in \{1..n\}^k} v_{i_1}\cdots v_{i_k}
7476 * \cdot P(\bigwedge_{i \in \TEXT{distinct}(i_1..i_k)} I_i)@f$.
7477 * We enumerate the @f$n^k@f$ tuples, conjoin the distinct inclusion
7478 * tokens (and @p prov when conditioning), and evaluate the
7479 * probability via @c probability_evaluate.
7480 * - <b>MIN / MAX</b>: replace @c v with @c v^k in the rank-based
7481 * enumeration that @c expected already uses; @c MAX is handled by
7482 * sign-flipping per the existing trick (negate vs. rerank), with
7483 * the outer multiplier becoming @f$(-1)^k@f$ instead of just @f$-1@f$.
7484 *
7485 * Cost: SUM is @f$O(n^k)@f$ probability evaluations -- tractable for
7486 * small @p k or small @p n; for larger sizes, prefer reaching for the
7487 * sampler. MIN / MAX stay linear in @p n.
7488 */
7489CREATE OR REPLACE FUNCTION agg_raw_moment(
7490 token AGG_TOKEN,
7491 k INTEGER,
7492 prov UUID = gate_one(),
7493 method TEXT = NULL,
7494 arguments TEXT = NULL)
7495 RETURNS DOUBLE PRECISION AS $$
7496DECLARE
7497 aggregation_function VARCHAR;
7498 child_pairs UUID[];
7499 pair_children UUID[];
7500 n INTEGER;
7501 i INTEGER;
7502 j INTEGER;
7503 vals float8[];
7504 toks UUID[];
7505 total float8;
7506 total_probability float8;
7507 tup INTEGER[];
7508 d INTEGER;
7509 prod_v float8;
7510 distinct_tok UUID[];
7511 conj_token UUID;
7512 prob float8;
7513 sign_max float8;
7514BEGIN
7515 IF token IS NULL OR k IS NULL THEN
7516 RETURN NULL;
7517 END IF;
7518 IF k < 0 THEN
7519 RAISE EXCEPTION 'agg_raw_moment(): k must be non-negative (got %)', k;
7520 END IF;
7521
7522 -- Aggregate-carrier CASE (a gate_case over aggregate branches): a first-match
7523 -- guarded selection. The moment is CONDITIONAL on the CASE's value being
7524 -- defined (NULL only when it never is, mirroring the MIN/MAX convention):
7525 -- E[pick^k | defined ∧ prov]
7526 -- = Σ_i P(region_i ∧ def_i) · E[value_i^k | region_i ∧ def_i]
7527 -- / Σ_i P(region_i ∧ def_i),
7528 -- where region_i = (¬g_1 ∧ … ∧ ¬g_{i-1}) ∧ g_i ∧ prov is the world set that
7529 -- selects branch i (the default's region is "all guards false") and def_i is
7530 -- the branch's defined event (agg_defined_event: gate_one for sum / count /
7531 -- constants, "some row present" for min / max / avg, recursive for a nested
7532 -- CASE). Both factors are exact: probability() over the region ∧ def event,
7533 -- and the conditional aggregate moment (a recursive agg_raw_moment on the
7534 -- branch aggregate, which conditions on its own definedness within the
7535 -- region, so the two factors weigh the same worlds). The regions are
7536 -- mutually exclusive, so the terms sum with no inclusion-exclusion, and
7537 -- correlation between a guard and its branch (shared input tuples) is
7538 -- carried by the conditioning, exactly as HAVING carries it. When every
7539 -- branch is defined everywhere, the defined mass equals P(prov) and the
7540 -- formula reduces to the plain region-weighted sum.
7541 IF get_gate_type(token) = 'case' THEN
7542 IF k = 0 THEN
7543 RETURN 1;
7544 END IF;
7545 DECLARE
7546 wires UUID[] := get_children(token);
7547 nw INTEGER := array_length(get_children(token), 1);
7548 m INTEGER := (array_length(get_children(token), 1) - 1) / 2;
7549 running_neg UUID := gate_one();
7550 region_full UUID;
7551 prov_p float8;
7552 p float8;
7553 total float8 := 0;
7554 def_mass float8 := 0;
7555 ci INTEGER;
7556 vuid UUID;
7557 bm float8;
7558 BEGIN
7559 prov_p := probability(prov);
7560 IF prov_p IS NULL OR prov_p <= 0 THEN
7561 RETURN NULL; -- impossible conditioning event
7562 END IF;
7563 -- Branches 1..m are the guarded WHENs; branch m+1 is the ELSE default,
7564 -- whose region is "all guards false".
7565 FOR ci IN 1 .. m + 1 LOOP
7566 IF ci <= m THEN
7567 region_full := provenance_times(running_neg, wires[2 * ci - 1], prov);
7568 vuid := wires[2 * ci];
7569 running_neg :=
7570 provenance_times(running_neg, provenance_not(wires[2 * ci - 1]));
7571 ELSE
7572 region_full := provenance_times(running_neg, prov);
7573 vuid := wires[nw];
7574 END IF;
7575 p := probability(provenance_times(region_full,
7576 agg_defined_event(vuid)));
7577 IF p > 0 THEN
7578 -- E[value_i^k | region_i ∧ def_i]: a constant branch is a Dirac
7579 -- (c^k, exact); a single aggregate or nested CASE is exact via
7580 -- agg_raw_moment (whose MIN/MAX/CASE arms condition on their own
7581 -- definedness within the region); an arithmetic / composite branch
7582 -- takes the Monte-Carlo scalar path (which composes with the
7583 -- aggregate leaves).
7584 IF get_gate_type(vuid) = 'value' THEN
7585 bm := power(CAST(get_extra(vuid) AS float8), k);
7586 ELSIF get_gate_type(vuid) IN ('agg', 'case') THEN
7587 bm := agg_raw_moment(agg_token_make(vuid, 0), k, region_full,
7588 method, arguments);
7589 ELSE
7590 bm := rv_moment(vuid, k, false, region_full);
7591 END IF;
7592 total := total + p * bm;
7593 def_mass := def_mass + p;
7594 END IF;
7595 END LOOP;
7596 IF def_mass <= epsilon() THEN
7597 RETURN NULL; -- the CASE's value is never defined under prov
7598 END IF;
7599 RETURN total / def_mass;
7600 END;
7601 END IF;
7602
7603 IF get_gate_type(token) <> 'agg' THEN
7604 IF get_gate_type(token) IN ('arith', 'conditioned') THEN
7605 RAISE EXCEPTION 'expected / variance / moment over an arithmetic '
7606 'combination of aggregates (e.g. SUM(x) + SUM(y) or SUM(x) + 5), or a '
7607 'conditioning of one, is not yet supported: a moment can be taken only '
7608 'over a single aggregate (SUM / COUNT / MIN / MAX), optionally '
7609 'conditioned (SUM(x) | C)'
7610 USING HINT = 'Take the moment of each aggregate separately, or condition '
7611 'the bare aggregate.';
7612 ELSE
7613 RAISE EXCEPTION USING MESSAGE='Wrong gate type for agg_raw_moment computation';
7614 END IF;
7615 END IF;
7616 IF k = 0 THEN
7617 RETURN 1;
7618 END IF;
7619
7620 SELECT pp.proname::varchar FROM pg_proc pp
7621 WHERE oid=(get_infos(token)).info1
7622 INTO aggregation_function;
7623
7624 child_pairs := get_children(token);
7625 n := COALESCE(array_length(child_pairs, 1), 0);
7626
7627 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
7628 -- count(col) keeps the COUNT identity at the gate level but its value is a
7629 -- SUM of per-row 0/1 indicators, so its moments are computed exactly like
7630 -- SUM (and its empty group is the real value 0, like SUM). count(*)
7631 -- arrives here as 'sum' (it normalises to F_SUM_INT4); count(col) as 'count'.
7632 -- Trivial empty aggregation: SUM = 0, so SUM^k = 0 for k >= 1.
7633 -- Note: AGG_TOKEN semantics treat the "no row included" world as
7634 -- SUM = 0, so this stays consistent with k = 1 (= expected()).
7635 IF n = 0 THEN
7636 RETURN 0;
7637 END IF;
7638
7639 -- Collapsed fast path: a correlated COUNT / SUM whose per-row selection
7640 -- events share a single continuous latent has an O(G·n) 1-D quadrature,
7641 -- vastly cheaper than the O(n^k) tuple enumeration below (which is the
7642 -- O(n^2) pair-probability bottleneck for the variance). Only fires
7643 -- unconditionally (prov = one) and for k in {1, 2}; agg_collapsed_moment
7644 -- returns NULL when the shared-latent pattern does not match, and we
7645 -- fall through to the exact enumeration.
7646 IF prov = gate_one() AND k <= 2 THEN
7647 total := agg_collapsed_moment((token)::UUID, k);
7648 IF total IS NOT NULL THEN
7649 RETURN total;
7650 END IF;
7651 END IF;
7652
7653 -- Extract per-child token + value arrays.
7654 vals := ARRAY[]::float8[];
7655 toks := ARRAY[]::UUID[];
7656 FOR i IN 1..n LOOP
7657 pair_children := get_children(child_pairs[i]);
7658 toks := toks || pair_children[1];
7659 vals := vals || CAST(get_extra(pair_children[2]) AS float8);
7660 END LOOP;
7661
7662 -- Enumerate all k-tuples (i_1, ..., i_k) in {1..n}^k. tup is the
7663 -- current tuple; we step through them in lexicographic order.
7664 total := 0;
7665 tup := array_fill(1, ARRAY[k]);
7666 LOOP
7667 prod_v := 1;
7668 FOR j IN 1..k LOOP
7669 prod_v := prod_v * vals[tup[j]];
7670 END LOOP;
7671
7672 SELECT array_agg(DISTINCT toks[idx]) INTO distinct_tok
7673 FROM unnest(tup) AS idx;
7674
7675 IF prov <> gate_one() THEN
7676 distinct_tok := distinct_tok || prov;
7677 END IF;
7678 conj_token := provenance_times(VARIADIC distinct_tok);
7679 prob := probability_evaluate(conj_token, method, arguments);
7680
7681 total := total + prod_v * prob;
7682
7683 d := k;
7684 WHILE d >= 1 AND tup[d] = n LOOP
7685 tup[d] := 1;
7686 d := d - 1;
7687 END LOOP;
7688 EXIT WHEN d = 0;
7689 tup[d] := tup[d] + 1;
7690 END LOOP;
7691 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
7692 -- Rank enumeration: per distinct value v, P(MIN = v) is the
7693 -- probability that some t_i with v_i=v is true and all t_j with
7694 -- smaller v are false. For MAX we negate values so the same
7695 -- "smaller-than" rank logic computes MIN-of-negated, then flip.
7696 -- The outer multiplier picks up the right sign for the k-th moment
7697 -- of MAX: E[MAX^k] = (-1)^k * E[MIN(-v)^k], so sign_max = (-1)^k.
7698 sign_max := CASE
7699 WHEN aggregation_function = 'max'
7700 THEN power(-1::float8, k)
7701 ELSE 1
7702 END;
7703
7704 -- MIN/MAX over the empty input world are NULL (no elements), not ±Infinity:
7705 -- SQL returns one row with a NULL value. The moment is therefore CONDITIONAL
7706 -- on the aggregate being defined (non-empty) -- the empty world is excluded
7707 -- and the result renormalised by P(prov AND non-empty). (count, whose empty
7708 -- value 0 is a real value, keeps the empty world; sum keeps it too, as 0.)
7709 IF n = 0 THEN
7710 RETURN NULL; -- structurally empty: MIN/MAX undefined
7711 END IF;
7712
7713 -- Numerator E[MIN^k . 1{prov AND non-empty}] (the rank sum naturally omits
7714 -- the empty world, since every term requires a present token).
7715 WITH tok_value AS (
7716 SELECT (get_children(c))[1] AS tok,
7717 (CASE WHEN aggregation_function='max' THEN -1 ELSE 1 END)
7718 * CAST(get_extra((get_children(c))[2]) AS DOUBLE PRECISION) AS v
7719 FROM UNNEST(child_pairs) AS c
7720 ) SELECT sign_max * COALESCE(SUM(p * power(v, k)), 0) FROM (
7721 SELECT t1.v AS v,
7722 probability_evaluate(
7723 CASE WHEN prov = gate_one()
7724 THEN provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
7725 provenance_plus(ARRAY_AGG(t2.tok)))
7726 ELSE provenance_times(prov,
7727 provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
7728 provenance_plus(ARRAY_AGG(t2.tok)))) END,
7729 method, arguments) AS p
7730 FROM tok_value t1 LEFT OUTER JOIN tok_value t2 ON t1.v > t2.v
7731 GROUP BY t1.v) tmp
7732 INTO total;
7733
7734 -- Denominator P(prov AND non-empty) = P(prov (x) (+) tokens).
7735 SELECT probability_evaluate(
7736 CASE WHEN prov = gate_one()
7737 THEN provenance_plus(ARRAY_AGG(tok))
7738 ELSE provenance_times(prov, provenance_plus(ARRAY_AGG(tok))) END,
7739 method, arguments)
7740 FROM (SELECT (get_children(c))[1] AS tok FROM UNNEST(child_pairs) AS c) s
7741 INTO total_probability;
7742
7743 IF total_probability <= epsilon() THEN
7744 RETURN NULL; -- never defined under prov: MIN/MAX undefined
7745 END IF;
7746 RETURN total / total_probability; -- already conditional; skip generic norm
7747 ELSIF aggregation_function = 'avg' THEN
7748 -- AVG = SUM/COUNT is a ratio of two correlated world-dependent
7749 -- quantities, so the k-tuple expansion above does not apply. Like
7750 -- MIN/MAX, AVG over the empty world is NULL, so its moment conditions
7751 -- on the aggregate being defined (COUNT >= 1), NULL when it never is.
7752 -- Two routes:
7753 -- * EXACT (independent rows, unconditional): the joint (sum, count)
7754 -- PMF folded in C by agg_avg_moment_exact --
7755 -- E[AVG^k | COUNT>=1] = Σ_{(s,c), c>=1} (s/c)^k pmf(s,c) / P(c>=1).
7756 -- * Monte-Carlo scalar fallback otherwise (an outer conditioning
7757 -- event, shared leaves, compound contributors): rv_moment samples
7758 -- the agg gate per world; its NaN-skip on empty draws implements
7759 -- the same conditional-on-defined convention, at the
7760 -- provsql.rv_mc_samples budget (0 raises, per convention).
7761 IF n = 0 THEN
7762 RETURN NULL; -- structurally empty: AVG undefined
7763 END IF;
7764 IF prov = gate_one() THEN
7765 total := agg_avg_moment_exact((token)::UUID, k);
7766 IF total IS NOT NULL THEN
7767 RETURN total;
7768 END IF;
7769 END IF;
7770 RETURN rv_moment((token)::UUID, k, false, prov);
7771 ELSE
7772 RAISE EXCEPTION USING MESSAGE=
7773 'Cannot compute moment for aggregation function ' || aggregation_function;
7774 END IF;
7775
7776 -- Conditional normalisation: E[X^k · 1_A] / P(A) = E[X^k | A].
7777 IF prov <> gate_one()
7778 AND total <> 0
7779 AND total <> 'Infinity'::float8
7780 AND total <> '-Infinity'::float8 THEN
7781 total := total / probability_evaluate(prov, method, arguments);
7782 END IF;
7783
7784 RETURN total;
7785END
7786$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7787
7788/**
7789 * @brief Compute the variance Var[X | prov] of a probabilistic scalar
7790 *
7791 * Polymorphic dispatcher that mirrors @c expected: @c random_variable
7792 * inputs go through the analytical / MC evaluator
7793 * (@c rv_moment(UUID, 2, true)); @c AGG_TOKEN inputs go through the
7794 * @c agg_raw_moment helper, computing
7795 * @f$\mathrm{Var}[X|A] = E[X^2|A] - E[X|A]^2@f$. Conditioning on
7796 * @c prov is supported for @c AGG_TOKEN (matching @c expected) but
7797 * not yet for @c random_variable.
7798 */
7799CREATE OR REPLACE FUNCTION variance(
7800 input ANYELEMENT,
7801 prov UUID = gate_one(),
7802 method TEXT = NULL,
7803 arguments TEXT = NULL)
7804 RETURNS DOUBLE PRECISION AS $$
7805DECLARE
7806 m1 float8;
7807 m2 float8;
7808BEGIN
7809 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7810 IF input IS NULL THEN
7811 RETURN NULL;
7812 END IF;
7813 -- Conditioning on prov is handled inside rv_moment: when prov
7814 -- resolves to gate_one() (the default, or load-time
7815 -- simplification of any always-true sub-circuit) the
7816 -- unconditional analytical path runs unchanged; otherwise the
7817 -- joint-circuit loader unifies shared gate_rv leaves between
7818 -- input and prov, and the conditional path runs either
7819 -- truncated-distribution closed form or MC rejection.
7820 RETURN provsql.rv_moment(
7821 rv_conditioned_target((input::random_variable)::UUID), 2, true,
7822 rv_conditioned_prov((input::random_variable)::UUID, prov));
7823 END IF;
7824
7825 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
7826 IF input IS NULL THEN
7827 RETURN NULL;
7828 END IF;
7829 -- Collapsed fast path: E[C] and E[C^2] from a single circuit load and plan
7830 -- build, instead of two agg_raw_moment() calls that each reload. Mirrors
7831 -- the guard in agg_raw_moment (unconditional only, prov = one); on any
7832 -- mismatch agg_collapsed_moments returns NULL and we fall through to the
7833 -- generic per-order path (which handles conditioning, SUM enumeration, ...).
7834 IF rv_conditioned_prov(input::UUID, prov) = gate_one() THEN
7835 DECLARE ms float8[];
7836 BEGIN
7837 ms := agg_collapsed_moments(
7838 (agg_conditioned_target(input::AGG_TOKEN))::UUID);
7839 IF ms IS NOT NULL THEN
7840 RETURN ms[2] - ms[1] * ms[1];
7841 END IF;
7842 END;
7843 END IF;
7844 m1 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
7845 rv_conditioned_prov(input::UUID, prov), method, arguments);
7846 m2 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 2,
7847 rv_conditioned_prov(input::UUID, prov), method, arguments);
7848 IF m1 IS NULL OR m2 IS NULL THEN
7849 RETURN NULL;
7850 END IF;
7851 RETURN m2 - m1 * m1;
7852 END IF;
7853
7854 -- Bernoulli event token (see moment()): Var[X] = p(1 - p).
7855 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
7856 IF input IS NULL THEN
7857 RETURN NULL;
7858 END IF;
7859 m1 := provsql.probability_evaluate(provsql.cond(input::UUID, prov),
7860 method, arguments);
7861 RETURN m1 * (1 - m1);
7862 END IF;
7863
7864 RAISE EXCEPTION 'variance() is not yet supported for input type %', pg_typeof(input);
7865END
7866$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7867
7868/**
7869 * @brief Compute the raw moment E[X^k | prov] of a probabilistic scalar
7870 *
7871 * @c k must be a non-negative INTEGER. @c k = 0 returns 1; @c k = 1
7872 * is equivalent to @c expected(input). Polymorphic dispatcher: routes
7873 * @c random_variable through @c rv_moment (analytical / MC) and
7874 * @c AGG_TOKEN through @c agg_raw_moment (SUM via tuple enumeration,
7875 * MIN / MAX via rank enumeration, AVG via the joint (sum, count)
7876 * distribution over independent / laminar rows with a Monte-Carlo
7877 * fallback).
7878 */
7879CREATE OR REPLACE FUNCTION moment(
7880 input ANYELEMENT,
7881 k INTEGER,
7882 prov UUID = gate_one(),
7883 method TEXT = NULL,
7884 arguments TEXT = NULL)
7885 RETURNS DOUBLE PRECISION AS $$
7886BEGIN
7887 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7888 IF input IS NULL OR k IS NULL THEN
7889 RETURN NULL;
7890 END IF;
7891 -- See variance() above: rv_moment handles the conditional/unconditional
7892 -- dispatch internally based on the resolved prov gate type.
7893 RETURN provsql.rv_moment(
7894 rv_conditioned_target((input::random_variable)::UUID), k, false,
7895 rv_conditioned_prov((input::random_variable)::UUID, prov));
7896 END IF;
7897
7898 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
7899 RETURN agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), k,
7900 rv_conditioned_prov(input::UUID, prov), method, arguments);
7901 END IF;
7902
7903 -- A bare provenance event token (a gate_cmp lifted from an RV comparison,
7904 -- e.g. expected(x <= c)) is a Bernoulli indicator: X in {0,1}, so every raw
7905 -- moment E[X^k] with k >= 1 equals P(event), and E[X^0] = 1. cond() applies
7906 -- the optional conditioning prov (a no-op for the default gate_one()).
7907 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
7908 IF input IS NULL OR k IS NULL THEN
7909 RETURN NULL;
7910 END IF;
7911 IF k = 0 THEN
7912 RETURN 1;
7913 END IF;
7914 RETURN provsql.probability_evaluate(provsql.cond(input::UUID, prov),
7915 method, arguments);
7916 END IF;
7917
7918 RAISE EXCEPTION 'moment() is not yet supported for input type %', pg_typeof(input);
7919END
7920$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7921
7922/**
7923 * @brief Internal: rv-side quantile computation.
7924 *
7925 * C entry point behind the polymorphic @c quantile dispatcher.
7926 * Closed-form inverse CDF where the family has one (Normal via
7927 * Beasley-Springer-Moro polished by Newton steps, Uniform and
7928 * Exponential by algebraic inversion), generic monotone-CDF bisection
7929 * otherwise (Erlang, Gamma), exact generalised inverse for categorical
7930 * mixtures, and the empirical Monte Carlo quantile for compound scalar
7931 * circuits. A non-trivial @p prov conditions (truncates) the
7932 * distribution first, in closed form when the event reduces to an
7933 * interval on a bare @c gate_rv.
7934 */
7935CREATE OR REPLACE FUNCTION rv_quantile(
7936 token UUID, p double precision,
7937 prov UUID DEFAULT gate_one())
7938 RETURNS double precision
7939 AS 'provsql','rv_quantile' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7940
7941/**
7942 * @brief Compute the p-quantile (inverse CDF) of a probabilistic scalar
7943 *
7944 * @f$F^{-1}(p) = \min\{x : P(X \le x) \ge p\}@f$ for @f$p \in [0,1]@f$:
7945 * medians (@c p = 0.5), percentiles, Value-at-Risk, and credible
7946 * intervals. @c p = 0 / @c p = 1 return the (possibly infinite)
7947 * support edges. Polymorphic dispatcher mirroring @c expected /
7948 * @c moment: @c random_variable routes through @c rv_quantile
7949 * (analytical inverse CDF / MC), plain numerics are their own quantile
7950 * (a Dirac's inverse CDF is constant), and the optional @p prov
7951 * argument conditions on a provenance event, e.g.
7952 * <tt>quantile(x | (x > 0), 0.5)</tt> for the median of a truncated
7953 * distribution.
7954 */
7955CREATE OR REPLACE FUNCTION quantile(
7956 input ANYELEMENT,
7957 p double precision,
7958 prov UUID = gate_one(),
7959 method TEXT = NULL,
7960 arguments TEXT = NULL)
7961 RETURNS DOUBLE PRECISION AS $$
7962BEGIN
7963 IF p IS NULL THEN
7964 RETURN NULL;
7965 END IF;
7966 IF p <> p OR p < 0 OR p > 1 THEN
7967 RAISE EXCEPTION 'quantile: p must be in [0, 1] (got %)', p;
7968 END IF;
7969
7970 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7971 IF input IS NULL THEN
7972 RETURN NULL;
7973 END IF;
7974 -- See variance(): rv_quantile handles the conditional/unconditional
7975 -- dispatch internally based on the resolved prov gate type.
7976 RETURN provsql.rv_quantile(
7977 rv_conditioned_target((input::random_variable)::UUID), p,
7978 rv_conditioned_prov((input::random_variable)::UUID, prov));
7979 END IF;
7980
7981 IF pg_typeof(input) IN ('smallint'::REGTYPE, 'INTEGER'::REGTYPE,
7982 'bigint'::REGTYPE, 'NUMERIC'::REGTYPE,
7983 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
7984 -- A deterministic scalar is a Dirac: every quantile is the value.
7985 RETURN input::double precision;
7986 END IF;
7987
7988 RAISE EXCEPTION 'quantile() is not yet supported for input type %', pg_typeof(input);
7989END
7990$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7991
7992/**
7993 * @brief Internal: rv-side support computation
7994 *
7995 * Lifts @c provsql.compute_support out of @c RangeCheck.cpp -- the
7996 * same interval-arithmetic propagation @c runRangeCheck uses to
7997 * decide @c gate_cmps. Returns @c [-Infinity, +Infinity] when the
7998 * tightest bound is the conservative all-real interval (e.g. for a
7999 * normal RV, or any sub-circuit that mixes a normal in).
8000 */
8001CREATE OR REPLACE FUNCTION rv_support(
8002 token UUID, prov UUID DEFAULT gate_one(),
8003 OUT lo float8, OUT hi float8)
8004 AS 'provsql','rv_support' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8005
8006/**
8007 * @brief Compute the support interval @c [lo, hi] of a probabilistic
8008 * (or deterministic) scalar
8009 *
8010 * Polymorphic dispatcher mirroring @c expected / @c variance /
8011 * @c moment / @c central_moment, with two extra "free" branches:
8012 *
8013 * - <b>Plain NUMERIC</b> (@c smallint / @c INTEGER / @c bigint /
8014 * @c NUMERIC / @c real / @c double @c precision): degenerate
8015 * point support @f$[c, c]@f$. Lets callers ask for the support
8016 * of a literal without round-tripping through @c as_random.
8017 * - <b>@c random_variable / bare @c UUID</b> (any provenance gate
8018 * token; the @c random_variable branch reinterprets the value via
8019 * the binary-coercible @c random_variable @c -> @c UUID cast):
8020 * routes to @c rv_support, which propagates distribution
8021 * supports (uniform exact, exponential @c [0,+∞), normal
8022 * @c (-∞,+∞)) through @c gate_arith via interval arithmetic.
8023 * @c gate_value gives the same @f$[c, c]@f$ point support as the
8024 * NUMERIC branch; any non-scalar gate (Boolean gates, aggregates,
8025 * ...) safely falls back to the conservative all-real interval
8026 * without raising. Conditioning on @c prov is not yet supported.
8027 *
8028 * - @c AGG_TOKEN: closed-form per aggregation function:
8029 * - @c SUM : @f$[\sum_i \min(0,v_i), \sum_i \max(0,v_i)]@f$
8030 * (every row is independently in or out of the included set; the
8031 * extreme SUMs are reached by including only positive or only
8032 * negative-valued rows).
8033 * - @c MIN : @f$[\min_i v_i, \max_i v_i]@f$ in the non-empty
8034 * subsets, plus @c +Infinity if the empty subset has positive
8035 * probability under @c prov.
8036 * - @c MAX : symmetric -- @c -Infinity if empty has positive
8037 * probability under @c prov, otherwise @c min_i v_i; @c hi is
8038 * always @c max_i v_i.
8039 *
8040 * Other aggregation functions raise.
8041 *
8042 * Returns the composite RECORD @c (lo, hi) via the function's
8043 * @c OUT parameters, with @c -Infinity / @c +Infinity marking
8044 * unbounded ends.
8045 */
8046CREATE OR REPLACE FUNCTION support(
8047 input ANYELEMENT,
8048 prov UUID = gate_one(),
8049 method TEXT = NULL,
8050 arguments TEXT = NULL,
8051 OUT lo float8,
8052 OUT hi float8)
8053 AS $$
8054DECLARE
8055 aggregation_function VARCHAR;
8056 child_pairs UUID[];
8057 values_arr float8[];
8058 total_probability float8;
8059BEGIN
8060 IF input IS NULL THEN
8061 lo := NULL; hi := NULL; RETURN;
8062 END IF;
8063
8064 -- Plain NUMERIC: degenerate point support. Lets `support(2.5)` /
8065 -- `support(42)` / etc. return (2.5, 2.5) without making the user
8066 -- wrap in `as_random`.
8067 IF pg_typeof(input) IN (
8068 'smallint'::REGTYPE, 'INTEGER'::REGTYPE, 'bigint'::REGTYPE,
8069 'NUMERIC'::REGTYPE, 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
8070 lo := input::double precision;
8071 hi := input::double precision;
8072 RETURN;
8073 END IF;
8074
8075 -- random_variable is binary-coercible to UUID (explicit cast
8076 -- below), so a single rv_support call covers both shapes.
8077 -- rv_support handles
8078 -- gate_value (point), gate_rv (distribution), gate_arith
8079 -- (propagated), and falls back to the conservative all-real
8080 -- interval for any other gate kind. Conditioning on prov is not
8081 -- supported (would require restricting the underlying joint
8082 -- distribution by the indicator of prov, which has no closed form
8083 -- for the basic distributions we ship).
8084 IF pg_typeof(input) IN ('random_variable'::REGTYPE, 'UUID'::REGTYPE) THEN
8085 -- Conditional support: rv_support folds the AND-conjunct interval
8086 -- constraints from prov into the unconditional support. When
8087 -- prov is gate_one() the unconditional support is returned
8088 -- unchanged.
8089 SELECT r.lo, r.hi INTO lo, hi
8090 FROM provsql.rv_support(
8091 rv_conditioned_target(input::UUID),
8092 rv_conditioned_prov(input::UUID, prov)) r;
8093 RETURN;
8094 END IF;
8095
8096 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
8097 -- A conditioned aggregate SUM(x)|C: the value-range support is that of
8098 -- the target aggregate (conditioning can only tighten it; the
8099 -- conservative range stays valid), so unpack to the target gate.
8100 DECLARE
8101 atok AGG_TOKEN := agg_conditioned_target(input::AGG_TOKEN);
8102 BEGIN
8103 IF get_gate_type(atok) <> 'agg' THEN
8104 RAISE EXCEPTION USING MESSAGE='Wrong gate type for support computation';
8105 END IF;
8106 SELECT pp.proname::varchar FROM pg_proc pp
8107 WHERE oid=(get_infos(atok)).info1
8108 INTO aggregation_function;
8109 child_pairs := get_children(atok);
8110
8111 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
8112 -- count(col) is a SUM of per-row 0/1 indicators (empty group = 0), so its
8113 -- support is computed like SUM; count(*) arrives as 'sum'.
8114 -- Empty AGG_TOKEN: SUM is identically 0.
8115 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
8116 lo := 0; hi := 0; RETURN;
8117 END IF;
8118 SELECT sum(LEAST(v, 0::float8)), sum(GREATEST(v, 0::float8))
8119 INTO lo, hi
8120 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
8121 FROM unnest(child_pairs) AS c) sub;
8122 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
8123 -- MIN/MAX over the empty input world are NULL, not ±Infinity (matching the
8124 -- moment surface): the empty world carries no value, so the support is just
8125 -- the range of the per-row values [min(v), max(v)]. A structurally empty
8126 -- aggregate has no defined value at all -> NULL support.
8127 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
8128 lo := NULL; hi := NULL; RETURN;
8129 END IF;
8130
8131 SELECT min(v), max(v)
8132 INTO lo, hi
8133 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
8134 FROM UNNEST(child_pairs) AS c) sub;
8135 ELSE
8136 RAISE EXCEPTION USING MESSAGE=
8137 'Cannot compute support for aggregation function ' || aggregation_function;
8138 END IF;
8139 RETURN;
8140 END;
8141 END IF;
8142
8143 RAISE EXCEPTION 'support() is not yet supported for input type %', pg_typeof(input);
8144END
8145$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
8146
8147/**
8148 * @brief Compute the central moment E[(X - E[X|prov])^k | prov]
8149 *
8150 * @c k = 0 returns 1; @c k = 1 returns 0; @c k = 2 is equivalent to
8151 * @c variance(input, prov, ...). Polymorphic dispatcher: routes
8152 * @c random_variable through @c rv_moment, and @c AGG_TOKEN through
8153 * the binomial expansion
8154 * @f$E[(X-\mu)^k|A] = \sum_{i=0}^{k} \binom{k}{i} (-\mu)^{k-i} E[X^i|A]@f$
8155 * with @f$\mu = E[X|A]@f$, where each @f$E[X^i|A]@f$ comes from
8156 * @c agg_raw_moment.
8157 */
8158CREATE OR REPLACE FUNCTION central_moment(
8159 input ANYELEMENT,
8160 k INTEGER,
8161 prov UUID = gate_one(),
8162 method TEXT = NULL,
8163 arguments TEXT = NULL)
8164 RETURNS DOUBLE PRECISION AS $$
8165DECLARE
8166 mu float8;
8167 total float8;
8168 i INTEGER;
8169 raw_i float8;
8170 binom float8;
8171 -- iterative binomial coefficient C(k, i)
8172 k_double float8;
8173BEGIN
8174 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
8175 IF input IS NULL OR k IS NULL THEN
8176 RETURN NULL;
8177 END IF;
8178 -- See variance() above: rv_moment handles the conditional/unconditional
8179 -- dispatch internally based on the resolved prov gate type.
8180 RETURN provsql.rv_moment(
8181 rv_conditioned_target((input::random_variable)::UUID), k, true,
8182 rv_conditioned_prov((input::random_variable)::UUID, prov));
8183 END IF;
8184
8185 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
8186 IF input IS NULL OR k IS NULL THEN
8187 RETURN NULL;
8188 END IF;
8189 IF k < 0 THEN
8190 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
8191 END IF;
8192 IF k = 0 THEN RETURN 1; END IF;
8193 IF k = 1 THEN RETURN 0; END IF;
8194
8195 mu := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
8196 rv_conditioned_prov(input::UUID, prov), method, arguments);
8197 IF mu IS NULL THEN RETURN NULL; END IF;
8198 -- mu may be ±Infinity for empty MIN / MAX with positive empty
8199 -- probability; central_moment is undefined in that case.
8200 IF mu = 'Infinity'::float8 OR mu = '-Infinity'::float8 THEN
8201 RETURN mu;
8202 END IF;
8203
8204 total := 0;
8205 binom := 1; -- C(k, 0)
8206 k_double := k;
8207 FOR i IN 0..k LOOP
8208 raw_i := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), i,
8209 rv_conditioned_prov(input::UUID, prov), method, arguments);
8210 IF raw_i IS NULL THEN RETURN NULL; END IF;
8211 total := total + binom * power(-mu, k - i) * raw_i;
8212 -- C(k, i+1) = C(k, i) * (k - i) / (i + 1)
8213 IF i < k THEN
8214 binom := binom * (k_double - i) / (i + 1);
8215 END IF;
8216 END LOOP;
8217 RETURN total;
8218 END IF;
8219
8220 -- Bernoulli event token (see moment()): with p = P(event),
8221 -- E[(X-p)^k] = (1-p)(-p)^k + p(1-p)^k; k = 0 -> 1, k = 1 -> 0.
8222 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
8223 IF input IS NULL OR k IS NULL THEN
8224 RETURN NULL;
8225 END IF;
8226 IF k < 0 THEN
8227 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
8228 END IF;
8229 IF k = 0 THEN RETURN 1; END IF;
8230 IF k = 1 THEN RETURN 0; END IF;
8231 mu := provsql.probability_evaluate(provsql.cond(input::UUID, prov),
8232 method, arguments);
8233 RETURN (1 - mu) * power(-mu, k) + mu * power(1 - mu, k);
8234 END IF;
8235
8236 RAISE EXCEPTION 'central_moment() is not yet supported for input type %', pg_typeof(input);
8237END
8238$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
8239
8240/** @brief C entry point behind @ref covariance (UUID-level binding). */
8241CREATE OR REPLACE FUNCTION rv_covariance(x UUID, y UUID, prov UUID)
8242 RETURNS double precision
8243 AS 'provsql','rv_covariance' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8244
8245/**
8246 * @brief Covariance Cov(X, Y) = E[XY] − E[X]·E[Y] of two random variables.
8247 *
8248 * The bivariate readout complementing the univariate moment surface
8249 * (@ref expected / @ref variance / @ref moment / @ref central_moment).
8250 * Exact tiers: an exact @c 0 when the two arguments' stochastic-leaf
8251 * footprints are structurally independent (given @p prov), a variance
8252 * readout when the two arguments coincide, and the closed-form
8253 * @c E[XY] − E[X]·E[Y] whenever every factor decomposes analytically.
8254 * When some factor has no closed form, a SINGLE coupled Monte-Carlo pass
8255 * over the joint circuit draws @c (x, y) pairs (shared leaves produce one
8256 * draw both observe) and returns the sample covariance -- the estimator's
8257 * noise then scales with the covariance signal itself, not with the
8258 * product of the means as the naive three-run E[XY] − E[X]·E[Y]
8259 * subtraction would.
8260 *
8261 * @param x the first random variable.
8262 * @param y the second random variable.
8263 * @param prov optional conditioning event (a provenance @c UUID); the
8264 * default @c gate_one() is the unconditional covariance. Conditioning
8265 * is applied jointly: the Monte-Carlo pass rejection-samples the pair on
8266 * @p prov, giving @c Cov(X, Y | prov).
8267 */
8268CREATE OR REPLACE FUNCTION covariance(
8269 x random_variable, y random_variable, prov UUID DEFAULT gate_one())
8270 RETURNS double precision AS $$
8271 SELECT provsql.rv_covariance((x)::UUID, (y)::UUID, prov);
8272$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8273
8274/**
8275 * @brief Standard deviation σ(X) = √Var(X) of a random variable.
8276 *
8277 * A thin NUMERIC readout over @ref variance. The square root is taken on
8278 * the scalar @c double result, so no RV-level @c sqrt is involved and this
8279 * carries no dependency on RV function application (@c pow / @c sqrt).
8280 * @c NULL propagates from a @c NULL input; the order-2 central moment is
8281 * non-negative by construction, so the root is always real.
8282 *
8283 * @param x the random variable.
8284 * @param prov optional conditioning event; default @c gate_one()
8285 * (unconditional).
8286 */
8287CREATE OR REPLACE FUNCTION stddev(
8288 x random_variable, prov UUID DEFAULT gate_one())
8289 RETURNS double precision AS $$
8290 SELECT sqrt(provsql.variance(x, prov));
8291$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8292
8293/** @brief C entry point behind @ref correlation (UUID-level binding). */
8294CREATE OR REPLACE FUNCTION rv_correlation(x UUID, y UUID, prov UUID)
8295 RETURNS double precision
8296 AS 'provsql','rv_correlation' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8297
8298/**
8299 * @brief Pearson correlation ρ(X, Y) = Cov(X, Y) / (σ(X)·σ(Y)).
8300 *
8301 * Same exact tiers as @ref covariance; on the Monte-Carlo path the
8302 * covariance and BOTH standard deviations are read off the same coupled
8303 * pass, instead of stacking five independent estimates (three for the
8304 * covariance, one per standard deviation). Returns @c NULL when either
8305 * standard deviation is @c 0 (a degenerate / constant variable, for which
8306 * correlation is undefined) rather than raising a division-by-zero.
8307 *
8308 * @param x the first random variable.
8309 * @param y the second random variable.
8310 * @param prov optional conditioning event; default @c gate_one()
8311 * (unconditional).
8312 */
8313CREATE OR REPLACE FUNCTION correlation(
8314 x random_variable, y random_variable, prov UUID DEFAULT gate_one())
8315 RETURNS double precision AS $$
8316 SELECT provsql.rv_correlation((x)::UUID, (y)::UUID, prov);
8317$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8318
8319/** @brief C entry point behind @ref entropy (UUID-level binding). */
8320CREATE OR REPLACE FUNCTION rv_entropy(token UUID, prov UUID)
8321 RETURNS double precision
8322 AS 'provsql','rv_entropy' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8323
8324/**
8325 * @brief Entropy H(X) of a random variable, in nats.
8326 *
8327 * Shannon entropy for a discrete distribution (a categorical / discrete
8328 * count / constant -- a point mass has entropy @c 0), differential
8329 * entropy for a continuous one (quadrature of @c -f ln f over the
8330 * family's integration range; also exact through independent-arm
8331 * Bernoulli mixture trees such as @ref gmm's). Shapes with no
8332 * closed density (arithmetic composites) and the conditional form fall
8333 * back to a Monte Carlo histogram plug-in estimate at the
8334 * @c provsql.rv_mc_samples budget.
8335 *
8336 * @param x the random variable.
8337 * @param prov optional conditioning event; default @c gate_one()
8338 * (unconditional).
8339 */
8340CREATE OR REPLACE FUNCTION entropy(
8341 x random_variable, prov UUID DEFAULT gate_one())
8342 RETURNS double precision AS $$
8343 SELECT provsql.rv_entropy((x)::UUID, prov);
8344$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8345
8346/** @brief C entry point behind @ref kl (UUID-level binding). */
8347CREATE OR REPLACE FUNCTION rv_kl(p UUID, q UUID)
8348 RETURNS double precision
8349 AS 'provsql','rv_kl' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8350
8351/**
8352 * @brief Kullback-Leibler divergence KL(P || Q), in nats.
8353 *
8354 * Exact: the defining sum for two discrete distributions (matching
8355 * outcomes by value) and the defining integral (quadrature over P's
8356 * integration window) for two continuous ones, including
8357 * independent-arm mixture trees. Returns @c Infinity when P is not
8358 * absolutely continuous with respect to Q -- an outcome of P that Q
8359 * gives zero mass, mismatched kinds (discrete vs continuous), or a
8360 * region of P's support where Q's density (under)flows to zero. Both
8361 * arguments must resolve to closed-form densities; arithmetic
8362 * composites and conditioned variables raise.
8363 */
8364CREATE OR REPLACE FUNCTION kl(p random_variable, q random_variable)
8365 RETURNS double precision AS $$
8366 SELECT provsql.rv_kl((p)::UUID, (q)::UUID);
8367$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8368
8369/** @brief C entry point behind @ref mutual_information (UUID-level
8370 * binding). */
8371CREATE OR REPLACE FUNCTION rv_mutual_information(x UUID, y UUID)
8372 RETURNS double precision
8373 AS 'provsql','rv_mutual_information' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8374
8375/**
8376 * @brief Mutual information I(X; Y), in nats.
8377 *
8378 * Exactly @c 0 for structurally independent variables (disjoint
8379 * stochastic-leaf footprints, the same test the moment evaluators use);
8380 * @c H(X) for a discrete variable paired with itself and @c Infinity
8381 * for a continuous one (I(X;X) diverges). A genuinely correlated pair
8382 * (shared leaves) is estimated by a 2-D histogram plug-in over coupled
8383 * joint Monte Carlo draws -- both roots evaluated against the same
8384 * per-iteration cache, so shared leaves keep their joint law -- at the
8385 * @c provsql.rv_mc_samples budget.
8386 */
8387CREATE OR REPLACE FUNCTION mutual_information(
8388 x random_variable, y random_variable)
8389 RETURNS double precision AS $$
8390 SELECT provsql.rv_mutual_information((x)::UUID, (y)::UUID);
8391$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8392
8393/**
8394 * @brief Compute the Shapley value of an input variable
8395 *
8396 * Measures the contribution of a specific input variable to the
8397 * truth of a provenance expression, using game-theoretic Shapley values.
8398 *
8399 * @param token provenance token to evaluate
8400 * @param variable UUID of the input variable
8401 * @param method knowledge compilation method
8402 * @param arguments additional arguments for the method
8403 * @param banzhaf if true, compute the Banzhaf value instead
8404 */
8405CREATE OR REPLACE FUNCTION shapley(
8406 token UUID,
8407 variable UUID,
8408 method TEXT = NULL,
8409 arguments TEXT = NULL,
8410 banzhaf BOOLEAN = 'f')
8411 RETURNS DOUBLE PRECISION AS
8412 'provsql','shapley' LANGUAGE C STABLE;
8413
8414/** @brief Compute Shapley values for all input variables at once */
8415CREATE OR REPLACE FUNCTION shapley_all_vars(
8416 IN token UUID,
8417 IN method TEXT = NULL,
8418 IN arguments TEXT = NULL,
8419 IN banzhaf BOOLEAN = 'f',
8420 OUT variable UUID,
8421 OUT value DOUBLE PRECISION)
8422 RETURNS SETOF RECORD AS
8423 'provsql', 'shapley_all_vars'
8424 LANGUAGE C STABLE;
8425
8426/** @brief Compute the Banzhaf power index of an input variable */
8427CREATE OR REPLACE FUNCTION banzhaf(
8428 token UUID,
8429 variable UUID,
8430 method TEXT = NULL,
8431 arguments TEXT = NULL)
8432 RETURNS DOUBLE PRECISION AS
8433 $$ SELECT provsql.shapley(token, variable, method, arguments, 't') $$
8434 LANGUAGE SQL;
8435
8436/** @brief Compute Banzhaf power indices for all input variables at once */
8437CREATE OR REPLACE FUNCTION banzhaf_all_vars(
8438 IN token UUID,
8439 IN method TEXT = NULL,
8440 IN arguments TEXT = NULL,
8441 OUT variable UUID,
8442 OUT value DOUBLE PRECISION)
8443 RETURNS SETOF RECORD AS
8444 $$ SELECT * FROM provsql.shapley_all_vars(token, method, arguments, 't') $$
8445 LANGUAGE SQL;
8446
8447/**
8448 * @brief Exact reachability probability over bounded-treewidth data
8449 * (columnar form)
8450 *
8451 * Computes the probability that @p target is reachable from @p source in
8452 * the probabilistic graph given by the parallel edge arrays
8453 * (two-terminal network reliability). Unlike
8454 * @c probability_evaluate(), which compiles the provenance circuit
8455 * built along the relational query plan, this compiles the query
8456 * along a tree decomposition of the *data* graph (in the spirit of the
8457 * provenance refinement of Courcelle's theorem), producing a d-DNNF
8458 * whose size is linear in the number of edges for data of bounded
8459 * treewidth. Exact, and linear-time, on cyclic data as well -- where
8460 * the recursive-query fixpoint cannot terminate structurally.
8461 *
8462 * Edges are independent events. Two array positions may share a token
8463 * only if they are mutual reverses (the natural encoding of an
8464 * undirected edge in a directed edge relation); they are then treated
8465 * as a single bidirectional edge. This is an internal/testing surface:
8466 * the user-facing route is a plain @c WITH @c RECURSIVE reachability
8467 * query under the 'absorptive' (or 'BOOLEAN') provenance class, which
8468 * the query rewriter compiles through @c eval_reachability() /
8469 * @c reachability_materialize().
8470 *
8471 * @param sources source vertex of each edge (dense INTEGER IDs)
8472 * @param destinations destination vertex of each edge
8473 * @param tokens provenance token of each edge tuple
8474 * @param probabilities probability of each edge tuple
8475 * @param source the vertex reachability starts from
8476 * @param target the vertex whose reachability is evaluated
8477 * @param directed if false, each edge can be traversed both ways
8478 */
8479CREATE OR REPLACE FUNCTION reachability_evaluate(
8480 sources INT[],
8481 destinations INT[],
8482 tokens UUID[],
8483 probabilities DOUBLE PRECISION[],
8484 source INT,
8485 target INT,
8486 directed BOOLEAN)
8487 RETURNS DOUBLE PRECISION AS
8488 'provsql','reachability_evaluate' LANGUAGE C IMMUTABLE PARALLEL SAFE;
8489
8490/**
8491 * @brief Reachability probability plus compilation statistics
8492 * (columnar form)
8493 *
8494 * Same compilation as @c reachability_evaluate(), returning the
8495 * probability together with the structural statistics that
8496 * substantiate the bounded-treewidth guarantee: the treewidth of the
8497 * min-fill decomposition of the data graph, its number of bags, the
8498 * maximum number of dynamic-programming states at any decomposition
8499 * node, and the size of the emitted d-DNNF (linear in the number of
8500 * edges for fixed data treewidth).
8501 *
8502 * @param sources source vertex of each edge (dense INTEGER IDs)
8503 * @param destinations destination vertex of each edge
8504 * @param tokens provenance token of each edge tuple
8505 * @param probabilities probability of each edge tuple
8506 * @param source the vertex reachability starts from
8507 * @param target the vertex whose reachability is evaluated
8508 * @param directed if false, each edge can be traversed both ways
8509 * @param[out] probability the reachability probability
8510 * @param[out] data_treewidth treewidth of the min-fill decomposition of the
8511 * data graph
8512 * @param[out] nb_bags number of bags in the decomposition
8513 * @param[out] max_states maximum number of dynamic-programming states at any
8514 * decomposition node
8515 * @param[out] nb_gates number of gates in the emitted d-DNNF
8516 * @param[out] nb_variables number of variables in the emitted d-DNNF
8517 */
8518CREATE OR REPLACE FUNCTION reachability_compile_stats(
8519 IN sources INT[],
8520 IN destinations INT[],
8521 IN tokens UUID[],
8522 IN probabilities DOUBLE PRECISION[],
8523 IN source INT,
8524 IN target INT,
8525 IN directed BOOLEAN,
8526 OUT probability DOUBLE PRECISION,
8527 OUT data_treewidth INT,
8528 OUT nb_bags BIGINT,
8529 OUT max_states BIGINT,
8530 OUT nb_gates BIGINT,
8531 OUT nb_variables BIGINT)
8532 AS 'provsql','reachability_compile_stats'
8533 LANGUAGE C IMMUTABLE PARALLEL SAFE;
8534
8535
8536
8537/**
8538 * @brief Boolean UCQ probability plus compilation statistics
8539 * (columnar form, internal)
8540 *
8541 * Same compilation as @c ucq_joint_compile_stats(query jsonb, ...),
8542 * returning the probability together with the three width columns that
8543 * substantiate thesis Prop. 4.2.11 empirically -- the adversarial family
8544 * has small data and circuit widths but large joint width -- and the
8545 * structural statistics.
8546 *
8547 * @param disjunct_nvars number of query variables of each disjunct
8548 * @param atom_disjunct disjunct index of each atom (parallel to @p atom_rel)
8549 * @param atom_rel relation id of each atom
8550 * @param atom_vars query-variable indices of all atom columns, concatenated
8551 * @param atom_arity number of columns of each atom (slices @p atom_vars)
8552 * @param fact_rel relation id of each fact
8553 * @param fact_elems element ids of all fact columns, concatenated
8554 * @param fact_arity number of columns of each fact (slices @p fact_elems)
8555 * @param fact_tokens provenance token of each fact
8556 * @param fact_probs probability of each fact
8557 * @param[out] probability the exact UCQ probability
8558 * @param[out] joint_treewidth width of the min-fill decomposition found
8559 * @param[out] data_treewidth_lb degeneracy lower bound of the data-only graph
8560 * @param[out] circuit_treewidth_lb degeneracy lower bound of the slice-only graph
8561 * @param[out] n_bags number of bags in the decomposition
8562 * @param[out] max_states peak number of DP states at any node
8563 * @param[out] dd_size number of gates in the emitted d-D
8564 * @param[out] n_enumerating maximum number of essential (enumerating) query
8565 * variables over the disjuncts -- the @c e of the @f$2^{O(k^e)}@f$
8566 * bound, with variables functionally determined by others (via FDs
8567 * mined from the data) removed
8568 */
8569CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
8570 IN disjunct_nvars INT[],
8571 IN atom_disjunct INT[],
8572 IN atom_rel INT[],
8573 IN atom_vars INT[],
8574 IN atom_arity INT[],
8575 IN fact_rel INT[],
8576 IN fact_elems INT[],
8577 IN fact_arity INT[],
8578 IN fact_tokens UUID[],
8579 IN fact_probs DOUBLE PRECISION[],
8580 OUT probability DOUBLE PRECISION,
8581 OUT joint_treewidth INT,
8582 OUT data_treewidth_lb INT,
8583 OUT circuit_treewidth_lb INT,
8584 OUT n_bags BIGINT,
8585 OUT max_states BIGINT,
8586 OUT dd_size BIGINT,
8587 OUT n_enumerating INT)
8588 AS 'provsql','ucq_joint_compile_stats'
8589 LANGUAGE C IMMUTABLE PARALLEL SAFE;
8590
8591
8592/**
8593 * @brief Boolean UCQ probability plus statistics from a JSON specification
8594 *
8595 * JSON-spec wrapper over the columnar @c ucq_joint_compile_stats()
8596 * (see @c ucq_joint_evaluate(query jsonb, ...) for the JSON format).
8597 */
8598CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
8599 IN query JSONB,
8600 IN fact_rel INT[],
8601 IN fact_elems INT[],
8602 IN fact_arity INT[],
8603 IN fact_tokens UUID[],
8604 IN fact_probs DOUBLE PRECISION[],
8605 OUT probability DOUBLE PRECISION,
8606 OUT joint_treewidth INT,
8607 OUT data_treewidth_lb INT,
8608 OUT circuit_treewidth_lb INT,
8609 OUT n_bags BIGINT,
8610 OUT max_states BIGINT,
8611 OUT dd_size BIGINT,
8612 OUT n_enumerating INT)
8613 AS $$
8614DECLARE
8615 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
8616 avars INT[] := '{}'; aarity INT[] := '{}';
8617 d JSONB; a JSONB; v TEXT; didx INT := 0;
8618BEGIN
8619 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
8620 dnv := dnv || (d->>'n_vars')::INT;
8621 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8622 adisj := adisj || didx;
8623 arel := arel || (a->>'rel')::INT;
8624 aarity := aarity || jsonb_array_length(a->'vars');
8625 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8626 avars := avars || v::INT;
8627 END LOOP;
8628 END LOOP;
8629 didx := didx + 1;
8630 END LOOP;
8631 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
8632 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
8633 s.n_enumerating
8634 INTO probability, joint_treewidth, data_treewidth_lb,
8635 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
8636 FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
8637 fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s;
8638END;
8639$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
8640
8641
8642
8643
8644
8645
8646
8647
8648
8649
8650
8651/**
8652 * @brief Correlated Boolean UCQ probability plus compilation statistics
8653 * (columnar form, internal)
8654 *
8655 * Same compilation as @c ucq_joint_evaluate_tracked(); the three width
8656 * columns substantiate thesis Prop. 4.2.11 on real correlated data (the
8657 * data-only and circuit-only degeneracy bounds can be small while the
8658 * joint width is large).
8659 */
8660CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
8661 IN disjunct_nvars INT[],
8662 IN atom_disjunct INT[],
8663 IN atom_rel INT[],
8664 IN atom_vars INT[],
8665 IN atom_arity INT[],
8666 IN fact_rel INT[],
8667 IN fact_elems INT[],
8668 IN fact_arity INT[],
8669 IN fact_tokens UUID[],
8670 OUT probability DOUBLE PRECISION,
8671 OUT joint_treewidth INT,
8672 OUT data_treewidth_lb INT,
8673 OUT circuit_treewidth_lb INT,
8674 OUT n_bags BIGINT,
8675 OUT max_states BIGINT,
8676 OUT dd_size BIGINT,
8677 OUT n_enumerating INT)
8678 AS 'provsql','ucq_joint_compile_stats_tracked'
8679 LANGUAGE C STABLE PARALLEL SAFE;
8680
8681
8682/**
8683 * @brief Correlated Boolean UCQ probability plus statistics from a JSON spec
8684 */
8685CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
8686 IN query JSONB,
8687 IN fact_rel INT[],
8688 IN fact_elems INT[],
8689 IN fact_arity INT[],
8690 IN fact_tokens UUID[],
8691 OUT probability DOUBLE PRECISION,
8692 OUT joint_treewidth INT,
8693 OUT data_treewidth_lb INT,
8694 OUT circuit_treewidth_lb INT,
8695 OUT n_bags BIGINT,
8696 OUT max_states BIGINT,
8697 OUT dd_size BIGINT,
8698 OUT n_enumerating INT)
8699 AS $$
8700DECLARE
8701 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
8702 avars INT[] := '{}'; aarity INT[] := '{}';
8703 d JSONB; a JSONB; v TEXT; didx INT := 0;
8704BEGIN
8705 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
8706 dnv := dnv || (d->>'n_vars')::INT;
8707 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8708 adisj := adisj || didx;
8709 arel := arel || (a->>'rel')::INT;
8710 aarity := aarity || jsonb_array_length(a->'vars');
8711 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8712 avars := avars || v::INT;
8713 END LOOP;
8714 END LOOP;
8715 didx := didx + 1;
8716 END LOOP;
8717 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
8718 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
8719 s.n_enumerating
8720 INTO probability, joint_treewidth, data_treewidth_lb,
8721 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
8722 FROM ucq_joint_compile_stats_tracked(dnv, adisj, arel, avars, aarity,
8723 fact_rel, fact_elems, fact_arity, fact_tokens) s;
8724END;
8725$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
8726
8727/**
8728 * @brief Compile a correlated UCQ and materialise its certified d-D,
8729 * returning the root provenance token (columnar form, internal)
8730 *
8731 * The architecturally-primary route: the compiler builds the
8732 * deterministic, decomposable circuit and materialises it as ordinary
8733 * @c plus / @c times / @c monus provenance gates (carrying the d-DNNF
8734 * certificate); the answer is then obtained through the standard entry
8735 * points on the returned token -- @c probability_evaluate(token),
8736 * @c shapley(token, ...), expectation -- so the joint-width path shares
8737 * the one evaluation pipeline. The token is the exact Boolean
8738 * provenance of the UCQ (no @c 'absorptive' marker).
8739 */
8740CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
8741 disjunct_nvars INT[],
8742 atom_disjunct INT[],
8743 atom_rel INT[],
8744 atom_vars INT[],
8745 atom_arity INT[],
8746 fact_rel INT[],
8747 fact_elems INT[],
8748 fact_arity INT[],
8749 fact_tokens UUID[])
8750 RETURNS UUID AS
8751 'provsql','ucq_joint_materialize_tracked' LANGUAGE C VOLATILE;
8752
8753/**
8754 * @brief Compile a correlated UCQ and materialise its certified d-D
8755 * from a JSON spec, returning the root provenance token
8756 *
8757 * JSON-spec wrapper over @c ucq_joint_materialize_tracked(). Evaluate
8758 * the answer with the standard surface, e.g.
8759 * @c probability_evaluate(ucq_joint_materialize_tracked(query, ...)).
8760 */
8761CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
8762 query JSONB,
8763 fact_rel INT[],
8764 fact_elems INT[],
8765 fact_arity INT[],
8766 fact_tokens UUID[])
8767 RETURNS UUID AS $$
8768DECLARE
8769 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
8770 avars INT[] := '{}'; aarity INT[] := '{}';
8771 d JSONB; a JSONB; v TEXT; didx INT := 0;
8772BEGIN
8773 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
8774 dnv := dnv || (d->>'n_vars')::INT;
8775 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8776 adisj := adisj || didx;
8777 arel := arel || (a->>'rel')::INT;
8778 aarity := aarity || jsonb_array_length(a->'vars');
8779 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8780 avars := avars || v::INT;
8781 END LOOP;
8782 END LOOP;
8783 didx := didx + 1;
8784 END LOOP;
8785 RETURN ucq_joint_materialize_tracked(dnv, adisj, arel, avars, aarity,
8786 fact_rel, fact_elems, fact_arity, fact_tokens);
8787END;
8788$$ LANGUAGE plpgsql VOLATILE;
8789
8790/**
8791 * @brief Compile a UCQ over named relations into a materialised certified
8792 * d-D, gathering the facts from the store -- the descriptor-driven engine
8793 *
8794 * The query-surface bridge for the joint-width compiler: instead of
8795 * hand-built columnar arrays, a JSON @p descriptor names the relations
8796 * and how their columns map to query variables, and this function
8797 * gathers the facts itself (the provenance rewriting is disabled around
8798 * the gather), builds the value-based element dictionary shared across
8799 * the relations (so equal join values get the same dense id), compiles
8800 * and materialises the certified d-D, and returns its provenance token.
8801 * The answer is then any standard evaluation on that token --
8802 * @c probability_evaluate(ucq_joint_provenance(...)),
8803 * @c shapley(...), expectation. This is also the engine the planner-time
8804 * query recogniser drives once it builds the descriptor from a query's
8805 * abstract syntax.
8806 *
8807 * Descriptor shape:
8808 * @verbatim
8809 * { "disjuncts": [ { "n_vars": k,
8810 * "atoms": [ {"rel": <relidx>, "vars": [..]}, ... ] }, ... ],
8811 * "relations": [ "schema.r", "schema.s", ... ], -- relidx -> relation
8812 * "elem_cols": [ ["x"], ["x","y"], ... ] } -- per relation: the
8813 * element columns, in
8814 * the atom's var order
8815 * @endverbatim
8816 *
8817 * @param descriptor the UCQ + the relations and their element columns
8818 * @param fallback token returned if the joint-width compiler declines
8819 * @return the materialised joint-width provenance token (NULL UUID-free
8820 * exact Boolean provenance of the UCQ)
8821 */
8822CREATE OR REPLACE FUNCTION ucq_joint_provenance(
8823 descriptor JSONB, fallback UUID DEFAULT NULL)
8824RETURNS UUID AS $$
8825DECLARE
8826 legs TEXT; sql TEXT; saved TEXT;
8827 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
8828 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
8829 avars INT[]:='{}'; aarity INT[]:='{}';
8830 d jsonb; a jsonb; v TEXT; didx INT:=0;
8831BEGIN
8832 -- Parse the UCQ structure into the columnar query arrays.
8833 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
8834 dnv := dnv || (d->>'n_vars')::INT;
8835 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8836 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
8837 aarity := aarity || jsonb_array_length(a->'vars');
8838 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8839 avars := avars || v::INT;
8840 END LOOP;
8841 END LOOP;
8842 didx := didx + 1;
8843 END LOOP;
8844
8845 -- One UNION ALL leg per relation: (relation index, TEXT element array,
8846 -- provenance token). No temp tables: a single gather query, with the
8847 -- value-based dense element dictionary built inline.
8848 SELECT string_agg(
8849 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
8850 rn - 1,
8851 (SELECT string_agg(format('(%I)::TEXT', c), ',')
8852 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
8853 rel,
8854 -- the lifted single-relation selection (a pre-filter), already
8855 -- deparsed to SQL by the recogniser; '' / absent = unfiltered.
8856 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
8857 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
8858 ELSE '' END),
8859 ' UNION ALL ')
8860 INTO legs
8861 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
8862
8863 sql := format($q$
8864 WITH facts(rel,elems,tok) AS (%s),
8865 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
8866 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
8867 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
8868 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
8869 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
8870 (SELECT array_agg(tok ORDER BY ord) FROM ord),
8871 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
8872 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
8873 JOIN dict dd ON dd.val = e.val)
8874 $q$, legs);
8875
8876 -- Read the raw rows with provenance rewriting disabled (we only read
8877 -- the existing provsql column; this internal gather is not tracked).
8878 saved := current_setting('provsql.active', true);
8879 PERFORM set_config('provsql.active','off', true);
8880 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
8881 PERFORM set_config('provsql.active', saved, true);
8882
8883 RETURN ucq_joint_materialize_tracked(dnv,adisj,arel,avars,aarity,
8884 fact_rel,fact_elems,fact_arity,fact_tokens);
8885EXCEPTION WHEN OTHERS THEN
8886 -- The joint-width compiler declined (unsupported gate type, joint
8887 -- width too large, ...): fall back to the normal provenance so the
8888 -- query never fails. Both give the same probability.
8889 RETURN fallback;
8890END;
8891$$ LANGUAGE plpgsql VOLATILE;
8892
8893-- ===========================================================================
8894-- Safe-UCQ Möbius-inversion route (mobius_evaluate.cpp).
8895--
8896-- The last missing exact route of the Dalvi-Suciu dichotomy: UCQs that are
8897-- safe only because the \#P-hard terms of their inclusion-exclusion expansion
8898-- carry a zero Möbius value on the CNF lattice and cancel (canonical witness:
8899-- QW / q9). Same TID gather as ucq_joint, then the lattice-walking compiler
8900-- materialises a gate_mobius-rooted circuit (a signed combination over
8901-- certified-independent islands), answered in PTIME data complexity by the
8902-- standard probability path.
8903-- ===========================================================================
8904
8905/**
8906 * @brief Materialise the safe-UCQ Möbius circuit and return its root token.
8907 * Columnar (TID) interface; see ucq_mobius_provenance for the gather.
8908 */
8909CREATE OR REPLACE FUNCTION ucq_mobius_materialize_tracked(
8910 disjunct_nvars INT[],
8911 atom_disjunct INT[],
8912 atom_rel INT[],
8913 atom_vars INT[],
8914 atom_arity INT[],
8915 fact_rel INT[],
8916 fact_elems INT[],
8917 fact_arity INT[],
8918 fact_tokens UUID[],
8919 lineage UUID DEFAULT NULL)
8920 RETURNS UUID AS
8921 'provsql','ucq_mobius_materialize_tracked' LANGUAGE C VOLATILE;
8922
8923/**
8924 * @brief Compile the Möbius circuit and return the lattice statistics plus the
8925 * probability (the demonstrability surface). @c cancelled_hard is the
8926 * single number that makes the mechanism legible: for q9 the 1 cancelled
8927 * element is \#P-hard, so the query is easy only because its hard part
8928 * cancels.
8929 */
8930CREATE OR REPLACE FUNCTION ucq_mobius_compile_stats(
8931 IN disjunct_nvars INT[],
8932 IN atom_disjunct INT[],
8933 IN atom_rel INT[],
8934 IN atom_vars INT[],
8935 IN atom_arity INT[],
8936 IN fact_rel INT[],
8937 IN fact_elems INT[],
8938 IN fact_arity INT[],
8939 IN fact_tokens UUID[],
8940 OUT probability DOUBLE PRECISION,
8941 OUT n_components INT,
8942 OUT n_cnf_conjuncts INT,
8943 OUT lattice_size INT,
8944 OUT n_nonzero INT,
8945 OUT n_cancelled INT,
8946 OUT cancelled_hard BOOLEAN,
8947 OUT dd_size BIGINT,
8948 OUT memo_hits BIGINT)
8949 AS 'provsql','ucq_mobius_compile_stats'
8950 LANGUAGE C VOLATILE;
8951
8952/**
8953 * @brief Pass a token through iff it is a @c gate_mobius, else return NULL.
8954 *
8955 * The Möbius-precedence dispatch (see @c make_provenance_expression) wraps the
8956 * Möbius call in this and then @c COALESCE\ s it before the joint-width call:
8957 * a Möbius *success* always roots a @c gate_mobius (the compiler wraps even a
8958 * thin selector around the lineage), so it short-circuits and the joint-width
8959 * compiler never runs; a Möbius *decline* returns the literal lineage (never a
8960 * @c gate_mobius), so this yields NULL and @c COALESCE falls through to
8961 * joint-width. The lineage token is a plain plus/times/input, so the test is
8962 * unambiguous.
8963 */
8964CREATE OR REPLACE FUNCTION mobius_or_null(tok UUID)
8965RETURNS UUID AS $$
8966 SELECT CASE WHEN tok IS NOT NULL AND provsql.get_gate_type(tok) = 'mobius'
8967 THEN tok END
8968$$ LANGUAGE sql STABLE;
8969
8970/**
8971 * @brief Möbius-route provenance from a descriptor (the planner-substituted
8972 * entry point, and the manual one). Same descriptor and TID gather as
8973 * @c ucq_joint_provenance; on any decline (unsafe shape, cap, not TID)
8974 * returns @p fallback, so a recognised query never fails.
8975 */
8976CREATE OR REPLACE FUNCTION ucq_mobius_provenance(
8977 descriptor JSONB, fallback UUID DEFAULT NULL)
8978RETURNS UUID AS $$
8979DECLARE
8980 legs TEXT; sql TEXT; saved TEXT;
8981 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
8982 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
8983 avars INT[]:='{}'; aarity INT[]:='{}';
8984 d jsonb; a jsonb; v TEXT; didx INT:=0;
8985BEGIN
8986 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
8987 dnv := dnv || (d->>'n_vars')::INT;
8988 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8989 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
8990 aarity := aarity || jsonb_array_length(a->'vars');
8991 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8992 avars := avars || v::INT;
8993 END LOOP;
8994 END LOOP;
8995 didx := didx + 1;
8996 END LOOP;
8997
8998 SELECT string_agg(
8999 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
9000 rn - 1,
9001 (SELECT string_agg(format('(%I)::TEXT', c), ',')
9002 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
9003 rel,
9004 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
9005 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
9006 ELSE '' END),
9007 ' UNION ALL ')
9008 INTO legs
9009 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
9010
9011 sql := format($q$
9012 WITH facts(rel,elems,tok) AS (%s),
9013 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
9014 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
9015 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
9016 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
9017 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
9018 (SELECT array_agg(tok ORDER BY ord) FROM ord),
9019 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
9020 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
9021 JOIN dict dd ON dd.val = e.val)
9022 $q$, legs);
9023
9024 saved := current_setting('provsql.active', true);
9025 PERFORM set_config('provsql.active','off', true);
9026 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
9027 PERFORM set_config('provsql.active', saved, true);
9028
9029 -- Pass the normal-provenance fallback as the lineage: it is carried on the
9030 -- gate_mobius so the token still answers Shapley / semiring / PROV on the
9031 -- literal lineage (the Möbius combination is a probability-only shortcut).
9032 RETURN ucq_mobius_materialize_tracked(dnv,adisj,arel,avars,aarity,
9033 fact_rel,fact_elems,fact_arity,fact_tokens, fallback);
9034EXCEPTION WHEN OTHERS THEN
9035 RETURN fallback;
9036END;
9037$$ LANGUAGE plpgsql VOLATILE;
9038
9039/**
9040 * @brief Möbius lattice statistics + probability from a descriptor: the
9041 * demonstrability SRF. Gathers
9042 * the same TID facts as @c ucq_mobius_provenance, then runs the columnar
9043 * @c ucq_mobius_compile_stats.
9044 */
9045CREATE OR REPLACE FUNCTION mobius_compile_stats(
9046 IN descriptor JSONB,
9047 OUT probability DOUBLE PRECISION,
9048 OUT n_components INT,
9049 OUT n_cnf_conjuncts INT,
9050 OUT lattice_size INT,
9051 OUT n_nonzero INT,
9052 OUT n_cancelled INT,
9053 OUT cancelled_hard BOOLEAN,
9054 OUT dd_size BIGINT,
9055 OUT memo_hits BIGINT)
9056RETURNS RECORD AS $$
9057DECLARE
9058 legs TEXT; sql TEXT; saved TEXT;
9059 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
9060 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
9061 avars INT[]:='{}'; aarity INT[]:='{}';
9062 d jsonb; a jsonb; v TEXT; didx INT:=0;
9063BEGIN
9064 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
9065 dnv := dnv || (d->>'n_vars')::INT;
9066 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9067 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
9068 aarity := aarity || jsonb_array_length(a->'vars');
9069 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9070 avars := avars || v::INT;
9071 END LOOP;
9072 END LOOP;
9073 didx := didx + 1;
9074 END LOOP;
9075
9076 SELECT string_agg(
9077 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
9078 rn - 1,
9079 (SELECT string_agg(format('(%I)::TEXT', c), ',')
9080 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
9081 rel,
9082 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
9083 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
9084 ELSE '' END),
9085 ' UNION ALL ')
9086 INTO legs
9087 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
9088
9089 sql := format($q$
9090 WITH facts(rel,elems,tok) AS (%s),
9091 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
9092 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
9093 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
9094 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
9095 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
9096 (SELECT array_agg(tok ORDER BY ord) FROM ord),
9097 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
9098 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
9099 JOIN dict dd ON dd.val = e.val)
9100 $q$, legs);
9101
9102 saved := current_setting('provsql.active', true);
9103 PERFORM set_config('provsql.active','off', true);
9104 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
9105 PERFORM set_config('provsql.active', saved, true);
9106
9107 SELECT s.probability, s.n_components, s.n_cnf_conjuncts, s.lattice_size,
9108 s.n_nonzero, s.n_cancelled, s.cancelled_hard, s.dd_size, s.memo_hits
9109 INTO probability, n_components, n_cnf_conjuncts, lattice_size,
9110 n_nonzero, n_cancelled, cancelled_hard, dd_size, memo_hits
9111 FROM ucq_mobius_compile_stats(dnv,adisj,arel,avars,aarity,
9112 fact_rel,fact_elems,fact_arity,fact_tokens) s;
9113END;
9114$$ LANGUAGE plpgsql VOLATILE;
9115
9116/**
9117 * @brief Internal gather for the per-answer joint route: parse @p descriptor
9118 * into the columnar UCQ arrays and gather every fact (relation index,
9119 * dense element ids, provenance token) with the value dictionary.
9120 *
9121 * Used only by the planner-substituted @c ucq_joint_provenance_answer (the C
9122 * single-DP entry point), which calls it ONCE per query and then computes all
9123 * answers in one sweep. No head pinning: the single DP discovers the answers.
9124 * @c val_by_id maps a dense element id back to its TEXT value (so an answer's
9125 * head ids can be matched to the @c GROUP @c BY head TEXT).
9126 */
9127CREATE OR REPLACE FUNCTION ucq_joint_gather(
9128 descriptor JSONB,
9129 OUT disjunct_nvars INT[], OUT atom_disjunct INT[], OUT atom_rel INT[],
9130 OUT atom_vars INT[], OUT atom_arity INT[],
9131 OUT fact_rel INT[], OUT fact_elems INT[], OUT fact_arity INT[],
9132 OUT fact_tokens UUID[], OUT val_by_id TEXT[])
9133AS $$
9134DECLARE
9135 legs TEXT; sql TEXT; saved TEXT; d jsonb; a jsonb; v TEXT; didx INT := 0;
9136BEGIN
9137 disjunct_nvars:='{}'; atom_disjunct:='{}'; atom_rel:='{}';
9138 atom_vars:='{}'; atom_arity:='{}';
9139 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
9140 disjunct_nvars := disjunct_nvars || (d->>'n_vars')::INT;
9141 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9142 atom_disjunct := atom_disjunct || didx;
9143 atom_rel := atom_rel || (a->>'rel')::INT;
9144 atom_arity := atom_arity || jsonb_array_length(a->'vars');
9145 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9146 atom_vars := atom_vars || v::INT;
9147 END LOOP;
9148 END LOOP;
9149 didx := didx + 1;
9150 END LOOP;
9151
9152 SELECT string_agg(
9153 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s', rn - 1,
9154 (SELECT string_agg(format('(%I)::TEXT', c), ',')
9155 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
9156 rel,
9157 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
9158 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
9159 ELSE '' END),
9160 ' UNION ALL ')
9161 INTO legs
9162 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
9163
9164 sql := format($q$
9165 WITH facts(rel,elems,tok) AS (%s),
9166 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
9167 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
9168 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
9169 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
9170 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
9171 (SELECT array_agg(tok ORDER BY ord) FROM ord),
9172 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
9173 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
9174 JOIN dict dd ON dd.val = e.val),
9175 (SELECT array_agg(val ORDER BY id) FROM dict)
9176 $q$, legs);
9177
9178 saved := current_setting('provsql.active', true);
9179 PERFORM set_config('provsql.active','off', true);
9180 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems, val_by_id;
9181 PERFORM set_config('provsql.active', saved, true);
9182END;
9183$$ LANGUAGE plpgsql VOLATILE;
9184
9185/**
9186 * @brief Per-answer joint-width provenance via the TOP-DOWN single DP
9187 * (planner-substituted, C).
9188 *
9189 * The transparent per-answer rewrite substitutes one call per output group.
9190 * On the FIRST call of a query the function gathers the facts once
9191 * (@c ucq_joint_gather), runs the single DP, and materialises EVERY answer's
9192 * certified d-D into the store, caching @c head_vals -> token in @c fn_extra;
9193 * each subsequent group call is an O(1) lookup -- so the whole GROUP BY costs
9194 * one gather + one decomposition + one sweep, not @p k of each. On any
9195 * decline (joint width too large) the @p fallback token (the normal
9196 * per-answer provenance) is returned, so the query never fails. The answer's
9197 * marginal / Shapley / expectation is then the standard evaluation on the
9198 * returned token -- one pipeline for the whole system.
9199 */
9200CREATE OR REPLACE FUNCTION ucq_joint_provenance_answer(
9201 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
9202RETURNS UUID AS 'provsql','ucq_joint_provenance_answer'
9203LANGUAGE C STABLE;
9204
9205/**
9206 * @brief Per-answer safe-UCQ Möbius provenance (planner-substituted): one
9207 * head-pinned Möbius circuit per output group. On the first call the
9208 * facts are gathered once (ucq_joint_gather) and cached; each group pins
9209 * @p head_vars to @p head_vals and compiles, caching head -> token. On
9210 * any decline returns @p fallback. STABLE: it caches per fn-call
9211 * context, so it is not re-evaluated within one scan.
9212 */
9213CREATE OR REPLACE FUNCTION ucq_mobius_provenance_answer(
9214 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
9215RETURNS UUID AS 'provsql','ucq_mobius_provenance_answer'
9216LANGUAGE C STABLE;
9217
9218
9219/**
9220 * @brief Compile and materialise the reachability provenance of every
9221 * vertex (columnar form, internal)
9222 *
9223 * All-targets variant of @c reachability_evaluate(): compiles, along a
9224 * tree decomposition of the data graph, one certified provenance
9225 * circuit per vertex reachable from some source in the all-edges-present
9226 * world, materialises the (shared, linear-size) circuits in the
9227 * provenance store -- @c plus / @c times gates carrying the d-DNNF
9228 * certificate, negated edges as @c monus(one, edge) -- and returns one
9229 * @c (vertex, token) row per such vertex. Sources form a possibly
9230 * *probabilistic source set*: each source arc is gated by the source
9231 * tuple's token, the nil UUID marking a certain (always present)
9232 * source. This is the engine behind the rewriter's
9233 * recursive-reachability route; the returned tokens are ordinary
9234 * provenance tokens usable with the whole evaluation surface, wrapped
9235 * in the 'absorptive' assumption marker (the compiled circuit is the
9236 * exact Boolean lineage but only the absorptive quotient of the
9237 * infinite recursive semiring provenance: probability and absorptive
9238 * semiring evaluations -- e.g. nonnegative min-plus -- are exact,
9239 * counting and why-provenance refuse).
9240 *
9241 * @param sources source vertex of each edge (dense INTEGER IDs)
9242 * @param destinations destination vertex of each edge
9243 * @param tokens provenance token of each edge tuple
9244 * @param probabilities probability of each edge tuple
9245 * @param block_keys per-edge BID key variable (nil UUID = independent
9246 * tuple; alternatives sharing a key are mutually exclusive, e.g.
9247 * from repair_key)
9248 * @param block_indices per-edge outcome index within its block
9249 * @param source_vertices the source vertices
9250 * @param source_tokens per-source provenance token (nil UUID = certain)
9251 * @param source_probabilities per-source probability
9252 * @param directed if false, each edge can be traversed both ways
9253 * @param[out] vertex a vertex reachable from some source
9254 * @param[out] token the materialised reachability provenance token of @c vertex
9255 */
9256CREATE OR REPLACE FUNCTION reachability_materialize(
9257 IN sources INT[],
9258 IN destinations INT[],
9259 IN tokens UUID[],
9260 IN probabilities DOUBLE PRECISION[],
9261 IN block_keys UUID[],
9262 IN block_indices INT[],
9263 IN source_vertices INT[],
9264 IN source_tokens UUID[],
9265 IN source_probabilities DOUBLE PRECISION[],
9266 IN directed BOOLEAN,
9267 OUT vertex INT,
9268 OUT token UUID)
9269 RETURNS SETOF RECORD AS
9270 'provsql','reachability_materialize' LANGUAGE C VOLATILE;
9271
9272
9273/**
9274 * @brief Bounded-hop variant of @c reachability_materialize() (internal)
9275 *
9276 * Compiles, along a tree decomposition of the data graph, one certified
9277 * provenance circuit per (vertex, walk length) pair achievable within
9278 * @p hop_bound edges -- the rows a hop-counting recursive CTE derives,
9279 * row @c (v,h) meaning "some *walk* of exactly @c h edges connects a
9280 * present source to @c v" -- and returns them as @c (vertex, hops,
9281 * token) with @p hop_seed added to the lengths (the CTE base arm's hop
9282 * constant). Also pre-creates, per vertex, the certified gate that a
9283 * hop-discarding query's deduplication will address, wired to the
9284 * compilation's native within-bound root, so the natural "within k
9285 * hops" probability evaluates through the linear certified route.
9286 *
9287 * @param sources source vertex of each edge (dense INTEGER IDs)
9288 * @param destinations destination vertex of each edge
9289 * @param tokens provenance token of each edge tuple
9290 * @param probabilities probability of each edge tuple
9291 * @param block_keys per-edge BID key variable (nil UUID = independent)
9292 * @param block_indices per-edge outcome index within its block
9293 * @param source_vertices the source vertices
9294 * @param source_tokens per-source provenance token (nil UUID = certain)
9295 * @param source_probabilities per-source probability
9296 * @param directed if false, each edge can be traversed both ways
9297 * @param hop_bound maximum walk length
9298 * @param hop_seed hop value of the base arm (added to reported lengths)
9299 * @param[out] vertex a reachable vertex
9300 * @param[out] hops the walk length at which @c vertex is reached
9301 * @param[out] token the materialised provenance token of the @c (vertex, hops) pair
9302 */
9303CREATE OR REPLACE FUNCTION reachability_materialize_hops(
9304 IN sources INT[],
9305 IN destinations INT[],
9306 IN tokens UUID[],
9307 IN probabilities DOUBLE PRECISION[],
9308 IN block_keys UUID[],
9309 IN block_indices INT[],
9310 IN source_vertices INT[],
9311 IN source_tokens UUID[],
9312 IN source_probabilities DOUBLE PRECISION[],
9313 IN directed BOOLEAN,
9314 IN hop_bound INT,
9315 IN hop_seed INT,
9316 OUT vertex INT,
9317 OUT hops INT,
9318 OUT token UUID)
9319 RETURNS SETOF RECORD AS
9320 'provsql','reachability_materialize_hops' LANGUAGE C VOLATILE;
9321
9322
9323/**
9324 * @brief Per-group "some member reachable" compilation (columnar form,
9325 * internal)
9326 *
9327 * For each distinct group in the parallel @p group_ids /
9328 * @p member_vertices arrays, compiles the certified circuit of "some
9329 * member vertex is reachable from a present source" along the data
9330 * decomposition -- the disjunction over the group's *correlated*
9331 * per-vertex reachability events, deterministic by construction
9332 * through the set-reachability state bit -- materialises it, and
9333 * returns one @c (group_id, token) row per group. Engine behind the
9334 * rewriter's cross-vertex aggregation planting.
9335 *
9336 * @param sources source vertex of each edge (dense INTEGER IDs)
9337 * @param destinations destination vertex of each edge
9338 * @param tokens provenance token of each edge tuple
9339 * @param probabilities probability of each edge tuple
9340 * @param block_keys per-edge BID key variable (nil UUID = independent)
9341 * @param block_indices per-edge outcome index within its block
9342 * @param source_vertices the source vertices
9343 * @param source_tokens per-source provenance token (nil UUID = certain)
9344 * @param source_probabilities per-source probability
9345 * @param directed if false, each edge can be traversed both ways
9346 * @param group_ids group identifier of each member row
9347 * @param member_vertices member vertex of each member row
9348 * @param[out] group_id a group whose every member is reachable
9349 * @param[out] token the materialised all-members-reachable provenance token of
9350 * @c group_id
9351 */
9352CREATE OR REPLACE FUNCTION reachability_materialize_any(
9353 IN sources INT[],
9354 IN destinations INT[],
9355 IN tokens UUID[],
9356 IN probabilities DOUBLE PRECISION[],
9357 IN block_keys UUID[],
9358 IN block_indices INT[],
9359 IN source_vertices INT[],
9360 IN source_tokens UUID[],
9361 IN source_probabilities DOUBLE PRECISION[],
9362 IN directed BOOLEAN,
9363 IN group_ids INT[],
9364 IN member_vertices INT[],
9365 OUT group_id INT,
9366 OUT token UUID)
9367 RETURNS SETOF RECORD AS
9368 'provsql','reachability_materialize_any' LANGUAGE C VOLATILE;
9369
9370/**
9371 * @brief Compile and materialise the "every member vertex reachable"
9372 * (k-terminal / coverage) circuit (columnar form, internal)
9373 *
9374 * Arguments as @c reachability_materialize_any() with a single member
9375 * set: compiles the certified circuit of "every member vertex is
9376 * reachable from a present source" -- the conjunction over the
9377 * members' *correlated* per-vertex events, deterministic by
9378 * construction through the pending rescuer-set congruence --
9379 * materialises it, and returns its token, wrapped in the
9380 * @c 'absorptive' assumption marker. Probability evaluation gives the
9381 * k-terminal reliability; nonnegative min-plus the cost of the
9382 * cheapest covering subgraph (directed Steiner cost), shared edges
9383 * paid once. A member vertex absent from the graph is unreachable:
9384 * the circuit is then constant false.
9385 *
9386 * @param sources source vertex of each edge (dense INTEGER IDs)
9387 * @param destinations destination vertex of each edge
9388 * @param tokens provenance token of each edge tuple
9389 * @param probabilities probability of each edge tuple
9390 * @param block_keys per-edge BID key variable (nil UUID = independent)
9391 * @param block_indices per-edge outcome index within its block
9392 * @param source_vertices the source vertices
9393 * @param source_tokens per-source provenance token (nil UUID = certain)
9394 * @param source_probabilities per-source probability
9395 * @param directed if false, each edge can be traversed both ways
9396 * @param member_vertices the member vertices (dense IDs)
9397 */
9398CREATE OR REPLACE FUNCTION reachability_materialize_cover(
9399 sources INT[],
9400 destinations INT[],
9401 tokens UUID[],
9402 probabilities DOUBLE PRECISION[],
9403 block_keys UUID[],
9404 block_indices INT[],
9405 source_vertices INT[],
9406 source_tokens UUID[],
9407 source_probabilities DOUBLE PRECISION[],
9408 directed BOOLEAN,
9409 member_vertices INT[])
9410 RETURNS UUID AS
9411 'provsql','reachability_materialize_cover' LANGUAGE C VOLATILE;
9412
9413/**
9414 * @brief Plant certified any-member-reachable gates for a grouped
9415 * reachability aggregation (internal)
9416 *
9417 * Called (at plan time, over SPI) by the recursive-CTE lowering when
9418 * the outer query aggregates a reachability working table by a column
9419 * of a joined, untracked member relation: @c GROUP @c BY collapses
9420 * each group's per-vertex reach tokens with @c provenance_plus, whose
9421 * disjuncts are correlated (they share edges) and would otherwise
9422 * leave the certified route. For each multi-member group this
9423 * pre-creates, at the canonical address of the group's token multiset,
9424 * a certified single-child plus over the group's native
9425 * any-member-reachable circuit (@c reachability_materialize_any), so
9426 * the natural aggregation stays on the linear evaluation route.
9427 * Best-effort: any failure leaves the generic path untouched (notice
9428 * under verbosity 10).
9429 *
9430 * @param work_name the lowered CTE's working table
9431 * @param node_attribute its vertex column
9432 * @param member_rel the joined member relation (must be untracked)
9433 * @param member_attribute the member relation's join column
9434 * @param group_attribute the member relation's grouping column
9435 * @param edge_rel the tracked edge relation (as for eval_reachability)
9436 * @param source_attribute name of the source-vertex column
9437 * @param destination_attribute name of the destination-vertex column
9438 * @param source_value the base arm's constant, as TEXT
9439 * @param directed if false, each edge can be traversed both ways
9440 * @param edge_quals optional deterministic filter over edge columns
9441 * @param source_rel source relation of a multi-source base arm
9442 * @param source_rel_attribute the source relation's vertex column
9443 * @param edge_sql deparsed edge subquery (join-defined edges)
9444 * @param member_quals optional deterministic filter over the member
9445 * relation's columns (table-qualified as @c t.column), restricting
9446 * which members participate in each group
9447 */
9448CREATE OR REPLACE FUNCTION plant_reach_any_groups(
9449 work_name TEXT,
9450 node_attribute TEXT,
9451 member_rel REGCLASS,
9452 member_attribute TEXT,
9453 group_attribute TEXT,
9454 edge_rel REGCLASS,
9455 source_attribute TEXT,
9456 destination_attribute TEXT,
9457 source_value TEXT,
9458 directed BOOLEAN,
9459 edge_quals TEXT DEFAULT NULL,
9460 source_rel REGCLASS DEFAULT NULL,
9461 source_rel_attribute TEXT DEFAULT NULL,
9462 edge_sql TEXT DEFAULT NULL,
9463 member_quals TEXT DEFAULT NULL)
9464 RETURNS VOID AS
9465$$
9466DECLARE
9467 e RECORD;
9468 grp RECORD;
9469 m RECORD;
9470 sv TEXT[];
9471 st UUID[];
9472 sp double precision[];
9473 gids INT[] := ARRAY[]::INT[];
9474 mids INT[] := ARRAY[]::INT[];
9475 vid INT;
9476 canonical UUID;
9477 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9478BEGIN
9479 BEGIN
9480 -- A tracked member relation would make the aggregated tokens
9481 -- per-row products, not the bare reach tokens: nothing to plant.
9482 IF EXISTS (SELECT 1 FROM pg_attribute
9483 WHERE attrelid = member_rel AND attname = 'provsql'
9484 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped) THEN
9485 RETURN;
9486 END IF;
9487
9488 IF source_rel IS NOT NULL THEN
9489 SELECT g.source_values, g.source_tokens, g.source_probabilities
9490 INTO sv, st, sp
9491 FROM provsql.gather_reachability_sources(source_rel,
9492 source_rel_attribute) g;
9493 IF sv IS NULL THEN
9494 sv := ARRAY[]::TEXT[];
9495 st := ARRAY[]::UUID[];
9496 sp := ARRAY[]::float8[];
9497 END IF;
9498 ELSE
9499 sv := ARRAY[source_value];
9500 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
9501 sp := ARRAY[1.0::float8];
9502 END IF;
9503
9504 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
9505 destination_attribute,
9506 sv, edge_quals, edge_sql);
9507
9508 -- The groups, replicating the user's join semantics: per group, the
9509 -- member vertices and the multiset of their reach tokens (with the
9510 -- multiplicity the join produces). Single-member groups need no
9511 -- planting (provenance_plus passes a single token through).
9512 -- Two steps: materialise the joined rows with their per-row tokens
9513 -- (tracked CTAS, then strip the automatic provsql column), and only
9514 -- then aggregate the now-plain table -- aggregating provenance()
9515 -- inside a grouped tracked query would be rewritten as a
9516 -- provenance-aware aggregation, which is not what the planting
9517 -- needs.
9518 DROP TABLE IF EXISTS provsql_reach_any_flat_tmp;
9519 EXECUTE format(
9520 'CREATE TEMP TABLE provsql_reach_any_flat_tmp AS '
9521 || 'SELECT w.%1$I::TEXT AS node_val, provsql.provenance() AS tok, '
9522 || ' t.%5$I AS grp_key '
9523 || 'FROM %2$I w JOIN %3$s t ON w.%1$I = t.%4$I'
9524 -- The member-relation filter restricts which members participate
9525 -- (deparsed table-qualified as t.column); the working table side
9526 -- carries no provenance distinction here.
9527 || coalesce(' WHERE ' || member_quals, ''),
9528 node_attribute, work_name, member_rel::TEXT, member_attribute,
9529 group_attribute);
9530 PERFORM provsql.remove_provenance('provsql_reach_any_flat_tmp');
9531 DROP TABLE IF EXISTS provsql_reach_any_groups_tmp;
9532 CREATE TEMP TABLE provsql_reach_any_groups_tmp AS
9533 SELECT (row_number() OVER ())::INT AS gid, members, toks FROM (
9534 SELECT array_agg(node_val) AS members, array_agg(tok) AS toks
9535 FROM provsql_reach_any_flat_tmp
9536 GROUP BY grp_key HAVING count(*) >= 2) g;
9537 DROP TABLE provsql_reach_any_flat_tmp;
9538
9539 FOR grp IN SELECT gid, members FROM provsql_reach_any_groups_tmp LOOP
9540 FOR m IN SELECT DISTINCT unnest(grp.members) AS val LOOP
9541 vid := array_position(e.vertices, m.val);
9542 IF vid IS NOT NULL THEN
9543 gids := gids || grp.gid;
9544 mids := mids || vid;
9545 END IF;
9546 END LOOP;
9547 END LOOP;
9548 IF cardinality(gids) = 0 THEN
9549 DROP TABLE provsql_reach_any_groups_tmp;
9550 RETURN;
9551 END IF;
9552
9553 FOR grp IN
9554 SELECT a.group_id, a.token AS any_token, t.toks
9555 FROM provsql.reachability_materialize_any(
9556 e.sources, e.destinations, e.tokens, e.probabilities,
9557 e.block_keys, e.block_indices, e.extra_ids, st, sp,
9558 directed, gids, mids) a
9559 JOIN provsql_reach_any_groups_tmp t ON t.gid = a.group_id
9560 LOOP
9561 canonical := public.uuid_generate_v5(
9562 provsql.uuid_ns_provsql(),
9563 concat('plus-canonical',
9564 (SELECT array_agg(tok ORDER BY tok)
9565 FROM unnest(grp.toks) tok)));
9566 PERFORM provsql.create_gate(canonical, 'plus', ARRAY[grp.any_token]);
9567 PERFORM provsql.set_infos(canonical, 1);
9568 END LOOP;
9569 DROP TABLE provsql_reach_any_groups_tmp;
9570 IF verbosity >= 20 THEN
9571 -- Lift the function-level client_min_messages = warning for the
9572 -- one RAISE; the function-level SET restores the caller's value.
9573 PERFORM set_config('client_min_messages', 'notice', true);
9574 RAISE NOTICE 'ProvSQL: certified any-member gates planted for the aggregation of "%" by %.%',
9575 work_name, member_rel, group_attribute;
9576 PERFORM set_config('client_min_messages', 'warning', true);
9577 END IF;
9578 EXCEPTION WHEN OTHERS THEN
9579 IF verbosity >= 10 THEN
9580 PERFORM set_config('client_min_messages', 'notice', true);
9581 RAISE NOTICE 'ProvSQL: any-member planting for "%" skipped (%)',
9582 work_name, SQLERRM;
9583 PERFORM set_config('client_min_messages', 'warning', true);
9584 END IF;
9585 END;
9586END
9587-- No SET search_path: the deparsed edge subquery must resolve against
9588-- the caller's path; ProvSQL internals are schema-qualified.
9589$$ LANGUAGE plpgsql SET client_min_messages = warning;
9590
9591/**
9592 * @brief Plant the certified all-members-reachable gate for a
9593 * reachability self-join conjunction (internal)
9594 *
9595 * Called (at plan time, over SPI) by the recursive-CTE lowering when
9596 * the outer query self-joins a reachability working table with one
9597 * constant node binding per reference -- "are these k vertices all
9598 * reachable" -- whose row provenance @c provenance_times() computes as
9599 * the product of *correlated* per-vertex reach tokens (they share
9600 * edges). This pre-creates, at the times-canonical address of that
9601 * token multiset, a certified single-child times over the native
9602 * all-members-reachable circuit (@c reachability_materialize_cover),
9603 * so the natural conjunction stays on the linear certified route --
9604 * with the joint-worlds semantics: probability evaluation gives the
9605 * k-terminal reliability, and nonnegative min-plus the cost of the
9606 * cheapest covering subgraph (directed Steiner cost), shared edges
9607 * paid once where the raw product would pay them once per factor.
9608 * Best-effort: any failure leaves the generic path untouched (notice
9609 * under verbosity 10).
9610 *
9611 * @param work_name the lowered CTE's working table
9612 * @param node_attribute its vertex column
9613 * @param edge_rel the tracked edge relation (as for eval_reachability)
9614 * @param source_attribute name of the source-vertex column
9615 * @param destination_attribute name of the destination-vertex column
9616 * @param source_value the base arm's constant, as TEXT
9617 * @param directed if false, each edge can be traversed both ways
9618 * @param node_values the constant node bindings, as TEXT (multiset:
9619 * one per self-join reference)
9620 * @param edge_quals optional deterministic filter over edge columns
9621 * @param source_rel source relation of a multi-source base arm
9622 * @param source_rel_attribute the source relation's vertex column
9623 * @param edge_sql deparsed edge subquery (join-defined edges)
9624 */
9625CREATE OR REPLACE FUNCTION plant_reach_cover(
9626 work_name TEXT,
9627 node_attribute TEXT,
9628 edge_rel REGCLASS,
9629 source_attribute TEXT,
9630 destination_attribute TEXT,
9631 source_value TEXT,
9632 directed BOOLEAN,
9633 node_values TEXT[],
9634 edge_quals TEXT DEFAULT NULL,
9635 source_rel REGCLASS DEFAULT NULL,
9636 source_rel_attribute TEXT DEFAULT NULL,
9637 edge_sql TEXT DEFAULT NULL)
9638 RETURNS VOID AS
9639$$
9640DECLARE
9641 e RECORD;
9642 sv TEXT[];
9643 st UUID[];
9644 sp double precision[];
9645 val TEXT;
9646 vid INT;
9647 vids INT[] := ARRAY[]::INT[];
9648 tok UUID;
9649 toks UUID[] := ARRAY[]::UUID[];
9650 cover_token UUID;
9651 canonical UUID;
9652 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9653BEGIN
9654 BEGIN
9655 IF source_rel IS NOT NULL THEN
9656 SELECT g.source_values, g.source_tokens, g.source_probabilities
9657 INTO sv, st, sp
9658 FROM provsql.gather_reachability_sources(source_rel,
9659 source_rel_attribute) g;
9660 IF sv IS NULL THEN
9661 sv := ARRAY[]::TEXT[];
9662 st := ARRAY[]::UUID[];
9663 sp := ARRAY[]::float8[];
9664 END IF;
9665 ELSE
9666 sv := ARRAY[source_value];
9667 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
9668 sp := ARRAY[1.0::float8];
9669 END IF;
9670
9671 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
9672 destination_attribute,
9673 sv, edge_quals, edge_sql);
9674
9675 -- The bound vertices and their per-row reach tokens, with the
9676 -- multiplicity the self-join produces. A vertex absent from the
9677 -- graph, or from the working table, means the join is empty: no
9678 -- row will exist, nothing to plant.
9679 FOREACH val IN ARRAY node_values LOOP
9680 vid := array_position(e.vertices, val);
9681 IF vid IS NULL THEN
9682 RETURN;
9683 END IF;
9684 vids := vids || vid;
9685 EXECUTE format('SELECT provsql FROM %I WHERE %I::TEXT = $1',
9686 work_name, node_attribute)
9687 INTO tok USING val;
9688 IF tok IS NULL THEN
9689 RETURN;
9690 END IF;
9691 toks := toks || tok;
9692 END LOOP;
9693
9694 cover_token := provsql.reachability_materialize_cover(
9695 e.sources, e.destinations, e.tokens, e.probabilities,
9696 e.block_keys, e.block_indices, e.extra_ids, st, sp,
9697 directed, vids);
9698
9699 SELECT public.uuid_generate_v5(
9700 provsql.uuid_ns_provsql(),
9701 concat('times-canonical', array_agg(t ORDER BY t)))
9702 FROM unnest(toks) t
9703 INTO canonical;
9704 PERFORM provsql.create_gate(canonical, 'times', ARRAY[cover_token]);
9705 PERFORM provsql.set_infos(canonical, 1);
9706 IF verbosity >= 20 THEN
9707 -- Lift the function-level client_min_messages = warning for the
9708 -- one RAISE; the function-level SET restores the caller's value.
9709 PERFORM set_config('client_min_messages', 'notice', true);
9710 RAISE NOTICE 'ProvSQL: certified all-members gate planted for the self-join of "%"',
9711 work_name;
9712 PERFORM set_config('client_min_messages', 'warning', true);
9713 END IF;
9714 EXCEPTION WHEN OTHERS THEN
9715 IF verbosity >= 10 THEN
9716 PERFORM set_config('client_min_messages', 'notice', true);
9717 RAISE NOTICE 'ProvSQL: all-members planting for "%" skipped (%)',
9718 work_name, SQLERRM;
9719 PERFORM set_config('client_min_messages', 'warning', true);
9720 END IF;
9721 END;
9722END
9723-- No SET search_path: the deparsed edge subquery must resolve against
9724-- the caller's path; ProvSQL internals are schema-qualified.
9725$$ LANGUAGE plpgsql SET client_min_messages = warning;
9726
9727/**
9728 * @brief Input leaves of a conjunction-shaped provenance token (internal)
9729 *
9730 * Descends a token's circuit through the conjunctive gate types
9731 * (@c times, and the pass-through @c project / @c eq where-provenance
9732 * wrappers) down to @c input leaves. Returns the distinct leaves, or
9733 * NULL when the circuit contains any other gate type (a disjunctive or
9734 * aggregate shape, which is not a conjunction of independent tuples).
9735 * Used by the reachability gathering to accept join-defined edges:
9736 * a derived edge whose token is a pure conjunction of base tuples.
9737 *
9738 * @param token the provenance token
9739 */
9740CREATE OR REPLACE FUNCTION token_conjunctive_leaves(token UUID)
9741 RETURNS UUID[] AS
9742$$
9743WITH RECURSIVE walk(g) AS (
9744 SELECT token
9745 UNION
9746 SELECT c FROM walk w, unnest(provsql.get_children(w.g)) AS c
9747 WHERE provsql.get_gate_type(w.g) IN ('times', 'project', 'eq', 'annotation')
9748)
9749SELECT CASE WHEN bool_and(provsql.get_gate_type(g)
9750 IN ('times', 'project', 'eq', 'annotation', 'input'))
9751 THEN array_agg(DISTINCT g)
9752 FILTER (WHERE provsql.get_gate_type(g) = 'input')
9753 ELSE NULL END
9754FROM walk;
9755$$ LANGUAGE sql STABLE;
9756
9757/**
9758 * @brief Gather the edges of a tracked relation in the columnar form
9759 * expected by reachability_evaluate (internal)
9760 *
9761 * Materializes the edge relation with its provenance tokens and
9762 * probabilities, maps arbitrary vertex values (compared as TEXT) onto
9763 * dense INTEGER IDs, and checks that every edge tuple carries a base
9764 * input token (independent tuples): reachability compilation along the
9765 * data is only correct when the edges are independent events, so views
9766 * or query results with derived provenance are rejected.
9767 *
9768 * @param rel the provenance-tracked edge relation
9769 * @param source_attribute name of the source-vertex column
9770 * @param destination_attribute name of the destination-vertex column
9771 * @param extra_vertices vertex values (as TEXT) that must be part of
9772 * the dense ID space even when they touch no edge -- the source
9773 * set in particular; their IDs come back in @c extra_ids
9774 * (aligned with the input)
9775 * @param edge_quals optional deterministic filter over the edge
9776 * relation's columns (SQL TEXT, deparsed by the rewriter from
9777 * the recursive arm's WHERE clause), restricting which edges
9778 * participate
9779 * @param rel_sql deparsed edge subquery to gather from instead of
9780 * @p rel (join-defined edges); the tokens are then conjunctions
9781 * of base tuples, validated for shape and disjoint supports
9782 *
9783 * The @c vertices output maps the dense IDs back to the original
9784 * vertex values (as TEXT, 1-indexed), for callers that need to label
9785 * per-vertex results.
9786 *
9787 * @param[out] sources source vertex (dense ID) of each gathered edge
9788 * @param[out] destinations destination vertex (dense ID) of each edge
9789 * @param[out] tokens provenance token of each edge tuple
9790 * @param[out] probabilities probability of each edge tuple
9791 * @param[out] block_keys per-edge BID key variable (nil UUID = independent)
9792 * @param[out] block_indices per-edge outcome index within its block
9793 * @param[out] extra_ids dense IDs assigned to the @p extra_vertices
9794 * @param[out] vertices dense-ID-to-original-value map (TEXT, 1-indexed)
9795 */
9796CREATE OR REPLACE FUNCTION gather_reachability_edges(
9797 IN rel REGCLASS,
9798 IN source_attribute TEXT,
9799 IN destination_attribute TEXT,
9800 IN extra_vertices TEXT[],
9801 IN edge_quals TEXT DEFAULT NULL,
9802 IN rel_sql TEXT DEFAULT NULL,
9803 OUT sources INT[],
9804 OUT destinations INT[],
9805 OUT tokens UUID[],
9806 OUT probabilities DOUBLE PRECISION[],
9807 OUT block_keys UUID[],
9808 OUT block_indices INT[],
9809 OUT extra_ids INT[],
9810 OUT vertices TEXT[])
9811AS
9812$$
9813DECLARE
9814 tkind TEXT;
9815 bkey_expr TEXT;
9816 sel_probs TEXT;
9817 sel_bkeys TEXT;
9818 sel_bidx TEXT;
9819 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9820BEGIN
9821 -- Consult the per-table characterisation registry (TID / BID / OPAQUE,
9822 -- maintained by add_provenance / repair_key and the CTAS lineage hook):
9823 -- a TID relation is certified all-independent-inputs, a BID relation
9824 -- holds input or mulinput rows with the block structure given by the
9825 -- registry's key columns. Derived (OPAQUE), unregistered, or
9826 -- subquery-defined edges take the fully dynamic per-token path.
9827 IF rel IS NOT NULL AND rel_sql IS NULL THEN
9828 tkind := (provsql.get_table_info(rel::oid)).kind;
9829 END IF;
9830 IF tkind NOT IN ('tid', 'bid') THEN
9831 tkind := NULL;
9832 END IF;
9833 IF tkind = 'bid' THEN
9834 SELECT string_agg(quote_ident(a.attname) || '::TEXT', ' || '','' || '
9835 ORDER BY k.ord)
9836 INTO bkey_expr
9837 FROM unnest((provsql.get_table_info(rel::oid)).block_key)
9838 WITH ORDINALITY AS k(attnum, ord)
9839 JOIN pg_attribute a ON a.attrelid = rel AND a.attnum = k.attnum;
9840 -- An empty registry key means the whole table is one block.
9841 bkey_expr := coalesce(bkey_expr, quote_literal(''));
9842 END IF;
9843 IF tkind IS NOT NULL AND verbosity >= 20 THEN
9844 -- The function-level client_min_messages = warning (which silences
9845 -- the CTAS / DROP TABLE chatter) would also swallow this notice;
9846 -- lift it for the one RAISE. The function-level SET restores the
9847 -- caller's value at exit regardless.
9848 PERFORM set_config('client_min_messages', 'notice', true);
9849 RAISE NOTICE 'ProvSQL: catalog characterises % as %', rel, upper(tkind);
9850 PERFORM set_config('client_min_messages', 'warning', true);
9851 END IF;
9852
9853 -- Materialize the edges with their tokens; the planner hook resolves
9854 -- provenance() over the tracked relation, and remove_provenance strips
9855 -- the automatic provsql column so the later aggregation is plain SQL.
9856 -- For a BID relation the synthetic per-block key (a v5 UUID over the
9857 -- registry key columns' values) is computed here, while the columns
9858 -- are in scope.
9859 DROP TABLE IF EXISTS provsql_reachability_edges_tmp;
9860 EXECUTE format(
9861 'CREATE TEMP TABLE provsql_reachability_edges_tmp AS '
9862 || 'SELECT %1$I::TEXT AS u, %2$I::TEXT AS v, '
9863 || 'provsql.strip_annotations(provsql.provenance()) AS token%5$s '
9864 || 'FROM %3$s WHERE %1$I IS NOT NULL AND %2$I IS NOT NULL%4$s',
9865 source_attribute, destination_attribute,
9866 CASE WHEN rel_sql IS NULL THEN rel::TEXT
9867 ELSE '(' || rel_sql || ') AS provsql_edge_subquery' END,
9868 CASE WHEN edge_quals IS NULL THEN ''
9869 ELSE ' AND (' || edge_quals || ')' END,
9870 CASE WHEN tkind = 'bid'
9871 THEN ', public.uuid_generate_v5(provsql.uuid_ns_provsql(), '
9872 || quote_literal('bidblock' || rel::TEXT || ':')
9873 || ' || ' || bkey_expr || ') AS bkey'
9874 ELSE ', NULL::UUID AS bkey' END);
9875 PERFORM provsql.remove_provenance('provsql_reachability_edges_tmp');
9876
9877 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
9878 IF tkind IS NULL THEN
9879 -- Dynamic path: validate the token shapes and, for conjunction-shaped
9880 -- (join-defined) tokens, the pairwise disjointness of their supports.
9881 IF EXISTS (SELECT 1 FROM provsql_reachability_edges_tmp
9882 WHERE provsql.get_gate_type(token) NOT IN ('input', 'mulinput', 'times',
9883 'project', 'eq')) THEN
9884 DROP TABLE provsql_reachability_edges_tmp;
9885 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input, repair_key, or conjunctive join tokens', coalesce(rel::TEXT, 'the edge query');
9886 END IF;
9887 CREATE TEMP TABLE provsql_reachability_support_tmp AS
9888 SELECT t.token, l.leaf
9889 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp
9890 WHERE provsql.get_gate_type(token) IN ('times', 'project', 'eq')) t,
9891 LATERAL unnest(provsql.token_conjunctive_leaves(t.token)) AS l(leaf);
9892 IF EXISTS (SELECT 1
9893 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp) t
9894 WHERE provsql.get_gate_type(t.token) IN ('times', 'project', 'eq')
9895 AND provsql.token_conjunctive_leaves(t.token) IS NULL) THEN
9896 DROP TABLE provsql_reachability_support_tmp;
9897 DROP TABLE provsql_reachability_edges_tmp;
9898 RAISE EXCEPTION 'reachability: a join-defined edge token is not a pure conjunction of base tuples';
9899 END IF;
9900 IF EXISTS (SELECT 1 FROM (
9901 SELECT leaf FROM provsql_reachability_support_tmp
9902 UNION ALL
9903 SELECT DISTINCT token FROM provsql_reachability_edges_tmp
9904 WHERE provsql.get_gate_type(token) = 'input'
9905 ) all_leaves
9906 GROUP BY leaf HAVING count(*) > 1) THEN
9907 DROP TABLE provsql_reachability_support_tmp;
9908 DROP TABLE provsql_reachability_edges_tmp;
9909 RAISE EXCEPTION 'reachability: join-defined edges share base tuples (their supports overlap), so they are not independent';
9910 END IF;
9911 END IF;
9912
9913 -- Per-kind classification expressions for the final aggregation: a TID
9914 -- relation needs no per-row gate introspection at all; a BID relation
9915 -- one get_gate_type per row (the input/mulinput split), block keys from
9916 -- the precomputed column-derived key and indices by numbering within
9917 -- the block; the dynamic path reads the gates.
9918 IF tkind = 'tid' THEN
9919 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
9920 sel_bkeys := $sql$'00000000-0000-0000-0000-000000000000'::UUID$sql$;
9921 sel_bidx := '0';
9922 ELSIF tkind = 'bid' THEN
9923 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
9924 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
9925 THEN e.bkey
9926 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
9927 sel_bidx := 'e.bidx';
9928 ELSE
9929 sel_probs := $sql$CASE WHEN provsql.get_gate_type(e.token) IN ('times','project','eq')
9930 THEN (SELECT CASE WHEN bool_or(coalesce(provsql.get_prob(s.leaf),1.0) = 0)
9931 THEN 0.0
9932 ELSE exp(sum(ln(coalesce(provsql.get_prob(s.leaf),1.0)))) END
9933 FROM provsql_reachability_support_tmp s
9934 WHERE s.token = e.token)
9935 ELSE coalesce(provsql.get_prob(e.token), 1.0) END$sql$;
9936 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
9937 THEN (provsql.get_children(e.token))[1]
9938 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
9939 sel_bidx := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
9940 THEN (provsql.get_infos(e.token)).info1 ELSE 0 END$sql$;
9941 END IF;
9942
9943 EXECUTE format(
9944 $sql$
9945 WITH verts AS (
9946 SELECT u AS x FROM provsql_reachability_edges_tmp
9947 UNION SELECT v FROM provsql_reachability_edges_tmp
9948 UNION SELECT unnest($1)),
9949 ids AS (
9950 SELECT x, (row_number() OVER (ORDER BY x))::INT AS id FROM verts)
9951 SELECT array_agg(iu.id), array_agg(iv.id),
9952 array_agg(e.token),
9953 array_agg(%s),
9954 array_agg(%s),
9955 array_agg(%s),
9956 (SELECT array_agg(i.id ORDER BY ev.ord)
9957 FROM unnest($1) WITH ORDINALITY AS ev(x, ord)
9958 JOIN ids i ON i.x = ev.x),
9959 (SELECT array_agg(x ORDER BY id) FROM ids)
9960 FROM (SELECT t.*,
9961 (row_number() OVER (PARTITION BY t.bkey))::INT AS bidx
9962 FROM provsql_reachability_edges_tmp t) e
9963 JOIN ids iu ON iu.x = e.u
9964 JOIN ids iv ON iv.x = e.v
9965 $sql$, sel_probs, sel_bkeys, sel_bidx)
9966 INTO sources, destinations, tokens, probabilities, block_keys,
9967 block_indices, extra_ids, vertices
9968 USING extra_vertices;
9969
9970 DROP TABLE provsql_reachability_edges_tmp;
9971 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
9972END
9973-- No SET search_path: the deparsed edge subquery (and the REGCLASS
9974-- rendering) must resolve against the caller's search_path; the ProvSQL
9975-- calls above are schema-qualified instead.
9976$$ LANGUAGE plpgsql SET client_min_messages = warning;
9977
9978
9979/**
9980 * @brief Gather a source relation's vertices, tokens and probabilities
9981 * (internal)
9982 *
9983 * For a provenance-tracked source relation, every tuple must carry a
9984 * base @c input token (a *probabilistic source set*); for an untracked
9985 * relation the sources are certain and the tokens come back as the nil
9986 * UUID. Vertex values are returned as TEXT, for the shared dense-ID
9987 * mapping of @c gather_reachability_edges().
9988 *
9989 * @param rel the source relation
9990 * @param source_attribute name of the vertex column
9991 * @param[out] source_values vertex value of each source tuple (as TEXT)
9992 * @param[out] source_tokens per-source base @c input token (nil UUID = certain)
9993 * @param[out] source_probabilities per-source probability
9994 */
9995CREATE OR REPLACE FUNCTION gather_reachability_sources(
9996 IN rel REGCLASS,
9997 IN source_attribute TEXT,
9998 OUT source_values TEXT[],
9999 OUT source_tokens UUID[],
10000 OUT source_probabilities DOUBLE PRECISION[])
10001AS
10002$$
10003DECLARE
10004 tracked BOOLEAN;
10005 tkind TEXT;
10006BEGIN
10007 SELECT EXISTS (
10008 SELECT 1 FROM pg_attribute
10009 WHERE attrelid = rel AND attname = 'provsql'
10010 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped)
10011 INTO tracked;
10012
10013 -- Registry consultation: a TID source relation is certified
10014 -- all-base-input, so the per-row gate check can be skipped; a BID one
10015 -- holds block-correlated tuples, which a probabilistic source set
10016 -- cannot model -- reject it before gathering anything.
10017 IF tracked THEN
10018 tkind := (get_table_info(rel::oid)).kind;
10019 IF tkind = 'bid' THEN
10020 RAISE EXCEPTION 'reachability: % is block-independent (repair_key); block-correlated source sets are not supported', rel;
10021 END IF;
10022 END IF;
10023
10024 DROP TABLE IF EXISTS provsql_reachability_sources_tmp;
10025 IF tracked THEN
10026 EXECUTE format(
10027 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
10028 || 'SELECT %1$I::TEXT AS x, provenance() AS token '
10029 || 'FROM %2$s WHERE %1$I IS NOT NULL',
10030 source_attribute, rel);
10031 PERFORM remove_provenance('provsql_reachability_sources_tmp');
10032 IF tkind IS DISTINCT FROM 'tid'
10033 AND EXISTS (SELECT 1 FROM provsql_reachability_sources_tmp
10034 WHERE get_gate_type(token) <> 'input') THEN
10035 DROP TABLE provsql_reachability_sources_tmp;
10036 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input tokens (independent tuples); views or query results are not supported', rel;
10037 END IF;
10038 SELECT array_agg(x), array_agg(token),
10039 array_agg(coalesce(get_prob(token), 1.0))
10040 INTO source_values, source_tokens, source_probabilities
10041 FROM provsql_reachability_sources_tmp;
10042 DROP TABLE provsql_reachability_sources_tmp;
10043 ELSE
10044 EXECUTE format(
10045 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
10046 || 'SELECT DISTINCT %1$I::TEXT AS x FROM %2$s WHERE %1$I IS NOT NULL',
10047 source_attribute, rel);
10048 SELECT array_agg(x),
10049 array_agg('00000000-0000-0000-0000-000000000000'::UUID),
10050 array_agg(1.0::float8)
10051 INTO source_values, source_tokens, source_probabilities
10052 FROM provsql_reachability_sources_tmp;
10053 DROP TABLE provsql_reachability_sources_tmp;
10054 END IF;
10055END
10056$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SET client_min_messages = warning;
10057
10058/**
10059 * @brief Fixpoint driver for the recursive reachability shape:
10060 * decomposition-aligned compilation with fallback to eval_recursive
10061 *
10062 * Called (at plan time, over SPI) by the recursive-CTE lowering when
10063 * the provenance class is 'absorptive' or 'BOOLEAN'
10064 * (@c provsql.provenance) and the CTE matches the linear
10065 * reachability shape over a tracked base edge relation. Attempts the
10066 * decomposition-aligned route -- gather the edges, compile every
10067 * reachable vertex's certified provenance circuit along a tree
10068 * decomposition of the data graph, materialise them, and fill the
10069 * working table with one tokenised row per reachable vertex. On any
10070 * failure (data treewidth above the cap, per-node state bound, edges
10071 * that are not independent base tuples...), falls back to the generic
10072 * @c eval_recursive() fixpoint, preserving its behaviour exactly.
10073 *
10074 * @param edge_rel the provenance-tracked edge relation
10075 * @param source_attribute name of the source-vertex column
10076 * @param destination_attribute name of the destination-vertex column
10077 * @param source_value the base arm's constant, as TEXT
10078 * @param directed if false, each edge can be traversed both ways
10079 * @param work_name name of the working temp table (the CTE name)
10080 * @param colnames comma-separated user column names (for the fallback)
10081 * @param coldef column definitions of the working table
10082 * @param coltype type of the CTE's single column
10083 * @param body_sql deparsed CTE body (for the fallback)
10084 * @param edge_quals optional deterministic filter over edge columns
10085 * (deparsed from the recursive arm's WHERE clause)
10086 * @param source_rel source relation of a multi-source base arm
10087 * (@c SELECT col FROM sources), NULL for the constant form;
10088 * tracked sources form a probabilistic source set, untracked
10089 * ones are certain
10090 * @param source_rel_attribute the source relation's vertex column
10091 * @param edge_sql deparsed edge subquery when the recursive arm joins a
10092 * derived (join-defined) edge relation instead of a base one;
10093 * NULL for the REGCLASS form
10094 * @param hop_bound maximum number of recursive steps for the
10095 * hop-counting CTE shape (NULL for plain reachability)
10096 * @param hop_seed the base arm's hop constant (hop-counting shape)
10097 * @param hops_position 1-based position of the hop column among the
10098 * CTE's two columns (hop-counting shape)
10099 */
10100CREATE OR REPLACE FUNCTION eval_reachability(
10101 edge_rel REGCLASS,
10102 source_attribute TEXT,
10103 destination_attribute TEXT,
10104 source_value TEXT,
10105 directed BOOLEAN,
10106 work_name TEXT,
10107 colnames TEXT,
10108 coldef TEXT,
10109 coltype TEXT,
10110 body_sql TEXT,
10111 edge_quals TEXT DEFAULT NULL,
10112 source_rel REGCLASS DEFAULT NULL,
10113 source_rel_attribute TEXT DEFAULT NULL,
10114 edge_sql TEXT DEFAULT NULL,
10115 hop_bound INT DEFAULT NULL,
10116 hop_seed INT DEFAULT NULL,
10117 hops_position INT DEFAULT NULL)
10118 RETURNS VOID AS
10119$$
10120DECLARE
10121 e RECORD;
10122 sv TEXT[];
10123 st UUID[];
10124 sp double precision[];
10125 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
10126BEGIN
10127 BEGIN
10128 IF source_rel IS NOT NULL THEN
10129 -- Multi-source: gather the source relation (probabilistic when
10130 -- tracked, certain otherwise).
10131 SELECT g.source_values, g.source_tokens, g.source_probabilities
10132 INTO sv, st, sp
10133 FROM provsql.gather_reachability_sources(source_rel,
10134 source_rel_attribute) g;
10135 IF sv IS NULL THEN
10136 sv := ARRAY[]::TEXT[];
10137 st := ARRAY[]::UUID[];
10138 sp := ARRAY[]::float8[];
10139 END IF;
10140 ELSE
10141 -- Constant base arm: one certain source.
10142 sv := ARRAY[source_value];
10143 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
10144 sp := ARRAY[1.0::float8];
10145 END IF;
10146
10147 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
10148 destination_attribute,
10149 sv, edge_quals, edge_sql);
10150 IF to_regclass(work_name) IS NOT NULL THEN
10151 EXECUTE format('DROP TABLE %I', work_name);
10152 END IF;
10153 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID)', work_name, coldef);
10154 IF hop_bound IS NULL THEN
10155 EXECUTE format(
10156 'INSERT INTO %I SELECT ($1::TEXT[])[m.vertex]::%s, m.token '
10157 || 'FROM provsql.reachability_materialize($2, $3, $4, $5, $6, $7, $8, $9, $10, $11) m',
10158 work_name, coltype)
10159 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
10160 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed;
10161 ELSE
10162 -- Hop-counting shape: one row per (vertex, walk length), the hop
10163 -- column in its CTE position.
10164 EXECUTE format(
10165 'INSERT INTO %I SELECT %s, m.token '
10166 || 'FROM provsql.reachability_materialize_hops($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) m',
10167 work_name,
10168 CASE WHEN hops_position = 1
10169 THEN format('m.hops, ($1::TEXT[])[m.vertex]::%s', coltype)
10170 ELSE format('($1::TEXT[])[m.vertex]::%s, m.hops', coltype) END)
10171 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
10172 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed,
10173 hop_bound, hop_seed;
10174 END IF;
10175 IF verbosity >= 20 THEN
10176 RAISE NOTICE 'ProvSQL: recursive CTE "%" compiled along a tree decomposition of %',
10177 work_name, coalesce(edge_rel::TEXT, 'the join-defined edge query');
10178 END IF;
10179 EXCEPTION WHEN OTHERS THEN
10180 IF verbosity >= 10 THEN
10181 RAISE NOTICE 'ProvSQL: reachability route for "%" fell back to the generic fixpoint (%)',
10182 work_name, SQLERRM;
10183 END IF;
10184 PERFORM provsql.eval_recursive(body_sql, work_name, colnames, coldef);
10185 END;
10186END
10187$$ LANGUAGE plpgsql;
10188
10189
10190
10191/** @} */
10192
10193/** @defgroup provenance_output Provenance output
10194 * Functions for visualizing and exporting provenance circuits
10195 * in various formats.
10196 * @{
10197 */
10198
10199/**
10200 * @brief Return a DOT or TEXT visualization of the provenance circuit
10201 *
10202 * @param token root provenance token
10203 * @param token2desc mapping table for gate descriptions
10204 * @param dbg debug level (0 = normal)
10205 */
10206CREATE OR REPLACE FUNCTION view_circuit(
10207 token UUID,
10208 token2desc REGCLASS,
10209 dbg INT = 0)
10210 RETURNS TEXT AS
10211 'provsql','view_circuit' LANGUAGE C;
10212
10213/**
10214 * @brief Return a DOT visualisation of the d-DNNF compiled from the
10215 * provenance circuit
10216 *
10217 * Runs the requested external knowledge compiler and renders the
10218 * resulting d-DNNF as a GraphViz digraph.
10219 *
10220 * @param token root provenance token
10221 * @param compiler external compiler or in-process meta-route to invoke;
10222 * empty (the default) picks the highest-preference available compiler
10223 */
10224CREATE OR REPLACE FUNCTION compile_to_ddnnf_dot(
10225 token UUID,
10226 compiler TEXT = '')
10227 RETURNS TEXT AS
10228 'provsql','compile_to_ddnnf_dot' LANGUAGE C;
10229
10230/**
10231 * @brief Return the compiled d-DNNF of a provenance circuit in the
10232 * c2d / d4 ".nnf" TEXT interchange format.
10233 *
10234 * Companion to compile_to_ddnnf_dot (DOT, for viewing): this is the
10235 * machine-readable form, suitable for feeding to an external d-DNNF
10236 * reasoner / verifier or saving next to tseytin_cnf (same variable
10237 * numbering). Accepts the same compiler / meta-route names.
10238 *
10239 * @param token root provenance token
10240 * @param compiler compiler or in-process meta-route to use; empty (the
10241 * default) picks the highest-preference available compiler
10242 */
10243CREATE OR REPLACE FUNCTION compile_to_ddnnf(
10244 token UUID,
10245 compiler TEXT = '')
10246 RETURNS TEXT AS
10247 'provsql','compile_to_ddnnf' LANGUAGE C;
10248
10249/**
10250 * @brief Structural statistics of the d-DNNF a compiler produces for a
10251 * provenance circuit.
10252 *
10253 * Compiles the circuit with the given compiler / meta-route (same names
10254 * as compile_to_ddnnf_dot: d4, d4v2, c2d, minic2d, dsharp, panini-*,
10255 * tree-decomposition, interpret-as-dd, default) and returns a jsonb
10256 * object: nodes, edges, and / or / not / inputs counts, smooth, depth
10257 * (longest path), treewidth (null when not computable), and compile_ms.
10258 * Lets clients compare what each compiler produces on the same circuit.
10259 *
10260 * @param token root provenance token
10261 * @param compiler compiler or in-process meta-route to use; empty (the
10262 * default) picks the highest-preference available compiler
10263 */
10264CREATE OR REPLACE FUNCTION ddnnf_stats(
10265 token UUID,
10266 compiler TEXT = '')
10267 RETURNS jsonb AS
10268 'provsql','ddnnf_stats' LANGUAGE C;
10269
10270/**
10271 * @brief Return the DIMACS CNF (Tseytin transformation) of the provenance circuit
10272 *
10273 * Returns the same encoding the extension writes to a temp file before
10274 * invoking d4 / c2d / minic2d / dsharp. With @c weighted true (the
10275 * default), per-input probability weights are appended as @c w lines.
10276 *
10277 * @param token root provenance token
10278 * @param weighted include probability weights when true
10279 * @param mapping prepend "c input <var> <UUID> <prob>" comment lines
10280 * documenting which provenance input each variable stands for
10281 */
10282CREATE OR REPLACE FUNCTION tseytin_cnf(
10283 token UUID,
10284 weighted BOOLEAN = TRUE,
10285 mapping BOOLEAN = TRUE)
10286 RETURNS TEXT AS
10287 'provsql','tseytin_cnf' LANGUAGE C;
10288
10289/**
10290 * @brief Map each DIMACS variable of tseytin_cnf back to its
10291 * provenance input.
10292 *
10293 * Returns one row per input gate: the variable index (matching
10294 * tseytin_cnf and compile_to_ddnnf's NNF), the original-circuit UUID
10295 * of that input, and its probability. Lets a satisfying assignment or
10296 * weighted model count obtained from an external tool be read against
10297 * the provenance circuit.
10298 *
10299 * @param token root provenance token
10300 */
10301CREATE OR REPLACE FUNCTION tseytin_cnf_mapping_json(token UUID)
10302 RETURNS jsonb AS
10303 'provsql','tseytin_cnf_mapping_json' LANGUAGE C;
10304
10305CREATE OR REPLACE FUNCTION tseytin_cnf_mapping(token UUID)
10306 RETURNS TABLE(variable INT, gate UUID, probability FLOAT8) AS $$
10307 SELECT variable, gate, probability
10308 FROM jsonb_to_recordset(tseytin_cnf_mapping_json(token))
10309 AS x(variable INT, gate UUID, probability FLOAT8)
10310 ORDER BY variable
10311$$ LANGUAGE SQL STABLE;
10312
10313/**
10314 * @brief Return a DOT visualisation of the tree decomposition of the
10315 * provenance circuit
10316 *
10317 * Computes the min-fill decomposition used by the in-process
10318 * knowledge compiler. The first line of the output is a comment of
10319 * the form @c "// treewidth=<n>".
10320 *
10321 * @param token root provenance token
10322 */
10323CREATE OR REPLACE FUNCTION tree_decomposition_dot(
10324 token UUID)
10325 RETURNS TEXT AS
10326 'provsql','tree_decomposition_dot' LANGUAGE C;
10327
10328/**
10329 * @brief Report whether an external tool is on the backend's resolved PATH
10330 *
10331 * Uses the same @c find_external_tool() helper that the compilers
10332 * (d4 / c2d / minic2d / dsharp / panini), model counters (ganak /
10333 * sharpsat-td / dpmc via htb+dmc / weightmc), and visualisation
10334 * wrappers (graph-easy, dot) themselves consult, so the result
10335 * reflects exactly what a subsequent @c probability_evaluate or
10336 * @c view_circuit call would see, including the
10337 * @c provsql.tool_search_path GUC prepended to @c $PATH.
10338 *
10339 * Names with a slash are treated as paths and tested directly via
10340 * @c access(X_OK); bare names are resolved through @c /bin/sh's
10341 * @c command -v under the backend's PATH.
10342 *
10343 * @param name bare executable (e.g. @c 'd4') or an absolute path
10344 * @return true iff the tool resolves to an executable file
10345 */
10346CREATE OR REPLACE FUNCTION tool_available(name TEXT)
10347 RETURNS BOOLEAN AS
10348 'provsql','tool_available' LANGUAGE C STRICT;
10349
10350/* ----------------------------------------------------------------------
10351 * External-tool registry
10352 *
10353 * A catalog of the external tools ProvSQL can invoke (the knowledge
10354 * compilers, weighted model counters, and the graph-easy DOT renderer).
10355 * The default tools and their invocations are compiled in (seeded in C), so
10356 * out-of-the-box behaviour is unchanged with no configuration.
10357 *
10358 * Administrators may add / repoint / reorder / disable tools at run time;
10359 * those changes are persisted in the @c provsql.tool_overrides table below
10360 * and overlaid on the compiled seed, so they survive across sessions and
10361 * backends (and dump/restore). An empty overrides table means exactly the
10362 * compiled defaults. The mutators are superuser-only because a tool RECORD
10363 * names an executable run as the PostgreSQL OS user (the same trust level as
10364 * provsql.tool_search_path).
10365 * ---------------------------------------------------------------------- */
10366
10367/**
10368 * @brief Persistent overrides overlaid on the compiled-in tool seed.
10369 *
10370 * Each row is the complete desired RECORD for a tool (added or modified) keyed
10371 * by logical @c name, or a tombstone (@c removed = true) hiding a seeded
10372 * default. The effective registry is the compiled seed with tombstoned names
10373 * removed and the remaining rows upserted over it. Written only by the
10374 * superuser-only register_tool / unregister_tool / set_tool_* functions;
10375 * read back into each backend's in-memory registry on demand. Marked as a
10376 * configuration table so pg_dump carries an operator's registrations.
10377 */
10378CREATE TABLE IF NOT EXISTS tool_overrides(
10379 name TEXT PRIMARY KEY,
10380 removed BOOLEAN NOT NULL DEFAULT false,
10381 kind TEXT,
10382 executable TEXT,
10383 operations TEXT[],
10384 input_formats TEXT[],
10385 output_format TEXT,
10386 parser TEXT,
10387 preference INT,
10388 enabled BOOLEAN,
10389 dependencies TEXT[],
10390 argtpl TEXT,
10391 argtpl_circuit TEXT,
10392 endpoint TEXT
10393);
10394SELECT pg_catalog.pg_extension_config_dump('tool_overrides', '');
10395
10396/**
10397 * @brief Set-returning listing backing the @c provsql.tools view.
10398 *
10399 * @c operations / @c input_formats / @c output_format use the KCMCP
10400 * shared-registry names (see the KCMCP server protocol), so a CLI RECORD and
10401 * a future kcmcp-server RECORD are comparable; @c parser is the CLI-only tag
10402 * for how to decode the tool's raw output. @c argtpl is the command template
10403 * ({in}/{out}/... placeholders). @c available is true iff @c executable
10404 * (when set) and every dependency currently resolve on the backend's PATH.
10405 */
10406CREATE OR REPLACE FUNCTION tool_registry_list()
10407 RETURNS TABLE(name TEXT, kind TEXT, executable TEXT, operations TEXT[],
10408 input_formats TEXT[], output_format TEXT, parser TEXT,
10409 preference INT, enabled BOOLEAN, argtpl TEXT,
10410 argtpl_circuit TEXT, endpoint TEXT, available BOOLEAN) AS
10411 'provsql','tool_registry_list' LANGUAGE C STABLE;
10412
10413/**
10414 * @brief Read-only view of the registered tools.
10415 */
10416CREATE OR REPLACE VIEW tools AS
10417 SELECT name, kind, executable, operations, input_formats, output_format,
10418 parser, preference, enabled, argtpl, argtpl_circuit, endpoint,
10419 available
10420 FROM tool_registry_list();
10421
10422/**
10423 * @brief Register a tool, or replace the RECORD with the same logical name.
10424 *
10425 * @param name logical id (e.g. @c 'd4-jm62300'); also the value
10426 * @c provsql.fallback_compiler / the wmc tool selector use
10427 * @param executable executable to resolve on PATH (defaults to @c name)
10428 * @param kind @c 'cli' (spawn @c executable) or @c 'kcmcp' (talk to
10429 * the KCMCP server at @c endpoint)
10430 * @param operations capabilities (KCMCP names): @c 'compile' / @c 'wmc'
10431 * (and ProvSQL-local @c 'render')
10432 * @param input_formats accepted inputs (KCMCP names): @c 'dimacs-cnf',
10433 * @c 'circuit-bcs12' (listing @c 'circuit-bcs12' enables
10434 * the native-circuit fast path)
10435 * @param output_format result encoding (KCMCP names): @c 'ddnnf-nnf',
10436 * @c 'decimal', @c 'rational', ... (local @c 'panini-dd'
10437 * / @c 'ascii' where KCMCP has no code)
10438 * @param parser CLI-only decode tag: @c 'nnf' (the tolerant d4 / c2d
10439 * NNF reader), @c 'panini-dd', @c 'wmc-line',
10440 * @c 'weightmc', @c 'ascii'
10441 * @param argtpl command template; placeholders @c {in} / @c {out}
10442 * (and @c {binary} / @c {tmpdir} / @c {pivotAC}). When
10443 * it omits @c {binary}, the executable is prepended.
10444 * @param argtpl_circuit command used when the @c 'circuit-bcs12' input is
10445 * selected (a BC-S1.2 circuit rather than a CNF); only a
10446 * tool accepting that input needs it
10447 * @param preference ordering within an operation (higher first)
10448 * @param enabled whether the dispatchers may select it
10449 * @param endpoint for a @c 'kcmcp' RECORD, the server address:
10450 * @c 'unix:/path' or @c 'host:port'
10451 *
10452 * Superuser-only: a CLI RECORD runs an arbitrary command as the PostgreSQL
10453 * OS user, and a kcmcp RECORD names a socket the server connects to.
10454 */
10455CREATE OR REPLACE FUNCTION register_tool(
10456 name TEXT,
10457 executable TEXT DEFAULT NULL,
10458 kind TEXT DEFAULT 'cli',
10459 operations TEXT[] DEFAULT NULL,
10460 input_formats TEXT[] DEFAULT NULL,
10461 output_format TEXT DEFAULT NULL,
10462 parser TEXT DEFAULT NULL,
10463 argtpl TEXT DEFAULT NULL,
10464 argtpl_circuit TEXT DEFAULT NULL,
10465 preference INT DEFAULT 0,
10466 enabled BOOLEAN DEFAULT true,
10467 endpoint TEXT DEFAULT NULL)
10468 RETURNS VOID AS
10469 'provsql','tool_registry_register' LANGUAGE C;
10470
10471/** @brief Unregister a tool; errors on an unknown tool name. Superuser-only. */
10472CREATE OR REPLACE FUNCTION unregister_tool(name TEXT)
10473 RETURNS VOID AS
10474 'provsql','tool_registry_unregister' LANGUAGE C STRICT;
10475
10476/** @brief Enable/disable a tool; errors on an unknown tool name. Superuser-only. */
10477CREATE OR REPLACE FUNCTION set_tool_enabled(name TEXT, enabled BOOLEAN)
10478 RETURNS VOID AS
10479 'provsql','tool_registry_set_enabled' LANGUAGE C STRICT;
10480
10481/** @brief Set a tool's preference; errors on an unknown tool name. Superuser-only. */
10482CREATE OR REPLACE FUNCTION set_tool_preference(name TEXT, preference INT)
10483 RETURNS VOID AS
10484 'provsql','tool_registry_set_preference' LANGUAGE C STRICT;
10485
10486-- The mutators guard at the C level too, but revoke from PUBLIC so the
10487-- superuser requirement is visible in the catalog.
10488REVOKE ALL ON FUNCTION register_tool(TEXT, TEXT, TEXT, TEXT[], TEXT[], TEXT, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT) FROM PUBLIC;
10489REVOKE ALL ON FUNCTION unregister_tool(TEXT) FROM PUBLIC;
10490REVOKE ALL ON FUNCTION set_tool_enabled(TEXT, BOOLEAN) FROM PUBLIC;
10491REVOKE ALL ON FUNCTION set_tool_preference(TEXT, INT) FROM PUBLIC;
10492
10493/**
10494 * @brief Return an XML representation of the provenance circuit
10495 *
10496 * @param token root provenance token
10497 * @param token2desc optional mapping table for gate descriptions
10498 */
10499CREATE OR REPLACE FUNCTION to_provxml(
10500 token UUID,
10501 token2desc REGCLASS = NULL)
10502 RETURNS TEXT AS
10503 'provsql','to_provxml' LANGUAGE C;
10504
10505/** @brief Return the provenance token of the current query result tuple */
10506CREATE OR REPLACE FUNCTION provenance() RETURNS UUID AS
10507 'provsql', 'provenance' LANGUAGE C;
10508
10509/**
10510 * @brief Compute where-provenance for a result tuple
10511 *
10512 * Returns a TEXT representation showing which input columns
10513 * contributed to each output column.
10514 */
10515CREATE OR REPLACE FUNCTION where_provenance(token UUID)
10516 RETURNS TEXT AS
10517 'provsql','where_provenance' LANGUAGE C;
10518
10519/** @} */
10520
10521/** @defgroup circuit_init Circuit initialization
10522 * Functions and statements executed at extension load time to
10523 * reset internal caches and create the constant zero/one gates.
10524 * @{
10525 */
10526
10527/** @brief Reset the internal cache of OID constants used by the query rewriter */
10528CREATE OR REPLACE FUNCTION reset_constants_cache()
10529 RETURNS VOID AS
10530 'provsql', 'reset_constants_cache' LANGUAGE C;
10531
10532SELECT reset_constants_cache();
10533
10534SELECT create_gate(gate_zero(), 'zero');
10535SELECT create_gate(gate_one(), 'one');
10536
10537/** @} */
10538
10539/** @brief Types of update operations tracked for temporal provenance */
10540CREATE TYPE QUERY_TYPE_ENUM AS ENUM (
10541 'INSERT', -- Row was inserted
10542 'DELETE', -- Row was deleted
10543 'UPDATE', -- Row was updated
10544 'UNDO', -- Previous operation was undone
10545 'TRANSACTION', -- The transaction the statements below belong to
10546 'REPLACE' -- An update gate given a different probability
10547 );
10548
10549/** @defgroup compiled_semirings Compiled semirings
10550 * Definitions of compiled semirings
10551 * @{
10552 */
10553
10554/** @brief Evaluate provenance as a symbolic formula (e.g., "a ⊗ b ⊕ c") */
10555-- The mapping is optional (as for sr_boolexpr): formula renders whatever
10556-- circuit it is given, and the measure-carrier circuits it is most useful
10557-- on (random variables, arithmetic, mixtures) have no leaf mapping at all.
10558-- Without one, input leaves render as the semiring's 𝟙.
10559CREATE FUNCTION sr_formula(token ANYELEMENT, token2value REGCLASS = NULL)
10560 RETURNS VARCHAR AS
10561$$
10562BEGIN
10563 IF token IS NULL THEN
10564 RETURN NULL;
10565 END IF;
10566 RETURN provsql.provenance_evaluate_compiled(
10567 token,
10568 token2value,
10569 'formula',
10570 '𝟙'::VARCHAR
10571 );
10572END
10573$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
10574
10575/** @brief Evaluate provenance over the counting semiring (ℕ) */
10576CREATE FUNCTION sr_counting(token ANYELEMENT, token2value REGCLASS)
10577 RETURNS INT AS
10578$$
10579BEGIN
10580 RETURN provsql.provenance_evaluate_compiled(
10581 token,
10582 token2value,
10583 'counting',
10584 1
10585 );
10586END
10587$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10588
10589/** @brief Evaluate provenance as why-provenance (set of witness sets) */
10590CREATE FUNCTION sr_why(token ANYELEMENT, token2value REGCLASS)
10591 RETURNS VARCHAR AS
10592$$
10593BEGIN
10594 RETURN provsql.provenance_evaluate_compiled(
10595 token,
10596 token2value,
10597 'why',
10598 '{}'::VARCHAR
10599 );
10600END
10601$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10602
10603/** @brief Evaluate provenance as how-provenance (canonical polynomial provenance ℕ[X], universal commutative-semiring provenance) */
10604CREATE FUNCTION sr_how(token ANYELEMENT, token2value REGCLASS)
10605 RETURNS VARCHAR AS
10606$$
10607BEGIN
10608 RETURN provsql.provenance_evaluate_compiled(
10609 token,
10610 token2value,
10611 'how',
10612 '{}'::VARCHAR
10613 );
10614END
10615$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10616
10617/** @brief Evaluate provenance as which-provenance (lineage: a single set of contributing labels) */
10618CREATE FUNCTION sr_which(token ANYELEMENT, token2value REGCLASS)
10619 RETURNS VARCHAR AS
10620$$
10621BEGIN
10622 RETURN provsql.provenance_evaluate_compiled(
10623 token,
10624 token2value,
10625 'which',
10626 '{}'::VARCHAR
10627 );
10628END
10629$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10630
10631/** @brief Evaluate provenance as a Boolean expression
10632 *
10633 * The optional @p token2value mapping labels the leaves of the
10634 * formula: when omitted, leaves are rendered as bare @c x@<id@>
10635 * placeholders.
10636 */
10637CREATE FUNCTION sr_boolexpr(token ANYELEMENT, token2value REGCLASS = NULL)
10638 RETURNS VARCHAR AS
10639$$
10640BEGIN
10641 IF token IS NULL THEN
10642 RETURN NULL;
10643 END IF;
10644 RETURN provsql.provenance_evaluate_compiled(
10645 token,
10646 token2value,
10647 'boolexpr',
10648 '⊤'::VARCHAR
10649 );
10650END
10651$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
10652
10653/** @brief Evaluate provenance over the Boolean semiring (true/false) */
10654CREATE FUNCTION sr_boolean(token ANYELEMENT, token2value REGCLASS)
10655 RETURNS BOOLEAN AS
10656$$
10657BEGIN
10658 RETURN provsql.provenance_evaluate_compiled(
10659 token,
10660 token2value,
10661 'BOOLEAN',
10662 TRUE
10663 );
10664END
10665$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10666
10667/** @brief Structural universal-zero test (C backend of nonzero's default mode) */
10668CREATE FUNCTION true_nonzero(token UUID)
10669 RETURNS BOOLEAN AS
10670 'provsql', 'true_nonzero' LANGUAGE C PARALLEL SAFE STABLE;
10671
10672/**
10673 * @brief Test whether a provenance annotation is nonzero.
10674 *
10675 * Returns false only on a *proof* that the annotation is zero; true
10676 * otherwise, so filtering with <tt>WHERE nonzero(provenance())</tt> never
10677 * discards a row whose annotation could be nonzero.
10678 *
10679 * The default mode (@p semiring NULL) tests *universal* zero-ness: zero in
10680 * every (m-)semiring under every leaf valuation, decided by sound
10681 * structural rules (zero propagation through the gates; a comparison gate
10682 * whose satisfying-world set is empty). Filtering on it can never
10683 * contradict any downstream semiring evaluation.
10684 *
10685 * A named @p semiring evaluates the circuit there and tests against that
10686 * semiring's zero: 'BOOLEAN' is presence in the vanilla SQL answer on this
10687 * instance (the mode that filters, e.g., the null-padded arm of a
10688 * difference), 'counting' is bag multiplicity. An absent @p mapping reads
10689 * every leaf as the semiring's one (true / 1); with a mapping, leaves take
10690 * their mapped values.
10691 *
10692 * A NULL @p token reads as the neutral 1 (an untracked row): true.
10693 *
10694 * @param token provenance token to test
10695 * @param semiring NULL (universal zero test), 'BOOLEAN', or 'counting'
10696 * @param mapping optional mapping table from tokens to leaf values
10697 */
10698CREATE FUNCTION nonzero(token UUID,
10699 semiring TEXT DEFAULT NULL,
10700 mapping REGCLASS DEFAULT NULL)
10701 RETURNS BOOLEAN AS
10702$$
10703BEGIN
10704 IF token IS NULL THEN
10705 RETURN true;
10706 END IF;
10707 IF semiring IS NULL THEN
10708 RETURN provsql.true_nonzero(token);
10709 ELSIF semiring = 'BOOLEAN' THEN
10710 RETURN provsql.provenance_evaluate_compiled(token, mapping, 'BOOLEAN', TRUE);
10711 ELSIF semiring = 'counting' THEN
10712 RETURN provsql.provenance_evaluate_compiled(token, mapping, 'counting', 1) <> 0;
10713 ELSE
10714 RAISE EXCEPTION 'nonzero: unsupported semiring "%" (supported: BOOLEAN, counting; NULL for the universal zero test)', semiring;
10715 END IF;
10716END
10717$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
10718
10719/**
10720 * @brief Presence in the vanilla SQL answer on this instance.
10721 *
10722 * Shorthand for <tt>nonzero(token, 'BOOLEAN')</tt> with every leaf true:
10723 * <tt>WHERE present(provenance())</tt> restores the result set the query
10724 * has without provenance tracking, filtering the zero-annotated extras
10725 * (antijoin arms, failed HAVING groups, unknown comparisons) that the
10726 * rewriting keeps visible.
10727 */
10728CREATE FUNCTION present(token UUID)
10729 RETURNS BOOLEAN AS
10730$$
10731 SELECT provsql.nonzero(token, 'BOOLEAN');
10732$$ LANGUAGE sql PARALLEL SAFE STABLE;
10733
10734/** @brief Evaluate provenance over the tropical (min-plus) m-semiring
10735 *
10736 * Inputs are read as %float8 cost values; the additive identity
10737 * is <tt>'Infinity'::%float8</tt> and the multiplicative identity is 0.
10738 * Returns the cost of the cheapest derivation.
10739 *
10740 * With @p nonnegative, input costs are checked nonnegative and the
10741 * semiring is *absorptive*: evaluation then also accepts circuits
10742 * carrying the @c 'absorptive' assumption marker -- notably cyclic
10743 * recursive queries truncated at the absorptive value fixpoint, giving
10744 * exact min-cost reachability on cyclic data.
10745 */
10746CREATE FUNCTION sr_tropical(token ANYELEMENT, token2value REGCLASS,
10747 nonnegative BOOLEAN = false)
10748 RETURNS FLOAT AS
10749$$
10750BEGIN
10751 RETURN provsql.provenance_evaluate_compiled(
10752 token,
10753 token2value,
10754 CASE WHEN nonnegative THEN 'tropical_nonneg' ELSE 'tropical' END,
10755 0::FLOAT
10756 );
10757END
10758$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10759
10760/** @brief Evaluate provenance over the Viterbi (max-times) m-semiring
10761 *
10762 * Inputs are read as %float8 probability values in @f$[0,1]@f$.
10763 * Returns the probability of the most likely derivation.
10764 */
10765CREATE FUNCTION sr_viterbi(token ANYELEMENT, token2value REGCLASS)
10766 RETURNS FLOAT AS
10767$$
10768BEGIN
10769 RETURN provsql.provenance_evaluate_compiled(
10770 token,
10771 token2value,
10772 'viterbi',
10773 1::FLOAT
10774 );
10775END
10776$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10777
10778/** @brief Evaluate provenance over the Łukasiewicz fuzzy m-semiring
10779 *
10780 * Inputs are read as %float8 graded-truth values in @f$[0,1]@f$.
10781 * Addition is @f$\max@f$; multiplication is the Łukasiewicz t-norm
10782 * @f$\max(a + b - 1, 0)@f$, which preserves crisp truth and avoids
10783 * the near-zero collapse of long product chains.
10784 */
10785CREATE FUNCTION sr_lukasiewicz(token ANYELEMENT, token2value REGCLASS)
10786 RETURNS FLOAT AS
10787$$
10788BEGIN
10789 RETURN provsql.provenance_evaluate_compiled(
10790 token,
10791 token2value,
10792 'lukasiewicz',
10793 1::FLOAT
10794 );
10795END
10796$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10797
10798/** @brief Evaluate provenance over the min-max m-semiring on a user ENUM
10799 *
10800 * Inputs are read as values of a user-defined ENUM carrier; addition
10801 * is ENUM-min, multiplication is ENUM-max. Bottom and top of the ENUM
10802 * are derived from @c pg_enum.enumsortorder. The third argument is a
10803 * sample value of the carrier ENUM, used only for type inference; its
10804 * value is ignored.
10805 *
10806 * The security shape: alternative derivations combine to the least
10807 * sensitive label, joins combine to the most sensitive label.
10808 *
10809 * @param token Provenance token to evaluate.
10810 * @param token2value Mapping from input gates to ENUM values.
10811 * @param element_one Sample value of the carrier ENUM (any value works).
10812 */
10813CREATE FUNCTION sr_minmax(token UUID, token2value REGCLASS, element_one ANYENUM)
10814 RETURNS ANYENUM AS
10815$$
10816BEGIN
10817 RETURN provsql.provenance_evaluate_compiled(
10818 token,
10819 token2value,
10820 'minmax',
10821 element_one
10822 );
10823END
10824$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10825
10826/** @brief Evaluate provenance over the max-min m-semiring on a user ENUM
10827 *
10828 * Dual of :sqlfunc:`sr_minmax`: addition is ENUM-max, multiplication
10829 * is ENUM-min. The fuzzy / availability / trust shape: alternatives
10830 * combine to the most permissive label, joins combine to the strictest
10831 * label. The third argument is a sample value of the carrier ENUM,
10832 * used only for type inference; its value is ignored.
10833 *
10834 * @param token Provenance token to evaluate.
10835 * @param token2value Mapping from input gates to ENUM values.
10836 * @param element_one Sample value of the carrier ENUM (any value works).
10837 */
10838CREATE FUNCTION sr_maxmin(token UUID, token2value REGCLASS, element_one ANYENUM)
10839 RETURNS ANYENUM AS
10840$$
10841BEGIN
10842 RETURN provsql.provenance_evaluate_compiled(
10843 token,
10844 token2value,
10845 'maxmin',
10846 element_one
10847 );
10848END
10849$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10850
10851/** @} */
10852
10853/** @defgroup choose_aggregate choose aggregate
10854 * Choose one value among many, used in particular to code a mutually
10855 * exclusive choice as an aggregate.
10856 * @{
10857 */
10858
10859/** @brief Transition function for the choose aggregate (keeps first non-NULL value) */
10860CREATE FUNCTION choose_function(state ANYELEMENT, data ANYELEMENT)
10861 RETURNS ANYELEMENT AS
10862$$
10863BEGIN
10864 IF state IS NULL THEN
10865 RETURN data;
10866 ELSE
10867 RETURN state;
10868 END IF;
10869END
10870$$ LANGUAGE plpgsql PARALLEL SAFE IMMUTABLE;
10871
10872/** @brief Aggregate that returns an arbitrary non-NULL value from a group */
10873CREATE AGGREGATE choose(ANYELEMENT) (
10874 SFUNC = choose_function,
10875 STYPE = ANYELEMENT
10876);
10877
10878/** @brief Explodes a table column containing aggregated provenance into multiple rows.
10879 *
10880 * For each row in the input table, this function unnests the children of the
10881 * specified aggregate token column and produces one output row per child.
10882 * It reconstructs the corresponding value and provenance (`provsql`) for
10883 * each resulting row.
10884 *
10885 * The original table is replaced by the transformed table.
10886 *
10887 * @param _tbl Name of the table to transform.
10888 * @param AGG_TOKEN Name of the column containing the aggregate to explode.
10889 */
10890CREATE OR REPLACE FUNCTION explode_table(_tbl TEXT, AGG_TOKEN TEXT)
10891RETURNS VOID AS $$
10892DECLARE
10893 _nsp TEXT;
10894BEGIN
10895 -- Resolve the schema actually holding _tbl so the rebuilt table is
10896 -- recreated in place (the provsql helper functions are schema-qualified
10897 -- so this works whatever the caller's search_path is).
10898 SELECT n.nspname INTO _nsp
10899 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
10900 WHERE c.oid = _tbl::REGCLASS;
10901
10902 EXECUTE format('
10903 CREATE TABLE %1$I.temp_exploded AS
10904 SELECT
10905 %2$I.*,
10906 provsql.get_extra(children[2]) AS new_t,
10907 provsql.provenance_times(children[1], provsql) AS new_provsql
10908 FROM %1$I.%2$I,
10909 LATERAL (
10910 SELECT provsql.get_children(sm) AS children
10911 FROM UNNEST(provsql.get_children(%3$I)) AS sm
10912 ) AS sub', _nsp, _tbl, AGG_TOKEN);
10913 EXECUTE format('DROP TABLE %I.%I', _nsp, _tbl);
10914 EXECUTE format('ALTER TABLE %I.temp_exploded DROP COLUMN %I, DROP COLUMN provsql', _nsp, AGG_TOKEN);
10915 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_t TO %I', _nsp, AGG_TOKEN);
10916 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_provsql TO provsql', _nsp);
10917 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME TO %I', _nsp, _tbl);
10918END;
10919$$ LANGUAGE plpgsql;
10920
10921/** @} */
10922
10923/**
10924 * @brief Append @c provsql to this database's default search_path, if missing.
10925 *
10926 * ProvSQL's operators and functions live in the @c provsql schema and
10927 * are resolved through @c search_path. When @c provsql is absent from
10928 * the path some surfaces fail with a clear error (RV/AGG_TOKEN
10929 * arithmetic), but others can be silently misrouted by an implicit
10930 * cross-domain cast. This helper makes the common case painless: it
10931 * reads the current <em>database-level</em> search_path setting from
10932 * @c pg_db_role_setting, appends @c provsql if not already present
10933 * (never replacing or reordering the existing entries), and applies the
10934 * result with @c ALTER @c DATABASE. It is idempotent and emits a
10935 * @c NOTICE describing what it did.
10936 *
10937 * Only @b new sessions pick up the change; the calling session keeps its
10938 * current path. Role-level settings (if any) take precedence over the
10939 * database-level setting and are left untouched. The caller must be the
10940 * database owner or a superuser (the privilege model of @c ALTER
10941 * @c DATABASE). Returns the resulting search_path value.
10942 */
10943CREATE OR REPLACE FUNCTION setup_search_path()
10944 RETURNS TEXT
10945 LANGUAGE plpgsql AS $$
10946DECLARE
10947 db TEXT := current_database();
10948 cfg TEXT[];
10949 cur TEXT; -- existing database-level search_path value
10950 new_path TEXT;
10951BEGIN
10952 -- setrole = 0 selects the database-wide default, not a per-role override.
10953 SELECT s.setconfig INTO cfg
10954 FROM pg_db_role_setting s
10955 JOIN pg_database d ON d.oid = s.setdatabase
10956 WHERE d.datname = db AND s.setrole = 0;
10957
10958 IF cfg IS NOT NULL THEN
10959 SELECT substr(e, length('search_path=') + 1) INTO cur
10960 FROM unnest(cfg) AS e
10961 WHERE e LIKE 'search_path=%';
10962 END IF;
10963
10964 IF cur IS NULL THEN
10965 -- No database-level search_path at all: install the documented
10966 -- default with provsql appended.
10967 new_path := '"$user", public, provsql';
10968 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
10969 RAISE NOTICE 'ProvSQL: set search_path = % for database "%" (no previous database-level setting). Only new sessions are affected.',
10970 new_path, db;
10971 RETURN new_path;
10972 END IF;
10973
10974 -- Already contains provsql as a path element? Idempotent no-op.
10975 IF EXISTS (
10976 SELECT 1 FROM unnest(string_to[](cur, ',')) AS p
10977 WHERE btrim(btrim(p), '"') = 'provsql')
10978 THEN
10979 RAISE NOTICE 'ProvSQL: search_path for database "%" already contains provsql (= %); no change.',
10980 db, cur;
10981 RETURN cur;
10982 END IF;
10983
10984 new_path := cur || ', provsql';
10985 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
10986 RAISE NOTICE 'ProvSQL: appended provsql to search_path for database "%" (now: %). Only new sessions are affected.',
10987 db, new_path;
10988 RETURN new_path;
10989END;
10990$$;
10991
10992GRANT USAGE ON SCHEMA provsql TO PUBLIC;
10993
10994SET search_path TO public;
10995
10996-- Installation-time advisory: if provsql is not in the database's default
10997-- search_path, point the user at setup_search_path(). reset_val reflects
10998-- the configured session default (postgresql.conf / ALTER DATABASE / ALTER
10999-- ROLE), unaffected by the SET search_path statements this script ran.
11000-- CREATE EXTENSION raises client_min_messages to WARNING for the duration
11001-- of the script, so we lower it around the RAISE NOTICE. SET LOCAL only:
11002-- it unwinds by itself when CREATE EXTENSION's transaction ends. An
11003-- explicit save/restore here would capture the WARNING clamp (already in
11004-- force when this block runs) and restore *that* at session level,
11005-- leaving the whole installing session with NOTICEs suppressed.
11006DO $$
11007DECLARE
11008 rp TEXT;
11009 has_provsql BOOLEAN;
11010BEGIN
11011 SELECT reset_val INTO rp FROM pg_settings WHERE name = 'search_path';
11012 SELECT bool_or(btrim(btrim(p), '"') = 'provsql')
11013 INTO has_provsql
11014 FROM unnest(string_to[](coalesce(rp, ''), ',')) AS p;
11015 IF NOT coalesce(has_provsql, false) THEN
11016 SET LOCAL client_min_messages = notice;
11017 RAISE NOTICE 'ProvSQL: schema "provsql" is not in your default search_path (currently: %).', rp;
11018 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());
11019 END IF;
11020END;
11021$$;
11022
11023-- Final constants-cache refresh. The planned SELECT statements earlier in
11024-- this script (reset_constants_cache itself, the zero/one create_gate calls)
11025-- make the installing session memoize the OID constants *mid-script*, while
11026-- objects defined later (notably the choose aggregate, used by the
11027-- scalar-subquery decorrelation) do not exist yet. Their optional lookups
11028-- then stay InvalidOid for the rest of the session, silently disabling the
11029-- corresponding rewrites (e.g. IN/NOT IN over a tracked relation would raise
11030-- "Subqueries ... not supported") until a new connection. Refreshing here,
11031-- after every object exists, repairs the installing session's cache.
11032SELECT provsql.reset_constants_cache();
11033SET search_path TO provsql;
11034
11035/** @defgroup update_provenance Update provenance (PostgreSQL 14+)
11036 * Extended provenance tracking for INSERT, UPDATE, DELETE, and UNDO
11037 * operations, including temporal validity ranges.
11038 * @{
11039 */
11040
11041/**
11042 * @brief Table recording the history of INSERT, UPDATE, DELETE, and UNDO operations
11043 *
11044 * Each row records one provenance-tracked modification, linking the
11045 * operation's provenance token to metadata (query TEXT, type, user,
11046 * TIMESTAMP) and the temporal validity range of the affected rows.
11047 *
11048 * A row of type @c TRANSACTION stands for the transaction the statements
11049 * around it belong to (see @c provsql.transaction_token): @c xid is its
11050 * transaction id and its own @c tx_token is NULL, while every statement
11051 * row of that transaction carries the transaction's token in @c tx_token.
11052 * A modified tuple's provenance names both -- the effect is
11053 * @c times(tx_token, statement_token) -- so @c undo() reverses either one
11054 * statement or the whole transaction, and the temporal semiring reads the
11055 * transaction's validity through the shared factor.
11056 *
11057 * @c ts and the lower bound of @c valid_time are stamped at commit, not
11058 * at the statement: @c CURRENT_TIMESTAMP is the transaction's start time,
11059 * so two overlapping transactions could otherwise commit in the opposite
11060 * order of their recorded validity.
11061 */
11062CREATE TABLE update_provenance (
11063 provsql UUID,
11064 query TEXT,
11065 query_type QUERY_TYPE_ENUM,
11066 username TEXT,
11067 ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
11068 valid_time TSTZMULTIRANGE DEFAULT TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
11069 xid xid8,
11070 tx_token UUID
11071);
11072/**
11073 * @brief The update gate standing for the current transaction
11074 *
11075 * Each data-modification statement already mints an @c update gate of its
11076 * own, but nothing tied the statements of one transaction together: a
11077 * reader of @c update_provenance could not tell that two rows came from
11078 * the same transaction, and @c undo() could reverse a statement but not
11079 * "the transaction". This mints one gate per transaction, on the first
11080 * tracked modification, and hands the same one back for the rest of it.
11081 *
11082 * Where a transaction's gate lives is @c SET @c LOCAL, so it vanishes
11083 * when the transaction ends, whether it commits or rolls back -- and a
11084 * rolled-back transaction leaves no @c update_provenance row for it
11085 * either, since that row is an ordinary heap insert.
11086 */
11087CREATE OR REPLACE FUNCTION transaction_token()
11088RETURNS UUID
11089LANGUAGE plpgsql
11090AS $$
11091DECLARE
11092 tok TEXT;
11093 new_tok UUID;
11094 query_text TEXT;
11095BEGIN
11096 tok := current_setting('provsql.transaction_token', true);
11097 IF tok IS NOT NULL AND tok <> '' THEN
11098 RETURN tok::UUID;
11099 END IF;
11100
11101 new_tok := public.uuid_generate_v4();
11102 PERFORM create_gate(new_tok, 'update');
11103 PERFORM set_config('provsql.transaction_token', new_tok::TEXT, true);
11104
11105 -- A transaction has no query TEXT of its own: query is left NULL, so
11106 -- looking a statement up by its TEXT finds the statement and not the
11107 -- transaction that carried it.
11108 query_text := NULL;
11109
11110 -- The transaction's own validity is the universal range, the
11111 -- multiplicative identity of the temporal m-semiring: it is a factor of
11112 -- every effect of the transaction, and what a tuple is valid for is the
11113 -- statement's business, not the transaction's. Giving it a real
11114 -- interval would intersect it into every one of them.
11115 INSERT INTO update_provenance(provsql, query, query_type, username, ts,
11116 valid_time, xid)
11117 VALUES (new_tok, query_text, 'TRANSACTION', current_user,
11118 CURRENT_TIMESTAMP, '{(,)}'::TSTZMULTIRANGE,
11119 pg_current_xact_id());
11120
11121 RETURN new_tok;
11122END;
11123$$;
11124
11125/**
11126 * @brief Deferred trigger stamping a log row with its commit time
11127 *
11128 * @c CURRENT_TIMESTAMP is the transaction's *start* time, so two
11129 * overlapping transactions can commit in the opposite order of the
11130 * validity they recorded. This fires at commit -- it is a constraint
11131 * trigger declared @c DEFERRABLE @c INITIALLY @c DEFERRED -- and moves
11132 * the row's TIMESTAMP and the lower bound of its validity to
11133 * @c clock_timestamp(), which by then is the commit time to within the
11134 * commit itself.
11135 */
11136CREATE OR REPLACE FUNCTION stamp_commit_time()
11137 RETURNS trigger AS
11138$$
11139DECLARE
11140 now_ts TIMESTAMPTZ := clock_timestamp();
11141BEGIN
11142 UPDATE update_provenance
11143 SET ts = now_ts,
11144 valid_time = CASE WHEN query_type = 'TRANSACTION' THEN valid_time
11145 ELSE TSTZMULTIRANGE(tstzrange(now_ts, NULL)) END
11146 WHERE provsql = NEW.provsql;
11147 RETURN NULL;
11148END;
11149$$ LANGUAGE plpgsql;
11150
11151DO $$ BEGIN
11152 IF NOT EXISTS (SELECT 1 FROM pg_trigger
11153 WHERE tgrelid = 'provsql.update_provenance'::REGCLASS
11154 AND tgname = 'stamp_commit_time') THEN
11155 CREATE CONSTRAINT TRIGGER stamp_commit_time
11156 AFTER INSERT ON provsql.update_provenance
11157 DEFERRABLE INITIALLY DEFERRED
11158 FOR EACH ROW EXECUTE PROCEDURE provsql.stamp_commit_time();
11159 END IF;
11160END $$;
11161
11162/** @cond INTERNAL */
11163/* Enable provenance tracking on an existing table (PostgreSQL 14+ version).
11164 * Overrides the common version; documented via add_provenance in provsql.common.sql. */
11165CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
11166 RETURNS VOID AS
11167$$
11168BEGIN
11169 -- Idempotence: a second add_provenance on an already-tracked table is
11170 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
11171 -- re-run freely.
11172 IF EXISTS (
11173 SELECT 1 FROM pg_attribute
11174 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
11175 ) THEN
11176 RAISE NOTICE 'table % already has provenance tracking', _tbl;
11177 RETURN;
11178 END IF;
11179 -- See the common-version body for the rationale of dropping the
11180 -- column DEFAULT and UNIQUE in favour of provenance_guard + a
11181 -- plain index.
11182 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
11183 EXECUTE format(
11184 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
11185 _tbl);
11186 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
11187 EXECUTE format(
11188 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
11189 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
11190 _tbl);
11191
11192 EXECUTE format('CREATE TRIGGER insert_statement AFTER INSERT ON %s REFERENCING NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE PROCEDURE provsql.insert_statement_trigger()', _tbl);
11193 EXECUTE format('CREATE TRIGGER delete_statement AFTER DELETE ON %s REFERENCING OLD TABLE AS OLD_TABLE FOR EACH STATEMENT EXECUTE PROCEDURE provsql.delete_statement_trigger()', _tbl);
11194 EXECUTE format('CREATE TRIGGER update_statement AFTER UPDATE ON %s REFERENCING OLD TABLE AS OLD_TABLE NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE PROCEDURE provsql.update_statement_trigger()', _tbl);
11195
11196 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
11197 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
11198END
11199$$ LANGUAGE plpgsql SECURITY DEFINER;
11200/** @endcond */
11201
11202/** @cond INTERNAL */
11203/* Trigger function for DELETE statement provenance tracking (PostgreSQL 14+).
11204 * Overrides the common version; documented via delete_statement_trigger in provsql.common.sql. */
11205CREATE OR REPLACE FUNCTION delete_statement_trigger()
11206 RETURNS TRIGGER AS
11207$$
11208DECLARE
11209 query_text TEXT;
11210 delete_token UUID;
11211 old_token UUID;
11212 new_token UUID;
11213 r RECORD;
11214 tx_token UUID;
11215 enable_trigger BOOL;
11216BEGIN
11217 enable_trigger := current_setting('provsql.update_provenance', true);
11218 IF enable_trigger = 'f' THEN
11219 RETURN NULL;
11220 END IF;
11221 delete_token := public.uuid_generate_v4();
11222
11223 PERFORM create_gate(delete_token, 'update');
11224
11225 SELECT query
11226 INTO query_text
11227 FROM pg_stat_activity
11228 WHERE pid = pg_backend_pid();
11229
11230 tx_token := transaction_token();
11231
11232 INSERT INTO update_provenance (provsql, query, query_type, username, ts,
11233 valid_time, xid, tx_token)
11234 VALUES (delete_token, query_text, 'DELETE', current_user, CURRENT_TIMESTAMP,
11235 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
11236 pg_current_xact_id(), tx_token);
11237
11238 -- The effect this statement has on a row names both the statement and
11239 -- the transaction it belongs to, so undo() can reverse either.
11240 delete_token := provenance_times(tx_token, delete_token);
11241
11242 PERFORM set_config('provsql.update_provenance', 'off', false);
11243 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
11244 PERFORM set_config('provsql.update_provenance', 'on', false);
11245
11246 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
11247 old_token := r.provsql;
11248 new_token := provenance_monus(old_token, delete_token);
11249
11250 PERFORM set_config('provsql.update_provenance', 'off', false);
11251 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
11252 USING new_token, old_token;
11253 PERFORM set_config('provsql.update_provenance', 'on', false);
11254 END LOOP;
11255
11256 RETURN NULL;
11257END
11258$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
11259/** @endcond */
11260
11261/**
11262 * @brief Trigger function for INSERT statement provenance tracking
11263 *
11264 * Records the insertion in update_provenance and multiplies provenance
11265 * tokens of inserted rows with the insert token.
11266 */
11267CREATE OR REPLACE FUNCTION insert_statement_trigger()
11268 RETURNS TRIGGER AS
11269$$
11270DECLARE
11271 query_text TEXT;
11272 insert_token UUID;
11273 old_token UUID;
11274 new_token UUID;
11275 r RECORD;
11276 tx_token UUID;
11277 enable_trigger BOOL;
11278BEGIN
11279 enable_trigger := current_setting('provsql.update_provenance', true);
11280 IF enable_trigger = 'f' THEN
11281 RETURN NULL;
11282 END IF;
11283
11284 insert_token := public.uuid_generate_v4();
11285
11286 PERFORM create_gate(insert_token, 'update');
11287
11288 SELECT query
11289 INTO query_text
11290 FROM pg_stat_activity
11291 WHERE pid = pg_backend_pid();
11292
11293 tx_token := transaction_token();
11294
11295 INSERT INTO update_provenance (provsql, query, query_type, username, ts,
11296 valid_time, xid, tx_token)
11297 VALUES (insert_token, query_text, 'INSERT', current_user, CURRENT_TIMESTAMP,
11298 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
11299 pg_current_xact_id(), tx_token);
11300
11301 -- The effect this statement has on a row names both the statement and
11302 -- the transaction it belongs to, so undo() can reverse either.
11303 insert_token := provenance_times(tx_token, insert_token);
11304
11305 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
11306 old_token := r.provsql;
11307 new_token := provenance_times(old_token, insert_token);
11308 PERFORM set_config('provsql.update_provenance', 'off', false);
11309 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
11310 USING new_token, old_token;
11311 PERFORM set_config('provsql.update_provenance', 'on', false);
11312 END LOOP;
11313
11314 RETURN NULL;
11315END
11316$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
11317
11318/**
11319 * @brief Trigger function for UPDATE statement provenance tracking
11320 *
11321 * Records the update in update_provenance. Multiplies new-row tokens
11322 * with the update token and applies monus to old-row tokens.
11323 */
11324CREATE OR REPLACE FUNCTION update_statement_trigger()
11325 RETURNS TRIGGER AS
11326$$
11327DECLARE
11328 query_text TEXT;
11329 update_token UUID;
11330 old_token UUID;
11331 new_token UUID;
11332 r RECORD;
11333 tx_token UUID;
11334 enable_trigger BOOL;
11335BEGIN
11336 enable_trigger := current_setting('provsql.update_provenance', true);
11337 IF enable_trigger = 'f' THEN
11338 RETURN NULL;
11339 END IF;
11340 update_token := public.uuid_generate_v4();
11341
11342 PERFORM create_gate(update_token, 'update');
11343
11344 SELECT query
11345 INTO query_text
11346 FROM pg_stat_activity
11347 WHERE pid = pg_backend_pid();
11348
11349 tx_token := transaction_token();
11350
11351 INSERT INTO update_provenance (provsql, query, query_type, username, ts,
11352 valid_time, xid, tx_token)
11353 VALUES (update_token, query_text, 'UPDATE', current_user, CURRENT_TIMESTAMP,
11354 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
11355 pg_current_xact_id(), tx_token);
11356
11357 -- The effect this statement has on a row names both the statement and
11358 -- the transaction it belongs to, so undo() can reverse either.
11359 update_token := provenance_times(tx_token, update_token);
11360
11361 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
11362 old_token := r.provsql;
11363 new_token := provenance_times(old_token, update_token);
11364
11365 PERFORM set_config('provsql.update_provenance', 'off', false);
11366 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
11367 USING new_token, old_token;
11368 PERFORM set_config('provsql.update_provenance', 'on', false);
11369 END LOOP;
11370
11371 PERFORM set_config('provsql.update_provenance', 'off', false);
11372 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
11373 PERFORM set_config('provsql.update_provenance', 'on', false);
11374
11375 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
11376 old_token := r.provsql;
11377 new_token := provenance_monus(old_token, update_token);
11378
11379 PERFORM set_config('provsql.update_provenance', 'off', false);
11380 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
11381 USING new_token, old_token;
11382 PERFORM set_config('provsql.update_provenance', 'on', false);
11383 END LOOP;
11384
11385 RETURN NULL;
11386END
11387$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
11388
11389
11390/** @} */
11391
11392/** @defgroup temporal_db Temporal DB (PostgreSQL 14+)
11393 * Functions for temporal database support. These use provenance
11394 * evaluation over the multirange semiring to track temporal validity
11395 * of tuples.
11396 * @{
11397 */
11398
11399SET search_path TO provsql;
11400
11401/**
11402 * @brief Evaluate provenance over the temporal (interval-union) m-semiring
11403 *
11404 * Inputs are read as %TSTZMULTIRANGE validity intervals; the additive
11405 * identity is <tt>'{}'::%TSTZMULTIRANGE</tt> (empty), the multiplicative
11406 * identity is <tt>'{(,)}'::%TSTZMULTIRANGE</tt> (universal). Returns the union
11407 * of intervals supporting the result, computed via the compiled circuit
11408 * traversal.
11409 *
11410 * @param token Provenance token to evaluate.
11411 * @param token2value Mapping from input gates to validity multiranges.
11412 */
11413CREATE FUNCTION sr_temporal(token ANYELEMENT, token2value REGCLASS)
11414 RETURNS TSTZMULTIRANGE AS
11415$$
11416BEGIN
11417 RETURN provsql.provenance_evaluate_compiled(
11418 token,
11419 token2value,
11420 'interval_union',
11421 '{(,)}'::TSTZMULTIRANGE
11422 );
11423END
11424$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11425
11426/**
11427 * @brief Evaluate provenance over the interval-union m-semiring
11428 * with a NUMERIC multirange carrier
11429 *
11430 * Inputs are read as %nummultirange validity ranges over a NUMERIC
11431 * domain (e.g. sensor measurement-validity ranges). Addition is
11432 * multirange union, multiplication is intersection, monus is set
11433 * difference; the additive identity is <tt>'{}'::%nummultirange</tt>
11434 * and the multiplicative identity is <tt>'{(,)}'::%nummultirange</tt>
11435 * (universal range).
11436 *
11437 * @param token Provenance token to evaluate.
11438 * @param token2value Mapping from input gates to NUMERIC multiranges.
11439 */
11440CREATE FUNCTION sr_interval_num(token ANYELEMENT, token2value REGCLASS)
11441 RETURNS nummultirange AS
11442$$
11443BEGIN
11444 RETURN provsql.provenance_evaluate_compiled(
11445 token,
11446 token2value,
11447 'interval_union',
11448 '{(,)}'::nummultirange
11449 );
11450END
11451$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11452
11453/**
11454 * @brief Evaluate provenance over the interval-union m-semiring
11455 * with an int4 multirange carrier
11456 *
11457 * Inputs are read as %int4multirange validity ranges over the
11458 * integers (e.g. page or line ranges of supporting documents).
11459 * Addition is multirange union, multiplication is intersection,
11460 * monus is set difference; the additive identity is
11461 * <tt>'{}'::%int4multirange</tt> and the multiplicative identity is
11462 * <tt>'{(,)}'::%int4multirange</tt>.
11463 *
11464 * @param token Provenance token to evaluate.
11465 * @param token2value Mapping from input gates to int4 multiranges.
11466 */
11467CREATE FUNCTION sr_interval_int(token ANYELEMENT, token2value REGCLASS)
11468 RETURNS int4multirange AS
11469$$
11470BEGIN
11471 RETURN provsql.provenance_evaluate_compiled(
11472 token,
11473 token2value,
11474 'interval_union',
11475 '{(,)}'::int4multirange
11476 );
11477END
11478$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11479
11480/**
11481 * @brief Evaluate temporal provenance as a TIMESTAMP multirange
11482 *
11483 * Thin wrapper around :sqlfunc:`sr_temporal` retained for backward
11484 * compatibility; both compute the same union of validity intervals.
11485 *
11486 * @param token provenance token to evaluate
11487 * @param token2value mapping table from tokens to temporal validity ranges
11488 */
11489CREATE OR REPLACE FUNCTION union_tstzintervals(
11490 token UUID,
11491 token2value REGCLASS
11492)
11493RETURNS TSTZMULTIRANGE AS
11494$$
11495 SELECT sr_temporal(token, token2value)
11496$$ LANGUAGE SQL PARALLEL SAFE STABLE;
11497
11498/**
11499 * @brief Query a table as it was at a specific point in time
11500 *
11501 * Returns all rows whose temporal validity includes the given TIMESTAMP.
11502 *
11503 * @param tablename name of the provenance-tracked table
11504 * @param at_time the point in time to query
11505 */
11506CREATE OR REPLACE FUNCTION timetravel(
11507 tablename TEXT,
11508 at_time TIMESTAMPTZ
11509)
11510RETURNS SETOF RECORD
11511LANGUAGE plpgsql
11512AS
11513$$
11514BEGIN
11515 RETURN QUERY EXECUTE format(
11516 '
11517 SELECT
11518 %1$I.*,
11519 sr_temporal(provenance(), %2$L)
11520 FROM
11521 %1$I
11522 WHERE
11523 sr_temporal(provenance(), %2$L) @> %3$L::TIMESTAMPTZ
11524 ',
11525 tablename,
11526 'provsql.time_validity_view',
11527 at_time::TEXT
11528 );
11529END;
11530$$;
11531
11532/**
11533 * @brief Query a table for rows valid during a time interval
11534 *
11535 * Returns all rows whose temporal validity overlaps the given range.
11536 *
11537 * @param tablename name of the provenance-tracked table
11538 * @param from_time start of the time interval
11539 * @param to_time end of the time interval
11540 */
11541CREATE OR REPLACE FUNCTION timeslice(
11542 tablename TEXT,
11543 from_time TIMESTAMPTZ,
11544 to_time TIMESTAMPTZ
11545)
11546RETURNS SETOF RECORD
11547LANGUAGE plpgsql
11548AS
11549$$
11550BEGIN
11551 RETURN QUERY EXECUTE format(
11552 '
11553 SELECT
11554 %1$I.*,
11555 sr_temporal(provenance(), %2$L)
11556 FROM
11557 %1$I
11558 WHERE
11559 sr_temporal(provenance(), %2$L)
11560 && tstzrange(%3$L::TIMESTAMPTZ, %4$L::TIMESTAMPTZ)
11561 ',
11562 tablename,
11563 'provsql.time_validity_view',
11564 from_time::TEXT,
11565 to_time::TEXT
11566 );
11567END;
11568$$;
11569
11570/**
11571 * @brief Query the full temporal history of specific rows
11572 *
11573 * Returns all versions of rows matching the given column values,
11574 * with their temporal validity ranges.
11575 *
11576 * @param tablename name of the provenance-tracked table
11577 * @param col_names array of column names to filter on
11578 * @param col_values array of corresponding values to match
11579 */
11580CREATE OR REPLACE FUNCTION history(
11581 tablename TEXT,
11582 col_names TEXT[],
11583 col_values TEXT[]
11584)
11585RETURNS SETOF RECORD
11586LANGUAGE plpgsql
11587AS
11588$$
11589DECLARE
11590 condition TEXT := '';
11591 i INT;
11592BEGIN
11593 IF array_length(col_names, 1) IS NULL
11594 OR array_length(col_values, 1) IS NULL
11595 OR array_length(col_names, 1) != array_length(col_values, 1)
11596 THEN
11597 RAISE EXCEPTION 'col_names and col_values must have the same (non-null) length';
11598 END IF;
11599
11600 FOR i IN 1..array_length(col_names, 1)
11601 LOOP
11602 IF i > 1 THEN
11603 condition := condition || ' AND ';
11604 END IF;
11605 condition := condition || format('%I = %L', col_names[i], col_values[i]);
11606 END LOOP;
11607
11608 RETURN QUERY EXECUTE format(
11609 '
11610 SELECT
11611 %I.*,
11612 sr_temporal(provenance(), %L)
11613 FROM
11614 %I
11615 WHERE
11616 %s
11617 ',
11618 tablename,
11619 'provsql.time_validity_view',
11620 tablename,
11621 condition
11622 );
11623END;
11624$$;
11625
11626/**
11627 * @brief Get the valid time range for a specific tuple
11628 *
11629 * @param token provenance token of the tuple
11630 * @param tablename name of the table containing the tuple
11631 */
11632CREATE OR REPLACE FUNCTION get_valid_time(
11633 token UUID,
11634 tablename TEXT
11635)
11636RETURNS TSTZMULTIRANGE
11637LANGUAGE plpgsql
11638AS $$
11639DECLARE
11640 result TSTZMULTIRANGE;
11641BEGIN
11642 EXECUTE format(
11643 '
11644 SELECT
11645 sr_temporal(provenance(), %L)
11646 FROM
11647 %I
11648 WHERE
11649 provsql = %L
11650 ',
11651 'provsql.time_validity_view',
11652 tablename,
11653 token
11654 )
11655 INTO result;
11656
11657 RETURN result;
11658END;
11659$$;
11660
11661/**
11662 * @brief Undo a previously recorded update operation
11663 *
11664 * Traverses all provenance-tracked tables and rewrites their circuits
11665 * to apply monus with respect to the given update token, effectively
11666 * undoing the operation.
11667 *
11668 * @param c UUID of the update operation to undo (from update_provenance)
11669 */
11670CREATE OR REPLACE FUNCTION undo(
11671 c UUID
11672)
11673RETURNS UUID
11674LANGUAGE plpgsql
11675AS $$
11676DECLARE
11677 undo_query TEXT;
11678 undone_query TEXT;
11679 undo_token UUID;
11680 schema_rec RECORD;
11681 table_rec RECORD;
11682 row_rec RECORD;
11683 new_x UUID;
11684BEGIN
11685 -- Test for the row, not for its query text: a TRANSACTION row has no
11686 -- query of its own, and undoing a whole transaction is exactly what it
11687 -- is there for.
11688 SELECT query INTO undone_query
11689 FROM update_provenance
11690 WHERE provsql = c
11691 LIMIT 1;
11692
11693 IF NOT FOUND THEN
11694 RAISE NOTICE 'Unable to find % in update_provenance', c;
11695 RETURN c;
11696 END IF;
11697
11698 SELECT query
11699 INTO undo_query
11700 FROM pg_stat_activity
11701 WHERE pid = pg_backend_pid();
11702
11703 undo_token := public.uuid_generate_v4();
11704 PERFORM create_gate(undo_token, 'update');
11705 INSERT INTO update_provenance(provsql, query, query_type, username, ts,
11706 valid_time, xid, tx_token)
11707 VALUES (
11708 undo_token,
11709 undo_query,
11710 'UNDO',
11711 current_user,
11712 CURRENT_TIMESTAMP,
11713 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
11714 pg_current_xact_id(),
11715 transaction_token()
11716 );
11717
11718 PERFORM set_config('provsql.update_provenance', 'off', false);
11719
11720 FOR schema_rec IN
11721 SELECT nspname
11722 FROM pg_namespace
11723 WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1')
11724 LOOP
11725 FOR table_rec IN
11726 EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname)
11727 LOOP
11728 IF EXISTS (
11729 SELECT 1
11730 FROM information_schema.columns
11731 WHERE table_schema = schema_rec.nspname
11732 AND table_name = table_rec.tname
11733 AND table_name <> 'update_provenance'
11734 AND column_name = 'provsql'
11735 ) THEN
11736 FOR row_rec IN
11737 EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname)
11738 LOOP
11739 new_x := replace_the_circuit(row_rec.x, c, undo_token);
11740 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2',
11741 schema_rec.nspname, table_rec.tname)
11742 USING new_x, row_rec.x;
11743 END LOOP;
11744 END IF;
11745 END LOOP;
11746 END LOOP;
11747
11748 PERFORM set_config('provsql.update_provenance', 'on', false);
11749
11750 RETURN undo_token;
11751END;
11752$$;
11753
11754/**
11755 * @brief Recursively rewrite a circuit to undo a specific operation
11756 *
11757 * Helper for undo(). Walks the circuit and replaces occurrences of
11758 * the target update gate with its monus.
11759 *
11760 * @param x provenance token to rewrite
11761 * @param c UUID of the update operation to undo
11762 * @param u UUID of the undo operation
11763 */
11764CREATE OR REPLACE FUNCTION replace_the_circuit(
11765 x UUID,
11766 c UUID,
11767 u UUID
11768)
11769RETURNS UUID
11770LANGUAGE plpgsql
11771AS $$
11772DECLARE
11773 nchildren UUID[];
11774 child UUID;
11775 ntoken UUID;
11776 ntype PROVENANCE_GATE;
11777BEGIN
11778 IF x = c THEN
11779 RETURN provenance_monus(c, u);
11780 -- update and input gates cannot have children
11781 ELSIF get_gate_type(x) = 'update' OR get_gate_type(x) = 'input' THEN
11782 RETURN x;
11783 ELSE
11784 nchildren := '{}';
11785 FOREACH child IN ARRAY get_children(x)
11786 LOOP
11787 nchildren := array_append(nchildren, replace_the_circuit(child, c, u));
11788 END LOOP;
11789
11790 ntoken := public.uuid_generate_v4();
11791 ntype := get_gate_type(x);
11792
11793 PERFORM create_gate(ntoken, ntype, nchildren);
11794 RETURN ntoken;
11795 END IF;
11796END;
11797$$;
11798
11799/**
11800 * @brief Rewrite a circuit, substituting one gate for another
11801 *
11802 * Walks @p x and rebuilds every gate above an occurrence of @p old over
11803 * @p new instead. Leaves that are not @p old come back unchanged, so a
11804 * token that does not mention @p old is returned as it is.
11805 *
11806 * @param x the token to rewrite
11807 * @param old the gate to substitute away
11808 * @param new the gate to put in its place
11809 */
11810CREATE OR REPLACE FUNCTION substitute_gate(
11811 x UUID,
11812 old UUID,
11813 new UUID
11814)
11815RETURNS UUID
11816LANGUAGE plpgsql
11817AS $$
11818DECLARE
11819 nchildren UUID[];
11820 child UUID;
11821 rewritten UUID;
11822 changed BOOLEAN := false;
11823 ntoken UUID;
11824 ntype PROVENANCE_GATE;
11825BEGIN
11826 IF x = old THEN
11827 RETURN new;
11828 END IF;
11829 ntype := get_gate_type(x);
11830 -- Leaves have no children to walk into.
11831 IF ntype IN ('input', 'update', 'rv', 'value', 'zero', 'one') THEN
11832 RETURN x;
11833 END IF;
11834 nchildren := '{}';
11835 FOREACH child IN ARRAY get_children(x)
11836 LOOP
11837 rewritten := substitute_gate(child, old, new);
11838 IF rewritten <> child THEN
11839 changed := true;
11840 END IF;
11841 nchildren := array_append(nchildren, rewritten);
11842 END LOOP;
11843 IF NOT changed THEN
11844 RETURN x;
11845 END IF;
11846 ntoken := public.uuid_generate_v4();
11847 PERFORM create_gate(ntoken, ntype, nchildren);
11848 RETURN ntoken;
11849END;
11850$$;
11851
11852/**
11853 * @brief Give a recorded data modification a different probability
11854 *
11855 * The @c update-gate counterpart of @c provsql.replace_input. A gate's
11856 * probability is written once, so "how likely is it that this
11857 * modification happened" is changed by minting a new @c update gate with
11858 * the new probability, logging it in @c update_provenance beside the one
11859 * it replaces, and rewriting every tracked row whose provenance mentions
11860 * the old gate to mention the new one instead -- the same walk @c undo
11861 * performs. The old gate and its log row are kept: the history of the
11862 * database is not rewritten, it is extended.
11863 *
11864 * @param old the @c update gate to replace, as found in
11865 * @c update_provenance
11866 * @param p the new probability, in [0,1]
11867 * @return the new @c update gate
11868 */
11869CREATE OR REPLACE FUNCTION replace_update(
11870 old UUID,
11871 p double precision
11872)
11873RETURNS UUID
11874LANGUAGE plpgsql
11875AS $$
11876DECLARE
11877 new_token UUID;
11878 old_row RECORD;
11879 schema_rec RECORD;
11880 table_rec RECORD;
11881 row_rec RECORD;
11882 new_x UUID;
11883BEGIN
11884 IF old IS NULL OR p IS NULL THEN
11885 RAISE EXCEPTION 'replace_update: neither argument may be NULL';
11886 END IF;
11887 IF get_gate_type(old) <> 'update' THEN
11888 RAISE EXCEPTION 'replace_update: % is not an update gate', old
11889 USING HINT = 'Use provsql.replace_input() for a tuple''s own input gate.';
11890 END IF;
11891
11892 SELECT * INTO old_row FROM update_provenance WHERE provsql = old LIMIT 1;
11893 IF old_row IS NULL THEN
11894 RAISE EXCEPTION 'replace_update: % is not recorded in update_provenance', old;
11895 END IF;
11896
11897 new_token := public.uuid_generate_v4();
11898 PERFORM create_gate(new_token, 'update');
11899 PERFORM set_prob(new_token, p);
11900
11901 INSERT INTO update_provenance(provsql, query, query_type, username, ts,
11902 valid_time, xid, tx_token)
11903 VALUES (new_token, old_row.query, 'REPLACE', current_user,
11904 CURRENT_TIMESTAMP,
11905 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
11906 pg_current_xact_id(), transaction_token());
11907
11908 PERFORM set_config('provsql.update_provenance', 'off', false);
11909
11910 FOR schema_rec IN
11911 SELECT nspname
11912 FROM pg_namespace
11913 WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1')
11914 LOOP
11915 FOR table_rec IN
11916 EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname)
11917 LOOP
11918 IF EXISTS (
11919 SELECT 1
11920 FROM information_schema.columns
11921 WHERE table_schema = schema_rec.nspname
11922 AND table_name = table_rec.tname
11923 AND table_name <> 'update_provenance'
11924 AND column_name = 'provsql'
11925 ) THEN
11926 FOR row_rec IN
11927 EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname)
11928 LOOP
11929 new_x := substitute_gate(row_rec.x, old, new_token);
11930 IF new_x <> row_rec.x THEN
11931 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2',
11932 schema_rec.nspname, table_rec.tname)
11933 USING new_x, row_rec.x;
11934 END IF;
11935 END LOOP;
11936 END IF;
11937 END LOOP;
11938 END LOOP;
11939
11940 PERFORM set_config('provsql.update_provenance', 'on', false);
11941
11942 RETURN new_token;
11943END;
11944$$;
11945
11946-- The base validity mapping is a plain view over the data-modification log:
11947-- update_provenance is append-only and never has its provsql rewritten, so a
11948-- view stays correct (unlike a tracked table's mapping, which must be a
11949-- maintained mapping table -- see create_provenance_mapping(maintained)).
11950CREATE VIEW provsql.time_validity_view AS
11951 SELECT valid_time AS value, provsql AS provenance FROM provsql.update_provenance;
11952
11953/** @} */
11954
11955SET search_path TO public;
11956
11957-- Final constants-cache refresh: same rationale as at the end of
11958-- provsql.common.sql. On PG14+ this file is appended after the common
11959-- script, so this is the last statement of the generated install script;
11960-- the refresh must come after every object has been created for the
11961-- installing session's memoized constants to be complete.
11962SELECT provsql.reset_constants_cache();