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/**
120 * @brief Create a gate together with what it records (internal)
121 *
122 * One unanswered message to the worker, where create_gate() followed by
123 * set_infos() or set_extra() would wait for the worker's verdict on each.
124 * For the callers that know what the gate records when they create it: a
125 * gate whose address hashes its infos and TEXT, or a fresh token nobody else
126 * can have written to. Infos and TEXT are still written once: should
127 * something else be recorded at that address, the first value stays and the
128 * worker logs it.
129 *
130 * @param token UUID identifying the new gate
131 * @param type gate type (see PROVENANCE_GATE)
132 * @param children optional array of child gate UUIDs
133 * The infos are used in different ways by different gate types: for
134 * mulinput, info1 is the value of the multivalued variable; for eq, info1
135 * and info2 are the attribute positions equated; for agg, info1 is the OID
136 * of the aggregate function and info2 that of its result type; for cmp,
137 * info1 is the OID of the comparison operator; for arith, info1 is the
138 * operator. The TEXT is used in different ways too: for project,
139 * a TEXT-encoded array of two-element arrays mapping input attributes to
140 * output attributes; for value and agg, the TEXT-encoded scalar value; for
141 * rv, the distribution family and its parameters; for assumed, the
142 * assumption kind; for annotation, inert metadata read only by the code
143 * that placed it.
144 *
145 * @param info1 first info, or NULL
146 * @param info2 second info, or NULL
147 * @param extra TEXT of the gate, or NULL
148 */
149CREATE OR REPLACE FUNCTION create_gate(
150 token UUID,
151 type PROVENANCE_GATE,
152 children UUID[],
153 info1 INT,
154 info2 INT,
155 extra TEXT)
156 RETURNS VOID AS
157 'provsql','create_gate' LANGUAGE C PARALLEL SAFE;
158/**
159 * @brief Return the gate type of a provenance token
160 *
161 * Returns @c 'input' for any token not yet materialized in the circuit,
162 * since input is the default semantics of an unmaterialized provenance token.
163 */
164CREATE OR REPLACE FUNCTION get_gate_type(
165 token UUID)
166 RETURNS PROVENANCE_GATE AS
167 'provsql','get_gate_type' LANGUAGE C IMMUTABLE PARALLEL SAFE;
168/** @brief Return the children of a provenance gate */
169CREATE OR REPLACE FUNCTION get_children(
170 token UUID)
171 RETURNS UUID[] AS
172 'provsql','get_children' LANGUAGE C IMMUTABLE PARALLEL SAFE;
173/**
174 * @brief Write the probability of an input gate, once
175 *
176 * A probability is a fact appended to the circuit, like the gate it
177 * belongs to: it can be written on a gate that has none, written again
178 * with the identical value (so setup scripts and notebook cells stay
179 * re-runnable), and is otherwise refused. A write made by a
180 * transaction that rolls back is cleared, leaving the circuit as the
181 * transaction found it.
182 *
183 * To give a tuple a *different* probability, mint a fresh input gate
184 * with @c provsql.replace_input and store it in the row's @c provsql
185 * column. The base table's token column is the place of truth, the
186 * same rule the data-modification triggers already follow, and the
187 * circuit keeps the old gate and everything derived from it.
188 *
189 * @param token UUID of the input gate
190 * @param p probability value in [0,1]
191 */
192CREATE OR REPLACE FUNCTION set_prob(
193 token UUID, p DOUBLE PRECISION)
194 RETURNS VOID AS
195 'provsql','set_prob' LANGUAGE C PARALLEL RESTRICTED;
196
197/**
198 * @brief Report whether a probability has been written on a gate
199 *
200 * @c get_prob returns the value an evaluation would use, so it answers
201 * 1 both for a gate written as certain and for one nobody has given a
202 * probability. This distinguishes them, which is what a client needs
203 * in order to offer "set" on the one and "replace" on the other.
204 */
205CREATE OR REPLACE FUNCTION probability_is_set(token UUID)
206 RETURNS BOOLEAN AS
207 'provsql','probability_is_set' LANGUAGE C STABLE PARALLEL SAFE;
208
209/**
210 * @brief Declare a just-minted leaf gate a replacement for a tracked row
211 *
212 * Internal. @c provsql.replace_input and @c provsql.replace_block call
213 * this so the @c UPDATE that stores the new token does not look, to
214 * @c provenance_guard, like a user pasting in an arbitrary UUID -- which
215 * would flip the table to @c OPAQUE. The declaration lasts for the
216 * transaction.
217 */
218CREATE OR REPLACE FUNCTION note_fresh_leaf(token UUID)
219 RETURNS VOID AS
220 'provsql','note_fresh_leaf' LANGUAGE C;
221
222/** @brief Whether @c token was minted as a replacement leaf by this
223 * transaction (see @c note_fresh_leaf). Internal. */
224CREATE OR REPLACE FUNCTION is_fresh_leaf(token UUID)
225 RETURNS BOOLEAN AS
226 'provsql','is_fresh_leaf' LANGUAGE C VOLATILE;
227
228/**
229 * @brief Mint a replacement input gate carrying a different probability
230 *
231 * Probabilities are written once, so a tuple's probability is changed
232 * the way its data is: by rewriting the row. This mints a fresh input
233 * gate with probability @c p and returns it, for the row's @c provsql
234 * column to carry:
235 *
236 * @code
237 * UPDATE s SET provsql = provsql.replace_input(provsql, 0.3) WHERE id = 42;
238 * @endcode
239 *
240 * The update is an ordinary heap write, so it has MVCC isolation, WAL,
241 * replication and @c pg_dump behind it, and the new gate and its
242 * probability roll back with it. The old gate and everything derived
243 * from it stay as they were: re-running a query over the base table
244 * builds new derived gates over the new token and so sees the new
245 * probability, while a table materialised earlier keeps the old tokens
246 * and the old probability -- a derived table reflects the base tables
247 * as they were when it was built, the same rule a @c DELETE under
248 * @c provsql.update_provenance already follows.
249 *
250 * @param old the token being replaced; must name an input gate
251 * @param p the new probability, in [0,1]
252 */
253CREATE OR REPLACE FUNCTION replace_input(old UUID, p DOUBLE PRECISION)
254 RETURNS UUID AS
255$$
256DECLARE
257 t UUID;
258 tp provsql.PROVENANCE_GATE;
259BEGIN
260 IF old IS NULL OR p IS NULL THEN
261 RAISE EXCEPTION 'replace_input: neither argument may be NULL';
262 END IF;
263 tp := provsql.get_gate_type(old);
264 IF tp = 'mulinput' THEN
265 RAISE EXCEPTION 'replace_input: % belongs to a repair_key block', old
266 USING HINT = 'A block''s values share one key gate and their masses '
267 'are meaningful together, so they are replaced together: '
268 'use provsql.replace_block().';
269 ELSIF tp = 'update' THEN
270 RAISE EXCEPTION 'replace_input: % is an update gate', old
271 USING HINT = 'Use provsql.replace_update() to give a recorded data '
272 'modification a different probability.';
273 ELSIF tp <> 'input' THEN
274 RAISE EXCEPTION 'replace_input: % is a gate of type %, not an input', old, tp
275 USING HINT = 'Only a leaf carries a probability of its own; a derived '
276 'gate''s is computed from its leaves.';
277 END IF;
278 t := public.uuid_generate_v4();
279 PERFORM provsql.create_gate(t, 'input');
280 PERFORM provsql.set_prob(t, p);
281 PERFORM provsql.note_fresh_leaf(t);
282 RETURN t;
283END
284$$ LANGUAGE plpgsql;
286/**
287 * @brief Replace a tracked row's input gate, rewriting the row
288 *
289 * The procedural form of @c replace_input: it mints the new gate and
290 * issues the @c UPDATE itself, which is what a client with only the
291 * token in hand (the Studio inspector, say) needs.
292 *
293 * @param _tbl the tracked table holding the row
294 * @param old the token the row carries
295 * @param p the new probability, in [0,1]
296 * @return the token the row now carries
297 */
298CREATE OR REPLACE FUNCTION replace_input(
299 _tbl REGCLASS, old UUID, p DOUBLE PRECISION)
300 RETURNS UUID AS
301$$
302DECLARE
303 t UUID;
304 n INT;
305BEGIN
306 t := provsql.replace_input(old, p);
307 EXECUTE format('UPDATE %s SET provsql = $1 WHERE provsql = $2', _tbl)
308 USING t, old;
309 GET DIAGNOSTICS n = ROW_COUNT;
310 IF n = 0 THEN
311 RAISE EXCEPTION 'replace_input: no row of % carries the token %', _tbl, old;
312 END IF;
313 RETURN t;
314END
315$$ LANGUAGE plpgsql;
317/**
318 * @brief Give a @c repair_key block a new set of probabilities
319 *
320 * The block-level counterpart of @c replace_input. A block's values
321 * share one key gate and their masses are meaningful together, so they
322 * are replaced together: this mints a new key gate and one new
323 * @c mulinput per row of the block, writes @c probs to them in the
324 * block's own order (the @c within_group index @c repair_key recorded),
325 * and rewrites the block's rows in @p _tbl to carry the new tokens.
326 *
327 * @param _tbl the tracked table holding the block's rows
328 * @param old_key the block's key gate -- the shared child of its rows'
329 * @c mulinput tokens, as reported by
330 * @c "get_children(provsql)[1]"
331 * @param probs one probability per row of the block, in block order;
332 * @c NULL leaves them unwritten, so the block evaluates
333 * at the uniform weight again
334 */
335CREATE OR REPLACE FUNCTION replace_block(
336 _tbl REGCLASS, old_key UUID, probs DOUBLE PRECISION[] DEFAULT NULL)
337 RETURNS VOID AS
338$$
339DECLARE
340 r RECORD;
341 n INT;
342 i INT := 0;
343 new_key UUID;
344 new_tok UUID;
345 was_active TEXT;
346BEGIN
347 IF provsql.get_gate_type(old_key) <> 'input' THEN
348 RAISE EXCEPTION 'replace_block: % is not a block key gate', old_key;
349 END IF;
350
351 -- The rewriter has no business in the bookkeeping below: the tokens of
352 -- _tbl are what this function is here to rewrite, not provenance to
353 -- carry into a temporary table. Restored before returning; a failure
354 -- aborts the transaction, which restores it too.
355 was_active := coalesce(current_setting('provsql.active', true), 'on');
356 PERFORM set_config('provsql.active', 'off', true);
357
358 EXECUTE format(
359 'CREATE TEMP TABLE provsql_replace_block_tmp ON COMMIT DROP AS
360 SELECT t.provsql AS old_token,
361 NULL::UUID AS new_token,
362 (provsql.get_infos(t.provsql)).info1 AS ord
363 FROM %s t
364 WHERE provsql.get_gate_type(t.provsql) = ''mulinput''
365 AND (provsql.get_children(t.provsql))[1] = %L', _tbl, old_key);
366
367 SELECT count(*) INTO n FROM provsql_replace_block_tmp;
368 IF n = 0 THEN
369 RAISE EXCEPTION 'replace_block: no row of % belongs to block %', _tbl, old_key;
370 END IF;
371 IF probs IS NOT NULL AND array_length(probs, 1) <> n THEN
372 RAISE EXCEPTION 'replace_block: block % has % rows but % probabilities were given',
373 old_key, n, array_length(probs, 1);
374 END IF;
375
376 new_key := public.uuid_generate_v4();
377 PERFORM provsql.create_gate(new_key, 'input');
378
379 FOR r IN SELECT old_token, ord FROM provsql_replace_block_tmp ORDER BY ord LOOP
380 i := i + 1;
381 new_tok := public.uuid_generate_v4();
382 PERFORM provsql.create_gate(new_tok, 'mulinput', ARRAY[new_key], r.ord, n, NULL);
383 IF probs IS NOT NULL THEN
384 PERFORM provsql.set_prob(new_tok, probs[i]);
385 END IF;
386 PERFORM provsql.note_fresh_leaf(new_tok);
387 UPDATE provsql_replace_block_tmp SET new_token = new_tok
388 WHERE old_token = r.old_token;
389 END LOOP;
390
391 EXECUTE format(
392 'UPDATE %s t SET provsql = b.new_token
393 FROM provsql_replace_block_tmp b WHERE t.provsql = b.old_token', _tbl);
394
395 DROP TABLE provsql_replace_block_tmp;
396 PERFORM set_config('provsql.active', was_active, true);
397END
398$$ LANGUAGE plpgsql;
399/** @brief Get the probability associated with an input gate */
400CREATE OR REPLACE FUNCTION get_prob(
401 token UUID)
402 RETURNS DOUBLE PRECISION AS
403 'provsql','get_prob' LANGUAGE C STABLE PARALLEL SAFE;
404
405
406/** @brief Get the INTEGER info values associated with a circuit gate */
407CREATE OR REPLACE FUNCTION get_infos(
408 token UUID, OUT info1 INT, OUT info2 INT)
409 RETURNS RECORD AS
410 'provsql','get_infos' LANGUAGE C STABLE PARALLEL SAFE;
411
412/**
413 * @brief Wrap @p token in a fresh @c gate_assumed carrying @p assumption
414 * as its label, and return the wrapper's UUID.
415 *
416 * Public primitive callable from any rewrite or driver that needs to
417 * flag a sub-circuit as sound only under an evaluation assumption:
418 *
419 * - @c 'BOOLEAN' -- the sub-circuit only preserves the Boolean function
420 * of the lineage (e.g. the safe-query rewrite collapses derivation
421 * multiplicities); transparent for semirings admitting a homomorphism
422 * from Boolean functions.
423 * - @c 'absorptive' -- the sub-circuit was truncated at the absorptive
424 * value fixpoint (cyclic recursive query); transparent for absorptive
425 * semirings (probability, BOOLEAN, min-plus over nonnegative
426 * costs...), fatal for the rest (counting, why-provenance).
427 *
428 * Incompatible evaluators raise a @c CircuitException. Always kept as
429 * an explicit node in PROV-XML export.
430 *
431 * The wrapper UUID is content-derived via @c uuid_generate_v5 on the
432 * assumption and the child, so identical children always wrap to the
433 * same outer UUID per assumption. No-op (returns NULL) on a NULL
434 * input.
435 *
436 * Implemented in C (<tt>gate_builders.c</tt>): the gate is created together
437 * with what it records, in one unanswered message.
438 */
439CREATE OR REPLACE FUNCTION provenance_assume(token UUID, assumption TEXT)
440 RETURNS UUID AS
441 'provsql','provenance_assume' LANGUAGE C COST 100 PARALLEL SAFE;
442
443/**
444 * @brief Wrap @p token in a Boolean-assumption marker (compatibility
445 * name; see @c provenance_assume).
446 *
447 * This is the entry point the safe-query (read-once) rewriter calls on
448 * every per-row root it produces, so the wrapper additionally carries
449 * @c PROVSQL_ROUTE_SQ_REWRITE in @c info1: the assumption kind alone does
450 * not identify the route (@c provenance_assume(t, 'BOOLEAN') is public),
451 * and the probability dispatcher reads the tag back to report
452 * @c sq-rewrite rather than the generic @c independent. Build an untagged
453 * Boolean-assumption wrapper with @c provenance_assume directly.
455 * Implemented in C (<tt>gate_builders.c</tt>): the gate is created together
456 * with what it records, in one unanswered message.
457 */
458CREATE OR REPLACE FUNCTION assume_boolean(token UUID) RETURNS UUID AS
459 'provsql','assume_boolean' LANGUAGE C COST 100 PARALLEL SAFE;
460
461/**
462 * @brief Wrap @p token in a fresh transparent @c gate_annotation carrying
463 * @p extra, and return the wrapper's UUID.
464 *
465 * Unlike every other gate, the annotation wrapper's UUID folds in @p extra
466 * (not just the child): @c uuid_generate_v5 over @c concat('annotation',
467 * token, extra). This is deliberate -- two annotations over the same child
468 * with different @p extra must be distinct gates (e.g. the same input tuple
469 * carrying different per-occurrence order keys, or two queries attaching
470 * different certificates to a shared root). The wrapper is transparent
471 * (identity) for EVERY evaluator; @p extra is inert metadata read only by the
472 * code that placed it. No-op (returns NULL) on a NULL input.
473 *
474 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
475 * function it replaces, so that plans stay the same.
476 */
477CREATE OR REPLACE FUNCTION annotate(token UUID, extra TEXT) RETURNS UUID AS
478 'provsql','annotate' LANGUAGE C COST 100 PARALLEL SAFE;
479
480/**
481 * @brief Peel every transparent @c gate_annotation wrapper off @p token,
482 * returning the first non-annotation gate underneath.
483 *
484 * The dual of @c annotate, for consumers keyed to gate *identity* rather
485 * than gate value: a provenance mapping matches input-gate UUIDs, and the
486 * reachability edge classifier matches token shapes, so both must see the
487 * wrapped gate, not the wrapper (e.g. the inversion-free certificate /
488 * order marker the planner attaches to a certified query's row roots).
489 * Identity on a token with no annotation wrapper; NULL on NULL.
490 */
491CREATE OR REPLACE FUNCTION strip_annotations(token UUID) RETURNS UUID AS
493WITH RECURSIVE peel(g) AS (
494 SELECT token
495 UNION ALL
496 SELECT (provsql.get_children(p.g))[1] FROM peel p
497 WHERE provsql.get_gate_type(p.g) = 'annotation'
498)
499SELECT g FROM peel WHERE provsql.get_gate_type(g) <> 'annotation' LIMIT 1;
500$$ LANGUAGE sql STABLE PARALLEL SAFE;
501
502/**
503 * @brief Condition a provenance token (a Boolean event) on another.
504 *
505 * Builds the terminal @c gate_conditioned that the measure evaluators read
506 * as @c "P(target ∧ evidence) / P(evidence)". This is the backing function
507 * of the binary @c | operator (@c "target | evidence", value-level
508 * conditioning of the UUID carrier).
509 *
510 * The gate stores three children @c [target, evidence, joint] with
511 * @c joint @c = @c times(target, @c evidence); evaluation is then the plain
512 * ratio @c P(joint)/P(evidence), and content-addressing makes a base tuple
513 * shared by @p target and @p evidence the same input gate in both circuits,
514 * so the conditional is exact and correlation-aware.
515 *
516 * Conventions:
517 * - Conditioning on a certain or absent event is a no-op: @c evidence NULL
518 * or @c gate_one() returns @p target unchanged (@c "P(X|true)=P(X)").
519 * - A @p target with no provenance defaults to the certain event 1, so
520 * @c "1 | c" is the well-defined certain-row posterior.
521 * - Nested conditioning folds (sequential Bayesian update):
522 * @c "(X | A) | B = X | (A ∧ B)" -- the gate never nests, it stays one
523 * level deep with the evidence accumulated by @c times.
524 *
525 * The result is TERMINAL: a conditioned token may not become a child of a
526 * @c plus / @c times / @c monus / @c agg gate (those constructors refuse
527 * it); the only operation it admits is more conditioning.
528 */
529CREATE OR REPLACE FUNCTION cond(target UUID, evidence UUID) RETURNS UUID AS
530$$
531DECLARE
532 tgt UUID;
533 ev UUID;
534 jnt UUID;
535 result UUID;
536 ch UUID[];
537BEGIN
538 -- P(X | true) = P(X): conditioning on a certain / absent event is inert.
539 IF evidence IS NULL OR evidence = gate_one() THEN
540 RETURN target;
541 END IF;
543 -- A row with no provenance defaults to the certain event 1.
544 tgt := coalesce(target, gate_one());
545
546 IF get_gate_type(tgt) = 'conditioned' THEN
547 -- Sequential update (X | A) | B = X | (A ∧ B): fold B into both the
548 -- evidence and the joint of the inner gate so the result stays a single
549 -- gate_conditioned over the ORIGINAL target.
550 ch := get_children(tgt);
551 tgt := ch[1]; -- original target X
552 ev := provenance_times(ch[2], evidence); -- A ∧ B
553 jnt := provenance_times(ch[3], evidence); -- (X ∧ A) ∧ B
554 ELSE
555 ev := evidence;
556 jnt := provenance_times(tgt, evidence); -- X ∧ C
557 END IF;
558
559 result := public.uuid_generate_v5(uuid_ns_provsql(),
560 concat('conditioned', tgt, ev, jnt));
561 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev, jnt]);
562 RETURN result;
563END
564$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
565 SECURITY DEFINER PARALLEL SAFE;
566
567/**
568 * @brief Binary @c | : value-level conditioning, @c "target | evidence".
569 *
570 * Carrier-parametric in its left operand; the UUID form builds the terminal
571 * @c gate_conditioned via @c cond. Does not collide with core PostgreSQL's
572 * INTEGER bitwise @c | (different argument types).
573 */
574CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=UUID, PROCEDURE=cond);
575
576/**
577 * @brief Placeholder for @c "X | (predicate)" on a UUID event.
578 *
579 * Lets the conditioning event be written as a natural Boolean combination of
580 * random_variable / aggregate comparisons (e.g. @c "event | (sensor > 3)")
581 * instead of a hand-built gate. Never executes: the ProvSQL planner hook
582 * converts the Boolean operand into a condition gate and emits @c cond.
583 */
584CREATE OR REPLACE FUNCTION cond_predicate(target UUID, predicate BOOLEAN)
585 RETURNS UUID AS
586$$
587BEGIN
588 RAISE EXCEPTION 'UUID | (predicate) must be rewritten by the ProvSQL '
589 'planner hook: the right operand must be a Boolean combination of '
590 'random_variable / aggregate comparisons (is provsql.active off?)'
591 USING ERRCODE = 'feature_not_supported',
592 DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
593END
594$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
595
596CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=BOOLEAN, PROCEDURE=cond_predicate);
597
598/**
599 * @brief Placeholder for @c "(predicate) | (predicate)" on two events.
600 *
601 * Conditions one comparison event on another when both operands are written
602 * as comparisons rather than pre-built tokens (e.g.
603 * @c "probability((x >= 2000) | (x >= 1000))"): an @c random_variable /
604 * @c AGG_TOKEN comparison is statically @c BOOLEAN-typed, so neither the
605 * @c "UUID | UUID" (@c cond) nor the @c "UUID | BOOLEAN" (@c cond_predicate)
606 * operator resolves. Never executes: the ProvSQL planner hook lowers each
607 * Boolean operand to its event gate and emits @c cond(target, evidence), so
608 * the result carries the correlation-aware @c Pr(A ∧ B) / Pr(B). Returns
609 * @c UUID, so @c "A | B" is a first-class event token in every position
610 * (a @c probability(UUID) argument, a projected column, a further @c "|").
611 */
612CREATE OR REPLACE FUNCTION predicate_cond_predicate(target BOOLEAN, evidence BOOLEAN)
613 RETURNS UUID AS
614$$
615BEGIN
616 RAISE EXCEPTION '(predicate) | (predicate) must be rewritten by the ProvSQL '
617 'planner hook: both operands must be Boolean combinations of '
618 'random_variable / aggregate comparisons (is provsql.active off?)'
619 USING ERRCODE = 'feature_not_supported',
620 DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
621END
622$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
623
624CREATE OPERATOR | (LEFTARG=BOOLEAN, RIGHTARG=BOOLEAN, PROCEDURE=predicate_cond_predicate);
625
626/**
627 * @brief Deterministic indicator gate for an ordinary (regular) comparison.
628 *
629 * The predicate-provenance of an ordinary comparison (both sides of regular
630 * type, e.g. @c "region = 'north'") is the deterministic indicator
631 * @c "χ(cond)": @c gate_one() when the comparison holds on the row,
632 * @c gate_zero() otherwise (Definition in the HAVING-provenance semantics).
633 * The planner emits this for a regular comparison appearing inside a MIXED
634 * conditioning predicate (one that also has a random_variable / aggregate
635 * comparison); @c cond is evaluated per row, so the indicator is the row's
636 * own truth value, combined by @c ⊗ / @c ⊕ with the probabilistic gates.
637 */
638CREATE OR REPLACE FUNCTION regular_indicator(cond BOOLEAN) RETURNS UUID AS
639$$
640 SELECT CASE WHEN cond THEN provsql.gate_one() ELSE provsql.gate_zero() END;
641$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
643/**
644 * @brief Whole-tuple output conditioning directive: @c "given(evidence)".
645 *
646 * Written as a term in the select list, @c given(c) conditions the OUTPUT
647 * provenance of the current query's rows on @p c:
649 * @code
650 * SELECT a, b, given((SELECT provenance() FROM tests
651 * WHERE patient_id = s.id AND result = 'positive'))
652 * FROM source s;
653 * -- visible columns: a, b (the given(...) term is stripped)
654 * -- per-row output provenance: provenance() | <that row's evidence>
655 * @endcode
656 *
657 * The query rewriter recognises the marker, STRIPS it from the visible
658 * projection, and wraps each output row's provenance expression in
659 * @c cond(row_provenance, c) -- deriving a new conditioned relation, never
660 * mutating any stored provenance. @p c is evaluated per output row and may
661 * correlate with the row's columns, so each tuple is conditioned on its own
662 * evidence. When the rewriter is inactive the call is a harmless identity
663 * (it returns @p evidence as an ordinary column).
664 *
665 * When @b executed rather than stripped -- i.e. nested inside an expression,
666 * the idiom @c "and_agg(given(Y = d))" that folds one observation per row
667 * into a latent-variable evidence circuit -- a point-equality @c "Y = d" on
668 * a bare random-variable leaf is turned into likelihood-weighting evidence
669 * (@c evidence_as_observation); any other evidence passes through unchanged.
670 */
671CREATE OR REPLACE FUNCTION given(evidence UUID) RETURNS UUID AS
672$$
673BEGIN
674 RETURN provsql.evidence_as_observation(evidence);
675END
676$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE
677 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
678
679/**
680 * @brief Prefix unary @c | : alias for @c given, @c "| evidence".
681 *
682 * Disambiguated from the binary @c | by the absence of a left operand
683 * (@c "a, | c" parses @c "| c" as the prefix form). PostgreSQL keeps
684 * prefix operators on every supported version (postfix operators were
685 * removed in PG14), so @c "| c" is safe across the CI matrix.
686 */
687CREATE OPERATOR | (RIGHTARG=UUID, PROCEDURE=given);
688
689/**
690 * @brief Conditioning-evidence from a predicate: @c "given(predicate)"
691 * (also the prefix @c "| (predicate)").
692 *
693 * Two uses of the same marker:
694 * - whole-tuple output conditioning written as a select-list term, the
695 * natural-predicate spelling of @c given (@c "SELECT a, given(sensor > 3)");
696 * - per-row evidence for a latent-variable posterior, folded with
697 * @c and_agg -- @c "and_agg(given(normal(mu,1) = x))" turns each row's
698 * observation into likelihood-weighting evidence.
699 *
700 * Never executes: the planner converts the Boolean operand into a condition
701 * gate and emits @c given(gate); a point-equality @c "Y = d" on a bare
702 * random-variable leaf then becomes an observation (see @c given(UUID) /
703 * @c evidence_as_observation).
704 */
705CREATE OR REPLACE FUNCTION given(predicate BOOLEAN) RETURNS UUID AS
706$$
707BEGIN
708 RAISE EXCEPTION 'given(predicate) / prefix | (predicate) must be rewritten '
709 'by the ProvSQL planner hook: the operand must be a Boolean combination '
710 'of random_variable / aggregate comparisons (is provsql.active off?)'
711 USING ERRCODE = 'feature_not_supported',
712 DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
713END
714$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
715
716CREATE OPERATOR | (RIGHTARG=BOOLEAN, PROCEDURE=given);
717
718/**
719 * @brief Event negation: @c "! event" / @c "provenance_not(event)".
720 *
721 * The complement of a Boolean provenance event: @c "!x" holds in exactly the
722 * worlds where @p x does not. It is sugar for @c "monus(one, x)" -- an
723 * ordinary m-semiring expression (Boolean @c NOT, probability @c "1 - P(x)"),
724 * NOT a measure-only marker -- so it composes like any @c monus, and a
725 * conditioned / terminal token is refused as its child (so @c "!(x | c)"
726 * errors, as conditioning cannot be buried under further algebra).
727 *
728 * The motivating use is conditioning on the NON-occurrence of an arbitrary
729 * violation query @p W (a denial constraint), where @p W itself is built with
730 * ordinary idioms and needs no hand-rolled gates:
731 *
732 * @code
733 * -- W = "some pair of overlapping same-room bookings is present"
734 * WITH w AS (SELECT provenance() AS tok
735 * FROM bookings a JOIN bookings b
736 * ON a.id < b.id AND a.room = b.room
737 * AND a.lo < b.hi AND b.lo < a.hi
738 * GROUP BY ())
739 * SELECT probability_evaluate((SELECT provenance() FROM bookings WHERE id=1)
740 * | !w.tok) -- P(booking 1 | no overlap)
741 * FROM w;
742 * @endcode
743 *
744 * Named @c provenance_not, after the @c "provenance_times / _plus / _monus"
745 * family; the prefix @c ! operator is the ergonomic form (SQL's reserved
746 * @c NOT keyword cannot serve as a function name).
747 */
748CREATE OR REPLACE FUNCTION provenance_not(event UUID) RETURNS UUID AS
749$$
750 SELECT provsql.provenance_monus(provsql.gate_one(), event);
751$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
752 SET search_path=provsql,pg_temp,public;
753
754/**
755 * @brief Prefix unary @c ! : alias for @c provenance_not, @c "! event".
756 *
757 * Prefix operators are kept on every supported PostgreSQL version (postfix
758 * operators were removed in PG14), and core PG defines no prefix @c ! on
759 * @c UUID, so @c "! event" is safe across the CI matrix.
760 */
761CREATE OPERATOR ! (RIGHTARG=UUID, PROCEDURE=provenance_not);
762
763/**
764 * @brief Build a per-input order-key string for the inversion-free path.
765 *
766 * Emitted by the planner per certified atom: @c K-prefixed, length-prefixed
767 * @c "K<factor> <octet_length(root)>:<root><octet_length(sec)>:<sec>", parsed
768 * back at evaluation by @c safe_cert_key_parse. @p root / @p sec are the
769 * tuple's root- and secondary-class column values (TEXT-cast by the caller);
770 * the byte-length prefixes keep the values unambiguous for @em any column type,
771 * including TEXT containing spaces, colons or digits. @p factor is the atom's
772 * factor id (or -1 for the shared self-join guard). @c IMMUTABLE so the planner
773 * can fold it and the marker dedups by content-addressing.
774 *
775 * Implemented in C (<tt>gate_builders.c</tt>); it was an inlined SQL function,
776 * hence the default cost.
777 */
778CREATE OR REPLACE FUNCTION inversion_free_key(root TEXT, sec TEXT, factor INT)
779 RETURNS TEXT AS
780 'provsql','inversion_free_key' LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
781
782/** @brief Get the TEXT-encoded extra data associated with a circuit gate */
783CREATE OR REPLACE FUNCTION get_extra(token UUID)
784 RETURNS TEXT AS
785 'provsql','get_extra' LANGUAGE C STABLE PARALLEL SAFE RETURNS NULL ON NULL INPUT;
786
787/**
788 * @brief Return the total number of materialized gates in the provenance circuit
789 *
790 * Input gates for provenance-tracked table rows are created lazily on
791 * first reference; rows that have never appeared in a query result are
792 * not counted.
793 */
794CREATE OR REPLACE FUNCTION get_nb_gates() RETURNS BIGINT AS
795 'provsql', 'get_nb_gates' LANGUAGE C PARALLEL SAFE;
796
797/**
798 * @brief Report what does not add up in this database's circuit store
799 *
800 * The store lives in four files under the database's directory, outside
801 * PostgreSQL's WAL and buffer manager, so PostgreSQL's own crash recovery
802 * says nothing about it. Writes are ordered so that an interrupted one
803 * leaves a RECORD nothing points at rather than a pointer to a RECORD
804 * that is not there, and the rehash of the token table is done by
805 * renaming a complete new file over the old one; this function checks
806 * that those invariants hold.
807 *
808 * Every count is 0 for a healthy store. @c unclean_shutdown is true when
809 * a file was still marked open-for-writing when it was opened, which an
810 * immediate shutdown or a crash of the server leaves behind and is not by
811 * itself a problem. A non-zero @c dangling_indices, @c bad_wires or
812 * @c bad_extra means the files do not agree with each other -- typically
813 * a file-level backup taken while the server was running, or a base
814 * backup; @c provsql.circuit_cleanup() rebuilds the store from what is
815 * still reachable. @c unreferenced counts gate records the token table
816 * does not point at: a handful is normal (an interrupted write), a large
817 * number means the token table is missing entries.
818 */
819CREATE OR REPLACE FUNCTION check_store(
820 OUT unclean_shutdown BOOLEAN,
821 OUT nb_gates BIGINT,
822 OUT nb_tokens BIGINT,
823 OUT next_index BIGINT,
824 OUT dangling_indices BIGINT,
825 OUT unreferenced BIGINT,
826 OUT bad_wires BIGINT,
827 OUT bad_extra BIGINT)
828 RETURNS RECORD AS
829 'provsql', 'check_store' LANGUAGE C;
831/**
832 * @brief Rebuild this database's circuit store, keeping only what the
833 * tokens stored in the database reach
834 *
835 * The store only grows: a gate is never removed, because a rolled-back
836 * transaction leaves an orphan rather than an inconsistency, and because
837 * the same expression recomputed lands on the same content-addressed
838 * gate. This is the complement of that -- the one operation allowed to
839 * remove gates, run explicitly, the way @c VACUUM @c FULL is the
840 * complement of MVCC. It is also the repair tool for a store an
841 * interrupted write left inconsistent (see @c provsql.check_store).
842 *
843 * It takes the database exclusively: it holds the lock @c DROP
844 * @c DATABASE holds, so sessions connecting from then on wait, and it
845 * refuses to run while another session is already connected. That is
846 * unavoidable -- a query running alongside can adopt an orphan gate a
847 * moment before the sweep removes it, since gates are re-created
848 * idempotently and a backend's own cache answers "it exists" without
849 * asking the store at all.
850 *
851 * A root is every value of a @c UUID, @c AGG_TOKEN or @c random_variable
852 * column, and of arrays of those, in every table and materialised view of
853 * the database -- not only columns named @c provsql -- plus the constants
854 * @c gate_zero, @c gate_one and @c gate_null. A token that lives only
855 * outside the database is **not** a root: one kept in a notebook cell, a
856 * deep link, a file, or a @c TEXT / @c jsonb column. Content-addressed
857 * gates come back by re-running the query that built them; freshly minted
858 * ones do not.
859 *
860 * @param dry_run report what would be kept without writing anything; the
861 * wire and byte totals are then NULL, since the size of
862 * the rewrite is not known without doing it
863 * @param[out] gates_before gate records in the store beforehand
864 * @param[out] gates_after gate records the roots reach, and so kept
865 * @param[out] wires_before child wires beforehand
866 * @param[out] wires_after child wires kept (NULL on a dry run)
867 * @param[out] extra_bytes_before bytes of gate annotations beforehand
868 * @param[out] extra_bytes_after bytes of gate annotations kept (NULL on a
869 * dry run)
870 */
871CREATE OR REPLACE FUNCTION circuit_cleanup(
872 dry_run BOOLEAN DEFAULT false,
873 OUT gates_before BIGINT,
874 OUT gates_after BIGINT,
875 OUT wires_before BIGINT,
876 OUT wires_after BIGINT,
877 OUT extra_bytes_before BIGINT,
878 OUT extra_bytes_after BIGINT)
879 RETURNS RECORD AS
880 'provsql', 'circuit_cleanup' LANGUAGE C;
881
882/** @} */
883
884/** @defgroup table_management Provenance table management
885 * Functions for enabling, disabling, and configuring provenance
886 * tracking on user tables.
887 * @{
888 */
889
890
891/**
892 * @brief Trigger function for DELETE statement provenance tracking
893 *
894 * Records the deletion and applies monus to provenance tokens of
895 * deleted rows. This is the version for PostgreSQL < 14.
896 */
897CREATE OR REPLACE FUNCTION delete_statement_trigger()
898 RETURNS TRIGGER AS
899$$
900DECLARE
901 query_text TEXT;
902 delete_token UUID;
903 old_token UUID;
904 new_token UUID;
905 r RECORD;
906BEGIN
907 delete_token := public.uuid_generate_v4();
908
909 PERFORM create_gate(delete_token, 'input');
910
911 SELECT query
912 INTO query_text
913 FROM pg_stat_activity
914 WHERE pid = pg_backend_pid();
915
916 INSERT INTO delete_provenance (delete_token, query, deleted_by, deleted_at)
917 VALUES (delete_token, query_text, current_user, CURRENT_TIMESTAMP);
918
919 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
920
921 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
922 old_token := r.provsql;
923 new_token := provenance_monus(old_token, delete_token);
925 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
926 USING new_token, old_token;
927 END LOOP;
928
929 RETURN NULL;
930END
931$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
932
933
934/**
935 * @brief Per-relation provenance metadata used by the safe-query
936 * optimisation.
937 *
938 * One row per relation ProvSQL tracks. @c relid is stored as
939 * @c REGCLASS so a dump carries the relation's *name*: OIDs are not
940 * stable across databases, and @c pg_dump / @c pg_restore resolve a
941 * @c REGCLASS value back to whatever OID the relation has in the
942 * target database. @c kind is one of @c 'tid' / @c 'bid' /
943 * @c 'opaque' (see @c set_table_info); @c block_key lists the
944 * block-key column numbers of a BID relation; @c ancestors lists the
945 * base relations this one's atoms ultimately come from.
946 *
947 * Being a heap table, every change follows the transaction that made
948 * it: a rolled-back @c add_provenance leaves no RECORD, a rolled-back
949 * @c DROP @c TABLE keeps one, and a concurrent session sees a change
950 * only once it commits. Marked as a configuration table so
951 * @c pg_dump carries it.
952 */
953CREATE TABLE IF NOT EXISTS table_info(
954 relid REGCLASS PRIMARY KEY,
955 kind TEXT NOT NULL,
956 block_key int2[] NOT NULL DEFAULT ARRAY[]::int2[],
957 ancestors oid[] NOT NULL DEFAULT ARRAY[]::oid[]
958);
959SELECT pg_catalog.pg_extension_config_dump('table_info', '');
960
961/**
962 * @brief Row trigger keeping every backend's metadata cache honest.
963 *
964 * Each backend caches the metadata of the relations its queries touch
965 * and drops an entry when PostgreSQL invalidates that relation's
966 * relcache entry. This trigger issues that invalidation for the
967 * relation named by every inserted, updated, or deleted row, so a
968 * hand-written @c UPDATE on the table and the @c COPY a @c pg_restore
969 * performs are as visible to the caches as the setter functions are.
970 */
971CREATE OR REPLACE FUNCTION table_info_invalidate()
972 RETURNS trigger AS
973 'provsql','provsql_table_info_invalidate' LANGUAGE C;
974
975DROP TRIGGER IF EXISTS table_info_invalidate ON table_info;
976CREATE TRIGGER table_info_invalidate
977 AFTER INSERT OR UPDATE OR DELETE ON table_info
978 FOR EACH ROW EXECUTE FUNCTION provsql.table_info_invalidate();
979
980/**
981 * @brief Record per-relation provenance metadata used by the
982 * safe-query optimisation.
983 *
984 * Upserts the @c (relid, kind, block_key) half of the relation's row
985 * in @c provsql.table_info, preserving its @c ancestors. @p kind is
986 * one of:
987 * - @c 'tid' -- independent input leaves (post-@c add_provenance default)
988 * - @c 'bid' -- block-correlated leaves; rows sharing the same value
989 * of @p block_key are mutually exclusive. An empty
990 * @p block_key means the whole table is one block.
991 * - @c 'opaque' -- arbitrary correlations from a derived source
992 * (CREATE TABLE AS SELECT, INSERT INTO SELECT,
993 * UPDATE under provsql.update_provenance); the
994 * safe-query rewriter must bail on these.
995 *
996 * @param relid pg_class OID of the relation.
997 * @param kind One of @c 'tid' / @c 'bid' / @c 'opaque'.
998 * @param block_key Block-key column numbers (only meaningful for
999 * @c 'bid'; ignored otherwise but conventionally
1000 * passed empty).
1001 */
1002CREATE OR REPLACE FUNCTION set_table_info(
1003 relid OID, kind TEXT, block_key INT2[] DEFAULT ARRAY[]::INT2[])
1004 RETURNS VOID AS
1005 'provsql','set_table_info' LANGUAGE C SECURITY DEFINER;
1006
1007/** @brief Remove a relation's row from @c provsql.table_info.
1008 * No-op when missing. */
1009CREATE OR REPLACE FUNCTION remove_table_info(relid OID)
1010 RETURNS VOID AS
1011 'provsql','remove_table_info' LANGUAGE C SECURITY DEFINER;
1012
1013/**
1014 * @brief Read per-relation provenance metadata.
1015 *
1016 * Returns NULL if no RECORD exists. @c kind is one of @c 'tid' /
1017 * @c 'bid' / @c 'opaque'; @c block_key is the (possibly empty) array
1018 * of block-key column numbers, only meaningful when @c kind = @c 'bid'.
1019 * Used by the planner-time hierarchy detector to gate the safe-query
1020 * rewrite.
1021 */
1022CREATE OR REPLACE FUNCTION get_table_info(
1023 relid OID, OUT kind TEXT, OUT block_key INT2[])
1024 RETURNS RECORD AS
1025 'provsql','get_table_info' LANGUAGE C STABLE PARALLEL SAFE;
1027/**
1028 * @brief Record the base-relation ancestor set of a tracked relation.
1029 *
1030 * Base tables created with @c add_provenance / @c repair_key carry
1031 * @c {self}; CTAS-derived tables inherit the union of their sources'
1032 * ancestor sets. The safe-query rewriter consults the registry to
1033 * enforce that joined FROM entries have disjoint base ancestors
1034 * before firing the read-once factoring.
1035 *
1036 * Preserves the relation's existing @c kind / @c block_key half on
1037 * update, and silently no-ops when no row exists for @p relid
1038 * (callers should run @c add_provenance / @c repair_key first). The
1039 * ancestor list is capped at 64 entries (clear error if exceeded).
1040 *
1041 * @param relid pg_class OID of the relation.
1042 * @param ancestors Sorted, deduplicated base-relation OIDs.
1043 */
1044CREATE OR REPLACE FUNCTION set_ancestors(
1045 relid OID, ancestors OID[] DEFAULT ARRAY[]::OID[])
1046 RETURNS VOID AS
1047 'provsql','set_ancestors' LANGUAGE C SECURITY DEFINER;
1048
1049/** @brief Clear the ancestor half of a per-relation RECORD (keeps kind/block_key).
1050 * No-op when missing. */
1051CREATE OR REPLACE FUNCTION remove_ancestors(relid OID)
1052 RETURNS VOID AS
1053 'provsql','remove_ancestors' LANGUAGE C SECURITY DEFINER;
1054
1055/**
1056 * @brief Copy per-relation metadata out of the legacy
1057 * @c provsql_table_info.mmap file into @c provsql.table_info.
1058 *
1059 * ProvSQL 1.13.0 moved this metadata from a fifth mmap file to a heap
1060 * table, so that it follows the transaction that writes it and is
1061 * carried by @c pg_dump. This function reads the legacy file, if the
1062 * database still has one, and inserts every RECORD it holds that the
1063 * heap table does not already have; it returns the number of rows
1064 * inserted, and 0 when there is no file to read. Idempotent, and a
1065 * no-op on a database that never had one.
1066 */
1067CREATE OR REPLACE FUNCTION migrate_table_info()
1068 RETURNS BIGINT AS
1069 'provsql','migrate_table_info' LANGUAGE C SECURITY DEFINER;
1070
1071/**
1072 * @brief Read the base-relation ancestor set of a tracked relation.
1073 *
1074 * Returns @c NULL when no ancestor RECORD exists for @p relid (or the
1075 * RECORD is empty -- both cases make the safe-query rewriter take
1076 * its conservative refuse path, so they collapse here).
1078CREATE OR REPLACE FUNCTION get_ancestors(relid OID)
1079 RETURNS OID[] AS
1080 'provsql','get_ancestors' LANGUAGE C STABLE PARALLEL SAFE;
1081
1082/**
1083 * @brief BEFORE INSERT OR UPDATE OF provsql row trigger installed by
1084 * @c add_provenance.
1086 * Two jobs:
1087 *
1088 * 1. Fill @c NEW.provsql with a fresh @c uuid_generate_v4 leaf when
1089 * the user did not supply one (a column DEFAULT would not do here:
1090 * it fires before the trigger sees the row, so we could not tell
1091 * "user omitted the column" from "user supplied a value").
1092 * 2. When the user does supply a non-NULL @c provsql on @c INSERT,
1093 * or changes it on @c UPDATE, flip the table's per-table
1094 * metadata to @c OPAQUE. The user is free to write whatever
1095 * UUIDs they want (cross-table reuse, compound tokens minted
1096 * via @c create_gate, ...); the cost is that the safe-query
1097 * rewriter then refuses to fire on this table, because TID
1098 * independence can no longer be assumed. The exception is a
1099 * leaf @c provsql.replace_input / @c replace_block minted in
1100 * this transaction: that one *is* an independent fresh leaf, so
1101 * the kind survives and the maintained mappings follow the
1102 * token to its replacement.
1103 */
1104CREATE OR REPLACE FUNCTION provenance_guard()
1105 RETURNS TRIGGER AS $$
1106DECLARE
1107 _m RECORD;
1108BEGIN
1109 IF TG_OP = 'INSERT' THEN
1110 IF NEW.provsql IS NULL THEN
1111 -- A genuine insert: mint a fresh atomic input variable. This is the
1112 -- one place a new input token is born, so it is also where any
1113 -- maintained mapping on this table is extended (keyed to that token).
1114 -- Data-modification re-insertions (INSERT ... SELECT * FROM OLD_TABLE)
1115 -- carry a supplied provsql and take the ELSE branch, so they are
1116 -- correctly skipped: the validity stays keyed to the original input,
1117 -- which is exactly the child a later monus/update gate wraps.
1118 NEW.provsql := public.uuid_generate_v4();
1119 FOR _m IN SELECT mapping, attribute
1120 FROM provsql.provenance_mapping_registry
1121 WHERE source = TG_RELID AND maintained
1122 LOOP
1123 EXECUTE format(
1124 'INSERT INTO %s(value, provenance) SELECT ($1).%I, $2',
1125 _m.mapping::REGCLASS, _m.attribute)
1126 USING NEW, NEW.provsql;
1127 END LOOP;
1128 ELSE
1129 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
1130 END IF;
1131 ELSIF TG_OP = 'UPDATE' THEN
1132 IF NEW.provsql IS DISTINCT FROM OLD.provsql THEN
1133 IF provsql.is_fresh_leaf(NEW.provsql) THEN
1134 -- A replacement leaf minted by provsql.replace_input /
1135 -- replace_block in this transaction: an independent fresh leaf by
1136 -- construction, so the table's kind survives. Carry the
1137 -- maintained mappings over from the token it replaces, the same
1138 -- job the INSERT branch does for a new row.
1139 FOR _m IN SELECT mapping, attribute
1140 FROM provsql.provenance_mapping_registry WHERE source = TG_RELID
1141 LOOP
1142 EXECUTE format(
1143 'INSERT INTO %1$s(value, provenance) '
1144 'SELECT value, $2 FROM %1$s WHERE provenance = $1',
1145 _m.mapping::REGCLASS)
1146 USING OLD.provsql, NEW.provsql;
1147 END LOOP;
1148 ELSE
1149 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
1150 END IF;
1151 END IF;
1152 END IF;
1153 RETURN NEW;
1154END;
1155$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
1156 SECURITY DEFINER;
1157
1158/**
1159 * @brief Enable provenance tracking on an existing table
1160 *
1161 * Adds a <tt>provsql</tt> UUID column to the table, an index for
1162 * fast UUID-keyed lookups, and a BEFORE INSERT/UPDATE row trigger
1163 * (@c provenance_guard) that mints a fresh @c uuid_generate_v4
1164 * leaf when the user omits the column on INSERT, or flips the
1165 * table's metadata to @c OPAQUE when the user supplies their own
1166 * value. Input gates for existing rows are created lazily when
1167 * first referenced by a query.
1168 *
1169 * @param _tbl the table to add provenance tracking to
1170 */
1171CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
1172 RETURNS VOID AS
1173$$
1174BEGIN
1175 -- Idempotence: a second add_provenance on an already-tracked table is
1176 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
1177 -- re-run freely.
1178 IF EXISTS (
1179 SELECT 1 FROM pg_attribute
1180 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
1181 ) THEN
1182 RAISE NOTICE 'table % already has provenance tracking', _tbl;
1183 RETURN;
1184 END IF;
1185 -- No DEFAULT: the guard trigger mints the UUID, so the trigger can
1186 -- distinguish "user omitted" (NULL) from "user supplied a value".
1187 -- No UNIQUE: we no longer rely on it to keep the table TID -- the
1188 -- guard does that semantically -- and a UNIQUE would reject the
1189 -- legitimate cross-table UUID copy that just flips the table to
1190 -- OPAQUE. We keep a plain index for fast UUID-keyed lookups.
1191 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
1192 EXECUTE format(
1193 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
1194 _tbl);
1195 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
1196 EXECUTE format(
1197 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
1198 'ON %s FOR EACH ROW EXECUTE FUNCTION provsql.provenance_guard()',
1199 _tbl);
1200 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
1201 -- Seed the base-ancestor set to {self}: a base TID table's atoms
1202 -- come from itself and no other relation. CTAS-derived tables
1203 -- inherit unions of source ancestor sets; that is handled by the
1204 -- CTAS hook (a separate slice), not here.
1205 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
1206 -- A view defined before this call selected the columns the table had then,
1207 -- so it has no provsql column and never will: PostgreSQL resolved its
1208 -- "SELECT *" at definition time. A query over such a view is answered
1209 -- without provenance and without a warning -- the rewriting sees a relation
1210 -- that carries none -- so the one place where saying it is useful is here,
1211 -- where recreating the view is the remedy.
1212 DECLARE
1213 stale TEXT;
1214 BEGIN
1215 SELECT string_agg(DISTINCT v.rel::REGCLASS::TEXT, ', ') INTO stale
1216 FROM (SELECT r.ev_class AS rel
1217 FROM pg_catalog.pg_depend d
1218 JOIN pg_catalog.pg_rewrite r ON r.oid = d.objid
1219 WHERE d.classid = 'pg_catalog.pg_rewrite'::REGCLASS
1220 AND d.refclassid = 'pg_catalog.pg_class'::REGCLASS
1221 AND d.refobjid = _tbl
1222 AND r.ev_class <> _tbl) AS v
1223 WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_attribute a
1224 WHERE a.attrelid = v.rel AND a.attname = 'provsql'
1225 AND NOT a.attisdropped);
1226 IF stale IS NOT NULL THEN
1227 RAISE WARNING 'ProvSQL: % is read by views defined before it was '
1228 'tracked, which have no provenance column of their own, '
1229 'so a query over one of them is answered as plain SQL, '
1230 'not tracked: %',
1231 _tbl, stale
1232 USING HINT = 'recreate the view (CREATE OR REPLACE VIEW ... or DROP and '
1233 'CREATE) so that its definition reads the tracked table',
1234 DETAIL = 'provsql-reason: view-defined-before-tracking; '
1235 'scope: gap';
1236 END IF;
1237 END;
1238END
1239$$ LANGUAGE plpgsql SECURITY DEFINER;
1240
1241/**
1242 * @brief Remove provenance tracking from a table
1244 * Drops the <tt>provsql</tt> column and associated triggers.
1245 *
1246 * @param _tbl the table to remove provenance tracking from
1247 */
1248CREATE OR REPLACE FUNCTION remove_provenance(_tbl REGCLASS)
1249 RETURNS VOID AS
1250$$
1251DECLARE
1252BEGIN
1253 PERFORM provsql.remove_table_info(_tbl::oid);
1254 -- Idempotence, mirroring add_provenance: removing provenance from a
1255 -- table that does not have it is a NOTICE-and-no-op, so setup scripts
1256 -- and notebook cells can be re-run freely. The metadata strip above
1257 -- still runs, so a table left half-tracked is cleaned up.
1258 IF NOT EXISTS (
1259 SELECT 1 FROM pg_attribute
1260 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
1261 ) THEN
1262 RAISE NOTICE 'table % does not have provenance tracking', _tbl;
1263 RETURN;
1264 END IF;
1265 -- Drop the BEFORE INSERT/UPDATE guard first: it has a column
1266 -- dependency on provsql (via the OF provsql clause), so the
1267 -- subsequent DROP COLUMN would otherwise raise.
1268 BEGIN
1269 EXECUTE format('DROP TRIGGER provenance_guard on %s', _tbl);
1270 EXCEPTION WHEN undefined_object THEN
1271 END;
1272 EXECUTE format('ALTER TABLE %s DROP COLUMN provsql', _tbl);
1273 BEGIN
1274 EXECUTE format('DROP TRIGGER add_gate on %s', _tbl);
1275 EXCEPTION WHEN undefined_object THEN
1276 END;
1277 BEGIN
1278 EXECUTE format('DROP TRIGGER insert_statement on %s', _tbl);
1279 EXECUTE format('DROP TRIGGER update_statement on %s', _tbl);
1280 EXECUTE format('DROP TRIGGER delete_statement on %s', _tbl);
1281 EXCEPTION WHEN undefined_object THEN
1282 END;
1283END
1284$$ LANGUAGE plpgsql;
1285
1286/**
1287 * @brief Set up provenance for a table with duplicate key values
1288 *
1289 * When a table has duplicate rows for a given key, this function
1290 * replaces simple input gates with multivalued input (mulinput) gates
1291 * that model a uniform distribution over duplicates. The uniform
1292 * weight is the default a row evaluates at, not a probability written
1293 * on it, so the usual next step -- @c "SELECT set_prob(provenance(),
1294 * p) FROM t" -- gives each row its real probability as a first write.
1295 *
1296 * @param _tbl the table to repair
1297 * @param key_att the key attribute(s) as a comma-separated string, or
1298 * empty string if the whole table is one group
1299 */
1300CREATE OR REPLACE FUNCTION repair_key(_tbl REGCLASS, key_att TEXT)
1301 RETURNS VOID AS
1302$$
1303DECLARE
1304 r RECORD;
1305 rows_query TEXT;
1306 block_key_cols INT2[];
1307BEGIN
1308 -- Resolve the (possibly comma-separated) key_att TEXT into the
1309 -- corresponding pg_attribute.attnum values for the safe-query
1310 -- metadata. Names are trimmed; quoting is not supported because
1311 -- repair_key has never accepted quoted identifiers in key_att.
1312 IF key_att = '' THEN
1313 block_key_cols := ARRAY[]::INT2[];
1314 ELSE
1315 SELECT array_agg(a.attnum ORDER BY t.ord)::INT2[]
1316 INTO block_key_cols
1317 FROM unnest(string_to[](key_att, ',')) WITH ORDINALITY AS t(name, ord)
1318 JOIN pg_attribute a
1319 ON a.attrelid = _tbl
1320 AND a.attname = trim(t.name)
1321 AND a.attnum > 0
1322 AND NOT a.attisdropped;
1323 IF block_key_cols IS NULL OR array_length(block_key_cols, 1) IS NULL THEN
1324 RAISE EXCEPTION 'repair_key: could not resolve key columns from "%"', key_att
1325 USING ERRCODE = 'feature_not_supported',
1326 DETAIL = 'provsql-reason: repair-key-columns; scope: gap';
1327 END IF;
1328 IF array_length(block_key_cols, 1) > 16 THEN
1329 RAISE EXCEPTION 'repair_key: block key wider than 16 columns is not supported'
1330 USING ERRCODE = 'feature_not_supported',
1331 DETAIL = 'provsql-reason: repair-key-too-wide; scope: gap';
1332 END IF;
1333 END IF;
1334
1335 -- Same column shape as add_provenance: no UNIQUE, no DEFAULT past
1336 -- the initial backfill (the guard trigger added after the rename
1337 -- takes over both jobs once the column has been renamed to its
1338 -- final name). The DEFAULT is kept here only so the second pass
1339 -- below can read provsql_temp from the user-visible rows
1340 -- without a separate UPDATE.
1341 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql_temp UUID DEFAULT public.uuid_generate_v4()', _tbl);
1342
1343 -- Build a per-group mapping (key columns + a fresh key_token + the
1344 -- group size) once, then use it for both the create_gate(key_token,
1345 -- 'input') first pass and the per-row mulinput second pass. Going
1346 -- through a temp table avoids re-running uuid_generate_v4() (which
1347 -- would produce different UUIDs the second time). USING (%1$s) on
1348 -- the second pass handles the multi-column case uniformly.
1349 -- ON COMMIT DROP plus the explicit DROP TABLE at the end of this
1350 -- function leave the temp table cleaned up across transactions and
1351 -- across repeated calls in the same transaction.
1352 IF key_att = '' THEN
1353 EXECUTE format(
1354 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
1355 SELECT public.uuid_generate_v4() AS provsql_key_token,
1356 COUNT(*) AS provsql_group_size
1357 FROM %s', _tbl);
1358 rows_query := format(
1359 'SELECT t.provsql_temp,
1360 k.provsql_key_token AS key_token,
1361 ROW_NUMBER() OVER (ORDER BY t.ctid) AS within_group,
1362 k.provsql_group_size AS group_size
1363 FROM %s t CROSS JOIN provsql_repair_key_tmp k', _tbl);
1364 ELSE
1365 EXECUTE format(
1366 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
1367 SELECT %1$s,
1368 public.uuid_generate_v4() AS provsql_key_token,
1369 COUNT(*) AS provsql_group_size
1370 FROM %2$s
1371 GROUP BY %1$s', key_att, _tbl);
1372 rows_query := format(
1373 'SELECT t.provsql_temp,
1374 k.provsql_key_token AS key_token,
1375 ROW_NUMBER() OVER (PARTITION BY k.provsql_key_token
1376 ORDER BY t.ctid) AS within_group,
1377 k.provsql_group_size AS group_size
1378 FROM %2$s t
1379 JOIN provsql_repair_key_tmp k USING (%1$s)', key_att, _tbl);
1380 END IF;
1381
1382 -- Pass 1: one input gate per group key.
1383 FOR r IN SELECT provsql_key_token FROM provsql_repair_key_tmp LOOP
1384 PERFORM provsql.create_gate(r.provsql_key_token, 'input');
1385 END LOOP;
1386
1387 -- Pass 2: per row, attach a mulinput gate to its group's key token.
1388 -- The block size goes in info2 rather than the uniform 1/size going
1389 -- in the probability: a repaired row's probability is the user's to
1390 -- write (the documented "repair_key then set_prob(provenance(), p)"
1391 -- pattern), and probabilities are written once. A row nobody gives
1392 -- a probability evaluates at 1/size all the same -- see
1393 -- MMappedCircuit::getProb.
1394 FOR r IN EXECUTE rows_query LOOP
1395 PERFORM provsql.create_gate(r.provsql_temp, 'mulinput', ARRAY[r.key_token],
1396 r.within_group::INT, r.group_size::INT, NULL);
1397 END LOOP;
1398
1399 DROP TABLE provsql_repair_key_tmp;
1400
1401 EXECUTE format('ALTER TABLE %s ALTER COLUMN provsql_temp DROP DEFAULT', _tbl);
1402 EXECUTE format('ALTER TABLE %s RENAME COLUMN provsql_temp TO provsql', _tbl);
1403 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
1404 EXECUTE format(
1405 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
1406 'ON %s FOR EACH ROW EXECUTE FUNCTION provsql.provenance_guard()',
1407 _tbl);
1408 PERFORM provsql.set_table_info(_tbl::oid, 'bid', block_key_cols);
1409 -- Base BID tables also have themselves as their sole ancestor. Same
1410 -- rationale as the @c add_provenance branch above.
1411 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
1413$$ LANGUAGE plpgsql;
1414
1415/**
1416 * @brief Event trigger that purges per-table provenance metadata when
1417 * a tracked relation is dropped outside of remove_provenance().
1418 *
1419 * Plain DROP TABLE bypasses remove_provenance() and would otherwise
1420 * leave a stale row in provsql.table_info keyed by a now-recycled
1421 * OID, with confusing consequences for the safe-query rewriter the
1422 * next time the OID is reused. This trigger forwards every dropped
1423 * relation OID to provsql.remove_table_info(), which is a no-op for
1424 * relations that were not tracked. Both the deletion and the
1425 * registry cleanup below roll back with the DROP that triggered
1426 * them.
1427 */
1428CREATE OR REPLACE FUNCTION cleanup_table_info()
1429 RETURNS event_trigger AS
1430$$
1431DECLARE
1432 r RECORD;
1433BEGIN
1434 FOR r IN
1435 SELECT objid FROM pg_event_trigger_dropped_objects()
1436 WHERE object_type IN ('table', 'foreign table', 'materialized view')
1437 LOOP
1438 PERFORM provsql.remove_table_info(r.objid);
1439 -- Forget any maintained mapping whose source or mapping table is gone.
1440 DELETE FROM provsql.provenance_mapping_registry
1441 WHERE source = r.objid OR mapping = r.objid;
1442 END LOOP;
1443END
1444$$ LANGUAGE plpgsql;
1445
1446DROP EVENT TRIGGER IF EXISTS provsql_cleanup_table_info;
1447-- @c EXECUTE @c PROCEDURE (rather than the PG 11+ @c EXECUTE
1448-- @c FUNCTION alias) so the extension installs on PG 10 too.
1449CREATE EVENT TRIGGER provsql_cleanup_table_info ON sql_drop
1450 EXECUTE FUNCTION provsql.cleanup_table_info();
1451
1452/**
1453 * @brief Registry of provenance mappings
1454 *
1455 * Each row records that mapping table @c mapping was built from the
1456 * @c attribute column of the provenance-tracked @c source table. When
1457 * @c maintained, every genuine insert into @c source also appends
1458 * @c (value, provenance) to it, so the mapping stays current; otherwise
1459 * the mapping is a snapshot and the row is here only so that a token
1460 * *replacement* carries the mapping over (see @c provenance_guard: a leaf
1461 * minted by @c replace_input names the same tuple, so its value is copied
1462 * to the new token in every mapping of the table, maintained or not).
1463 * Keyed on the mapping table, indexed on the source so the guard can look
1464 * up a table's mappings cheaply. Entries are removed when either table is
1465 * dropped (see @c cleanup_table_info).
1466 */
1467CREATE TABLE IF NOT EXISTS provsql.provenance_mapping_registry(
1468 mapping oid PRIMARY KEY,
1469 source oid NOT NULL,
1470 attribute name NOT NULL,
1471 maintained BOOLEAN NOT NULL DEFAULT false
1472);
1473ALTER TABLE provsql.provenance_mapping_registry
1474 ADD COLUMN IF NOT EXISTS maintained BOOLEAN NOT NULL DEFAULT false;
1475CREATE INDEX IF NOT EXISTS provenance_mapping_registry_source_idx
1476 ON provsql.provenance_mapping_registry(source);
1477
1478/**
1479 * @brief Create a provenance mapping table from an attribute
1480 *
1481 * Creates a new table mapping provenance tokens to values of a given
1482 * attribute, for use with semiring evaluation functions.
1483 * Idempotent: if the mapping table already exists, raises a NOTICE and
1484 * changes nothing (drop it first to rebuild).
1485 *
1486 * @param newtbl name of the mapping table to create
1487 * @param oldtbl source table with provenance tracking
1488 * @param att attribute whose values populate the mapping
1489 * @param preserve_case if true, quote the table name to preserve case
1490 * @param maintained if true, later inserts into @c oldtbl keep the mapping
1491 * current, and it stays correct after data modification
1492 * (deletes/updates rewrite a row's provsql, but the validity stays
1493 * keyed to the original input token). @c att must then be a plain
1494 * column name. When false (the default) the table is a one-off
1495 * snapshot; either way the mapping is recorded in
1496 * @c provenance_mapping_registry, so a row whose input gate is
1497 * replaced by @c provsql.replace_input keeps its value in it.
1498 */
1499CREATE OR REPLACE FUNCTION create_provenance_mapping(
1500 newtbl TEXT,
1501 oldtbl REGCLASS,
1502 att TEXT,
1503 preserve_case BOOL DEFAULT 'f',
1504 maintained BOOL DEFAULT false
1505) RETURNS VOID AS
1506$$
1507DECLARE
1508BEGIN
1509 -- Idempotence: when the mapping table already exists, leave it alone
1510 -- with a NOTICE (re-runnable setup scripts / notebook cells). Drop it
1511 -- first to rebuild a stale mapping.
1512 IF (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
1513 ELSE to_regclass(newtbl) END) IS NOT NULL THEN
1514 RAISE NOTICE 'mapping table % already exists', newtbl;
1515 RETURN;
1516 END IF;
1517 -- ON COMMIT DROP only fires at COMMIT: several mapping creations in
1518 -- one transaction (a notebook cell, a setup script run via psql -1)
1519 -- would otherwise collide on the leftover temp table. The to_regclass
1520 -- probe (rather than DROP IF EXISTS) keeps the first call NOTICE-free.
1521 IF to_regclass('pg_temp.tmp_provsql') IS NOT NULL THEN
1522 DROP TABLE tmp_provsql;
1523 END IF;
1524 EXECUTE format('CREATE TEMP TABLE tmp_provsql ON COMMIT DROP AS TABLE %s', oldtbl);
1525 ALTER TABLE tmp_provsql RENAME provsql TO provenance;
1526 -- The mapping is keyed by gate identity (input-token UUIDs), so peel any
1527 -- transparent annotation wrapper (e.g. the inversion-free certificate a
1528 -- certified query attaches to its row roots) off the captured tokens.
1529 UPDATE tmp_provsql SET provenance = provsql.strip_annotations(provenance)
1530 WHERE provsql.get_gate_type(provenance) = 'annotation';
1531 IF preserve_case THEN
1532 EXECUTE format('CREATE TABLE %I AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
1533 EXECUTE format('CREATE INDEX ON %I(provenance)', newtbl);
1534 ELSE
1535 EXECUTE format('CREATE TABLE %s AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
1536 EXECUTE format('CREATE INDEX ON %s(provenance)', newtbl);
1537 END IF;
1538 -- Register the mapping. When maintained, genuine inserts into oldtbl
1539 -- keep it current (see provenance_guard); keyed to the input token, so
1540 -- it survives the provsql rewrites that data modification performs.
1541 -- A snapshot mapping is registered too, so that replacing a row's input
1542 -- gate (provsql.replace_input) carries the row's value over to the new
1543 -- token: the tuple is the same one, only its token moved.
1544 INSERT INTO provsql.provenance_mapping_registry(mapping, source, attribute, maintained)
1545 VALUES (
1546 (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
1547 ELSE to_regclass(newtbl) END)::oid,
1548 oldtbl::oid, att, maintained)
1549 ON CONFLICT (mapping)
1550 DO UPDATE SET source = EXCLUDED.source, attribute = EXCLUDED.attribute,
1551 maintained = EXCLUDED.maintained;
1552END
1553$$ LANGUAGE plpgsql;
1554
1555/** @} */
1556
1557/** @defgroup internal_constants Internal constants
1558 * UUID namespace and identity element functions used for
1559 * deterministic gate generation.
1560 * @{
1561 */
1562
1563/** @brief Return the ProvSQL UUID namespace (used for deterministic gate UUIDs) */
1564CREATE OR REPLACE FUNCTION uuid_ns_provsql() RETURNS UUID AS
1565$$
1566 -- uuid_generate_v5(uuid_ns_url(),'http://pierre.senellart.com/software/provsql/')
1567 SELECT '920d4f02-8718-5319-9532-d4ab83a64489'::UUID
1568$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1569
1570/** @brief Return the UUID of the semiring zero gate */
1571CREATE OR REPLACE FUNCTION gate_zero() RETURNS UUID AS
1572$$
1573 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'zero');
1574$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1575
1576/** @brief Return the UUID of the semiring one gate */
1577CREATE OR REPLACE FUNCTION gate_one() RETURNS UUID AS
1578$$
1579 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'one');
1580$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1581
1582/**
1583 * @brief Return the UUID of the value gate standing for the NULL value
1584 *
1585 * A constant, like gate_zero() and gate_one(); the gate itself is a
1586 * <tt>value</tt> gate that displays as <tt>NULL</tt>. Its UUID is what
1587 * tells it apart from the value gate of the string <tt>'NULL'</tt>; the
1588 * seed <tt>'null'</tt> is no <tt>'value' || TEXT</tt>, so no actual value
1589 * shares it.
1590 */
1591CREATE OR REPLACE FUNCTION gate_null() RETURNS UUID AS
1592$$
1593 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'null');
1594$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1596/** @} */
1597
1598/** @defgroup semiring_operations Semiring operations
1599 * Functions that build provenance circuit gates for semiring operations.
1600 * These are called internally by the query rewriter.
1601 *
1602 * They are declared @c IMMUTABLE: each derives its gate UUID
1603 * deterministically from its arguments (a @c uuid5 content address) and
1604 * the @c create_gate write at that address is idempotent, so the token a
1605 * call returns is a pure function of its inputs. The marking matters for
1606 * parallelism: PL/pgSQL runs a non-volatile function's inner SPI
1607 * read-only, so the per-row builders the rewriter injects into a scan do
1608 * not call @c CommandCounterIncrement -- which would raise "cannot start
1609 * commands during a parallel operation" once the enclosing statement has
1610 * gone parallel. A @c VOLATILE builder both blocks that parallel plan and
1611 * loses the query-wide speed-up.
1612 * @{
1613 */
1614
1615/**
1616 * @brief Create a times (product) gate from multiple provenance tokens
1617 *
1618 * Filters out NULL and one-gates; returns gate_one() if all tokens
1619 * are trivial, or a single token if only one remains.
1620 *
1621 * When the surviving multiset is one for which this session planted a
1622 * certified equivalent (see @c plant_canonical; the reachability rewriter
1623 * does so for self-join conjunctions of reachability tokens, see
1624 * @c plant_reach_cover), the planted gate is returned. The ordinary
1625 * order-dependent recipe is used otherwise, so ordinary times gates (and
1626 * their formula rendering) are untouched.
1627 *
1628 * Implemented in C (<tt>gate_builders.c</tt>). The cost is declared as that
1629 * of a PL/pgSQL function, which this function was: the planner then keeps
1630 * evaluating it after the cheaper conditions, and the plans of rewritten
1631 * queries, on which the order of the children of a ⊕ depends, stay the same.
1632 */
1633CREATE OR REPLACE FUNCTION provenance_times(VARIADIC tokens UUID[])
1634 RETURNS UUID AS
1635 'provsql','provenance_times' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
1636
1637/**
1638 * @brief Create a monus (difference) gate from two provenance tokens
1639 *
1640 * Implements m-semiring monus. Returns token1 if token2 is NULL
1641 * (used for LEFT OUTER JOIN semantics in the EXCEPT rewriting).
1642 *
1643 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
1644 * PL/pgSQL function it was, so that plans stay the same.
1645 */
1646CREATE OR REPLACE FUNCTION provenance_monus(token1 UUID, token2 UUID)
1647 RETURNS UUID AS
1648 'provsql','provenance_monus' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
1649
1651 * @brief Create a project gate for where-provenance tracking
1652 *
1653 * Records the mapping between input and output attribute positions.
1654 *
1655 * @param token child provenance token
1656 * @param positions array encoding attribute position mappings
1657 *
1658 * Implemented in C (<tt>gate_builders.c</tt>): the gate is created together
1659 * with what it records, in one unanswered message.
1660 */
1661CREATE OR REPLACE FUNCTION provenance_project(token UUID, VARIADIC positions INT[])
1662 RETURNS UUID AS
1663 'provsql','provenance_project' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
1664
1665/**
1666 * @brief Create an equijoin gate for where-provenance tracking
1667 *
1668 * @param token child provenance token
1669 * @param pos1 attribute index in the first relation
1670 * @param pos2 attribute index in the second relation
1671 *
1672 * Implemented in C (<tt>gate_builders.c</tt>): the gate is created together
1673 * with what it records, in one unanswered message.
1674 */
1675CREATE OR REPLACE FUNCTION provenance_eq(token UUID, pos1 INT, pos2 INT)
1676 RETURNS UUID AS
1677 'provsql','provenance_eq' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
1678
1679/**
1680 * @brief Create a plus (sum) gate from an array of provenance tokens
1681 *
1682 * Filters out NULL and zero-gates; returns gate_zero() if all tokens
1683 * are trivial, or a single token if only one remains. When the
1684 * multiset is one for which this session planted a certified gate
1685 * computing the same sum (see @c plant_canonical), that gate is returned.
1686 * This is how the bounded-hop reachability route keeps the natural
1687 * hop-discarding query on the linear evaluation route: it plants, for a
1688 * vertex's per-length tokens, a certified gate over its native
1689 * within-bound circuit. Otherwise the ordinary order-dependent recipe
1690 * is used, so ordinary plus gates (and their formula rendering) are
1691 * untouched.
1692 */
1693CREATE OR REPLACE FUNCTION provenance_plus(tokens UUID[])
1694 RETURNS UUID AS
1695 'provsql','provenance_plus' LANGUAGE C COST 100 STRICT PARALLEL SAFE IMMUTABLE;
1696
1697/**
1698 * @brief Declare the working table of a recursive CTE, just (re)created
1699 * (internal)
1700 *
1701 * Gates planted from now on belong to it; those planted for the table of
1702 * the same name it replaces, and for working tables that no longer exist,
1703 * are forgotten.
1704 *
1705 * @param work_name name of the temporary working table
1706 */
1707CREATE OR REPLACE FUNCTION planted_scope(work_name TEXT)
1708 RETURNS VOID AS
1709 'provsql','planted_scope' LANGUAGE C STRICT;
1710
1711/**
1712 * @brief Plant a certified gate in place of a sum or a product (internal)
1713 *
1714 * Creates a @p kind gate with the single child @p target at the canonical
1715 * address of the multiset @p tokens -- the v5 UUID of
1716 * @c 'plus-canonical{sorted tokens}' or @c 'times-canonical{…}', a recipe under which nothing else
1717 * creates gates -- and remembers the address in this session: while the
1718 * working table @p work_name exists, @c provenance_plus /
1719 * @c provenance_times given that very multiset return the planted gate.
1720 * The tokens are row tokens of the working table, a temporary table, so
1721 * the sum or product is computed by the session that planted, never by a
1722 * parallel worker; the store is not consulted, and sessions that planted
1723 * nothing pay nothing.
1724 *
1725 * @param work_name working table the tokens belong to
1726 * @param kind 'plus' or 'times'
1727 * @param tokens the multiset the planted gate stands for
1728 * @param target root of the certified circuit
1729 * @param info1 first info of the planted gate
1730 * @param info2 second info of the planted gate
1731 * @return the address of the planted gate
1732 */
1733CREATE OR REPLACE FUNCTION plant_canonical(
1734 work_name TEXT, kind TEXT, tokens UUID[], target UUID,
1735 info1 INT, info2 INT DEFAULT 0)
1736 RETURNS UUID AS
1737 'provsql','plant_canonical' LANGUAGE C STRICT;
1738
1739/**
1740 * @brief Driver for provenance over recursive queries (WITH RECURSIVE).
1741 *
1742 * Invoked by the planner hook (@c lower_recursive_cte in @c provsql.c) when it
1743 * lowers a recursive CTE whose body touches provenance-tracked relations. The
1744 * hook deparses the CTE body to SQL and calls this function, which runs naive
1745 * bottom-up (fixpoint) evaluation: each round re-evaluates the body
1746 * @c base @c UNION @c recursive over a tracked working table until the
1747 * provenance tokens stop changing. Every round goes through ProvSQL's normal
1748 * rewriting, so the recursive join yields @c times gates, the untracked base
1749 * branch yields @c gate_one, and the @c UNION yields the @c plus merge of
1750 * alternative derivations -- no provenance is plumbed by hand here. The result
1751 * is left in a tracked temp table named @p work_name, which the hook then scans
1752 * in place of the CTE.
1753 *
1754 * The working tables (@p work_name and a scratch @c provsql_rec_new) are created once and
1755 * reused across rounds (TRUNCATE + INSERT), so the round count never
1756 * accumulates relation locks. Because content-addressed gate UUIDs make
1757 * structurally identical sub-circuits share, the fixpoint test is an exact
1758 * relational @c EXCEPT and the circuit stays the shared (polynomial) form.
1759 *
1760 * Scope: UNION (set) recursion. On *acyclic* input the structural fixpoint is
1761 * reached and the resulting circuit is the universal provenance, sound for any
1762 * semiring. On *cyclic* input the circuit never stabilises structurally; when
1763 * the session's provenance class (@c provsql.provenance) is @c 'absorptive' or
1764 * @c 'BOOLEAN' we instead stop at the value-fixpoint bound (number of
1765 * derivable tuples) -- every minimal, tuple-repetition-free derivation is then
1766 * covered, and the longer ones are absorbed in any absorptive semiring (after
1767 * Deutch, Milo, Roy & Tannen, ICDT 2014) -- and wrap the resulting tokens in
1768 * the @c 'absorptive' assumption marker, so that non-absorptive semiring
1769 * evaluations (counting, why-provenance: genuinely infinite on cyclic data)
1770 * refuse them while probability, Boolean, formula-as-circuit and min-plus
1771 * evaluations proceed. Under the general classes, cyclic input trips the
1772 * @p max_iter guard.
1773 *
1774 * This function has no @c SET @c search_path on purpose: @p body_sql is the
1775 * caller's deparsed query and must resolve relation names in the caller's path.
1776 *
1777 * @param body_sql the recursive CTE body, e.g.
1778 * @c 'SELECT 1 UNION SELECT e.dst FROM edge e JOIN reach r ON e.src=r.node'
1779 * @param work_name the working relation name @p body_sql references (the CTE name)
1780 * @param colnames comma-separated user columns, e.g. @c 'node'
1781 * @param coldef column definitions for the working table, e.g. @c 'node INTEGER'
1782 * @param max_iter safety bound on fixpoint rounds (non-termination guard)
1783 */
1784CREATE OR REPLACE FUNCTION eval_recursive(
1785 body_sql TEXT,
1786 work_name TEXT,
1787 colnames TEXT,
1788 coldef TEXT,
1789 max_iter INT DEFAULT 1000)
1790 RETURNS VOID AS
1791$$
1792DECLARE
1793 changed BOOLEAN; -- circuit changed structurally this round
1794 set_stable BOOLEAN; -- user-column tuple set unchanged this round
1795 iters INT := 0;
1796 new_count INT; -- rows in provsql_rec_new this round (INSERT ROW_COUNT)
1797 -- The derivations of a tuple can repeat through the tuple itself although
1798 -- the tuple set stabilises: on cyclic data, but also on acyclic data through
1799 -- a null-padded row that re-derives itself or a projection onto constants.
1800 -- The circuit then keeps growing, one summand per round, and only an
1801 -- absorptive class has a value for it: 1 ⊕ a = 1 gives x ⊕ x ⊗ y = x, so a
1802 -- derivation that extends another is absorbed by it, and every step of a
1803 -- cycle contracts -- a ⊖ b <= a by residuation, so a monus in the cycle is
1804 -- covered too, not only a product. (Where the surplus derivation adds
1805 -- nothing at all, as a null-padded row re-deriving itself does, the same
1806 -- fact reads as idempotence, a ⊕ a = a.) A
1807 -- minimal derivation cannot repeat a tuple, so it has depth <= (number of
1808 -- derivable tuples); after that many naive rounds the value equals the least
1809 -- fixpoint of an absorptive class, and the surplus derivations are absorbed
1810 -- at evaluation time. We learn that bound from the tuple-set fixpoint: in
1811 -- an absorptive class we stop there and mark the tokens with the
1812 -- 'absorptive' assumption, so evaluation under a non-absorptive semiring
1813 -- refuses rather than silently returning a truncated value; in another class
1814 -- there is no value to give, and reaching the bound is what tells us so --
1815 -- the refusal comes then, rather than after max_iter rounds of building
1816 -- circuit for an answer that will not come.
1817 absorptive_mode BOOLEAN :=
1818 coalesce(current_setting('provsql.provenance', true), 'semiring')
1819 IN ('absorptive', 'BOOLEAN');
1820 truncated BOOLEAN := false; -- exited at the value fixpoint
1821 ntuples INT := NULL; -- the bound above, set once the tuple set stabilises
1822BEGIN
1823 EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
1824 DROP TABLE IF EXISTS provsql_rec_new;
1825
1826 -- Tracked working table (carries provsql), initially empty, plus a scratch
1827 -- table of the same shape; both reused across rounds.
1828 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID) ON COMMIT DROP',
1829 work_name, coldef);
1830 PERFORM provsql.planted_scope(work_name);
1831 EXECUTE format('CREATE TEMP TABLE provsql_rec_new (LIKE %I) ON COMMIT DROP',
1832 work_name);
1833
1834 LOOP
1835 iters := iters + 1;
1836 -- Hard safety bound (also catches genuinely unbounded recursion, e.g. an
1837 -- unbounded counter, where even the tuple set never stabilises).
1838 IF iters > max_iter THEN
1839 /* Not even the rows stop changing: the recursion derives tuples without
1840 * end (an unbounded counter), which SQL does not terminate on either.
1841 * A recursion whose rows settle while its derivations repeat exits at
1842 * the value fixpoint below, tagged, and never reaches this. */
1843 RAISE EXCEPTION 'ProvSQL: the rounds of this recursion do not reach a '
1844 'fixpoint (after % of them): the rows it derives keep '
1845 'changing, so there is no fixpoint to annotate -- plain '
1846 'SQL does not terminate on such a recursion either',
1847 max_iter
1848 USING ERRCODE = 'feature_not_supported',
1849 DETAIL = 'provsql-reason: recursion-no-fixpoint; scope: deliberate';
1850 END IF;
1851
1852 -- One round of naive evaluation: re-run the CTE body over the current
1853 -- working table. INSERT targets a tracked table, so ProvSQL fills provsql.
1854 -- Take the row count from the INSERT itself (counting provsql_rec_new directly would be
1855 -- an aggregate over a provenance-tracked table -> an AGG_TOKEN).
1856 EXECUTE 'TRUNCATE provsql_rec_new';
1857 EXECUTE format('INSERT INTO provsql_rec_new(%s) %s', colnames, body_sql);
1858 GET DIAGNOSTICS new_count = ROW_COUNT;
1859
1860 -- Exact structural fixpoint test (content-addressed tokens => set equality).
1861 EXECUTE format(
1862 'SELECT EXISTS((TABLE provsql_rec_new EXCEPT TABLE %1$I) UNION ALL (TABLE %1$I EXCEPT TABLE provsql_rec_new))',
1863 work_name) INTO changed;
1864
1865 -- Learn the round bound from the tuple-set fixpoint (the set stabilises
1866 -- after finitely many rounds even where the derivations do not).
1867 IF ntuples IS NULL THEN
1868 EXECUTE format(
1869 'SELECT NOT EXISTS('
1870 || '(SELECT %2$s FROM provsql_rec_new EXCEPT SELECT %2$s FROM %1$I) UNION ALL '
1871 || '(SELECT %2$s FROM %1$I EXCEPT SELECT %2$s FROM provsql_rec_new))',
1872 work_name, colnames) INTO set_stable;
1873 IF set_stable THEN
1874 ntuples := new_count;
1875 END IF;
1876 END IF;
1877
1878 -- Copy provsql_rec_new into the working table (tracked -> tracked carries the tokens).
1879 EXECUTE format('TRUNCATE %I', work_name);
1880 EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM provsql_rec_new', work_name, colnames);
1881
1882 -- Structural fixpoint: done (acyclic / fully converged) -- sound for any
1883 -- semiring.
1884 EXIT WHEN NOT changed;
1885
1886 -- The derivations repeat through a tuple: the rows have settled, the
1887 -- circuit has not, and it will not (one summand per round from here on).
1888 -- The bound is the tuple-set fixpoint plus one confirming round, so that a
1889 -- recursion whose token depth merely lags the tuple-set saturation still
1890 -- exits through the structural test above, untagged.
1891 IF ntuples IS NOT NULL AND iters >= ntuples + 1 THEN
1892 IF absorptive_mode THEN
1893 -- The value of an absorptive class is reached: stop, tagged below.
1894 truncated := true;
1895 EXIT;
1896 END IF;
1897 /* No absorption: the annotation of such a tuple gains a term per round
1898 * and has no value, so there is nothing to return -- a refusal by what
1899 * the recursion means, not a limit of the driver. */
1900 RAISE EXCEPTION 'ProvSQL: the rounds of this recursion do not reach a '
1901 'fixpoint (the rows settled after % of them, the '
1902 'derivations did not): a tuple is derived through '
1903 'itself, which cyclic data does, and so does a '
1904 'null-padded row that re-derives itself or a projection '
1905 'onto constants on acyclic data, so its annotation '
1906 'gains a term at every round. Only an absorptive '
1907 'provenance class has a value for it (set '
1908 'provsql.provenance to absorptive or to BOOLEAN)',
1909 iters
1910 USING ERRCODE = 'feature_not_supported',
1911 DETAIL = 'provsql-reason: recursion-no-fixpoint; scope: deliberate';
1912 END IF;
1913 END LOOP;
1914
1915 -- Tokens of a truncated fixpoint are sound only under absorptive
1916 -- evaluation: RECORD that in the circuit itself.
1917 IF truncated THEN
1918 EXECUTE format(
1919 'UPDATE %I SET provsql = provsql.provenance_assume(provsql, ''absorptive'')',
1920 work_name);
1921 END IF;
1922END
1923$$ LANGUAGE plpgsql SET client_min_messages = warning;
1924
1925/**
1926 * @brief Drive a @c UNION @c ALL recursion, one round per bag of derivations
1927 *
1928 * The bag recursion is not a fixpoint over a set: its rounds are
1929 * @c M0 @c = @c q0 and @c M(i+1) @c = @c q1 over @c Mi -- the PREVIOUS round,
1930 * not what has been derived so far -- and its answer is the bag union of every
1931 * round, which ends when a round derives nothing. Each tuple of it is one
1932 * derivation, annotated by the product along that derivation, and two
1933 * derivations of the same tuple are two rows and are not merged: that is what
1934 * distinguishes it from @c UNION, whose driver (@c eval_recursive) sums the
1935 * derivations of a tuple into one row and stops when the set of rows stops
1936 * changing.
1937 *
1938 * @c work_name holds the previous round, which the recursive term reads by the
1939 * name of the CTE; @c all_name accumulates the answer and is what the query
1940 * reads. Ending on an empty round is SQL's own rule, so a recursion PostgreSQL
1941 * runs to completion ends here too, and one it does not is caught by
1942 * @p max_iter as before.
1943 *
1944 * @param q0_sql the non-recursive term, as SQL
1945 * @param q1_sql the recursive term, reading @p work_name
1946 * @param work_name temp table of the previous round (the CTE's name)
1947 * @param all_name temp table accumulating the rounds
1948 * @param colnames comma-separated user column names
1949 * @param coldef column definitions ("name type, ...")
1950 * @param max_iter safety bound on the number of rounds
1951 */
1952CREATE OR REPLACE FUNCTION eval_recursive_all(
1953 q0_sql TEXT,
1954 q1_sql TEXT,
1955 work_name TEXT,
1956 all_name TEXT,
1957 colnames TEXT,
1958 coldef TEXT,
1959 max_iter INT DEFAULT 1000)
1960 RETURNS VOID AS
1961$$
1962DECLARE
1963 iters INT := 0;
1964 new_count INT;
1965BEGIN
1966 EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
1967 EXECUTE format('DROP TABLE IF EXISTS %I', all_name);
1968 DROP TABLE IF EXISTS provsql_rec_new;
1969
1970 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID) ON COMMIT DROP',
1971 work_name, coldef);
1972 EXECUTE format('CREATE TEMP TABLE %I (LIKE %I) ON COMMIT DROP',
1973 all_name, work_name);
1974 EXECUTE format('CREATE TEMP TABLE provsql_rec_new (LIKE %I) ON COMMIT DROP',
1975 work_name);
1976 PERFORM provsql.planted_scope(all_name);
1977
1978 -- The first round: the non-recursive term.
1979 EXECUTE format('INSERT INTO provsql_rec_new(%s) %s', colnames, q0_sql);
1980 GET DIAGNOSTICS new_count = ROW_COUNT;
1981
1982 LOOP
1983 EXIT WHEN new_count = 0; -- a round that derives nothing ends the answer
1984
1985 -- Every row of the round is an answer, kept as it is: a second derivation
1986 -- of a tuple is a second row, which is what UNION ALL says.
1987 EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM provsql_rec_new',
1988 all_name, colnames);
1989
1990 -- The round becomes the relation the recursive term reads.
1991 EXECUTE format('TRUNCATE %I', work_name);
1992 EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM provsql_rec_new',
1993 work_name, colnames);
1994
1995 iters := iters + 1;
1996 IF iters > max_iter THEN
1997 /* A bag recursion is defined when a round derives nothing; one whose
1998 * rounds do not end has no answer to give, in SQL either (PostgreSQL
1999 * runs it forever), so this is what the recursion means and not a limit
2000 * of the driver. */
2001 RAISE EXCEPTION 'ProvSQL: the rounds of this UNION ALL recursion do not '
2002 'end (after % of them): its answer is the rows of every '
2003 'round, which SQL itself does not reach either on such '
2004 'data', max_iter
2005 USING ERRCODE = 'feature_not_supported',
2006 DETAIL = 'provsql-reason: recursion-does-not-end; scope: deliberate';
2007 END IF;
2008
2009 EXECUTE format('TRUNCATE provsql_rec_new');
2010 EXECUTE format('INSERT INTO provsql_rec_new(%s) %s', colnames, q1_sql);
2011 GET DIAGNOSTICS new_count = ROW_COUNT;
2012 END LOOP;
2013END
2014$$ LANGUAGE plpgsql SET client_min_messages = warning;
2015
2016/**
2017 * @brief Create a comparison gate for HAVING clause provenance
2018 *
2019 * @param left_token provenance token for the left operand
2020 * @param comparison_op OID of the comparison operator
2021 * @param right_token provenance token for the right operand
2022 *
2023 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
2024 * PL/pgSQL function it was, so that plans stay the same.
2025 */
2026CREATE OR REPLACE FUNCTION provenance_cmp(
2027 left_token UUID,
2028 comparison_op OID,
2029 right_token UUID
2030)
2031RETURNS UUID AS
2032 'provsql','provenance_cmp' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
2033
2034/**
2035 * @brief The factors of a row annotation an aggregate comparison does not
2036 * subsume.
2037 *
2038 * A lifted comparison entails the existence of the group it ranges over, so it
2039 * supersedes that group's @c gate_delta instead of multiplying with it. This
2040 * reports which factors of @p tokens survive that supersede: a bare δ over the
2041 * compared group disappears, a @c times keeps its other factors, and anything
2042 * else -- an earlier comparison on the same group, an input -- is kept whole.
2043 */
2044CREATE FUNCTION cmp_surviving_factors(tokens UUID[], cmp UUID)
2045 RETURNS UUID[] AS
2046 'provsql', 'cmp_surviving_factors' LANGUAGE C PARALLEL SAFE STABLE;
2047
2048/**
2049 * @brief Combine a lifted aggregate comparison with the row annotation it
2050 * supersedes only part of.
2051 *
2052 * @param cmp Gate of the lifted comparison.
2053 * @param tokens Row-annotation factors at the level owning the comparison.
2054 * @return @c cmp multiplied with whatever of @p tokens it does not subsume.
2056CREATE OR REPLACE FUNCTION provenance_cmp_times(cmp UUID, tokens UUID[])
2057 RETURNS UUID AS
2058$$
2059DECLARE
2060 kept UUID[];
2061BEGIN
2062 kept := provsql.cmp_surviving_factors(tokens, cmp);
2063 IF kept IS NULL OR array_length(kept, 1) IS NULL THEN
2064 RETURN cmp;
2065 END IF;
2066 RETURN provsql.provenance_times(VARIADIC kept || cmp);
2067END
2068$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
2069
2070/**
2071 * @brief Create an arithmetic gate over scalar-valued provenance children
2072 *
2073 * Builds a deterministic @c gate_arith from an operator tag and an
2074 * ordered list of children. The tag is one of the @c provsql_arith_op
2075 * ENUM values declared in @c src/provsql_utils.h
2076 * (@c PLUS=0, @c TIMES=1, @c MINUS=2, @c DIV=3, @c NEG=4) and is
2077 * stored in the gate's @c info1 field. Children must be UUIDs of
2078 * scalar-producing gates (@c gate_rv, @c gate_value, or another
2079 * @c gate_arith). The token UUID is derived deterministically from
2080 * @p op and @p children so identical sub-expressions share their gate.
2081 *
2082 * @param op Operator tag (@c provsql_arith_op).
2083 * @param children Ordered list of child gate UUIDs.
2084 * @return UUID of the (possibly pre-existing) @c gate_arith.
2085 *
2086 * Implemented in C (<tt>gate_builders.c</tt>): the gate is created together
2087 * with what it records, in one unanswered message.
2088 */
2089CREATE OR REPLACE FUNCTION provenance_arith(
2090 op INTEGER,
2091 children UUID[]
2092)
2093RETURNS UUID AS
2094 'provsql','provenance_arith' LANGUAGE C COST 100 STRICT PARALLEL SAFE IMMUTABLE;
2095
2096/**
2097 * @brief Create a guarded-selection gate over scalar (RV) children.
2098 *
2099 * Builds a deterministic @c gate_case from the flattened wire list
2100 * @c [guard_1, value_1, ..., guard_k, value_k, default] (odd length): the
2101 * value of the first guard event that holds, else the default (first-match
2102 * semantics). Each guard is a Boolean event token (a @c gate_cmp or Boolean
2103 * combination); each value and the default are scalar-producing gates
2104 * (@c gate_rv, @c gate_value, @c gate_arith, another @c gate_case, ...). The
2105 * token UUID is derived deterministically from @p children so identical
2106 * @c CASE expressions share their gate.
2107 *
2108 * @param children Flattened guard/value wires ending with the default
2109 * (@c array_length must be odd and @c >= 1).
2110 * @return UUID of the (possibly pre-existing) @c gate_case.
2111 */
2112CREATE OR REPLACE FUNCTION provenance_case(
2113 children UUID[]
2114)
2115RETURNS UUID AS
2116$$
2117DECLARE
2118 case_token UUID;
2119BEGIN
2120 IF array_length(children, 1) IS NULL OR array_length(children, 1) % 2 = 0 THEN
2121 RAISE EXCEPTION 'provenance_case expects an odd number of children '
2122 '(guard/value pairs followed by a default), got %',
2123 coalesce(array_length(children, 1), 0);
2124 END IF;
2125 case_token := public.uuid_generate_v5(
2126 uuid_ns_provsql(),
2127 concat('case', children::TEXT)
2128 );
2129 PERFORM create_gate(case_token, 'case', children);
2130 RETURN case_token;
2131END
2132$$ LANGUAGE plpgsql
2133 SET search_path=provsql,pg_temp,public
2134 SECURITY DEFINER
2135 IMMUTABLE
2136 PARALLEL SAFE
2137 STRICT;
2138
2139/** @} */
2140
2141/** @defgroup semiring_evaluation Semiring evaluation
2142 * Functions for evaluating provenance circuits over semirings,
2143 * both user-defined (via function references) and compiled (built-in).
2144 * @{
2145 */
2146
2147/**
2148 * @brief Evaluate provenance using a compiled (built-in) semiring
2149 *
2150 * This C function handles semiring evaluation entirely in C++ for
2151 * better performance. The semiring is specified by name.
2152 *
2153 * @param token provenance token to evaluate
2154 * @param token2value mapping table from tokens to semiring values
2155 * @param semiring name of the compiled semiring (e.g., "formula", "counting")
2156 * @param element_one identity element of the semiring
2157 */
2158CREATE OR REPLACE FUNCTION provenance_evaluate_compiled(
2159 token UUID,
2160 token2value REGCLASS,
2161 semiring TEXT,
2162 element_one ANYELEMENT)
2163RETURNS ANYELEMENT AS
2164 'provsql', 'provenance_evaluate_compiled' LANGUAGE C PARALLEL SAFE STABLE;
2165
2166
2167/**
2168 * @brief Evaluate provenance over a user-defined semiring (PL/pgSQL version)
2169 *
2170 * Recursively walks the provenance circuit and evaluates each gate
2171 * using the provided semiring operations. This is the generic version
2172 * that accepts semiring operations as function references.
2173 *
2174 * @param token provenance token to evaluate
2175 * @param token2value mapping table from tokens to semiring values
2176 * @param element_one identity element of the semiring
2177 * @param value_type OID of the semiring value type
2178 * @param plus_function semiring addition (aggregate)
2179 * @param times_function semiring multiplication (aggregate)
2180 * @param monus_function semiring monus (binary), or NULL
2181 * @param delta_function δ-semiring operator, or NULL
2182 */
2183CREATE OR REPLACE FUNCTION provenance_evaluate(
2184 token UUID,
2185 token2value REGCLASS,
2186 element_one ANYELEMENT,
2187 value_type REGTYPE,
2188 plus_function REGPROC,
2189 times_function REGPROC,
2190 monus_function REGPROC,
2191 delta_function REGPROC)
2192 RETURNS ANYELEMENT AS
2193$$
2194DECLARE
2195 gate_type PROVENANCE_GATE;
2196 result ALIAS FOR $0;
2197 children UUID[];
2198-- cmp_value ANYELEMENT;
2199-- temp_result ANYELEMENT;
2200 value_text TEXT;
2201BEGIN
2202 SELECT get_gate_type(token) INTO gate_type;
2203
2204 IF gate_type IS NULL THEN
2205 RETURN NULL;
2206
2207 ELSIF gate_type = 'input' THEN
2208 EXECUTE format('SELECT value FROM %s WHERE provenance=%L', token2value, token)
2209 INTO result;
2210 IF result IS NULL THEN
2211 result := element_one;
2212 END IF;
2213 ELSIF gate_type = 'mulinput' THEN
2214 SELECT concat('{',(get_children(token))[1]::TEXT,'=',(get_infos(token)).info1,'}')
2215 INTO result;
2216 ELSIF gate_type='update' THEN
2217 EXECUTE format('SELECT value FROM %s WHERE provenance=%L',token2value,token) INTO result;
2218 IF result IS NULL THEN
2219 result:=element_one;
2220 END IF;
2221 ELSIF gate_type = 'plus' THEN
2222 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
2223 plus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
2224 INTO result;
2225
2226 ELSIF gate_type = 'times' THEN
2227 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
2228 times_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
2229 INTO result;
2230
2231 ELSIF gate_type = 'monus' THEN
2232 IF monus_function IS NULL THEN
2233 RAISE EXCEPTION USING MESSAGE='Provenance with negation evaluated over a semiring without monus function',
2234 DETAIL = 'provsql-reason: semiring-no-monus; scope: deliberate';
2235 ELSE
2236 EXECUTE format('SELECT %s(a1,a2) FROM (SELECT provsql.provenance_evaluate(c[1],%L,%L::%s,%L,%L,%L,%L,%L) AS a1, ' ||
2237 'provsql.provenance_evaluate(c[2],%L,%L::%s,%L,%L,%L,%L,%L) AS a2 FROM get_children(%L) c) tmp',
2238 monus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function,
2239 token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
2240 INTO result;
2241 END IF;
2242
2243 ELSIF gate_type = 'eq' THEN
2244 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2245 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2246 INTO result;
2247
2248/* elsif gate_type = 'cmp' then
2250 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2251 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2252 INTO temp_result;
2253
2254 EXECUTE format('SELECT get_extra((get_children(%L))[2])', token)
2255 INTO cmp_value;
2256
2257 IF temp_result::TEXT = cmp_value::TEXT THEN
2258 SELECT concat('{',temp_result::TEXT,'=',cmp_value::TEXT,'}')
2259 INTO result;
2260 ELSE
2261 RETURN gate_zero()
2262 */
2263
2264
2266 ELSIF gate_type = 'delta' THEN
2267 IF delta_function IS NULL THEN
2268 RAISE EXCEPTION USING MESSAGE='Provenance with aggregation evaluated over a semiring without delta function',
2269 DETAIL = 'provsql-reason: semiring-no-delta; scope: deliberate';
2270 ELSE
2271 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',
2272 delta_function, token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2273 INTO result;
2274 END IF;
2275
2276 ELSIF gate_type = 'zero' THEN
2277 EXECUTE format('SELECT %I(a) FROM (SELECT %L::%I AS a WHERE FALSE) temp', plus_function, element_one, value_type)
2278 INTO result;
2279
2280 ELSIF gate_type = 'one' THEN
2281 EXECUTE format('SELECT %L::%I', element_one, value_type)
2282 INTO result;
2283
2284 ELSIF gate_type = 'project' THEN
2285 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2286 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2287 INTO result;
2289 ELSIF gate_type = 'annotation' THEN
2290 -- Transparent single-child wrapper (carries the inversion-free certificate
2291 -- / per-input order keys in extra, inert for every semiring): evaluate
2292 -- through to the child, like 'project'.
2293 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
2294 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
2295 INTO result;
2297 ELSE
2298 RAISE EXCEPTION USING MESSAGE='provenance_evaluate cannot be called on formulas using ' || gate_type || ' gates; use compiled semirings instead',
2299 DETAIL = 'provsql-reason: evaluate-gate-kind; scope: gap';
2300 END IF;
2301
2302 RETURN result;
2303END
2304$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
2305
2306
2307/**
2308 * @brief Evaluate provenance over a user-defined semiring (C version)
2310 * Optimized C implementation of provenance_evaluate. Infers the
2311 * value type from element_one. Monus and delta functions are optional.
2312 *
2313 * @param token provenance token to evaluate
2314 * @param token2value mapping table from tokens to semiring values
2315 * @param element_one identity element of the semiring
2316 * @param plus_function semiring addition (aggregate)
2317 * @param times_function semiring multiplication (aggregate)
2318 * @param monus_function semiring monus, or NULL if not needed
2319 * @param delta_function δ-semiring operator, or NULL if not needed
2320 */
2321CREATE OR REPLACE FUNCTION provenance_evaluate(
2322 token UUID,
2323 token2value REGCLASS,
2324 element_one ANYELEMENT,
2325 plus_function REGPROC,
2326 times_function REGPROC,
2327 monus_function REGPROC = NULL,
2328 delta_function REGPROC = NULL)
2329 RETURNS ANYELEMENT AS
2330 'provsql','provenance_evaluate' LANGUAGE C STABLE;
2331
2332/** @} */
2333
2334/** @defgroup circuit_introspection Circuit introspection
2335 * Functions for examining the structure of provenance circuits,
2336 * used by visualization and where-provenance features.
2337 * @{
2338 */
2339
2340/** @brief Row type for sub_circuit_with_desc results */
2341CREATE TYPE GATE_WITH_DESC AS (f UUID, t UUID, gate_type PROVENANCE_GATE, desc_str CHARACTER VARYING, infos INTEGER[], extra TEXT);
2342
2343/**
2344 * @brief Return the sub-circuit reachable from a token, with descriptions
2345 *
2346 * Recursively traverses the provenance circuit from the given token and
2347 * returns all edges together with input gate descriptions from the
2348 * mapping table.
2350 * @param token root provenance token
2351 * @param token2desc mapping table providing descriptions for input gates
2352 */
2353CREATE OR REPLACE FUNCTION sub_circuit_with_desc(
2354 token UUID,
2355 token2desc REGCLASS) RETURNS SETOF GATE_WITH_DESC AS
2356$$
2357BEGIN
2358 RETURN QUERY EXECUTE
2359 'WITH RECURSIVE transitive_closure(f,t,gate_type) AS (
2360 SELECT $1,t,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) AS t
2361 UNION ALL
2362 SELECT p1.t,u,provsql.get_gate_type(p1.t) FROM transitive_closure p1, unnest(provsql.get_children(p1.t)) AS u)
2363 SELECT *, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
2364 SELECT f::UUID,t::UUID,gate_type,NULL FROM transitive_closure
2365 UNION ALL
2366 SELECT p2.provenance::UUID as f, NULL::UUID, ''input'', CAST (p2.value AS varchar) FROM transitive_closure p1 JOIN ' || token2desc || ' AS p2
2367 ON p2.provenance=t
2368 UNION ALL
2369 SELECT provenance::UUID as f, NULL::UUID, ''input'', CAST (value AS varchar) FROM ' || token2desc || ' WHERE provenance=$1
2370 ) t'
2371 USING token LOOP;
2372 RETURN;
2373END
2374$$ LANGUAGE plpgsql PARALLEL SAFE;
2375
2376/**
2377 * @brief Identify which table and how many columns a provenance token belongs to
2378 *
2379 * Searches all provenance-tracked tables for a row matching the given
2380 * token and returns the table name and column count.
2381 *
2382 * @param token provenance token to look up
2383 * @param table_name (OUT) the table containing this token
2384 * @param nb_columns (OUT) number of non-provenance columns in that table
2385 */
2386CREATE OR REPLACE FUNCTION identify_token(
2387 token UUID, OUT table_name REGCLASS, OUT nb_columns INTEGER) AS
2388$$
2389DECLARE
2390 t RECORD;
2391 result RECORD;
2392BEGIN
2393 table_name:=NULL;
2394 nb_columns:=-1;
2395 FOR t IN
2396 SELECT relname,
2397 (SELECT count(*) FROM pg_attribute a2 WHERE a2.attrelid=a1.attrelid AND attnum>0 AND atttypid<>0)-1 c
2398 FROM pg_attribute a1 JOIN pg_type ON atttypid=pg_type.oid
2399 JOIN pg_class ON attrelid=pg_class.oid
2400 JOIN pg_namespace ON relnamespace=pg_namespace.oid
2401 WHERE typname='UUID' AND relkind='r'
2402 AND nspname<>'provsql'
2403 AND attname='provsql'
2404 LOOP
2405 EXECUTE format('SELECT * FROM %I WHERE provsql=%L',t.relname,token) INTO result;
2406 -- Test result.provsql rather than the whole RECORD: "RECORD IS NOT NULL"
2407 -- is true only when every field is non-null, so a matched row that has any
2408 -- NULL data column would be wrongly skipped. The provsql column is the
2409 -- (non-null) token we matched on, so it is set iff a row was found.
2410 IF result.provsql IS NOT NULL THEN
2411 table_name:=t.relname;
2412 nb_columns:=t.c;
2413 EXIT;
2414 END IF;
2415 END LOOP;
2416END
2417$$ LANGUAGE plpgsql STRICT;
2418
2419/**
2420 * @brief Return the sub-circuit for where-provenance computation
2421 *
2422 * Similar to sub_circuit_with_desc but resolves input gates to their
2423 * source table and column count for where-provenance evaluation.
2424 */
2425CREATE OR REPLACE FUNCTION sub_circuit_for_where(token UUID)
2426 RETURNS TABLE(f UUID, t UUID, gate_type PROVENANCE_GATE, table_name REGCLASS, nb_columns INTEGER, infos INTEGER[], extra TEXT) AS
2427$$
2428 WITH RECURSIVE transitive_closure(f,t,idx,gate_type) AS (
2429 SELECT $1,t,id,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) WITH ORDINALITY AS a(t,id)
2430 UNION ALL
2431 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)
2432 ) SELECT f, t, gate_type, table_name, nb_columns, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
2433 -- One row per distinct (parent, child, child-position) edge. The
2434 -- recursive closure (UNION ALL) re-emits a gate's outgoing edges once per
2435 -- path that reaches it, so a *shared* non-input gate would otherwise be
2436 -- reported with duplicate edges; DISTINCT on the (f,t,idx) triple
2437 -- collapses those while keeping genuine repeated children (same f,t,
2438 -- different idx, e.g. a self-product). Without this, a shared
2439 -- single-child gate (notably an inversion-free order-marker annotation)
2440 -- gets its child wired k times in the where-circuit -> the locator sets
2441 -- are duplicated k-fold.
2442 SELECT DISTINCT f, t::UUID, idx, gate_type, NULL::REGCLASS AS table_name, NULL::INTEGER AS nb_columns FROM transitive_closure
2443 UNION ALL
2444 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
2445 UNION ALL
2446 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
2447 ) t
2448 -- order each parent's edges by child position so the where-circuit's TIMES
2449 -- concatenation reproduces the column order (input rows have idx NULL).
2450 ORDER BY f, idx
2451$$
2452LANGUAGE sql;
2453
2454/**
2455 * @brief BFS expansion of a provenance circuit, capped at @p max_depth
2456 *
2457 * Returns one row per (parent, child) edge in the BFS-bounded subgraph
2458 * rooted at @p root, plus one row for the root with <tt>parent</tt> and
2459 * <tt>child_pos</tt> NULL. Provenance circuits are DAGs, so a child gate
2460 * may have several parents within the bound; each such edge is reported
2461 * as a separate row, so callers must deduplicate on <tt>node</tt> if they
2462 * need a one-row-per-node view.
2463 *
2464 * <tt>depth</tt> is the node's longest-path distance from @p root
2465 * within the depth bound (the standard circuit-depth notion), so for
2466 * an edge (parent, child) it is the case that
2467 * <tt>child.depth &gt;= parent.depth + 1</tt>, except at the
2468 * <tt>max_depth</tt> truncation frontier. A node at
2469 * <tt>depth = max_depth</tt> is not
2470 * expanded; callers can detect a partial expansion by comparing
2471 * <tt>provsql.get_children</tt> length against the number of outgoing
2472 * edges reported.
2473 *
2474 * <tt>info1</tt> and <tt>info2</tt> are the INTEGER values recorded on
2475 * the gate when it was created, formatted as TEXT; their meaning is
2476 * gate-type-specific.
2477 *
2478 * @param root root provenance token
2479 * @param max_depth maximum BFS depth (default 8)
2480 */
2481CREATE OR REPLACE FUNCTION circuit_subgraph(root UUID, max_depth INT DEFAULT 8)
2482 RETURNS TABLE(node UUID, parent UUID, child_pos INT, gate_type TEXT, info1 TEXT, info2 TEXT, depth INT) AS
2483$$
2484 WITH RECURSIVE bfs(node, parent, child_pos, depth) AS (
2485 SELECT root, NULL::UUID, NULL::INT, 0
2486 UNION ALL
2487 SELECT c.t, b.node, c.idx::INT, b.depth + 1
2488 FROM bfs b
2489 CROSS JOIN LATERAL unnest(provsql.get_children(b.node))
2490 WITH ORDINALITY AS c(t, idx)
2491 WHERE b.depth < max_depth
2492 ),
2493 -- Each node's canonical depth is its longest-path distance from the
2494 -- root (the standard circuit-depth notion: the longest chain of
2495 -- gates separating the node from the output). The recursive CTE
2496 -- enumerates paths up to @c max_depth, so MAX over those is the
2497 -- longest path of length at most @c max_depth.
2498 node_depth AS (
2499 SELECT node, MAX(depth) AS depth FROM bfs GROUP BY node
2500 ),
2501 -- All distinct (parent, child, child_pos) triples seen during the BFS.
2502 -- A child reached from k parents within the bound contributes k rows.
2503 -- Self-joins (times(x, x)) contribute one row per child position.
2504 edges AS (
2505 SELECT DISTINCT parent, node AS child, child_pos
2506 FROM bfs WHERE parent IS NOT NULL
2507 )
2508 SELECT
2509 d.node,
2510 e.parent,
2511 e.child_pos,
2512 provsql.get_gate_type(d.node)::TEXT,
2513 i.info1::TEXT,
2514 i.info2::TEXT,
2515 d.depth
2516 FROM node_depth d
2517 LEFT JOIN edges e ON e.child = d.node
2518 LEFT JOIN LATERAL provsql.get_infos(d.node) i ON TRUE
2519 ORDER BY d.depth, d.node, e.parent;
2520$$ LANGUAGE sql STABLE PARALLEL SAFE;
2521
2522/**
2523 * @brief BFS subgraph of the IN-MEMORY simplified circuit rooted at @p root.
2524 *
2525 * Same row shape as @ref circuit_subgraph plus an inline @c extra
2526 * column, but built from the @c GenericCircuit returned by
2527 * @c getGenericCircuit -- i.e. AFTER @c provsql.simplify_on_load
2528 * passes (RangeCheck, ...) have rewritten any decidable @c gate_cmp
2529 * into Bernoulli @c gate_input / @c gate_zero / @c gate_one leaves.
2530 * Lets a renderer show the user what the evaluator actually sees,
2531 * without mutating the persisted DAG.
2532 *
2533 * Returns @c jsonb (an array of objects) rather than @c SETOF RECORD
2534 * to keep the C++ implementation free of SRF / @c FuncCallContext
2535 * boilerplate; callers either consume the array directly or expand
2536 * it via @c jsonb_array_elements.
2537 *
2538 * @param root Root provenance token.
2539 * @param max_depth Maximum BFS depth (default 8).
2540 */
2541CREATE OR REPLACE FUNCTION simplified_circuit_subgraph(
2542 root UUID, max_depth INT DEFAULT 8) RETURNS jsonb
2543 AS 'provsql','simplified_circuit_subgraph'
2544 LANGUAGE C STABLE PARALLEL SAFE;
2545
2546/**
2547 * @brief Empirical histogram of a scalar sub-circuit
2548 *
2549 * Returns a jsonb array of @c {bin_lo, bin_hi, count} objects covering
2550 * the observed @c [min, max] range of @p bins equal-width samples from
2551 * the sub-circuit rooted at @p token. Sample count is taken from
2552 * @c provsql.rv_mc_samples; pinning @c provsql.monte_carlo_seed makes
2553 * the result reproducible.
2554 *
2555 * Accepted root gate types are the scalar ones: @c gate_value (Dirac
2556 * at the constant, single bin), @c gate_rv (sampled from the leaf's
2557 * distribution), and @c gate_arith (sampled by recursing through the
2558 * arithmetic DAG, with shared @c gate_rv leaves correctly correlated
2559 * within an iteration). Any other gate type raises.
2560 *
2561 * @param token Root provenance token of a scalar sub-circuit.
2562 * @param bins Number of equal-width histogram bins (default 30).
2563 * @param prov Conditioning event (defaults to @c gate_one() = no
2564 * conditioning). When non-trivial, the histogram is
2565 * over the conditional distribution recovered by
2566 * rejection sampling on the joint circuit with @p token.
2567 */
2568CREATE OR REPLACE FUNCTION rv_histogram(
2569 token UUID, bins INT DEFAULT 30, prov UUID DEFAULT gate_one())
2570 RETURNS jsonb
2571 AS 'provsql','rv_histogram'
2572 LANGUAGE C VOLATILE PARALLEL SAFE;
2573
2574/**
2575 * @brief Sample the closed-form PDF and CDF of a (possibly truncated)
2576 * scalar distribution.
2577 *
2578 * Returns @c {"pdf": [{x, p}, ...], "cdf": [{x, p}, ...]} with @p samples
2579 * evenly-spaced points spanning the distribution's natural display
2580 * range (intersected with the conditioning event's interval when
2581 * @c prov is non-trivial). Used by ProvSQL Studio's Distribution
2582 * profile panel to overlay the analytical curve on the empirical
2583 * histogram from :sqlfunc:`rv_histogram` -- the simplifier's
2584 * analytical wins (e.g. @c c·Exp(λ) folding to @c Exp(λ/c)) become
2585 * visible as a smooth curve riding over the MC-sampled bars.
2586 *
2587 * Returns @c NULL when the root sub-circuit is not a closed-form
2588 * shape (V1: only bare @c gate_rv of Normal / Uniform / Exponential
2589 * / INTEGER-Erlang). The frontend reads @c NULL as "skip overlay"
2590 * without erroring, so the caller can dispatch this in parallel with
2591 * @c rv_histogram regardless of the underlying shape.
2592 *
2593 * @param token Scalar gate token (random_variable's UUID).
2594 * @param samples Number of (x, p) points; must be >= 2.
2595 * @param prov Conditioning event (defaults to @c gate_one() = no
2596 * conditioning). When non-trivial, the curves are
2597 * over the truncated distribution.
2598 */
2599CREATE OR REPLACE FUNCTION rv_analytical_curves(
2600 token UUID, samples INT DEFAULT 100, prov UUID DEFAULT gate_one())
2601 RETURNS jsonb
2602 AS 'provsql','rv_analytical_curves'
2603 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2605/**
2606 * @brief Draw conditional Monte Carlo samples from a scalar gate.
2607 *
2608 * Returns up to @c n samples of the scalar value at @c token; when
2609 * @c prov is not the trivial @c gate_one() event, draws are accepted
2610 * only on iterations where @c prov evaluates true (rejection
2611 * sampling). Shared @c gate_rv leaves between @c token and @c prov
2612 * are loaded into a single joint circuit so the indicator's draw
2613 * and the value's draw share their per-iteration state.
2615 * @param token Scalar sub-circuit root.
2616 * @param n Number of accepted samples to attempt.
2617 * @param prov Conditioning event (defaults to @c gate_one() = no
2618 * conditioning).
2619 *
2620 * Emits a @c NOTICE when the conditional acceptance rate yields fewer
2621 * than @c n samples within the @c provsql.rv_mc_samples budget so the
2622 * caller can choose to widen the budget.
2623 */
2624CREATE OR REPLACE FUNCTION rv_sample(
2625 token UUID, n INTEGER, prov UUID DEFAULT gate_one())
2626 RETURNS SETOF float8
2627 AS 'provsql','rv_sample'
2628 LANGUAGE C VOLATILE PARALLEL SAFE;
2629
2630/**
2631 * @brief Resolve an input gate UUID back to its source row
2632 *
2633 * Searches every provenance-tracked relation for a row whose
2634 * <tt>provsql</tt> column equals @p UUID and returns the relation's
2635 * REGCLASS together with the row encoded as JSONB. Returns zero
2636 * rows when @p UUID is not the provenance token of any tracked row,
2637 * including when it identifies an internal gate (<tt>plus</tt>,
2638 * <tt>times</tt>, ...) rather than an input.
2639 *
2640 * Ordinarily exactly one row is returned, but if the same UUID
2641 * happens to appear as a <tt>provsql</tt> value in several tracked
2642 * tables, all matches are returned.
2644 * @param UUID token to resolve
2645 */
2646CREATE OR REPLACE FUNCTION resolve_input(UUID UUID)
2647 RETURNS TABLE(relation REGCLASS, row_data JSONB) AS
2649DECLARE
2650 t RECORD;
2651 rel REGCLASS;
2652 rd JSONB;
2653 -- ProvSQL's rewriter unconditionally appends a provsql column to the
2654 -- targetlist of any SELECT reading from a tracked relation; capture and
2655 -- discard it here rather than disabling the rewriter for the whole call.
2656 ign UUID;
2657BEGIN
2658 FOR t IN
2659 SELECT c.oid::REGCLASS AS regc
2660 FROM pg_attribute a
2661 JOIN pg_class c ON a.attrelid = c.oid
2662 JOIN pg_namespace ns ON c.relnamespace = ns.oid
2663 JOIN pg_type ty ON a.atttypid = ty.oid
2664 WHERE a.attname = 'provsql'
2665 AND ty.typname = 'UUID'
2666 AND c.relkind = 'r'
2667 AND ns.nspname <> 'provsql'
2668 AND a.attnum > 0
2669 LOOP
2670 FOR rel, rd, ign IN
2671 EXECUTE format(
2672 'SELECT %L::REGCLASS, to_jsonb(t) - ''provsql'', t.provsql FROM %s AS t WHERE provsql = $1',
2673 t.regc, t.regc)
2674 USING UUID
2675 LOOP
2676 relation := rel;
2677 row_data := rd;
2678 RETURN NEXT;
2679 END LOOP;
2680 END LOOP;
2681END
2682$$ LANGUAGE plpgsql STABLE;
2683
2684/** @} */
2685
2686/** @defgroup agg_token_type Type for the result of aggregate queries
2687 *
2688 * Custom type <tt>AGG_TOKEN</tt> for a provenance semimodule value, to
2689 * be used in attributes that are computed as a result of aggregation.
2690 * As for provenance tokens, this is simply a UUID, but this UUID is
2691 * displayed in a specific way (as the result of the aggregation
2692 * followed by a "(*)") to help with readability.
2693 *
2694 * The TEXT output is controlled by the
2695 * <tt>provsql.aggtoken_text_as_uuid</tt> GUC. By default it is off and
2696 * the cell renders as <tt>"value (*)"</tt>. When set to on (typical
2697 * for UI layers such as ProvSQL Studio), the cell renders as the
2698 * underlying UUID instead, so the caller can click through to the
2699 * provenance circuit; the value side is then recovered via
2700 * <tt>provsql.agg_token_value_text(UUID)</tt>.
2701 *
2702 * @{
2703 */
2704
2705CREATE TYPE AGG_TOKEN;
2706
2707/** @brief Input function for the AGG_TOKEN type (parses TEXT representation) */
2708CREATE OR REPLACE FUNCTION agg_token_in(CSTRING)
2709 RETURNS AGG_TOKEN
2710 AS 'provsql','agg_token_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2711
2712/**
2713 * @brief Output function for the AGG_TOKEN type
2714 *
2715 * Default: produces the human-friendly @c "value (*)" form, where
2716 * @c value is the running aggregate state.
2717 *
2718 * When the @c provsql.aggtoken_text_as_uuid GUC is on, returns the
2719 * underlying provenance UUID instead. UI layers (notably ProvSQL
2720 * Studio) flip this on per session so aggregate cells expose the
2721 * circuit root UUID for click-through; the @c "value (*)" display
2722 * string is recovered via @c provsql.agg_token_value_text(UUID).
2723 *
2724 * Marked STABLE rather than IMMUTABLE because the chosen output
2725 * shape now depends on a GUC that the same session can flip at
2726 * runtime.
2727 */
2728CREATE OR REPLACE FUNCTION agg_token_out(AGG_TOKEN)
2729 RETURNS CSTRING
2730 AS 'provsql','agg_token_out' LANGUAGE C STABLE STRICT PARALLEL SAFE;
2731
2732/** @brief Cast an AGG_TOKEN to its TEXT representation */
2733CREATE OR REPLACE FUNCTION agg_token_cast(AGG_TOKEN)
2734 RETURNS TEXT
2735 AS 'provsql','agg_token_cast' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2736
2737CREATE TYPE AGG_TOKEN (
2738 internallength = 117,
2739 input = agg_token_in,
2740 output = agg_token_out,
2741 alignment = char
2742);
2743
2744/** @brief Extract the UUID from an AGG_TOKEN (implicit cast to UUID) */
2745CREATE OR REPLACE FUNCTION agg_token_uuid(aggtok AGG_TOKEN)
2746 RETURNS UUID AS
2748BEGIN
2749 RETURN agg_token_cast(aggtok)::UUID;
2751$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE PARALLEL SAFE;
2752
2753/** @brief Implicit PostgreSQL cast from AGG_TOKEN to UUID (delegates to agg_token_uuid()) */
2754CREATE CAST (AGG_TOKEN AS UUID) WITH FUNCTION agg_token_uuid(AGG_TOKEN) AS IMPLICIT;
2755
2756/** @brief Whether @p token carries a value but records none in the actual
2757 * data: an aggregate over no row there, a value gate without a constant.
2758 *
2759 * Told apart from a value this reading does not take -- a TIMESTAMP, which
2760 * @c agg_gate_value gives up on because it reads numbers, or a random
2761 * variable, which has no value in the actual data at all -- because the two
2762 * call for opposite answers in @c agg_guard_holds: a comparison with a side
2763 * that HAS no value there does not hold, while one whose value is simply not
2764 * read leaves the truth undecided (internal use). */
2765CREATE OR REPLACE FUNCTION agg_gate_value_missing(token UUID)
2766 RETURNS BOOLEAN AS
2767$$
2768DECLARE
2769 gt provsql.PROVENANCE_GATE := provsql.get_gate_type(token);
2770 ch UUID[];
2771BEGIN
2772 IF gt IN ('agg', 'arith', 'value') THEN
2773 RETURN provsql.get_extra(token) IS NULL;
2774 ELSIF gt = 'semimod' THEN
2775 ch := provsql.get_children(token);
2776 RETURN array_length(ch, 1) = 2
2777 AND provsql.get_extra(ch[2]) IS NULL;
2778 ELSIF gt = 'conditioned' THEN
2779 ch := provsql.get_children(token);
2780 RETURN array_length(ch, 1) >= 1
2781 AND provsql.agg_gate_value_missing(ch[1]);
2782 END IF;
2783 /* Any other gate: nothing is claimed */
2784 RETURN false;
2785END
2786$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2787 SET search_path=provsql,pg_temp,public;
2788
2789/**
2790 * @brief The TEXT a value-carrying gate records in the actual data, whatever
2791 * type it is of.
2792 *
2793 * @c agg_gate_value reads that TEXT as a number and gives up on anything else.
2794 * A Boolean aggregate's value is one of those: @c "true" is no number, and yet
2795 * @c = and @c <> compare it, which is what the guards of a
2796 * @c "bool_or(flag)::INT" need. Descends the value-carrying gates as
2797 * @c agg_gate_value does, and answers @c NULL for a gate that records no value
2798 * of its own (internal use).
2799 */
2800CREATE OR REPLACE FUNCTION agg_gate_value_text(token UUID)
2801 RETURNS TEXT AS
2802$$
2803DECLARE
2804 gt provsql.PROVENANCE_GATE := provsql.get_gate_type(token);
2805BEGIN
2806 IF gt IN ('agg', 'arith', 'value') THEN
2807 RETURN provsql.get_extra(token);
2808 ELSIF gt = 'semimod' THEN
2809 RETURN provsql.agg_gate_value_text((provsql.get_children(token))[2]);
2810 ELSIF gt = 'conditioned' THEN
2811 RETURN provsql.agg_gate_value_text((provsql.get_children(token))[1]);
2812 END IF;
2813 RETURN NULL;
2814END
2815$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2816 SET search_path=provsql,pg_temp,public;
2817
2819 * @brief Deterministic truth of a Boolean guard sub-circuit over aggregate
2820 * comparisons, evaluated in the actual world (all input tuples present).
2821 *
2822 * Guards are the shapes @c having_Expr_to_provenance_cmp mints: @c cmp gates
2823 * over aggregate-valued children (comparison-operator OID in @c info1),
2824 * @c times / @c plus combinations (AND / OR, with negation pushed into the
2825 * comparison operators), and the @c one / @c zero indicators of regular
2826 * (aggregate-free) conditions. Uses Kleene three-valued logic: returns
2827 * @c NULL on any other gate shape, or when an operand's deterministic value
2828 * cannot be resolved.
2829 */
2830CREATE OR REPLACE FUNCTION agg_guard_holds(token UUID)
2831 RETURNS BOOLEAN AS
2832$$
2833DECLARE
2834 gt PROVENANCE_GATE := get_gate_type(token);
2835 ch UUID[];
2836 opname TEXT;
2837 l NUMERIC;
2838 r NUMERIC;
2839 lt TEXT;
2840 rt TEXT;
2841 all_true BOOLEAN;
2842 any_true BOOLEAN;
2843 any_null BOOLEAN;
2844BEGIN
2845 IF gt = 'one' THEN
2846 RETURN true;
2847 ELSIF gt = 'zero' THEN
2848 RETURN false;
2849 ELSIF gt IN ('times', 'plus') THEN
2850 SELECT bool_and(h), bool_or(h), bool_or(h IS NULL)
2851 INTO all_true, any_true, any_null
2852 FROM (SELECT provsql.agg_guard_holds(c) AS h
2853 FROM unnest(get_children(token)) AS c) AS s;
2854 IF gt = 'times' THEN
2855 -- AND: false dominates unknown (bool_and skips NULL inputs, so it is
2856 -- false exactly when some child is false).
2857 RETURN CASE WHEN NOT all_true THEN false
2858 WHEN any_null THEN NULL
2859 ELSE true END;
2860 ELSE
2861 -- OR: true dominates unknown.
2862 RETURN CASE WHEN any_true THEN true
2863 WHEN any_null THEN NULL
2864 ELSE false END;
2865 END IF;
2866 ELSIF gt = 'cmp' THEN
2867 ch := get_children(token);
2868 l := agg_gate_value(ch[1]);
2869 r := agg_gate_value(ch[2]);
2870 IF l IS NULL OR r IS NULL THEN
2871 /* A side with no value in the actual data -- an aggregate over no row
2872 * there, as a group kept only for other worlds is -- makes the
2873 * comparison unknown there, which is what provenance_cmp annotates
2874 * zero: it does not hold. A side whose value this reading does not
2875 * take, a TIMESTAMP or a random variable, leaves the truth undecided
2876 * instead: the value is there, only not as a number. */
2877 IF agg_gate_value_missing(ch[1]) OR agg_gate_value_missing(ch[2]) THEN
2878 RETURN false;
2879 END IF;
2880 /* A Boolean pair: the value is there, only not as a number, and = / <>
2881 * compare it all the same -- the guards a "bool_or(flag)::INT" lowers to
2882 * are these. The literals are checked against a list rather than cast
2883 * inside an exception block, which a parallel worker cannot afford, for
2884 * the reason agg_gate_value reads its number with a regex. */
2885 lt := lower(agg_gate_value_text(ch[1]));
2886 rt := lower(agg_gate_value_text(ch[2]));
2887 IF lt IN ('true', 'false', 't', 'f') AND
2888 rt IN ('true', 'false', 't', 'f') THEN
2889 SELECT oprname INTO opname
2890 FROM pg_catalog.pg_operator WHERE oid = (get_infos(token)).info1;
2891 IF opname = '=' THEN
2892 RETURN lt::BOOLEAN = rt::BOOLEAN;
2893 ELSIF opname = '<>' THEN
2894 RETURN lt::BOOLEAN <> rt::BOOLEAN;
2895 END IF;
2896 END IF;
2897 RETURN NULL;
2898 END IF;
2899 SELECT oprname INTO opname
2900 FROM pg_catalog.pg_operator WHERE oid = (get_infos(token)).info1;
2901 RETURN CASE opname
2902 WHEN '<' THEN l < r
2903 WHEN '<=' THEN l <= r
2904 WHEN '=' THEN l = r
2905 WHEN '<>' THEN l <> r
2906 WHEN '>=' THEN l >= r
2907 WHEN '>' THEN l > r
2908 END;
2909 ELSIF gt IN ('input', 'delta', 'monus', 'project', 'eq', 'mulinput',
2910 'assumed', 'annotation') THEN
2911 /* An ordinary provenance expression, not a comparison: the guard a
2912 * COALESCE over an aggregate lowers to is the NullTest one,
2913 * delta(+Kn) for IS NOT NULL and 1 - +Kn for IS NULL. Such a guard
2914 * holds in the actual data exactly when its Boolean provenance does
2915 * with every input row present, which is what plain_truth reads. */
2916 RETURN provsql.plain_truth(token);
2917 END IF;
2918 RETURN NULL;
2919END
2920$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2921 SET search_path=provsql,pg_temp,public;
2922
2923/**
2924 * @brief Deterministic (actual-world) scalar value of an aggregate-carrying
2925 * gate.
2926 *
2927 * Resolves the value an aggregate expression takes on the actual data -- the
2928 * value an @c AGG_TOKEN display cell carries: @c agg / @c arith gates RECORD
2929 * it in @c extra (set by aggregate evaluation and @c agg_arith_make), a
2930 * @c value gate carries its constant, a @c semimod wraps a value gate, a
2931 * @c conditioned gate has its target's value, and a @c case gate selects the
2932 * first branch whose guard holds in the actual world (per
2933 * @c agg_guard_holds), else the default. Returns @c NULL when the gate is
2934 * not aggregate-carrying or the value cannot be resolved (e.g. a
2935 * non-NUMERIC aggregate).
2936 */
2937CREATE OR REPLACE FUNCTION agg_gate_value(token UUID)
2938 RETURNS NUMERIC AS
2939$$
2940DECLARE
2941 gt PROVENANCE_GATE := get_gate_type(token);
2942 ch UUID[];
2943 n INTEGER;
2944 holds BOOLEAN;
2945 extra TEXT;
2946BEGIN
2947 IF gt IN ('agg', 'arith', 'value') THEN
2948 /* Reading the TEXT as a number without a PL/pgSQL exception block, which
2949 * a parallel worker cannot afford: entering one starts a subtransaction,
2950 * and this function is called from the evaluator, which runs wherever the
2951 * query does (PostgreSQL 11 raises "cannot start subtransactions during a
2952 * parallel operation"). What is not the TEXT of a number is the value of
2953 * a non-NUMERIC aggregate (a min over TEXT, a TIMESTAMP), which this
2954 * reading does not take. */
2955 extra := get_extra(token);
2956 IF extra ~ '^\s*([-+]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][-+]?[0-9]+)?|[Nn][Aa][Nn])\s*$' THEN
2957 RETURN extra::NUMERIC;
2958 END IF;
2959 RETURN NULL;
2960 ELSIF gt = 'semimod' THEN
2961 RETURN agg_gate_value((get_children(token))[2]);
2962 ELSIF gt = 'conditioned' THEN
2963 RETURN agg_gate_value((get_children(token))[1]);
2964 ELSIF gt = 'case' THEN
2965 ch := get_children(token);
2966 n := array_length(ch, 1);
2967 FOR i IN 1 .. (n - 1) / 2 LOOP
2968 holds := agg_guard_holds(ch[2 * i - 1]);
2969 IF holds IS NULL THEN
2970 RETURN NULL;
2971 ELSIF holds THEN
2972 RETURN agg_gate_value(ch[2 * i]);
2973 END IF;
2974 END LOOP;
2975 RETURN agg_gate_value(ch[n]);
2976 END IF;
2977 RETURN NULL;
2978END
2979$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2980 SET search_path=provsql,pg_temp,public;
2981
2982/**
2983 * @brief Recover the @c "value (*)" display string for an aggregation gate
2984 *
2985 * Companion helper to the @c provsql.aggtoken_text_as_uuid GUC. With
2986 * the GUC on, an @c AGG_TOKEN cell prints as the underlying provenance
2987 * UUID, which is convenient for tooling that wants to click through to
2988 * the circuit but loses the human-readable aggregate value. This
2989 * function takes such a UUID and returns the original @c "value (*)"
2990 * string by reading the gate's @c extra (set by aggregate evaluation
2991 * for @c agg gates, and by @c agg_arith_make for the @c arith gates
2992 * that AGG_TOKEN arithmetic mints); for the other aggregate-carrying
2993 * gates (@c case, @c conditioned, @c semimod, @c value) the value is
2994 * resolved through the circuit by @c agg_gate_value. Returns @c NULL
2995 * if @p token does not resolve to an aggregate-carrying gate.
2997 * @param token UUID of an @c agg gate (typically obtained from an
2998 * @c AGG_TOKEN cell when @c aggtoken_text_as_uuid is on,
2999 * or via a manual UUID cast otherwise).
3000 */
3001CREATE OR REPLACE FUNCTION agg_token_value_text(token UUID)
3002 RETURNS TEXT AS
3003$$
3004 SELECT CASE
3005 -- agg gates: extra is set by aggregate evaluation, in the aggregate's own
3006 -- type, so it reads as it is.
3007 WHEN provsql.get_gate_type(token) = 'agg'
3008 THEN provsql.get_extra(token) || ' (*)'
3009 -- arith gates: extra is the value agg_arith_make computed, in NUMERIC
3010 -- whatever the expression's type, so it is read in that type -- a double
3011 -- precision result prints its 16 digits and not NUMERIC's twenty.
3012 WHEN provsql.get_gate_type(token) = 'arith'
3013 THEN CASE provsql.agg_token_value_type(token)
3014 WHEN 'float8'::REGTYPE::oid
3015 THEN (provsql.get_extra(token)::NUMERIC::float8)::TEXT || ' (*)'
3016 WHEN 'float4'::REGTYPE::oid
3017 THEN (provsql.get_extra(token)::NUMERIC::float4)::TEXT || ' (*)'
3018 ELSE provsql.get_extra(token) || ' (*)'
3019 END
3020 -- other aggregate-carrying gates: resolve the actual-world value
3021 -- through the circuit.
3022 WHEN provsql.get_gate_type(token) IN ('case', 'conditioned', 'semimod', 'value')
3023 THEN provsql.agg_gate_value(token)::TEXT || ' (*)'
3024 ELSE NULL
3025 END;
3026$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE;
3027
3028/** @brief Cast an AGG_TOKEN to NUMERIC (extracts the aggregate value, loses provenance) */
3029CREATE OR REPLACE FUNCTION agg_token_to_numeric(AGG_TOKEN)
3030 RETURNS NUMERIC
3031 AS 'provsql','agg_token_to_numeric' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3032
3033/** @brief Cast an AGG_TOKEN to double precision (extracts the aggregate value, loses provenance) */
3034CREATE OR REPLACE FUNCTION agg_token_to_float8(AGG_TOKEN)
3035 RETURNS double precision
3036 AS 'provsql','agg_token_to_float8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3037
3038/** @brief Cast an AGG_TOKEN to INTEGER (extracts the aggregate value, loses provenance) */
3039CREATE OR REPLACE FUNCTION agg_token_to_int4(AGG_TOKEN)
3040 RETURNS INTEGER
3041 AS 'provsql','agg_token_to_int4' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3042
3043/** @brief Cast an AGG_TOKEN to bigint (extracts the aggregate value, loses provenance) */
3044CREATE OR REPLACE FUNCTION agg_token_to_int8(AGG_TOKEN)
3045 RETURNS bigint
3046 AS 'provsql','agg_token_to_int8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3047
3048/** @brief Cast an AGG_TOKEN to BOOLEAN (extracts the aggregate value, loses provenance) */
3049CREATE OR REPLACE FUNCTION agg_token_to_bool(AGG_TOKEN)
3050 RETURNS BOOLEAN
3051 AS 'provsql','agg_token_to_bool' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3053/** @brief Cast an AGG_TOKEN to TEXT (extracts the aggregate value, loses provenance) */
3054CREATE OR REPLACE FUNCTION agg_token_to_text(AGG_TOKEN)
3055 RETURNS TEXT
3056 AS 'provsql','agg_token_to_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3057
3058/** @brief Assignment cast from AGG_TOKEN to NUMERIC (extracts the scalar
3059 * value, dropping provenance). ASSIGNMENT, not IMPLICIT: provenance-
3060 * preserving arithmetic on aggregates is provided by the native
3061 * AGG_TOKEN operators below, so an implicit NUMERIC coercion would only
3062 * silently steal `s + 1` away from them (and reroute it differently
3063 * depending on whether provsql is in search_path). Write `s::NUMERIC`
3064 * to opt into the lossy scalar. */
3065CREATE CAST (AGG_TOKEN AS NUMERIC) WITH FUNCTION agg_token_to_numeric(AGG_TOKEN) AS ASSIGNMENT;
3066
3067-- ---------------------------------------------------------------------
3068-- Arithmetic on aggregates (AGG_TOKEN)
3069--
3070-- Mirrors the random_variable arithmetic surface: the operators build a
3071-- `gate_arith` over the operand provenance UUIDs (via provenance_arith,
3072-- info1 = PROVSQL_ARITH_*), so the arithmetic is recorded symbolically
3073-- in the circuit and can be resolved when a comparison (gate_cmp) over
3074-- the result is evaluated. Unlike random_variable (a bare UUID), an
3075-- AGG_TOKEN also carries a running scalar value, so each operator
3076-- additionally computes the resulting value and bundles it back with the
3077-- new gate.
3078-- ---------------------------------------------------------------------
3079
3080/** @brief Running value of an AGG_TOKEN as NUMERIC, without the
3081 * provenance-loss warning the public cast emits (internal use). */
3082CREATE OR REPLACE FUNCTION agg_token_value(AGG_TOKEN)
3083 RETURNS NUMERIC
3084 AS 'provsql','agg_token_value' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3085
3086/** @brief Value of an AGG_TOKEN as TEXT, NULL for a NULL value, without the
3087 * provenance-loss warning the public cast emits: the sort key of an
3088 * ORDER BY on an aggregate result (internal use). */
3089CREATE FUNCTION agg_token_plain_text(AGG_TOKEN)
3090 RETURNS TEXT
3091 AS 'provsql','agg_token_plain_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3092
3093/** @brief The children of the aggregation gate @p token, one per
3094 * contribution, to explode an aggregate result into rows (a join on it,
3095 * @c explode_table). Only for @c choose(), whose value is one of its
3096 * contributions; any other aggregate is refused, its value being none of
3097 * them (@c count(*) contributes a 1 per row) (internal use). */
3098CREATE FUNCTION agg_token_explode_children(token UUID)
3099 RETURNS UUID[] AS
3100$$
3101BEGIN
3102 IF provsql.get_gate_type(token) <> 'agg' THEN
3103 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3104 MESSAGE = 'ProvSQL: only the result of an aggregate can be exploded '
3105 'into rows',
3106 DETAIL = 'provsql-reason: explode-not-an-aggregate; scope: deliberate';
3107 END IF;
3108 IF (provsql.get_infos(token)).info1 <>
3109 'provsql.choose(ANYELEMENT)'::regprocedure::oid THEN
3110 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3111 MESSAGE = format('ProvSQL: the result of %s() cannot be exploded into '
3112 'rows, one per value it aggregates: only that of '
3113 'choose() is one of them; compare it in a HAVING '
3114 'clause, or mark it plain() to read its plain value',
3115 (SELECT proname FROM pg_catalog.pg_proc
3116 WHERE oid = (provsql.get_infos(token)).info1)),
3117 DETAIL = 'provsql-reason: explode-rows-aggregate-kind; scope: gap';
3118 END IF;
3119 RETURN provsql.get_children(token);
3120END
3121$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
3123/** @brief The values the aggregate result @p token takes over the possible
3124 * worlds, as TEXT, to explode that result into one row per value: the value
3125 * of an aggregate read as data (a GROUP BY key, a DISTINCT, an arm of a set
3126 * operation). The planner annotates the row of a value @c v with the
3127 * comparison gate @c [token @c = @c v], so that the rows of one group are
3128 * pairwise exclusive and exactly one of them is in each world where the
3129 * group is.
3130 *
3131 * A @c count() takes every number of its contributions, a @c min(), a
3132 * @c max() and a @c choose() one of their contributed values. Any other
3133 * aggregate is refused (SQLSTATE 0A000): the values of a @c sum() are its
3134 * subset sums, those of a @c string_agg() one per ordering, and reading
3135 * them off the contributions one by one would be wrong. A NULL
3136 * contribution is refused as well: whether the result is NULL is then a
3137 * value of its own, which a comparison cannot express (internal use). */
3138CREATE FUNCTION agg_possible_values(input ANYELEMENT,
3139 with_null BOOLEAN DEFAULT false)
3140 RETURNS TEXT[] AS
3141$$
3142DECLARE
3143 max_values CONSTANT INT := 1000; -- an explosion multiplies the rows
3144 token UUID;
3145 fn TEXT;
3146 ns TEXT;
3147 vals TEXT[];
3148 n INT;
3149 counted INT;
3150 first INT;
3151 sums NUMERIC[];
3152 one NUMERIC;
3153 scalar_agg BOOLEAN;
3154BEGIN
3155 /* The planner hands the aggregate result itself (an AGG_TOKEN), whatever
3156 * the type the query declares for that column. */
3157 IF pg_typeof(input) <> 'provsql.AGG_TOKEN'::REGTYPE THEN
3158 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3159 MESSAGE = 'ProvSQL: only the result of an aggregate can be exploded '
3160 'into one row per value it takes over the possible worlds',
3161 DETAIL = 'provsql-reason: explode-not-an-aggregate; scope: deliberate';
3162 END IF;
3163 token := input::UUID;
3164 IF provsql.get_gate_type(token) <> 'agg' THEN
3165 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3166 MESSAGE = 'ProvSQL: an arithmetic expression over aggregate results '
3167 'cannot be exploded into one row per value it takes over '
3168 'the possible worlds',
3169 DETAIL = 'provsql-reason: explode-arithmetic; scope: gap';
3170 END IF;
3171 SELECT p.proname, s.nspname INTO fn, ns
3172 FROM pg_catalog.pg_proc p
3173 JOIN pg_catalog.pg_namespace s ON s.oid = p.pronamespace
3174 WHERE p.oid = (provsql.get_infos(token)).info1;
3175
3176 vals := ARRAY(SELECT provsql.get_extra((provsql.get_children(sm))[2])
3177 FROM unnest(provsql.get_children(token)) AS sm);
3178 n := coalesce(array_length(vals, 1), 0);
3179 /* NULL is a value of the aggregate like any other -- the one it takes where
3180 * no row contributes, which an aggregation over the whole table reaches in
3181 * the world holding none of its rows (a grouped one has no row there at all).
3182 * Whether to offer it is the caller's to decide: the rewriting asks for it
3183 * only where it can annotate that row with the aggregate having no value,
3184 * and never for a count, which is 0 rather than NULL over no row. */
3185 scalar_agg := with_null;
3186
3187 IF (ns, fn) = ('pg_catalog', 'count') THEN
3188 /* A count contributes 1 per row it counts and 0 per row it does not -- a
3189 * row whose value is NULL, as the null-padded row of an outer join is --
3190 * so its values are the numbers of rows counted, up to how many there are
3191 * to count. Zero is one of them where a row that is not counted can be
3192 * the only one there, or where the aggregation is over the whole table; a
3193 * group of counted rows only has none of its rows in no world, a group
3194 * being no group without a row. */
3195 counted := (SELECT count(*) FROM unnest(vals) AS v
3196 WHERE v IS NOT NULL AND v <> '0');
3197 first := CASE WHEN (provsql.get_infos(token)).info2 < 0 OR counted < n
3198 THEN 0 ELSE 1 END;
3199 IF counted + 1 - first > max_values THEN
3200 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3201 MESSAGE = format('ProvSQL: the result of %s() aggregates %s rows, '
3202 'so it takes too many values over the possible '
3203 'worlds to explode it into one row per value',
3204 fn, n),
3205 HINT = 'mark it plain() to read its plain value',
3206 DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
3207 END IF;
3208 RETURN ARRAY(SELECT i::TEXT
3209 FROM generate_series(least(first, counted), counted) AS i);
3210 END IF;
3211
3212 IF (ns, fn) NOT IN (('pg_catalog', 'min'), ('pg_catalog', 'max'),
3213 ('pg_catalog', 'sum'), ('provsql', 'choose')) THEN
3214 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3215 MESSAGE = format('ProvSQL: the result of %s() cannot be exploded into '
3216 'one row per value it takes over the possible worlds: '
3217 'only count(), min(), max(), sum() and choose() have '
3218 'values that can be enumerated', fn),
3219 HINT = 'compare it in a HAVING clause, or mark it plain() to read its '
3220 'plain value',
3221 DETAIL = 'provsql-reason: explode-aggregate-kind; scope: gap';
3222 END IF;
3223
3224 IF EXISTS (SELECT 1 FROM unnest(vals) AS v WHERE v IS NULL) THEN
3225 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3226 MESSAGE = format('ProvSQL: the result of %s() aggregates a NULL value, '
3227 'so it cannot be exploded into one row per value it '
3228 'takes over the possible worlds: a comparison with '
3229 'NULL does not say that the result is NULL', fn),
3230 DETAIL = 'provsql-reason: explode-null-value; scope: gap';
3231 END IF;
3232
3233 IF (ns, fn) = ('pg_catalog', 'sum') THEN
3234 /* The value of a sum is the sum of the rows that are there, so its values
3235 * are its subset sums, reached by adding the contributions one at a time,
3236 * equal sums collapsing (three rows of 1 take three values, not eight).
3237 * The empty subset is the NULL above, which a scalar aggregation takes and
3238 * a grouped one does not (a group without a row is no group). The planner only sends sums over an
3239 * INTEGER column here, whose subset sums the evaluator's own arithmetic
3240 * reaches exactly. */
3241 sums := ARRAY[]::NUMERIC[];
3242 FOREACH one IN ARRAY ARRAY(SELECT v::NUMERIC FROM unnest(vals) AS v) LOOP
3243 sums := ARRAY(SELECT DISTINCT s FROM
3244 (SELECT unnest(sums) AS s
3245 UNION ALL SELECT one
3246 UNION ALL SELECT unnest(sums) + one) AS u);
3247 IF array_length(sums, 1) > max_values THEN
3248 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3249 MESSAGE = format('ProvSQL: the result of %s() takes more than %s '
3250 'values over the possible worlds, too many to '
3251 'explode it into one row per value', fn,
3252 max_values),
3253 HINT = 'mark it plain() to read its plain value',
3254 DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
3255 END IF;
3256 END LOOP;
3257 RETURN ARRAY(SELECT s::TEXT FROM unnest(sums) AS s ORDER BY s)
3258 || CASE WHEN scalar_agg THEN ARRAY[NULL::TEXT]
3259 ELSE ARRAY[]::TEXT[] END;
3260 END IF;
3261
3262 IF n > max_values THEN
3263 RAISE EXCEPTION USING ERRCODE = 'feature_not_supported',
3264 MESSAGE = format('ProvSQL: the result of %s() aggregates %s rows, so '
3265 'it takes too many values over the possible worlds to '
3266 'explode it into one row per value', fn, n),
3267 HINT = 'mark it plain() to read its plain value',
3268 DETAIL = 'provsql-reason: explode-too-many-values; scope: gap';
3269 END IF;
3270
3271 /* One row per distinct contributed value: each is the minimum (maximum,
3272 * choice) of the world where only its own row is. */
3273 RETURN ARRAY(SELECT DISTINCT v FROM unnest(vals) AS v ORDER BY v)
3274 || CASE WHEN scalar_agg THEN ARRAY[NULL::TEXT]
3275 ELSE ARRAY[]::TEXT[] END;
3276END
3277$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE;
3278
3279/** @brief Value of an AGG_TOKEN as TEXT, NULL for a NULL value, without the
3280 * provenance-loss warning of the public casts: the value of an aggregate
3281 * result read as a plain value where ProvSQL casts it (in a function, an
3282 * operator, a comparison), which the planner reports once as evaluated as
3283 * plain SQL (internal use). */
3284CREATE FUNCTION agg_token_frozen_value(AGG_TOKEN)
3285 RETURNS TEXT
3286 AS 'provsql','agg_token_plain_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3287
3288/** @brief Bundle a provenance gate UUID with a running value into an
3289 * AGG_TOKEN (inverse of the agg_token_uuid / agg_token_value
3290 * accessors). */
3291CREATE OR REPLACE FUNCTION agg_token_make(tok UUID, val NUMERIC)
3292 RETURNS AGG_TOKEN AS
3293$$
3294 SELECT format('( %s , %s )', tok::TEXT, val::TEXT)::provsql.AGG_TOKEN;
3295$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
3296 SET search_path=provsql,pg_temp,public;
3297
3298/** @brief Lift a scalar NUMERIC constant into a gate_value leaf and
3299 * return its UUID, so it can be a child of a gate_arith (the agg-side
3300 * analogue of as_random for random_variable). */
3301CREATE OR REPLACE FUNCTION agg_value_gate(v NUMERIC)
3302 RETURNS UUID AS
3303$$
3304DECLARE
3305 token UUID := public.uuid_generate_v5(
3306 provsql.uuid_ns_provsql(), concat('value', v::TEXT));
3307BEGIN
3308 PERFORM provsql.create_gate(token, 'value', NULL, NULL, NULL, v::TEXT);
3309 RETURN token;
3310END
3311$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE
3312 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
3313
3314/** @brief Mint (or reuse) the gate_arith for an AGG_TOKEN arithmetic
3315 * result and return the AGG_TOKEN carrying it.
3316 *
3317 * Also records the computed scalar in the gate's @c extra -- exactly
3318 * what aggregate evaluation does for @c agg gates -- so
3319 * @c agg_token_value_text can recover the @c "value (*)" display from
3320 * the bare UUID (as ProvSQL Studio does for result cells under
3321 * @c provsql.aggtoken_text_as_uuid). The gate UUID is deterministic in
3322 * (op, children), so re-recording the (identical) value is idempotent. */
3323/**
3324 * @brief The SQL type of the value an @c AGG_TOKEN carries.
3325 *
3326 * An @c agg gate holds the aggregate's own result type in @c info2, next to
3327 * the scalar flag in its top bit; an @c arith gate holds the result type of
3328 * the operation it stands for, put there by @c agg_arith_make. Every other
3329 * aggregate-carrying gate holds a value the rewriting computed in @c NUMERIC,
3330 * which is what it answers for them.
3331 */
3332CREATE OR REPLACE FUNCTION agg_token_value_type(token UUID)
3333 RETURNS oid AS
3334$$
3335 SELECT CASE provsql.get_gate_type(token)
3336 WHEN 'agg' THEN (i.info2 & 2147483647)::oid
3337 WHEN 'arith' THEN CASE WHEN coalesce(i.info2, 0) = 0
3338 THEN 'NUMERIC'::REGTYPE::oid ELSE i.info2::oid END
3339 ELSE 'NUMERIC'::REGTYPE::oid
3340 END
3341 FROM provsql.get_infos(token) i;
3342$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE
3343 SET search_path=provsql,pg_temp,public;
3344
3345/**
3346 * @brief The type the operation @p op over @p children answers in, by SQL's
3347 * own NUMERIC tower.
3348 *
3349 * The gate computes in @c NUMERIC whatever the types are -- it is the value
3350 * that is stored, and a wider one loses nothing -- but the value is READ in
3351 * the type the query's expression has, and @c double @c precision prints 16
3352 * digits where @c NUMERIC prints every one it holds. Recording the type is
3353 * what lets the reading round as SQL does; deriving it from the children makes
3354 * it a function of the gate, so two statements building the same expression
3355 * still agree on its token.
3357 * Two operations are not the tower: the INTEGER division SQL writes as @c "/"
3358 * over integers (the @c INTDIV gate) answers in the INTEGER type, and
3359 * @c round(v, d) exists only over @c NUMERIC, so it answers in @c NUMERIC.
3360 */
3361CREATE OR REPLACE FUNCTION agg_arith_result_type(op INT, children UUID[])
3362 RETURNS oid AS
3363$$
3364DECLARE
3365 t oid;
3366 c UUID;
3367 has_float8 BOOLEAN := false;
3368 has_float4 BOOLEAN := false;
3369 has_numeric BOOLEAN := false;
3370 widest_int oid := NULL;
3371BEGIN
3372 IF children IS NULL THEN
3373 RETURN 'NUMERIC'::REGTYPE::oid;
3374 END IF;
3375 FOREACH c IN ARRAY children LOOP
3376 t := provsql.agg_token_value_type(c);
3377 IF t = 'float8'::REGTYPE::oid THEN has_float8 := true;
3378 ELSIF t = 'float4'::REGTYPE::oid THEN has_float4 := true;
3379 ELSIF t = 'NUMERIC'::REGTYPE::oid THEN has_numeric := true;
3380 ELSIF t = 'int8'::REGTYPE::oid THEN widest_int := 'int8'::REGTYPE::oid;
3381 ELSIF t = 'int4'::REGTYPE::oid THEN
3382 widest_int := coalesce(nullif(widest_int, 'int2'::REGTYPE::oid),
3383 'int4'::REGTYPE::oid);
3384 ELSIF t = 'int2'::REGTYPE::oid THEN
3385 widest_int := coalesce(widest_int, 'int2'::REGTYPE::oid);
3386 END IF;
3387 END LOOP;
3388 IF op = 16 THEN -- the value as double precision reads it
3389 RETURN 'float8'::REGTYPE::oid;
3390 END IF;
3391 IF op = 17 THEN -- ... as real reads it
3392 RETURN 'float4'::REGTYPE::oid;
3393 END IF;
3394 IF op = 11 THEN -- INTDIV
3395 RETURN coalesce(widest_int, 'int8'::REGTYPE::oid);
3396 END IF;
3397 IF op = 12 AND array_length(children, 1) = 2 THEN -- round(v, digits)
3398 RETURN 'NUMERIC'::REGTYPE::oid;
3399 END IF;
3400 IF has_float8 THEN
3401 RETURN 'float8'::REGTYPE::oid;
3402 END IF;
3403 IF has_float4 THEN -- no real-with-NUMERIC operator: SQL widens both
3404 RETURN CASE WHEN has_numeric THEN 'float8'::REGTYPE::oid
3405 ELSE 'float4'::REGTYPE::oid END;
3406 END IF;
3407 IF has_numeric THEN
3408 RETURN 'NUMERIC'::REGTYPE::oid;
3409 END IF;
3410 RETURN coalesce(widest_int, 'NUMERIC'::REGTYPE::oid);
3411END
3412$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
3413 SET search_path=provsql,pg_temp,public;
3414
3415CREATE OR REPLACE FUNCTION agg_arith_make(op INT, children UUID[], val NUMERIC)
3416 RETURNS AGG_TOKEN AS
3417$$
3418DECLARE
3419 token UUID := public.uuid_generate_v5(
3420 provsql.uuid_ns_provsql(), concat('arith', op::TEXT, children::TEXT));
3421 restype oid;
3422 shown TEXT;
3423BEGIN
3424 IF op IS NULL OR children IS NULL THEN
3425 RETURN NULL;
3426 END IF;
3427 -- The gate keeps the value as it was computed, in NUMERIC: that is what the
3428 -- evaluators read, and the wider type loses nothing. The type of the
3429 -- expression goes beside it, so the value can be READ as SQL reads it.
3430 restype := provsql.agg_arith_result_type(op, children);
3431 PERFORM provsql.create_gate(token, 'arith', children, op, restype::INT,
3432 val::TEXT);
3433 -- A NULL value (a division by zero on a row the database as it is may not
3434 -- have) keeps the gate: its value in the other worlds is in the circuit.
3435 shown := CASE
3436 WHEN val IS NULL THEN 'NULL'
3437 WHEN restype = 'float8'::REGTYPE::oid THEN (val::float8)::TEXT
3438 WHEN restype = 'float4'::REGTYPE::oid THEN (val::float4)::TEXT
3439 ELSE val::TEXT
3440 END;
3441 RETURN format('( %s , %s )', token::TEXT, shown)::provsql.AGG_TOKEN;
3442END
3443$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
3444 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
3445
3446-- AGG_TOKEN <op> AGG_TOKEN --------------------------------------------
3447/** @brief AGG_TOKEN + AGG_TOKEN (gate_arith PLUS). */
3448CREATE OR REPLACE FUNCTION agg_token_plus(a AGG_TOKEN, b AGG_TOKEN)
3449 RETURNS AGG_TOKEN AS
3450$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, (b)::UUID],
3451 provsql.agg_token_value(a) + provsql.agg_token_value(b)); $$
3452 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3453
3454/** @brief AGG_TOKEN - AGG_TOKEN (gate_arith MINUS). */
3455CREATE OR REPLACE FUNCTION agg_token_minus(a AGG_TOKEN, b AGG_TOKEN)
3456 RETURNS AGG_TOKEN AS
3457$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, (b)::UUID],
3458 provsql.agg_token_value(a) - provsql.agg_token_value(b)); $$
3459 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3460
3461/** @brief AGG_TOKEN * AGG_TOKEN (gate_arith TIMES). */
3462CREATE OR REPLACE FUNCTION agg_token_times(a AGG_TOKEN, b AGG_TOKEN)
3463 RETURNS AGG_TOKEN AS
3464$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, (b)::UUID],
3465 provsql.agg_token_value(a) * provsql.agg_token_value(b)); $$
3466 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3467
3468/** @brief AGG_TOKEN / AGG_TOKEN (gate_arith DIV). */
3469CREATE OR REPLACE FUNCTION agg_token_div(a AGG_TOKEN, b AGG_TOKEN)
3470 RETURNS AGG_TOKEN AS
3471$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, (b)::UUID],
3472 provsql.agg_token_value(a) / NULLIF(provsql.agg_token_value(b), 0)); $$
3473 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3475/** @brief Unary -AGG_TOKEN (gate_arith NEG). */
3476CREATE OR REPLACE FUNCTION agg_token_neg(a AGG_TOKEN)
3477 RETURNS AGG_TOKEN AS
3478$$ SELECT provsql.agg_arith_make(4, ARRAY[(a)::UUID],
3479 - provsql.agg_token_value(a)); $$
3480 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3481
3482-- round / floor / ceil / abs of an AGG_TOKEN ---------------------------
3483-- Named as the SQL functions they stand for, so that the rewriter's
3484-- re-resolution of a call whose argument became an AGG_TOKEN finds them
3485-- (try_swap_agg_func), exactly as the operators above are found. They compute
3486-- in NUMERIC, as the operators do, and the gate records the operation so the
3487-- value is read per possible world; the moment evaluators sample them, since
3488-- rounding does not commute with expectation.
3489/** @brief round(AGG_TOKEN) (gate_arith ROUND). */
3490CREATE OR REPLACE FUNCTION provsql_round(a AGG_TOKEN)
3491 RETURNS AGG_TOKEN AS
3492$$ SELECT provsql.agg_arith_make(12, ARRAY[(a)::UUID],
3493 pg_catalog.round(provsql.agg_token_value(a))); $$
3494 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3495
3496/** @brief round(AGG_TOKEN, INTEGER): to @p d decimal digits. */
3497CREATE OR REPLACE FUNCTION provsql_round(a AGG_TOKEN, d INTEGER)
3498 RETURNS AGG_TOKEN AS
3499$$ SELECT provsql.agg_arith_make(12,
3500 ARRAY[(a)::UUID, provsql.agg_value_gate(d::NUMERIC)],
3501 pg_catalog.round(provsql.agg_token_value(a), d)); $$
3502 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3503
3504/** @cond INTERNAL */
3506 * @brief A cast of an aggregate result to a number, as the function of its
3507 * value that it is.
3508 *
3509 * A cast is a function call like any other, so the rewriting swaps
3510 * @c pg_catalog.NUMERIC(x) over an AGG_TOKEN for @c provsql.provsql_numeric(AGG_TOKEN)
3511 * wherever one is declared -- which is how round() and floor() became gate
3512 * operations. Between numbers the value is carried rather than frozen: a
3513 * widening (an INTEGER to @c NUMERIC, to a float) keeps it as it is, and a
3514 * narrowing to an INTEGER is the rounding gate, PostgreSQL's own cast rounding
3515 * half away from zero as that gate does. A cast to TEXT, to a BOOLEAN or to a
3516 * date has no arithmetic behind it and stays a reading of the plain value.
3517 */
3518CREATE OR REPLACE FUNCTION provsql_numeric(a AGG_TOKEN)
3519 RETURNS AGG_TOKEN AS $$ SELECT a $$
3520 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3521
3522/**
3523 * @brief float8(AGG_TOKEN): the value, as a double precision reads it.
3524 *
3525 * Not the identity it once was: the gates compute in @c NUMERIC, and this is
3526 * what says that the expression is a float, so that its value is read as SQL
3527 * reads it -- 2 and not 2.0000000000000000 for @c sum(x)/3 over such a column.
3528 * The gate is the rounding to that type, and every pass that walks arithmetic
3529 * treats it as the value of its child. A token already read in that type is
3530 * returned as it is, so nothing is wrapped twice.
3531 */
3532CREATE OR REPLACE FUNCTION provsql_float8(a AGG_TOKEN)
3533 RETURNS AGG_TOKEN AS
3534$$ SELECT CASE
3535 WHEN provsql.agg_token_value_type((a)::UUID) = 'float8'::REGTYPE::oid
3536 THEN a
3537 ELSE provsql.agg_arith_make(16, ARRAY[(a)::UUID],
3538 (provsql.agg_token_value(a)::float8)::NUMERIC)
3539 END $$
3540 LANGUAGE sql STABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3541
3542/** @brief float4(AGG_TOKEN): the value, as a real reads it (see float8). */
3543CREATE OR REPLACE FUNCTION provsql_float4(a AGG_TOKEN)
3544 RETURNS AGG_TOKEN AS
3545$$ SELECT CASE
3546 WHEN provsql.agg_token_value_type((a)::UUID) = 'float4'::REGTYPE::oid
3547 THEN a
3548 ELSE provsql.agg_arith_make(17, ARRAY[(a)::UUID],
3549 (provsql.agg_token_value(a)::float4)::NUMERIC)
3550 END $$
3551 LANGUAGE sql STABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3552
3553/** @brief int8(AGG_TOKEN): the value rounded, as the cast to bigint rounds. */
3554CREATE OR REPLACE FUNCTION provsql_int8(a AGG_TOKEN)
3555 RETURNS AGG_TOKEN AS $$ SELECT provsql.provsql_round(a) $$
3556 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3557
3558/** @brief int4(AGG_TOKEN): the value rounded, as the cast to INTEGER rounds. */
3559CREATE OR REPLACE FUNCTION provsql_int4(a AGG_TOKEN)
3560 RETURNS AGG_TOKEN AS $$ SELECT provsql.provsql_round(a) $$
3561 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3562
3563/** @brief int2(AGG_TOKEN): the value rounded, as the cast to smallint rounds. */
3564CREATE OR REPLACE FUNCTION provsql_int2(a AGG_TOKEN)
3565 RETURNS AGG_TOKEN AS $$ SELECT provsql.provsql_round(a) $$
3566 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3567/** @endcond */
3568
3569/** @brief floor(AGG_TOKEN) (gate_arith FLOOR). */
3570CREATE OR REPLACE FUNCTION provsql_floor(a AGG_TOKEN)
3571 RETURNS AGG_TOKEN AS
3572$$ SELECT provsql.agg_arith_make(13, ARRAY[(a)::UUID],
3573 pg_catalog.floor(provsql.agg_token_value(a))); $$
3574 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3575
3576/** @brief ceil(AGG_TOKEN) (gate_arith CEIL). */
3577CREATE OR REPLACE FUNCTION provsql_ceil(a AGG_TOKEN)
3578 RETURNS AGG_TOKEN AS
3579$$ SELECT provsql.agg_arith_make(14, ARRAY[(a)::UUID],
3580 pg_catalog.ceil(provsql.agg_token_value(a))); $$
3581 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3582
3583/** @brief ceiling(AGG_TOKEN), the SQL synonym of ceil. */
3584CREATE OR REPLACE FUNCTION provsql_ceiling(a AGG_TOKEN)
3585 RETURNS AGG_TOKEN AS
3586$$ SELECT provsql.agg_arith_make(14, ARRAY[(a)::UUID],
3587 pg_catalog.ceil(provsql.agg_token_value(a))); $$
3588 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3589
3590/** @brief abs(AGG_TOKEN) (gate_arith ABS). */
3591CREATE OR REPLACE FUNCTION provsql_abs(a AGG_TOKEN)
3592 RETURNS AGG_TOKEN AS
3593$$ SELECT provsql.agg_arith_make(15, ARRAY[(a)::UUID],
3594 pg_catalog.abs(provsql.agg_token_value(a))); $$
3595 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3596
3597-- AGG_TOKEN <op> NUMERIC ----------------------------------------------
3598/** @brief AGG_TOKEN + NUMERIC (gate_arith PLUS, constant lifted to a value gate). */
3599CREATE OR REPLACE FUNCTION agg_token_plus_numeric(a AGG_TOKEN, b NUMERIC)
3600 RETURNS AGG_TOKEN AS
3601$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3602 provsql.agg_token_value(a) + b); $$
3603 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3604
3605/** @brief AGG_TOKEN - NUMERIC. */
3606CREATE OR REPLACE FUNCTION agg_token_minus_numeric(a AGG_TOKEN, b NUMERIC)
3607 RETURNS AGG_TOKEN AS
3608$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3609 provsql.agg_token_value(a) - b); $$
3610 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3612/** @brief AGG_TOKEN * NUMERIC. */
3613CREATE OR REPLACE FUNCTION agg_token_times_numeric(a AGG_TOKEN, b NUMERIC)
3614 RETURNS AGG_TOKEN AS
3615$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3616 provsql.agg_token_value(a) * b); $$
3617 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3618
3619/** @brief AGG_TOKEN / NUMERIC. */
3620CREATE OR REPLACE FUNCTION agg_token_div_numeric(a AGG_TOKEN, b NUMERIC)
3621 RETURNS AGG_TOKEN AS
3622$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3623 provsql.agg_token_value(a) / NULLIF(b, 0)); $$
3624 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3625
3626-- NUMERIC <op> AGG_TOKEN ----------------------------------------------
3627/** @brief NUMERIC + AGG_TOKEN. */
3628CREATE OR REPLACE FUNCTION numeric_plus_agg_token(a NUMERIC, b AGG_TOKEN)
3629 RETURNS AGG_TOKEN AS
3630$$ SELECT provsql.agg_arith_make(0, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3631 a + provsql.agg_token_value(b)); $$
3632 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3633
3634/** @brief NUMERIC - AGG_TOKEN. */
3635CREATE OR REPLACE FUNCTION numeric_minus_agg_token(a NUMERIC, b AGG_TOKEN)
3636 RETURNS AGG_TOKEN AS
3637$$ SELECT provsql.agg_arith_make(2, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3638 a - provsql.agg_token_value(b)); $$
3639 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3640
3641/** @brief NUMERIC * AGG_TOKEN. */
3642CREATE OR REPLACE FUNCTION numeric_times_agg_token(a NUMERIC, b AGG_TOKEN)
3643 RETURNS AGG_TOKEN AS
3644$$ SELECT provsql.agg_arith_make(1, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3645 a * provsql.agg_token_value(b)); $$
3646 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3647
3648/** @brief NUMERIC / AGG_TOKEN. */
3649CREATE OR REPLACE FUNCTION numeric_div_agg_token(a NUMERIC, b AGG_TOKEN)
3650 RETURNS AGG_TOKEN AS
3651$$ SELECT provsql.agg_arith_make(3, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3652 a / NULLIF(provsql.agg_token_value(b), 0)); $$
3653 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3654
3655/**
3656 * @brief AGG_TOKEN / AGG_TOKEN, in INTEGER division (internal)
3657 *
3658 * The rewriter calls it instead of agg_token_div when both operands were
3659 * integers in the query, as for count(*) / count(x): SQL divides them
3660 * with truncation toward zero, and the displayed value does too. The gate
3661 * is an arith gate of its own operator (11, PROVSQL_ARITH_INTDIV), which
3662 * the evaluators truncate in every world.
3663 */
3664CREATE OR REPLACE FUNCTION agg_token_intdiv(a AGG_TOKEN, b AGG_TOKEN)
3665 RETURNS AGG_TOKEN AS
3666$$ SELECT provsql.agg_arith_make(11, ARRAY[(a)::UUID, (b)::UUID],
3667 trunc(provsql.agg_token_value(a) / NULLIF(provsql.agg_token_value(b), 0))); $$
3668 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3669
3670/** @brief AGG_TOKEN / NUMERIC, in INTEGER division (internal; see agg_token_intdiv). */
3671CREATE OR REPLACE FUNCTION agg_token_intdiv_numeric(a AGG_TOKEN, b NUMERIC)
3672 RETURNS AGG_TOKEN AS
3673$$ SELECT provsql.agg_arith_make(11, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3674 trunc(provsql.agg_token_value(a) / NULLIF(b, 0))); $$
3675 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3676
3677/** @brief NUMERIC / AGG_TOKEN, in INTEGER division (internal; see agg_token_intdiv). */
3678CREATE OR REPLACE FUNCTION numeric_intdiv_agg_token(a NUMERIC, b AGG_TOKEN)
3679 RETURNS AGG_TOKEN AS
3680$$ SELECT provsql.agg_arith_make(11, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
3681 trunc(a / NULLIF(provsql.agg_token_value(b), 0))); $$
3682 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3683
3684-- Operator declarations -----------------------------------------------
3685CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_plus, COMMUTATOR = +);
3686CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_minus);
3687CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_times, COMMUTATOR = *);
3688CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_div);
3689CREATE OPERATOR - (RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_neg);
3690/* Prefix @ is PostgreSQL's absolute value. Its own procedure is numeric_abs
3691 or int8abs rather than abs, so the swap cannot reach the counterpart by
3692 resolving the operator's function: the operator over AGG_TOKEN is declared
3693 here instead, and ORDER BY @(2 - max(x)) carries its gate like abs(...). */
3694CREATE OPERATOR @ (RIGHTARG=AGG_TOKEN, PROCEDURE=provsql_abs);
3695
3696/** @brief ln(AGG_TOKEN) (gate_arith LN): the logarithm of the value the
3697 * aggregate takes, in every world, rather than of the one it takes in the
3698 * database as it is. */
3699/**
3700 * @brief @p fn of the value of @p a, computed in the type the value is read in.
3701 *
3702 * The gates compute in @c NUMERIC, which loses nothing for the four basic
3703 * operations: computing exactly and reading the result as @c double
3704 * @c precision gives what floating-point arithmetic gives, since IEEE requires
3705 * each of them to be correctly rounded. @c sqrt, @c ln and @c exp are not
3706 * like that -- @c NUMERIC computes them to a fixed scale, so @c sqrt(3) comes
3707 * out to fifteen decimals and the value read back differs from SQL's in its
3708 * last digit -- so where the expression is a float, they are computed as SQL
3709 * computes them, in that type, and the NUMERIC the gate stores is that value.
3711CREATE OR REPLACE FUNCTION agg_transcendental(fn TEXT, a AGG_TOKEN)
3712 RETURNS NUMERIC AS
3713$$
3714 SELECT CASE
3715 WHEN provsql.agg_token_value_type((a)::UUID)
3716 IN ('float8'::REGTYPE::oid, 'float4'::REGTYPE::oid)
3717 THEN (CASE fn
3718 WHEN 'sqrt' THEN sqrt(provsql.agg_token_value(a)::float8)
3719 WHEN 'ln' THEN ln(provsql.agg_token_value(a)::float8)
3720 WHEN 'exp' THEN exp(provsql.agg_token_value(a)::float8)
3721 END)::text::NUMERIC /* through the float's own text: the cast to
3722 * NUMERIC keeps fifteen digits, its TEXT
3723 * keeps every one the value has */
3724 ELSE CASE fn
3725 WHEN 'sqrt' THEN sqrt(provsql.agg_token_value(a))
3726 WHEN 'ln' THEN ln(provsql.agg_token_value(a))
3727 WHEN 'exp' THEN exp(provsql.agg_token_value(a))
3728 END
3729 END;
3730$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE
3731 SET search_path=provsql,pg_temp,public;
3732
3733CREATE OR REPLACE FUNCTION provsql_ln(a AGG_TOKEN)
3734 RETURNS AGG_TOKEN AS
3735$$ SELECT provsql.agg_arith_make(8, ARRAY[(a)::UUID],
3736 provsql.agg_transcendental('ln', a)); $$
3737 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3738
3739/** @brief exp(AGG_TOKEN) (gate_arith EXP). */
3740CREATE OR REPLACE FUNCTION provsql_exp(a AGG_TOKEN)
3741 RETURNS AGG_TOKEN AS
3742$$ SELECT provsql.agg_arith_make(9, ARRAY[(a)::UUID],
3743 provsql.agg_transcendental('exp', a)); $$
3744 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3745
3746/** @brief sqrt(AGG_TOKEN): the square root is the power of one half
3747 * (gate_arith POW, whose exponent is a value gate). */
3748CREATE OR REPLACE FUNCTION provsql_sqrt(a AGG_TOKEN)
3749 RETURNS AGG_TOKEN AS
3750$$ SELECT provsql.agg_arith_make(7,
3751 ARRAY[(a)::UUID, provsql.agg_value_gate(0.5)],
3752 provsql.agg_transcendental('sqrt', a)); $$
3753 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3754
3755/** @brief AGG_TOKEN ^ NUMERIC (gate_arith POW, constant lifted to a value
3756 * gate). */
3757CREATE OR REPLACE FUNCTION agg_token_pow_numeric(a AGG_TOKEN, b NUMERIC)
3758 RETURNS AGG_TOKEN AS
3759$$ SELECT provsql.agg_arith_make(7,
3760 ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3761 provsql.agg_token_value(a) ^ b); $$
3762 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3763
3764CREATE OPERATOR ^ (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC,
3765 PROCEDURE=agg_token_pow_numeric);
3766
3767/** @brief power(AGG_TOKEN, NUMERIC) (gate_arith POW, the exponent lifted to
3768 * a value gate): what @c power(sum(x), 2) is carried as, the way
3769 * @c sum(x) ^ 2 already is. */
3770CREATE OR REPLACE FUNCTION provsql_power(a AGG_TOKEN, b NUMERIC)
3771 RETURNS AGG_TOKEN AS
3772$$ SELECT provsql.agg_arith_make(7,
3773 ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
3774 power(provsql.agg_token_value(a), b)); $$
3775 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3776
3777/** @brief power(AGG_TOKEN, double precision): computed in double precision,
3778 * as SQL computes it, and the exponent stored through its own TEXT, which
3779 * keeps every digit the float has (see @c agg_transcendental). */
3780CREATE OR REPLACE FUNCTION provsql_power(a AGG_TOKEN, b double precision)
3781 RETURNS AGG_TOKEN AS
3782$$ SELECT provsql.agg_arith_make(7,
3783 ARRAY[(a)::UUID, provsql.agg_value_gate(b::text::NUMERIC)],
3784 power(provsql.agg_token_value(a)::float8, b)::text::NUMERIC); $$
3785 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3786
3787/** @brief pow(AGG_TOKEN, NUMERIC): @c pow is @c power's other name. */
3788CREATE OR REPLACE FUNCTION provsql_pow(a AGG_TOKEN, b NUMERIC)
3789 RETURNS AGG_TOKEN AS
3790$$ SELECT provsql.provsql_power(a, b); $$
3791 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3792
3793/** @brief pow(AGG_TOKEN, double precision). */
3794CREATE OR REPLACE FUNCTION provsql_pow(a AGG_TOKEN, b double precision)
3795 RETURNS AGG_TOKEN AS
3796$$ SELECT provsql.provsql_power(a, b); $$
3797 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3798
3799CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_plus_numeric, COMMUTATOR = +);
3800CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_minus_numeric);
3801CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_times_numeric, COMMUTATOR = *);
3802CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_div_numeric);
3803
3804CREATE OPERATOR + (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_plus_agg_token, COMMUTATOR = +);
3805CREATE OPERATOR - (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_minus_agg_token);
3806CREATE OPERATOR * (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_times_agg_token, COMMUTATOR = *);
3807CREATE OPERATOR / (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_div_agg_token);
3808
3809/** @brief Assignment cast from AGG_TOKEN to double precision */
3810CREATE CAST (AGG_TOKEN AS double precision) WITH FUNCTION agg_token_to_float8(AGG_TOKEN) AS ASSIGNMENT;
3811/** @brief Assignment cast from AGG_TOKEN to INTEGER */
3812CREATE CAST (AGG_TOKEN AS INTEGER) WITH FUNCTION agg_token_to_int4(AGG_TOKEN) AS ASSIGNMENT;
3813/** @brief Assignment cast from AGG_TOKEN to bigint */
3814CREATE CAST (AGG_TOKEN AS bigint) WITH FUNCTION agg_token_to_int8(AGG_TOKEN) AS ASSIGNMENT;
3815/** @brief Assignment cast from AGG_TOKEN to TEXT (extracts value, not UUID) */
3816CREATE CAST (AGG_TOKEN AS TEXT) WITH FUNCTION agg_token_to_text(AGG_TOKEN) AS ASSIGNMENT;
3817/** @brief Assignment cast from AGG_TOKEN to BOOLEAN (bool_and, bool_or, every) */
3818CREATE CAST (AGG_TOKEN AS BOOLEAN) WITH FUNCTION agg_token_to_bool(AGG_TOKEN) AS ASSIGNMENT;
3819
3820/**
3821 * @brief Condition a discrete aggregate's distribution on an event:
3822 * @c "SUM(x) | C".
3823 *
3824 * Mirrors @c random_variable_cond for the @c AGG_TOKEN carrier: returns a
3825 * conditioned @c AGG_TOKEN that flows onward, its provenance token wrapped in
3826 * the composable two-child @c gate_conditioned @c [agg_target, condition]
3827 * while its running value is preserved. The moment / support dispatchers
3828 * unpack it (@c agg_conditioned_target + @c rv_conditioned_prov) and route
3829 * through the existing @c agg_raw_moment with the condition conjoined into the
3830 * @c prov argument, so @c expected(SUM(x)|C) / @c variance(SUM(x)|C) report
3831 * the conditional aggregate distribution. Nested conditioning folds.
3832 */
3833CREATE OR REPLACE FUNCTION agg_token_cond(a AGG_TOKEN, cond UUID)
3834 RETURNS AGG_TOKEN AS
3835$$
3836DECLARE
3837 tok UUID;
3838 ev UUID;
3839 result UUID;
3840 ch UUID[];
3841BEGIN
3842 IF cond IS NULL OR cond = gate_one() THEN
3843 RETURN a;
3844 END IF;
3845
3846 tok := (a)::UUID;
3847 IF get_gate_type(tok) = 'conditioned'
3848 AND array_length(get_children(tok), 1) = 2 THEN
3849 ch := get_children(tok);
3850 tok := ch[1];
3851 ev := provenance_times(ch[2], cond);
3852 ELSE
3853 ev := cond;
3854 END IF;
3855
3856 result := public.uuid_generate_v5(uuid_ns_provsql(),
3857 concat('conditioned', tok, ev));
3858 PERFORM create_gate(result, 'conditioned', ARRAY[tok, ev]);
3859 RETURN agg_token_make(result, agg_token_value(a));
3860END
3861$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
3862 SECURITY DEFINER PARALLEL SAFE;
3863
3864CREATE OPERATOR | (
3865 LEFTARG = AGG_TOKEN,
3866 RIGHTARG = UUID,
3867 PROCEDURE = agg_token_cond
3868);
3869
3870/**
3871 * @brief Placeholder for @c "SUM(x) | (predicate)" on an AGG_TOKEN.
3872 *
3873 * Lets the conditioning event be a natural Boolean predicate (e.g.
3874 * @c "SUM(x) | (SUM(x) > 5)") instead of a hand-built gate. Never executes:
3875 * the planner converts the Boolean operand into a condition gate and emits
3876 * @c agg_token_cond.
3877 */
3878CREATE OR REPLACE FUNCTION agg_token_cond_predicate(
3879 a AGG_TOKEN, predicate BOOLEAN) RETURNS AGG_TOKEN AS
3880$$
3881BEGIN
3882 RAISE EXCEPTION 'AGG_TOKEN | (predicate) must be rewritten by the ProvSQL '
3883 'planner hook: the right operand must be a Boolean combination of '
3884 'aggregate / random_variable comparisons (is provsql.active off?)'
3885 USING ERRCODE = 'feature_not_supported',
3886 DETAIL = 'provsql-reason: operator-not-rewritten; scope: gap';
3888$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
3889
3890CREATE OPERATOR | (
3891 LEFTARG = AGG_TOKEN,
3892 RIGHTARG = BOOLEAN,
3893 PROCEDURE = agg_token_cond_predicate
3894);
3896/**
3897 * @brief Unpack the target of a conditioned @c AGG_TOKEN.
3898 *
3899 * For a @c "SUM(x) | C" whose provenance token is the two-child
3900 * @c gate_conditioned @c [agg_target, condition] returns the AGG_TOKEN over
3901 * @c agg_target (same running value); for any other AGG_TOKEN returns it
3902 * unchanged. The conditioning event itself is recovered separately via
3903 * @c rv_conditioned_prov on the token's UUID.
3904 */
3905CREATE OR REPLACE FUNCTION agg_conditioned_target(a AGG_TOKEN)
3906 RETURNS AGG_TOKEN AS
3907$$
3908 SELECT CASE
3909 WHEN provsql.get_gate_type((a)::UUID) = 'conditioned'
3910 AND array_length(provsql.get_children((a)::UUID), 1) = 2
3911 THEN provsql.agg_token_make(
3912 (provsql.get_children((a)::UUID))[1], provsql.agg_token_value(a))
3913 ELSE a
3914 END;
3915$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3917/**
3918 * @brief Placeholder comparison of AGG_TOKEN with NUMERIC
3919 *
3920 * This function is never actually called; it exists so the SQL parser
3921 * accepts comparison operators between AGG_TOKEN and NUMERIC values.
3922 * The ProvSQL query rewriter replaces these comparisons at plan time.
3923 */
3924CREATE OR REPLACE FUNCTION agg_token_comp_numeric(a AGG_TOKEN, b NUMERIC)
3925RETURNS BOOLEAN
3926LANGUAGE plpgsql
3927IMMUTABLE STRICT PARALLEL SAFE
3928AS $$
3929BEGIN
3930 RAISE EXCEPTION 'Comparison AGG_TOKEN-NUMERIC not implemented, should be replaced by ProvSQL behavior'
3931 USING ERRCODE = 'feature_not_supported',
3932 DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
3933END;
3934$$;
3935
3936/**
3937 * @brief Placeholder comparison of NUMERIC with AGG_TOKEN
3938 *
3939 * Symmetric to agg_token_comp_numeric; never actually called.
3940 * The ProvSQL query rewriter replaces these comparisons at plan time.
3941 */
3942CREATE OR REPLACE FUNCTION numeric_comp_agg_token(a NUMERIC, b AGG_TOKEN)
3943RETURNS BOOLEAN
3944LANGUAGE plpgsql
3945IMMUTABLE STRICT PARALLEL SAFE
3946AS $$
3947BEGIN
3948 RAISE EXCEPTION 'Comparison NUMERIC-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior'
3949 USING ERRCODE = 'feature_not_supported',
3950 DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
3951END;
3952$$;
3953
3954/** @brief SQL operator AGG_TOKEN < NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3955CREATE OPERATOR < (
3956 LEFTARG = AGG_TOKEN,
3957 RIGHTARG = NUMERIC,
3958 PROCEDURE = agg_token_comp_numeric,
3959 COMMUTATOR = >,
3960 NEGATOR = >=
3961);
3962/** @brief SQL operator NUMERIC < AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3963CREATE OPERATOR < (
3964 LEFTARG = NUMERIC,
3965 RIGHTARG = AGG_TOKEN,
3966 PROCEDURE = numeric_comp_agg_token,
3967 COMMUTATOR = >,
3968 NEGATOR = >=
3969);
3970
3971/** @brief SQL operator AGG_TOKEN <= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3972CREATE OPERATOR <= (
3973 LEFTARG = AGG_TOKEN,
3974 RIGHTARG = NUMERIC,
3975 PROCEDURE = agg_token_comp_numeric,
3976 COMMUTATOR = >=,
3977 NEGATOR = >
3979/** @brief SQL operator NUMERIC <= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3980CREATE OPERATOR <= (
3981 LEFTARG = NUMERIC,
3982 RIGHTARG = AGG_TOKEN,
3983 PROCEDURE = numeric_comp_agg_token,
3984 COMMUTATOR = >=,
3985 NEGATOR = >
3986);
3987
3988/** @brief SQL operator AGG_TOKEN = NUMERIC (placeholder rewritten by ProvSQL at plan time) */
3989CREATE OPERATOR = (
3990 LEFTARG = AGG_TOKEN,
3991 RIGHTARG = NUMERIC,
3992 PROCEDURE = agg_token_comp_numeric,
3993 COMMUTATOR = =,
3994 NEGATOR = <>
3995);
3996/** @brief SQL operator NUMERIC = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3997CREATE OPERATOR = (
3998 LEFTARG = NUMERIC,
3999 RIGHTARG = AGG_TOKEN,
4000 PROCEDURE = numeric_comp_agg_token,
4001 COMMUTATOR = =,
4002 NEGATOR = <>
4003);
4004
4005/** @brief SQL operator AGG_TOKEN <> NUMERIC (placeholder rewritten by ProvSQL at plan time) */
4006CREATE OPERATOR <> (
4007 LEFTARG = AGG_TOKEN,
4008 RIGHTARG = NUMERIC,
4009 PROCEDURE = agg_token_comp_numeric,
4010 COMMUTATOR = <>,
4011 NEGATOR = =
4012);
4013/** @brief SQL operator NUMERIC <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
4014CREATE OPERATOR <> (
4015 LEFTARG = NUMERIC,
4016 RIGHTARG = AGG_TOKEN,
4017 PROCEDURE = numeric_comp_agg_token,
4018 COMMUTATOR = <>,
4019 NEGATOR = =
4020);
4021
4022/** @brief SQL operator AGG_TOKEN >= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
4023CREATE OPERATOR >= (
4024 LEFTARG = AGG_TOKEN,
4025 RIGHTARG = NUMERIC,
4026 PROCEDURE = agg_token_comp_numeric,
4027 COMMUTATOR = <=,
4028 NEGATOR = <
4029);
4030/** @brief SQL operator NUMERIC >= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
4031CREATE OPERATOR >= (
4032 LEFTARG = NUMERIC,
4033 RIGHTARG = AGG_TOKEN,
4034 PROCEDURE = numeric_comp_agg_token,
4035 COMMUTATOR = <=,
4036 NEGATOR = <
4037);
4038
4039/** @brief SQL operator AGG_TOKEN > NUMERIC (placeholder rewritten by ProvSQL at plan time) */
4040CREATE OPERATOR > (
4041 LEFTARG = AGG_TOKEN,
4042 RIGHTARG = NUMERIC,
4043 PROCEDURE = agg_token_comp_numeric,
4044 COMMUTATOR = <,
4045 NEGATOR = <=
4046);
4047/** @brief SQL operator NUMERIC > AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
4048CREATE OPERATOR > (
4049 LEFTARG = NUMERIC,
4050 RIGHTARG = AGG_TOKEN,
4051 PROCEDURE = numeric_comp_agg_token,
4052 COMMUTATOR = <,
4053 NEGATOR = <=
4054);
4055
4056/**
4057 * @brief Placeholder comparison of two AGG_TOKEN values (the diagonal)
4058 *
4059 * Never actually called; lets the parser accept AGG_TOKEN \<op\> AGG_TOKEN
4060 * (e.g. sum(x) > sum(y) on materialised tokens), which the ProvSQL
4061 * rewriter lowers to a gate_cmp at plan time. Declaring this diagonal
4062 * also disambiguates `s = s2` (otherwise "operator is not unique",
4063 * because both AGG_TOKEN -> UUID and AGG_TOKEN -> NUMERIC casts apply).
4064 */
4065CREATE OR REPLACE FUNCTION agg_token_comp_agg_token(a AGG_TOKEN, b AGG_TOKEN)
4066RETURNS BOOLEAN
4067LANGUAGE plpgsql
4068IMMUTABLE STRICT PARALLEL SAFE
4069AS $$
4070BEGIN
4071 RAISE EXCEPTION 'Comparison AGG_TOKEN-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior'
4072 USING ERRCODE = 'feature_not_supported',
4073 DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
4074END;
4075$$;
4076
4077/** @brief SQL operator AGG_TOKEN < AGG_TOKEN (placeholder rewritten at plan time) */
4078CREATE OPERATOR < (
4079 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
4080 COMMUTATOR = >, NEGATOR = >=
4081);
4082/** @brief SQL operator AGG_TOKEN <= AGG_TOKEN (placeholder rewritten at plan time) */
4083CREATE OPERATOR <= (
4084 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
4085 COMMUTATOR = >=, NEGATOR = >
4087/** @brief SQL operator AGG_TOKEN > AGG_TOKEN (placeholder rewritten at plan time) */
4088CREATE OPERATOR > (
4089 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
4090 COMMUTATOR = <, NEGATOR = <=
4091);
4092/** @brief SQL operator AGG_TOKEN >= AGG_TOKEN (placeholder rewritten at plan time) */
4093CREATE OPERATOR >= (
4094 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
4095 COMMUTATOR = <=, NEGATOR = <
4096);
4097/** @brief SQL operator AGG_TOKEN = AGG_TOKEN (placeholder rewritten at plan time) */
4098CREATE OPERATOR = (
4099 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
4100 COMMUTATOR = =, NEGATOR = <>
4101);
4102/** @brief SQL operator AGG_TOKEN <> AGG_TOKEN (placeholder rewritten at plan time) */
4103CREATE OPERATOR <> (
4104 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
4105 COMMUTATOR = <>, NEGATOR = =
4106);
4107
4108/**
4109 * @brief Placeholder comparison of AGG_TOKEN with TEXT
4110 *
4111 * This function is never actually called; it exists so the SQL parser
4112 * accepts comparison operators between AGG_TOKEN and TEXT values.
4113 * The ProvSQL query rewriter replaces these comparisons at plan time.
4114 */
4115CREATE OR REPLACE FUNCTION agg_token_comp_text(a AGG_TOKEN, b TEXT)
4116RETURNS BOOLEAN
4117LANGUAGE plpgsql
4118IMMUTABLE STRICT PARALLEL SAFE
4119AS $$
4120BEGIN
4121 RAISE EXCEPTION 'Comparison AGG_TOKEN-TEXT not implemented, should be replaced by ProvSQL behavior'
4122 USING ERRCODE = 'feature_not_supported',
4123 DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
4124END;
4125$$;
4126
4127/**
4128 * @brief Placeholder comparison of TEXT with AGG_TOKEN
4129 *
4130 * Symmetric to agg_token_comp_text; never actually called.
4131 * The ProvSQL query rewriter replaces these comparisons at plan time.
4132 */
4133CREATE OR REPLACE FUNCTION text_comp_agg_token(a TEXT, b AGG_TOKEN)
4134RETURNS BOOLEAN
4135LANGUAGE plpgsql
4136IMMUTABLE STRICT PARALLEL SAFE
4137AS $$
4138BEGIN
4139 RAISE EXCEPTION 'Comparison TEXT-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior'
4140 USING ERRCODE = 'feature_not_supported',
4141 DETAIL = 'provsql-reason: agg-comparison-not-rewritten; scope: gap';
4142END;
4143$$;
4144
4145/** @brief SQL operator AGG_TOKEN = TEXT (placeholder rewritten by ProvSQL at plan time) */
4146CREATE OPERATOR = (
4147 LEFTARG = AGG_TOKEN,
4148 RIGHTARG = TEXT,
4149 PROCEDURE = agg_token_comp_text,
4150 COMMUTATOR = =,
4151 NEGATOR = <>
4152);
4153/** @brief SQL operator TEXT = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
4154CREATE OPERATOR = (
4155 LEFTARG = TEXT,
4156 RIGHTARG = AGG_TOKEN,
4157 PROCEDURE = text_comp_agg_token,
4158 COMMUTATOR = =,
4159 NEGATOR = <>
4160);
4161
4162/** @brief SQL operator AGG_TOKEN <> TEXT (placeholder rewritten by ProvSQL at plan time) */
4163CREATE OPERATOR <> (
4164 LEFTARG = AGG_TOKEN,
4165 RIGHTARG = TEXT,
4166 PROCEDURE = agg_token_comp_text,
4167 COMMUTATOR = <>,
4168 NEGATOR = =
4169);
4170/** @brief SQL operator TEXT <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
4171CREATE OPERATOR <> (
4172 LEFTARG = TEXT,
4173 RIGHTARG = AGG_TOKEN,
4174 PROCEDURE = text_comp_agg_token,
4175 COMMUTATOR = <>,
4176 NEGATOR = =
4177);
4178
4179/** @} */
4180
4181/** @defgroup random_variable_type Type for continuous random variables
4182 *
4183 * Custom type <tt>random_variable</tt>: a thin wrapper around a
4184 * provenance gate UUID, used to expose continuous probabilistic
4185 * c-tables in SQL. The UUID indexes either a <tt>gate_rv</tt>
4186 * (an actual distribution) or a <tt>gate_value</tt> (a
4187 * zero-variance constant produced by <tt>provsql.as_random</tt>).
4188 * Binary-coercible with <tt>UUID</tt> (same 16-byte layout), so an
4189 * <tt>rv</tt>-typed expression flows directly into any function
4190 * expecting a UUID at zero runtime cost.
4191 *
4192 * Constructors live in this group: <tt>provsql.normal(μ, σ)</tt>,
4193 * <tt>provsql.uniform(a, b)</tt>, <tt>provsql.exponential(λ)</tt>,
4194 * <tt>provsql.erlang(k, λ)</tt>, <tt>provsql.gamma(k, λ)</tt>,
4195 * <tt>provsql.chi_squared(k)</tt>, <tt>provsql.lognormal(μ, σ)</tt>,
4196 * <tt>provsql.weibull(k, λ)</tt>, <tt>provsql.pareto(xₘ, α)</tt>,
4197 * <tt>provsql.beta(α, β)</tt>,
4198 * the discrete count constructors (<tt>provsql.poisson(λ)</tt>,
4199 * <tt>provsql.binomial(n, p)</tt>, <tt>provsql.geometric(p)</tt>,
4200 * <tt>provsql.hypergeometric(N, K, n)</tt>,
4201 * <tt>provsql.negative_binomial(r, p)</tt>, all lowering to
4202 * @c categorical via @c categorical_from_log_pmf),
4203 * and <tt>provsql.as_random(c)</tt>.
4204 * Operator overloads
4205 * (<tt>+ - * /</tt> and the six comparators) are defined further
4206 * below, alongside direct <tt>rv_cmp_*</tt> UUID constructors for
4207 * callers that want a <tt>gate_cmp</tt> token without going through
4208 * the planner hook.
4209 * @{
4210 */
4211
4212CREATE TYPE random_variable;
4213
4214/** @brief Input function for the random_variable type */
4215CREATE OR REPLACE FUNCTION random_variable_in(CSTRING)
4216 RETURNS random_variable
4217 AS 'provsql','random_variable_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
4218
4219/** @brief Output function for the random_variable type */
4220CREATE OR REPLACE FUNCTION random_variable_out(random_variable)
4221 RETURNS CSTRING
4222 AS 'provsql','random_variable_out' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
4223
4224CREATE TYPE random_variable (
4225 internallength = 16,
4226 input = random_variable_in,
4227 output = random_variable_out,
4228 alignment = char
4229);
4230
4231/** @brief Build a random_variable from a UUID (internal). */
4232CREATE OR REPLACE FUNCTION random_variable_make(tok UUID)
4233 RETURNS random_variable
4234 AS 'provsql','random_variable_make' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
4235
4236/** @brief Binary-coercible cast random_variable -> UUID.
4237 * A random_variable is byte-for-byte a pg_uuid_t (alignment char,
4238 * length 16), so WITHOUT FUNCTION lets PostgreSQL reinterpret the
4239 * bytes at zero runtime cost. The cast is ASSIGNMENT (not IMPLICIT):
4240 * an implicit cross-domain cast would silently reroute a comparison
4241 * such as `v < w` to `UUID < UUID` (raw byte comparison) whenever
4242 * `provsql` is not in search_path, since operators are resolved
4243 * through search_path but casts are not. Demoting to ASSIGNMENT
4244 * turns that silent wrong result into a clean parse error. Passing a
4245 * random_variable to a UUID-taking function now needs an explicit
4246 * `v::UUID` (function resolution never applies assignment casts). */
4247CREATE CAST (random_variable AS UUID) WITHOUT FUNCTION AS ASSIGNMENT;
4248CREATE CAST (UUID AS random_variable) WITHOUT FUNCTION;
4249
4250/**
4251 * @brief Coerce an @c AGG_TOKEN to a @c random_variable (its circuit token).
4252 *
4253 * An aggregate over probabilistic tuples IS a random variable: its
4254 * @c AGG_TOKEN carries the provenance circuit of the aggregate distribution.
4255 * Exposing that as a @c random_variable lets a comparison / conditioning
4256 * predicate mix the two -- e.g. conditioning a latent leaf on a count,
4257 * @c "R | (poisson(lambda) = C)" with @c C a @c count(*) AGG_TOKEN -- resolve
4258 * to the ordinary @c random_variable comparison operators (which the planner
4259 * hook rewrites into a @c gate_cmp). IMPLICIT so the mixed comparison
4260 * type-checks without an explicit cast; the polymorphic dispatchers keep
4261 * their exact @c AGG_TOKEN overloads (an exact match beats the cast).
4262 */
4263CREATE OR REPLACE FUNCTION agg_token_to_random_variable(a AGG_TOKEN)
4264 RETURNS random_variable AS
4265$$ SELECT provsql.random_variable_make(provsql.agg_token_uuid($1)); $$
4266 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4267CREATE CAST (AGG_TOKEN AS random_variable)
4268 WITH FUNCTION agg_token_to_random_variable(AGG_TOKEN) AS IMPLICIT;
4269
4270/**
4271 * @brief Internal: true iff @p x is a finite (non-NaN, non-±∞) float8.
4272 *
4273 * PostgreSQL's <tt>isnan</tt> is defined for <tt>NUMERIC</tt> only,
4274 * not for <tt>double precision</tt>; we use the inequality form,
4275 * which works because PG defines <tt>NaN = NaN</tt> as <tt>TRUE</tt>
4276 * for floats (so <tt>NaN <> 'NaN'::float8</tt> is <tt>FALSE</tt>).
4277 */
4278CREATE OR REPLACE FUNCTION is_finite_float8(x double precision)
4279 RETURNS BOOL AS
4280$$
4281 SELECT $1 <> 'NaN'::float8 AND $1 <> 'Infinity'::float8 AND $1 <> '-Infinity'::float8;
4282$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4283
4284/*
4285 * Latent (token-valued) distribution parameters.
4286 *
4287 * A distribution parameter may be a scalar provenance token -- another
4288 * random_variable (or an AGG_TOKEN cast to UUID) -- rather than a
4289 * concrete double. The parameter is then a random variable itself,
4290 * making the leaf a compound (hierarchical) distribution: e.g.
4291 * normal(M, 1) with M ~ normal(0, 10). The token constructors below
4292 * wire such parameters as children of the gate_rv, encoding each wired
4293 * slot as "$i" in the extra TEXT (a literal slot keeps its decimal TEXT,
4294 * so an all-literal call is byte-identical to the plain NUMERIC
4295 * constructor). Only the Monte Carlo sampler resolves the wires (per
4296 * iteration); every analytic path recognises the wired form and falls
4297 * through to MC.
4298 */
4299
4300/**
4301 * @brief Internal: build a two-parameter latent @c gate_rv.
4302 *
4303 * Each parameter is supplied as EITHER a token (@p pN_tok, a scalar
4304 * gate @c UUID) OR a literal (@p pN_lit); exactly one is non-NULL per
4305 * parameter. Token parameters are appended to the gate's wire vector
4306 * in order and referenced as @c "$i" in the @c extra TEXT; literal
4307 * parameters keep their decimal TEXT. Not @c STRICT: the NULLs are the
4308 * literal-vs-token sentinels.
4309 */
4310CREATE OR REPLACE FUNCTION rv_parametric2(
4311 family TEXT,
4312 p1_tok UUID, p1_lit double precision,
4313 p2_tok UUID, p2_lit double precision)
4314 RETURNS random_variable AS
4315$$
4316DECLARE
4317 token UUID;
4318 wires UUID[] := ARRAY[]::UUID[];
4319 s1 TEXT;
4320 s2 TEXT;
4321BEGIN
4322 IF p1_tok IS NOT NULL THEN
4323 wires := wires || p1_tok;
4324 s1 := '$' || (array_length(wires, 1) - 1);
4325 ELSE
4326 IF NOT provsql.is_finite_float8(p1_lit) THEN
4327 RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
4328 family, p1_lit;
4329 END IF;
4330 s1 := p1_lit::TEXT;
4331 END IF;
4332 IF p2_tok IS NOT NULL THEN
4333 wires := wires || p2_tok;
4334 s2 := '$' || (array_length(wires, 1) - 1);
4335 ELSE
4336 IF NOT provsql.is_finite_float8(p2_lit) THEN
4337 RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
4338 family, p2_lit;
4339 END IF;
4340 s2 := p2_lit::TEXT;
4341 END IF;
4342 token := public.uuid_generate_v4();
4343 PERFORM provsql.create_gate(token, 'rv', wires, NULL, NULL,
4344 family || ':' || s1 || ',' || s2);
4345 RETURN provsql.random_variable_make(token);
4346END
4347$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE;
4348
4349/**
4350 * @brief Internal: build a one-parameter latent @c gate_rv (rate/scale).
4351 */
4352CREATE OR REPLACE FUNCTION rv_parametric1(family TEXT, p_tok UUID)
4353 RETURNS random_variable AS
4354$$
4355DECLARE
4356 token UUID;
4357BEGIN
4358 token := public.uuid_generate_v4();
4359 PERFORM provsql.create_gate(token, 'rv', ARRAY[p_tok], NULL, NULL, family || ':$0');
4360 RETURN provsql.random_variable_make(token);
4361END
4362$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4363
4364/*
4365 * Token-accepting constructor overloads. For each NUMERIC family the
4366 * three mixed-arity forms (token, literal), (literal, token), (token,
4367 * token) let any parameter be a random_variable; an AGG_TOKEN parameter
4368 * is passed as @c (agg)::UUID::random_variable. The all-literal call
4369 * still resolves to the plain NUMERIC constructor (an exact match beats
4370 * the implicit NUMERIC->random_variable cast), so the literal fast path
4371 * is unchanged. STRICT: a NULL parameter yields a NULL random_variable.
4372 */
4373
4374-- normal(mu, sigma)
4375CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma double precision)
4376 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::UUID, NULL, NULL, $2); $$
4377 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4378CREATE OR REPLACE FUNCTION normal(mu double precision, sigma random_variable)
4379 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', NULL, $1, ($2)::UUID, NULL); $$
4380 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4381CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma random_variable)
4382 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4383 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4384
4385-- logistic(mu, s)
4386CREATE OR REPLACE FUNCTION logistic(mu random_variable, s double precision)
4387 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::UUID, NULL, NULL, $2); $$
4388 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4389CREATE OR REPLACE FUNCTION logistic(mu double precision, s random_variable)
4390 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', NULL, $1, ($2)::UUID, NULL); $$
4391 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4392CREATE OR REPLACE FUNCTION logistic(mu random_variable, s random_variable)
4393 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4394 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4396-- uniform(a, b)
4397CREATE OR REPLACE FUNCTION uniform(a random_variable, b double precision)
4398 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::UUID, NULL, NULL, $2); $$
4399 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4400CREATE OR REPLACE FUNCTION uniform(a double precision, b random_variable)
4401 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', NULL, $1, ($2)::UUID, NULL); $$
4402 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4403CREATE OR REPLACE FUNCTION uniform(a random_variable, b random_variable)
4404 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4405 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4406
4407-- exponential(lambda)
4408CREATE OR REPLACE FUNCTION exponential(lambda random_variable)
4409 RETURNS random_variable AS $$ SELECT provsql.rv_parametric1('exponential', ($1)::UUID); $$
4410 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4411
4412-- gamma(k, lambda)
4413CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda double precision)
4414 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::UUID, NULL, NULL, $2); $$
4415 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4416CREATE OR REPLACE FUNCTION gamma(k double precision, lambda random_variable)
4417 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', NULL, $1, ($2)::UUID, NULL); $$
4418 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4419CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda random_variable)
4420 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4421 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4422
4423-- lognormal(mu, sigma)
4424CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma double precision)
4425 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::UUID, NULL, NULL, $2); $$
4426 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4427CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma random_variable)
4428 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', NULL, $1, ($2)::UUID, NULL); $$
4429 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4430CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma random_variable)
4431 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4432 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4433
4434-- weibull(k, lambda)
4435CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda double precision)
4436 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::UUID, NULL, NULL, $2); $$
4437 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4438CREATE OR REPLACE FUNCTION weibull(k double precision, lambda random_variable)
4439 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', NULL, $1, ($2)::UUID, NULL); $$
4440 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4441CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda random_variable)
4442 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4443 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4444
4445-- pareto(xm, alpha)
4446CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha double precision)
4447 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::UUID, NULL, NULL, $2); $$
4448 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4449CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha random_variable)
4450 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', NULL, $1, ($2)::UUID, NULL); $$
4451 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4452CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha random_variable)
4453 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4454 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4455
4456-- beta(alpha, beta)
4457CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta double precision)
4458 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::UUID, NULL, NULL, $2); $$
4459 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4460CREATE OR REPLACE FUNCTION beta(alpha double precision, beta random_variable)
4461 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', NULL, $1, ($2)::UUID, NULL); $$
4462 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4463CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta random_variable)
4464 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4465 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4466
4467-- inverse_gamma(alpha, beta)
4468CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta double precision)
4469 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::UUID, NULL, NULL, $2); $$
4470 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4471CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta random_variable)
4472 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', NULL, $1, ($2)::UUID, NULL); $$
4473 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4474CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta random_variable)
4475 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4476 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4477
4478-- inverse_gaussian(mu, lambda)
4479CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda double precision)
4480 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::UUID, NULL, NULL, $2); $$
4481 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4482CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda random_variable)
4483 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', NULL, $1, ($2)::UUID, NULL); $$
4484 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4485CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda random_variable)
4486 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4487 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4488
4490 * @brief Construct a normal-distribution random variable
4491 *
4492 * Creates a fresh <tt>gate_rv</tt> with @c "normal:μ,σ" stored in
4493 * the gate's @c extra field, and returns a <tt>random_variable</tt>
4494 * pointing at it.
4495 *
4496 * Validation:
4497 * - @p mu and @p sigma must be finite (no @c NaN, no @c ±Infinity).
4498 * - @p sigma must be non-negative.
4499 * - When @p sigma is zero the distribution degenerates to the Dirac
4500 * at @p mu; the call is silently routed through @c as_random(mu),
4501 * producing a @c gate_value rather than a zero-variance @c gate_rv.
4502 * This keeps the sampler / moment / boundcheck paths free of σ=0
4503 * special cases and lets <tt>normal(x, 0)</tt> share its gate with
4504 * <tt>as_random(x)</tt>.
4505 *
4506 * @warning The <tt>VOLATILE</tt> marking is load-bearing and must
4507 * not be weakened. Each call mints a fresh <tt>uuid_generate_v4</tt>
4508 * token because two calls to <tt>normal(0, 1)</tt> are *independent*
4509 * random variables; if PostgreSQL were allowed to fold the function
4510 * (which it would under <tt>STABLE</tt> / <tt>IMMUTABLE</tt>), two
4511 * calls in the same query would share a UUID and collapse into a
4512 * single dependent RV, silently breaking the c-table semantics.
4513 * Same warning applies to @c uniform and @c exponential below.
4514 *
4515 * @sa <a href="https://en.wikipedia.org/wiki/Normal_distribution">Wikipedia: Normal distribution</a>
4516 */
4517CREATE OR REPLACE FUNCTION normal(mu double precision, sigma double precision)
4518 RETURNS random_variable AS
4519$$
4520DECLARE
4521 token UUID;
4522BEGIN
4523 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
4524 RAISE EXCEPTION 'provsql.normal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
4525 END IF;
4526 IF sigma < 0 THEN
4527 RAISE EXCEPTION 'provsql.normal: sigma must be non-negative (got %)', sigma;
4528 END IF;
4529 IF sigma = 0 THEN
4530 RETURN provsql.as_random(mu);
4531 END IF;
4532 token := public.uuid_generate_v4();
4533 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'normal:' || mu || ',' || sigma);
4534 RETURN provsql.random_variable_make(token);
4535END
4536$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4537
4538/**
4539 * @brief Construct a logistic-distribution random variable Logistic(μ, s)
4540 *
4541 * The location-scale family whose CDF is the logistic sigmoid; a threshold
4542 * event over a Logistic(0, 1) noise realises the logit link exactly
4543 * (@c P(eps < score) = 1/(1 + exp(-score))), the natural link for a
4544 * log-odds / latent-utility selection model.
4545 *
4546 * Validation:
4547 * - @p mu and @p s must be finite.
4548 * - @p s (the scale) must be non-negative; <tt>s = 0</tt> is the Dirac at
4549 * @p mu, routed through @c as_random(mu) as with @c normal's sigma = 0.
4551 * @param mu location (the mean and median).
4552 * @param s scale (> 0); the variance is @f$\pi^2 s^2 / 3@f$.
4553 * @return a @c random_variable token for Logistic(μ, s).
4555 * @sa <a href="https://en.wikipedia.org/wiki/Logistic_distribution">Wikipedia: Logistic distribution</a>
4556 */
4557CREATE OR REPLACE FUNCTION logistic(mu double precision, s double precision)
4558 RETURNS random_variable AS
4559$$
4560DECLARE
4561 token UUID;
4562BEGIN
4563 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(s) THEN
4564 RAISE EXCEPTION 'provsql.logistic: parameters must be finite (got mu=%, s=%)', mu, s;
4565 END IF;
4566 IF s < 0 THEN
4567 RAISE EXCEPTION 'provsql.logistic: scale s must be non-negative (got %)', s;
4568 END IF;
4569 IF s = 0 THEN
4570 RETURN provsql.as_random(mu);
4571 END IF;
4572 token := public.uuid_generate_v4();
4573 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'logistic:' || mu || ',' || s);
4574 RETURN provsql.random_variable_make(token);
4575END
4576$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4577
4578/**
4579 * @brief Construct a uniform-distribution random variable on [a, b]
4580 *
4581 * Validation:
4582 * - @p a and @p b must be finite.
4583 * - @p a must be ≤ @p b (reversed bounds are rejected).
4584 * - When <tt>a = b</tt> the distribution is the Dirac at @p a; the
4585 * call is silently routed through @c as_random(a) for the same
4586 * reason as @c normal with @p sigma = 0.
4588 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4589 * @ref normal.
4590 *
4591 * @sa <a href="https://en.wikipedia.org/wiki/Continuous_uniform_distribution">Wikipedia: Continuous uniform distribution</a>
4592 */
4593CREATE OR REPLACE FUNCTION uniform(a double precision, b double precision)
4594 RETURNS random_variable AS
4596DECLARE
4597 token UUID;
4598BEGIN
4599 IF NOT provsql.is_finite_float8(a) OR NOT provsql.is_finite_float8(b) THEN
4600 RAISE EXCEPTION 'provsql.uniform: bounds must be finite (got a=%, b=%)', a, b;
4601 END IF;
4602 IF a > b THEN
4603 RAISE EXCEPTION 'provsql.uniform: a must be <= b (got a=%, b=%)', a, b;
4604 END IF;
4605 IF a = b THEN
4606 RETURN provsql.as_random(a);
4607 END IF;
4608 token := public.uuid_generate_v4();
4609 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'uniform:' || a || ',' || b);
4610 RETURN provsql.random_variable_make(token);
4611END
4612$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4613
4614/**
4615 * @brief Construct an exponential-distribution random variable with rate λ
4616 *
4617 * Validation:
4618 * - @p lambda must be finite and strictly positive. No degenerate
4619 * form exists for the exponential distribution, so there is no
4620 * silent route through @c as_random.
4621 *
4622 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4623 * @ref normal.
4625 * @sa <a href="https://en.wikipedia.org/wiki/Exponential_distribution">Wikipedia: Exponential distribution</a>
4626 */
4627CREATE OR REPLACE FUNCTION exponential(lambda double precision)
4628 RETURNS random_variable AS
4629$$
4630DECLARE
4631 token UUID;
4632BEGIN
4633 IF NOT provsql.is_finite_float8(lambda) THEN
4634 RAISE EXCEPTION 'provsql.exponential: lambda must be finite (got %)', lambda;
4635 END IF;
4636 IF lambda <= 0 THEN
4637 RAISE EXCEPTION 'provsql.exponential: lambda must be strictly positive (got %)', lambda;
4638 END IF;
4639 token := public.uuid_generate_v4();
4640 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'exponential:' || lambda);
4641 RETURN provsql.random_variable_make(token);
4642END
4643$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4644
4646 * @brief Construct an Erlang-distribution random variable, sum of
4647 * @p k i.i.d. exponentials with shared rate @p lambda
4648 *
4649 * The Erlang distribution is the sum of @p k independent
4650 * <tt>Exp(λ)</tt> random variables (equivalently the gamma with
4651 * INTEGER shape). It is the natural closure of i.i.d.
4652 * exponentials under addition, and is materialised here as a single
4653 * <tt>gate_rv</tt> so the analytic CDF and closed-form moments fire
4654 * directly (rather than the sampler having to draw and sum @p k
4655 * exponential leaves per Monte-Carlo iteration).
4656 *
4657 * Validation:
4658 * - @p k must be ≥ 1. The degenerate @c k=1 case is silently routed
4659 * through @c exponential so <tt>erlang(1, λ)</tt> shares its gate
4660 * with <tt>exponential(λ)</tt>.
4661 * - @p lambda must be finite and strictly positive.
4662 *
4663 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4664 * @ref normal.
4665 *
4666 * @sa <a href="https://en.wikipedia.org/wiki/Erlang_distribution">Wikipedia: Erlang distribution</a>
4667 */
4668CREATE OR REPLACE FUNCTION erlang(k INTEGER, lambda double precision)
4669 RETURNS random_variable AS
4670$$
4671DECLARE
4672 token UUID;
4673BEGIN
4674 IF k < 1 THEN
4675 RAISE EXCEPTION 'provsql.erlang: k must be >= 1 (got %)', k;
4676 END IF;
4677 IF NOT provsql.is_finite_float8(lambda) THEN
4678 RAISE EXCEPTION 'provsql.erlang: lambda must be finite (got %)', lambda;
4679 END IF;
4680 IF lambda <= 0 THEN
4681 RAISE EXCEPTION 'provsql.erlang: lambda must be strictly positive (got %)', lambda;
4682 END IF;
4683 IF k = 1 THEN
4684 RETURN provsql.exponential(lambda);
4685 END IF;
4686 token := public.uuid_generate_v4();
4687 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'erlang:' || k || ',' || lambda);
4688 RETURN provsql.random_variable_make(token);
4689END
4690$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4691
4692/**
4693 * @brief Construct a gamma-distribution random variable with shape @p k
4694 * (any positive real) and rate @p lambda
4695 *
4696 * The gamma distribution generalises Erlang to non-INTEGER shape; its
4697 * CDF is the regularised lower incomplete gamma, evaluated in closed
4698 * form by the analytic passes. Sums of independent gammas with the
4699 * same rate fold to a single gamma in the simplifier.
4700 *
4701 * Validation:
4702 * - @p k must be finite and strictly positive. An INTEGER @p k (in
4703 * @c INTEGER range) is silently routed through @c erlang -- the gamma
4704 * with INTEGER shape *is* Erlang -- so <tt>gamma(2, λ)</tt> shares
4705 * its gate encoding and closure interplay with <tt>erlang(2, λ)</tt>.
4706 * - @p lambda must be finite and strictly positive.
4707 *
4708 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4709 * @ref normal.
4710 *
4711 * @sa <a href="https://en.wikipedia.org/wiki/Gamma_distribution">Wikipedia: Gamma distribution</a>
4712 */
4713CREATE OR REPLACE FUNCTION gamma(k double precision, lambda double precision)
4714 RETURNS random_variable AS
4715$$
4716DECLARE
4717 token UUID;
4718BEGIN
4719 IF NOT provsql.is_finite_float8(k) THEN
4720 RAISE EXCEPTION 'provsql.gamma: k must be finite (got %)', k;
4721 END IF;
4722 IF k <= 0 THEN
4723 RAISE EXCEPTION 'provsql.gamma: k must be strictly positive (got %)', k;
4724 END IF;
4725 IF NOT provsql.is_finite_float8(lambda) THEN
4726 RAISE EXCEPTION 'provsql.gamma: lambda must be finite (got %)', lambda;
4727 END IF;
4728 IF lambda <= 0 THEN
4729 RAISE EXCEPTION 'provsql.gamma: lambda must be strictly positive (got %)', lambda;
4730 END IF;
4731 IF k = floor(k) AND k <= 2147483647 THEN
4732 RETURN provsql.erlang(k::INTEGER, lambda);
4733 END IF;
4734 token := public.uuid_generate_v4();
4735 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'gamma:' || k || ',' || lambda);
4736 RETURN provsql.random_variable_make(token);
4737END
4738$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4739
4740/**
4741 * @brief Construct a chi-squared random variable with @p k degrees of
4742 * freedom: syntactic sugar for <tt>gamma(k/2, 1/2)</tt>
4743 *
4744 * @p k is accepted as @c double @c precision so fractional degrees of
4745 * freedom work; it must be finite and strictly positive. Even degrees
4746 * of freedom route through @c erlang via @c gamma's INTEGER-shape rule.
4747 *
4748 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4749 * @ref normal.
4750 *
4751 * @sa <a href="https://en.wikipedia.org/wiki/Chi-squared_distribution">Wikipedia: Chi-squared distribution</a>
4752 */
4753CREATE OR REPLACE FUNCTION chi_squared(k double precision)
4754 RETURNS random_variable AS
4755$$
4756BEGIN
4757 IF NOT provsql.is_finite_float8(k) THEN
4758 RAISE EXCEPTION 'provsql.chi_squared: k must be finite (got %)', k;
4759 END IF;
4760 IF k <= 0 THEN
4761 RAISE EXCEPTION 'provsql.chi_squared: k must be strictly positive (got %)', k;
4762 END IF;
4763 RETURN provsql.gamma(k / 2, 0.5);
4764END
4765$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4766
4768 * @brief Construct a log-normal random variable: @c exp of a
4769 * Normal(@p mu, @p sigma), parameterised by the underlying
4770 * normal (so its median is <tt>exp(mu)</tt> and its mean
4771 * <tt>exp(mu + sigma^2/2)</tt>)
4772 *
4773 * The multiplicative counterpart of @c normal: products of independent
4774 * lognormals fold to a lognormal in the simplifier, and the
4775 * <tt>exp(normal(...))</tt> / <tt>ln(lognormal(...))</tt> bridges fold
4776 * in both directions, so log-scale models stay closed-form.
4777 *
4778 * Validation mirrors @c normal: both parameters must be finite,
4779 * @p sigma non-negative; the degenerate @c sigma = 0 case is silently
4780 * routed through @c as_random (a Dirac at <tt>exp(mu)</tt>).
4781 *
4782 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4783 * @ref normal.
4785 * @sa <a href="https://en.wikipedia.org/wiki/Log-normal_distribution">Wikipedia: Log-normal distribution</a>
4786 */
4787CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma double precision)
4788 RETURNS random_variable AS
4789$$
4790DECLARE
4791 token UUID;
4792BEGIN
4793 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
4794 RAISE EXCEPTION 'provsql.lognormal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
4795 END IF;
4796 IF sigma < 0 THEN
4797 RAISE EXCEPTION 'provsql.lognormal: sigma must be non-negative (got %)', sigma;
4798 END IF;
4799 IF sigma = 0 THEN
4800 RETURN provsql.as_random(exp(mu));
4801 END IF;
4802 token := public.uuid_generate_v4();
4803 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'lognormal:' || mu || ',' || sigma);
4804 RETURN provsql.random_variable_make(token);
4805END
4806$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4807
4808/**
4809 * @brief Construct a Weibull random variable with shape @p k and
4810 * scale @p lambda
4811 *
4812 * @p lambda is the SCALE (the 63.2% quantile), not a rate: @c k = 1 is
4813 * the exponential with rate <tt>1/lambda</tt>, and that case is
4814 * silently routed through @c exponential to share its gate. The shape
4815 * tunes the hazard: @c k < 1 infant mortality, @c k > 1 wear-out.
4816 * Quantiles are exact, truncated moments are closed-form (via the
4817 * regularised incomplete gamma), and the min of i.i.d. Weibulls has a
4818 * closed-form mean (min-stability).
4819 *
4820 * Validation: both parameters must be finite and strictly positive.
4821 *
4822 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4823 * @ref normal.
4824 *
4825 * @sa <a href="https://en.wikipedia.org/wiki/Weibull_distribution">Wikipedia: Weibull distribution</a>
4826 */
4827CREATE OR REPLACE FUNCTION weibull(k double precision, lambda double precision)
4828 RETURNS random_variable AS
4829$$
4830DECLARE
4831 token UUID;
4832BEGIN
4833 IF NOT provsql.is_finite_float8(k) OR NOT provsql.is_finite_float8(lambda) THEN
4834 RAISE EXCEPTION 'provsql.weibull: parameters must be finite (got k=%, lambda=%)', k, lambda;
4835 END IF;
4836 IF k <= 0 OR lambda <= 0 THEN
4837 RAISE EXCEPTION 'provsql.weibull: parameters must be strictly positive (got k=%, lambda=%)', k, lambda;
4838 END IF;
4839 IF k = 1 THEN
4840 RETURN provsql.exponential(1 / lambda);
4841 END IF;
4842 token := public.uuid_generate_v4();
4843 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'weibull:' || k || ',' || lambda);
4844 RETURN provsql.random_variable_make(token);
4845END
4846$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4847
4848/**
4849 * @brief Construct a Pareto random variable with scale (minimum)
4850 * @p xm and shape @p alpha
4851 *
4852 * The canonical heavy-tailed power law. Raw moments are @b infinite
4853 * for <tt>alpha <= k</tt> and reported as <tt>Infinity</tt> (the mean
4854 * for <tt>alpha <= 1</tt>, the variance for <tt>alpha <= 2</tt>)
4855 * rather than estimated; quantiles, truncated moments, conditional
4856 * sampling (self-similarity: <tt>X | X > a</tt> is Pareto(a, alpha)),
4857 * and Pareto-vs-Pareto comparisons are all exact.
4858 *
4859 * Validation: both parameters must be finite and strictly positive.
4860 *
4861 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4862 * @ref normal.
4863 *
4864 * @sa <a href="https://en.wikipedia.org/wiki/Pareto_distribution">Wikipedia: Pareto distribution</a>
4865 */
4866CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha double precision)
4867 RETURNS random_variable AS
4868$$
4869DECLARE
4870 token UUID;
4871BEGIN
4872 IF NOT provsql.is_finite_float8(xm) OR NOT provsql.is_finite_float8(alpha) THEN
4873 RAISE EXCEPTION 'provsql.pareto: parameters must be finite (got xm=%, alpha=%)', xm, alpha;
4874 END IF;
4875 IF xm <= 0 OR alpha <= 0 THEN
4876 RAISE EXCEPTION 'provsql.pareto: parameters must be strictly positive (got xm=%, alpha=%)', xm, alpha;
4877 END IF;
4878 token := public.uuid_generate_v4();
4879 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'pareto:' || xm || ',' || alpha);
4880 RETURN provsql.random_variable_make(token);
4881END
4882$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4883
4884/**
4885 * @brief Construct an inverse-gamma random variable with shape
4886 * @p alpha and scale @p beta
4887 *
4888 * The distribution of <tt>1/Y</tt> for <tt>Y ~ gamma(alpha, beta)</tt>
4889 * (the conjugate prior for a Gaussian variance). Its CDF is the
4890 * regularised upper incomplete gamma, evaluated in closed form by the
4891 * analytic passes; raw moments are @b infinite for <tt>alpha <= k</tt>
4892 * and reported as <tt>Infinity</tt> (the mean for <tt>alpha <= 1</tt>,
4893 * the variance for <tt>alpha <= 2</tt>) rather than estimated. Positive
4894 * scalings rescale @p beta in the simplifier.
4895 *
4896 * Validation: both parameters must be finite and strictly positive.
4897 *
4898 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4899 * @ref normal.
4900 *
4901 * @sa <a href="https://en.wikipedia.org/wiki/Inverse-gamma_distribution">Wikipedia: Inverse-gamma distribution</a>
4902 */
4903CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta double precision)
4904 RETURNS random_variable AS
4905$$
4906DECLARE
4907 token UUID;
4908BEGIN
4909 IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
4910 RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
4911 END IF;
4912 IF alpha <= 0 OR beta <= 0 THEN
4913 RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
4914 END IF;
4915 token := public.uuid_generate_v4();
4916 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'inverse_gamma:' || alpha || ',' || beta);
4917 RETURN provsql.random_variable_make(token);
4918END
4919$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4921/**
4922 * @brief Construct an inverse-Gaussian (Wald) random variable with mean
4923 * @p mu and shape @p lambda
4924 *
4925 * The first-passage time of Brownian motion with drift: a positive,
4926 * right-skewed family. Its CDF has a closed form in the standard normal
4927 * @c Phi, so comparisons and quantiles are analytic; all raw moments are
4928 * finite. Positive scalings map <tt>c·IG(mu, lambda)</tt> to
4929 * <tt>IG(c·mu, c·lambda)</tt>, and a sum of independent inverse
4930 * Gaussians sharing the ratio <tt>lambda/mu²</tt> folds to a single
4931 * inverse Gaussian in the simplifier. @ref wald is an alias.
4932 *
4933 * Validation: both parameters must be finite and strictly positive.
4934 *
4935 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
4936 * @ref normal.
4937 *
4938 * @sa <a href="https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution">Wikipedia: Inverse Gaussian distribution</a>
4939 */
4940CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda double precision)
4941 RETURNS random_variable AS
4942$$
4943DECLARE
4944 token UUID;
4945BEGIN
4946 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(lambda) THEN
4947 RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be finite (got mu=%, lambda=%)', mu, lambda;
4948 END IF;
4949 IF mu <= 0 OR lambda <= 0 THEN
4950 RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be strictly positive (got mu=%, lambda=%)', mu, lambda;
4951 END IF;
4952 token := public.uuid_generate_v4();
4953 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'inverse_gaussian:' || mu || ',' || lambda);
4954 RETURN provsql.random_variable_make(token);
4955END
4956$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4957
4958/**
4959 * @brief Wald distribution: alias for @ref inverse_gaussian.
4960 *
4961 * @sa <a href="https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution">Wikipedia: Inverse Gaussian distribution</a>
4962 */
4963CREATE OR REPLACE FUNCTION wald(mu double precision, lambda double precision)
4964 RETURNS random_variable AS
4965$$
4966 SELECT provsql.inverse_gaussian(mu, lambda);
4967$$ LANGUAGE sql VOLATILE PARALLEL SAFE;
4968
4969/**
4970 * @brief Build a discrete (categorical) random variable from outcomes
4971 * and UNNORMALISED log-masses
4972 *
4973 * The shared back end of the discrete count constructors (@c poisson,
4974 * @c binomial, @c geometric, @c hypergeometric,
4975 * @c negative_binomial), and directly usable for any custom discrete
4976 * pmf: the log-masses are shifted by their maximum (so only relative
4977 * magnitudes matter and no @c exp underflows), outcomes whose relative
4978 * mass is below <tt>1e-15</tt> are dropped, and the rest is
4979 * renormalised before being handed to @c categorical. Working in log
4980 * space keeps arbitrarily large parameters stable (e.g. a
4981 * <tt>Poisson(1000)</tt> pmf whose linear-space recurrence would
4982 * underflow at @c exp(-1000)).
4983 *
4984 * @param outcomes outcome values, same length as @p log_pmf
4985 * @param log_pmf natural logs of the (unnormalised) masses
4986 */
4987CREATE OR REPLACE FUNCTION categorical_from_log_pmf(
4988 outcomes double precision[], log_pmf double precision[])
4989 RETURNS random_variable AS
4990$$
4991DECLARE
4992 n INT := array_length(outcomes, 1);
4993 max_lp double precision := '-Infinity';
4994 kept_o double precision[] := '{}';
4995 kept_p double precision[] := '{}';
4996 total double precision := 0;
4997 v double precision;
4998 i INT;
4999BEGIN
5000 IF n IS NULL OR n = 0 OR n <> coalesce(array_length(log_pmf, 1), 0) THEN
5001 RAISE EXCEPTION 'provsql.categorical_from_log_pmf: outcomes and log_pmf must be non-empty arrays of the same length';
5002 END IF;
5003 FOR i IN 1..n LOOP
5004 IF log_pmf[i] > max_lp THEN max_lp := log_pmf[i]; END IF;
5005 END LOOP;
5006 IF max_lp = '-Infinity' THEN
5007 RAISE EXCEPTION 'provsql.categorical_from_log_pmf: all masses are zero';
5008 END IF;
5009 FOR i IN 1..n LOOP
5010 v := exp(log_pmf[i] - max_lp);
5011 IF v >= 1e-15 THEN
5012 kept_o := array_append(kept_o, outcomes[i]);
5013 kept_p := array_append(kept_p, v);
5014 total := total + v;
5015 END IF;
5016 END LOOP;
5017 FOR i IN 1..array_length(kept_p, 1) LOOP
5018 kept_p[i] := kept_p[i] / total;
5019 END LOOP;
5020 RETURN provsql.categorical(kept_p, kept_o);
5021END
5022$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5023
5024/**
5025 * @brief Construct a Poisson random variable with mean @p lambda, as a
5026 * truncated categorical
5027 *
5028 * The pmf is enumerated over <tt>[max(0, λ-12√λ), λ+12√λ+30]</tt> (the
5029 * omitted tails carry ~1e-30 of mass) by the log-space recurrence
5030 * <tt>ln p(k+1) = ln p(k) + ln λ - ln(k+1)</tt> and handed to
5031 * @c categorical_from_log_pmf, so moments, quantiles, and (in)equality
5032 * comparisons are exact over the enumerated support. @c lambda = 0 is
5033 * a Dirac at @c 0 (routed through @c as_random); supports up to 10000
5034 * outcomes (λ up to ~170000), beyond which it raises -- approximate
5035 * huge means by @c normal(λ, √λ) instead.
5036 *
5037 * @sa <a href="https://en.wikipedia.org/wiki/Poisson_distribution">Wikipedia: Poisson distribution</a>
5038 */
5039CREATE OR REPLACE FUNCTION poisson(lambda double precision)
5040 RETURNS random_variable AS
5041$$
5042DECLARE
5043 lo INT;
5044 hi INT;
5045 outcomes double precision[] := '{}';
5046 lps double precision[] := '{}';
5047 lp double precision := 0;
5048 k INT;
5049BEGIN
5050 IF NOT provsql.is_finite_float8(lambda) OR lambda < 0 THEN
5051 RAISE EXCEPTION 'provsql.poisson: lambda must be finite and non-negative (got %)', lambda;
5052 END IF;
5053 IF lambda = 0 THEN
5054 RETURN provsql.as_random(0);
5055 END IF;
5056 lo := greatest(0, floor(lambda - 12 * sqrt(lambda)))::INT;
5057 hi := ceil(lambda + 12 * sqrt(lambda))::INT + 30;
5058 IF hi - lo + 1 > 10000 THEN
5059 RAISE EXCEPTION 'provsql.poisson: support window of % outcomes exceeds 10000; approximate with normal(%, sqrt(%))', hi - lo + 1, lambda, lambda;
5060 END IF;
5061 -- ln p(0) = -λ; walk the recurrence, keeping only the window.
5062 lp := -lambda;
5063 FOR k IN 1..hi LOOP
5064 lp := lp + ln(lambda) - ln(k::double precision);
5065 IF k >= lo THEN
5066 outcomes := array_append(outcomes, k::double precision);
5067 lps := array_append(lps, lp);
5068 END IF;
5069 END LOOP;
5070 IF lo = 0 THEN
5071 outcomes := array_prepend(0::double precision, outcomes);
5072 lps := array_prepend(-lambda, lps);
5073 END IF;
5074 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
5075END
5076$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5078/**
5079 * @brief Poisson with a LATENT rate: @c poisson(random_variable).
5080 *
5081 * A latent (token-valued) rate cannot be enumerated into a categorical at
5082 * construction, so this builds a parametric @c gate_rv leaf (family
5083 * @c "poisson") wiring the rate, exactly like the continuous latent
5084 * constructors. Only the Monte Carlo sampler resolves the rate (per draw,
5085 * then draws a Poisson); @c observe weights by the Poisson pmf; the mean is
5086 * exact (E[Poisson(Λ)] = E[Λ], affine). Unblocks discrete-likelihood
5087 * posteriors such as @c "R | (poisson(120*R) = observed_count)".
5088 */
5089CREATE OR REPLACE FUNCTION poisson(lambda random_variable)
5090 RETURNS random_variable AS
5091$$ SELECT provsql.rv_parametric1('poisson', ($1)::UUID); $$
5092 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
5093
5094/**
5095 * @brief Construct a Beta(α, β) random variable on the unit interval
5096 *
5097 * The conjugate prior of Bernoulli / binomial success probabilities:
5098 * closed-form moments, CDF through the regularised incomplete beta,
5099 * quantiles through the generic CDF bisection over the finite
5100 * @c [0, 1] support, and closed-form truncated moments (interval
5101 * conditioning). <tt>Beta(1, 1)</tt> IS <tt>Uniform(0, 1)</tt> and is
5102 * silently routed through @c uniform to share its richer closed forms.
5103 *
5104 * Validation: both shapes must be finite and strictly positive.
5105 *
5106 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
5107 * @ref normal.
5109 * @sa <a href="https://en.wikipedia.org/wiki/Beta_distribution">Wikipedia: Beta distribution</a>
5110 */
5111CREATE OR REPLACE FUNCTION beta(alpha double precision, beta double precision)
5112 RETURNS random_variable AS
5113$$
5114DECLARE
5115 token UUID;
5116BEGIN
5117 IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
5118 RAISE EXCEPTION 'provsql.beta: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
5119 END IF;
5120 IF alpha <= 0 OR beta <= 0 THEN
5121 RAISE EXCEPTION 'provsql.beta: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
5122 END IF;
5123 IF alpha = 1 AND beta = 1 THEN
5124 RETURN provsql.uniform(0, 1);
5125 END IF;
5126 token := public.uuid_generate_v4();
5127 PERFORM provsql.create_gate(token, 'rv', NULL, NULL, NULL, 'beta:' || alpha || ',' || beta);
5128 RETURN provsql.random_variable_make(token);
5129END
5130$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5131
5132/**
5133 * @brief Construct a Binomial(n, p) random variable (number of
5134 * successes in @p n independent trials), as a categorical
5135 *
5136 * Enumerated over <tt>{0..n}</tt> by the log-space recurrence
5137 * <tt>ln p(k+1) = ln p(k) + ln((n-k)/(k+1)) + ln(p/(1-p))</tt>
5138 * (outcomes below 1e-15 relative mass are dropped). @c p = 0 /
5139 * @c p = 1 are Diracs at @c 0 / @c n; @c n is capped at 10000.
5140 *
5141 * @sa <a href="https://en.wikipedia.org/wiki/Binomial_distribution">Wikipedia: Binomial distribution</a>
5142 */
5143CREATE OR REPLACE FUNCTION binomial(n INTEGER, p double precision)
5144 RETURNS random_variable AS
5145$$
5146DECLARE
5147 outcomes double precision[] := '{}';
5148 lps double precision[] := '{}';
5149 lp double precision;
5150 k INT;
5151BEGIN
5152 IF n IS NULL OR n < 0 THEN
5153 RAISE EXCEPTION 'provsql.binomial: n must be non-negative (got %)', n;
5154 END IF;
5155 IF NOT provsql.is_finite_float8(p) OR p < 0 OR p > 1 THEN
5156 RAISE EXCEPTION 'provsql.binomial: p must be in [0, 1] (got %)', p;
5157 END IF;
5158 IF n > 10000 THEN
5159 RAISE EXCEPTION 'provsql.binomial: n = % exceeds 10000; approximate with normal(n*p, sqrt(n*p*(1-p)))', n;
5160 END IF;
5161 IF n = 0 OR p = 0 THEN
5162 RETURN provsql.as_random(0);
5163 END IF;
5164 IF p = 1 THEN
5165 RETURN provsql.as_random(n);
5166 END IF;
5167 lp := n * ln(1 - p); -- ln p(0)
5168 outcomes := array_append(outcomes, 0::double precision);
5169 lps := array_append(lps, lp);
5170 FOR k IN 0..(n - 1) LOOP
5171 lp := lp + ln((n - k)::double precision / (k + 1)) + ln(p / (1 - p));
5172 outcomes := array_append(outcomes, (k + 1)::double precision);
5173 lps := array_append(lps, lp);
5174 END LOOP;
5175 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
5176END
5177$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5178
5179/**
5180 * @brief Binomial with a fixed trial count and a LATENT success
5181 * probability: @c binomial(INTEGER, random_variable).
5182 *
5183 * @p n is a literal trial count; @p p is a latent (token-valued) success
5184 * probability (e.g. @c "40.0 / N" for a latent population size @c N).
5185 * Builds a parametric @c gate_rv leaf (family @c "binomial", @c extra
5186 * @c "binomial:n,$0") the Monte Carlo sampler resolves per draw; @c observe
5187 * weights by the Binomial pmf. Unblocks capture-recapture-style posteriors
5188 * such as @c "N | (binomial(50, 40.0/N) = recaptured_count)".
5189 */
5190CREATE OR REPLACE FUNCTION binomial(n INTEGER, p random_variable)
5191 RETURNS random_variable AS
5192$$ SELECT provsql.rv_parametric2('binomial', NULL, $1::double precision,
5193 ($2)::UUID, NULL); $$
5194 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
5195
5196/**
5197 * @brief Construct a Geometric(p) random variable -- the number of
5198 * TRIALS up to and including the first success (support
5199 * starting at 1; subtract 1 for the failures convention)
5200 *
5201 * <tt>P(X = k) = (1-p)^{k-1} p</tt>, enumerated up to the 1e-15
5202 * relative-mass tail and renormalised. @c p = 1 is a Dirac at @c 1.
5203 *
5204 * @sa <a href="https://en.wikipedia.org/wiki/Geometric_distribution">Wikipedia: Geometric distribution</a>
5205 */
5206CREATE OR REPLACE FUNCTION geometric(p double precision)
5207 RETURNS random_variable AS
5208$$
5209DECLARE
5210 k_max INT;
5211 outcomes double precision[] := '{}';
5212 lps double precision[] := '{}';
5213 k INT;
5214BEGIN
5215 IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
5216 RAISE EXCEPTION 'provsql.geometric: p must be in (0, 1] (got %)', p;
5217 END IF;
5218 IF p = 1 THEN
5219 RETURN provsql.as_random(1);
5220 END IF;
5221 k_max := 1 + ceil(ln(1e-15) / ln(1 - p))::INT;
5222 IF k_max > 10000 THEN
5223 RAISE EXCEPTION 'provsql.geometric: support window of % outcomes exceeds 10000 (p = % is too small); approximate with exponential(%)', k_max, p, p;
5224 END IF;
5225 FOR k IN 1..k_max LOOP
5226 outcomes := array_append(outcomes, k::double precision);
5227 lps := array_append(lps, (k - 1) * ln(1 - p) + ln(p));
5228 END LOOP;
5229 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
5230END
5231$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5232
5233/**
5234 * @brief Geometric with a LATENT success probability: @c geometric(random_variable).
5235 *
5236 * A latent (token-valued) @p p cannot be enumerated at construction, so this
5237 * builds a parametric @c gate_rv leaf (family @c "geometric") wiring the
5238 * probability, resolved per draw by the sampler. @c observe weights by the
5239 * geometric pmf; unblocks a Beta-Geometric conjugate posterior.
5240 */
5241CREATE OR REPLACE FUNCTION geometric(p random_variable)
5242 RETURNS random_variable AS
5243$$ SELECT provsql.rv_parametric1('geometric', ($1)::UUID); $$
5244 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
5245
5247 * @brief Construct a Hypergeometric(N, K, n) random variable: the
5248 * number of marked items among @p n draws WITHOUT replacement
5249 * from a population of @p pop_n items of which @p k_marked are
5250 * marked
5251 *
5252 * The exact finite support <tt>[max(0, n-(N-K)), min(n, K)]</tt> is
5253 * enumerated by the pmf ratio recurrence (in log space, so large
5254 * populations cannot overflow) and normalised -- exact "sampling
5255 * without replacement" probabilities with no combinatorial functions
5256 * needed.
5257 *
5258 * @sa <a href="https://en.wikipedia.org/wiki/Hypergeometric_distribution">Wikipedia: Hypergeometric distribution</a>
5259 */
5260CREATE OR REPLACE FUNCTION hypergeometric(pop_n INTEGER, k_marked INTEGER, n INTEGER)
5261 RETURNS random_variable AS
5263DECLARE
5264 lo INT;
5265 hi INT;
5266 outcomes double precision[] := '{}';
5267 lps double precision[] := '{}';
5268 lp double precision := 0; -- relative log-mass; normalised later
5269 k INT;
5270BEGIN
5271 IF pop_n IS NULL OR k_marked IS NULL OR n IS NULL
5272 OR pop_n < 0 OR k_marked < 0 OR n < 0
5273 OR k_marked > pop_n OR n > pop_n THEN
5274 RAISE EXCEPTION 'provsql.hypergeometric: need 0 <= k_marked, n <= pop_n (got pop_n=%, k_marked=%, n=%)', pop_n, k_marked, n;
5275 END IF;
5276 lo := greatest(0, n - (pop_n - k_marked));
5277 hi := least(n, k_marked);
5278 IF hi - lo + 1 > 10000 THEN
5279 RAISE EXCEPTION 'provsql.hypergeometric: support window of % outcomes exceeds 10000', hi - lo + 1;
5280 END IF;
5281 outcomes := array_append(outcomes, lo::double precision);
5282 lps := array_append(lps, lp);
5283 FOR k IN lo..(hi - 1) LOOP
5284 -- pmf(k+1)/pmf(k) = (K-k)(n-k) / ((k+1)(N-K-n+k+1))
5285 lp := lp + ln((k_marked - k)::double precision * (n - k))
5286 - ln((k + 1)::double precision * (pop_n - k_marked - n + k + 1));
5287 outcomes := array_append(outcomes, (k + 1)::double precision);
5288 lps := array_append(lps, lp);
5289 END LOOP;
5290 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
5291END
5292$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5293
5294/**
5295 * @brief Construct a negative-binomial random variable: the number of
5296 * FAILURES before the @p r-th success (support starting at 0),
5297 * with real @p r > 0 allowed (the Polya / overdispersed-count
5298 * parameterisation, the Poisson-Gamma mixture)
5299 *
5300 * <tt>P(X = k) = C(k+r-1, k) p^r (1-p)^k</tt>, enumerated by the
5301 * log-space recurrence
5302 * <tt>ln p(k+1) = ln p(k) + ln((k+r)/(k+1)) + ln(1-p)</tt> up to the
5303 * 1e-15 relative-mass tail. @c p = 1 is a Dirac at @c 0.
5304 *
5305 * @sa <a href="https://en.wikipedia.org/wiki/Negative_binomial_distribution">Wikipedia: Negative binomial distribution</a>
5306 */
5307CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p double precision)
5308 RETURNS random_variable AS
5309$$
5310DECLARE
5311 outcomes double precision[] := '{}';
5312 lps double precision[] := '{}';
5313 lp double precision;
5314 max_lp double precision;
5315 mean double precision;
5316 k INT := 0;
5317BEGIN
5318 IF NOT provsql.is_finite_float8(r) OR r <= 0 THEN
5319 RAISE EXCEPTION 'provsql.negative_binomial: r must be finite and strictly positive (got %)', r;
5320 END IF;
5321 IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
5322 RAISE EXCEPTION 'provsql.negative_binomial: p must be in (0, 1] (got %)', p;
5323 END IF;
5324 IF p = 1 THEN
5325 RETURN provsql.as_random(0);
5326 END IF;
5327 mean := r * (1 - p) / p;
5328 lp := r * ln(p); -- ln p(0)
5329 max_lp := lp;
5330 outcomes := array_append(outcomes, 0::double precision);
5331 lps := array_append(lps, lp);
5332 LOOP
5333 lp := lp + ln((k + r) / (k + 1)) + ln(1 - p);
5334 k := k + 1;
5335 IF lp > max_lp THEN max_lp := lp; END IF;
5336 outcomes := array_append(outcomes, k::double precision);
5337 lps := array_append(lps, lp);
5338 EXIT WHEN k > mean AND lp < max_lp + ln(1e-15);
5339 IF k >= 10000 THEN
5340 RAISE EXCEPTION 'provsql.negative_binomial: support window exceeds 10000 outcomes (r=%, p=%)', r, p;
5341 END IF;
5342 END LOOP;
5343 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
5344END
5345$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5346
5347/*
5348 * NegativeBinomial with a LATENT parameter: the count r (number of successes)
5349 * stays a plain number while the success probability p is a random_variable,
5350 * built as a parametric gate_rv leaf (family "negative_binomial") -- the
5351 * Beta-NegativeBinomial conjugate shape. The all-random and latent-r forms
5352 * are provided for uniformity with the continuous constructors.
5353 */
5354CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p random_variable)
5355 RETURNS random_variable AS
5356$$ SELECT provsql.rv_parametric2('negative_binomial', NULL, $1, ($2)::UUID, NULL); $$
5357 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
5358CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p double precision)
5359 RETURNS random_variable AS
5360$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::UUID, NULL, NULL, $2); $$
5361 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
5362CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p random_variable)
5363 RETURNS random_variable AS
5364$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
5365 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
5366
5367/**
5368 * @brief Catalog of the registered continuous-distribution families.
5369 *
5370 * One row per @c gate_rv family known to this build of the extension:
5371 * @c name is the on-disk token (the part before the colon in the gate's
5372 * @c extra encoding), @c nparams the parameter count, @c param_names the
5373 * conventional parameter symbols in @c extra order (e.g.
5374 * <tt>{μ, σ}</tt>), and @c label a short display glyph (e.g. @c "N",
5375 * @c "Γ"). UI clients (ProvSQL Studio's circuit inspector) read this to
5376 * render families they were not hard-coded for, so a newly added family
5377 * shows up without a client release.
5378 */
5379CREATE OR REPLACE FUNCTION rv_families()
5380 RETURNS TABLE(name TEXT, nparams INT, param_names TEXT[], label TEXT) AS
5381 'provsql','rv_families' LANGUAGE C STABLE PARALLEL SAFE;
5382
5383/**
5384 * @brief Construct a probabilistic-mixture random variable.
5385 *
5386 * Returns a @c random_variable whose distribution is a Bernoulli
5387 * mixture of two scalar RV roots: with probability <tt>P(p = true)</tt>
5388 * the mixture samples @p x, with the complementary probability it
5389 * samples @p y. The mixing token @p p is a @c gate_input Bernoulli
5390 * whose probability has been pinned with @c set_prob, and the same
5391 * @p p can be shared with other branches of the circuit -- the
5392 * Monte-Carlo sampler's per-iteration cache couples every reference
5393 * to the same draw, so users can build joint conditional structures
5394 * (e.g. <tt>mixture(p, X1, Y1) + mixture(p, X2, Y2)</tt> samples
5395 * X1 + X2 with prob π and Y1 + Y2 with prob 1-π).
5396 *
5397 * @p x and @p y may be any scalar RV root: a base @c gate_rv
5398 * (@c normal / @c uniform / @c exponential / @c erlang), a
5399 * @c gate_value Dirac (@c as_random), a @c gate_arith expression, or
5400 * another @c mixture. N-ary mixtures are built by composition --
5401 * <tt>mixture(p1, A, mixture(p2, B, C))</tt> realises a 3-component
5402 * mixture with effective weights <tt>π1, (1-π1)·π2, (1-π1)·(1-π2)</tt>.
5403 *
5404 * Validation:
5405 * - @p p must point to a Boolean gate (@c input, @c mulinput,
5406 * @c update, @c plus, @c times, @c monus, @c project, @c eq,
5407 * @c cmp, @c zero, @c one). Compound Boolean gates derive their
5408 * probability from their atoms via the active probability-evaluation
5409 * method; a bare @c gate_input's probability is whatever @c set_prob
5410 * pinned (@c set_prob is responsible for keeping it in [0, 1]).
5411 * - @p x and @p y must be scalar RV roots; aggregate / Boolean roots
5412 * are rejected at construction.
5413 *
5414 * Two calls to @c mixture with the same @c (p, x, y) operands collapse
5415 * to the same @c gate_mixture node by v5-hash, exactly like
5416 * @c arith(PLUS, X, Y). Draw independence is controlled by @p p:
5417 * sharing @p p couples branch selection across consumers via the
5418 * sampler's @c bool_cache_; minting independent Bernoullis (e.g. via
5419 * the @c mixture(p_value, …) overload) decouples them.
5420 *
5421 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
5423CREATE OR REPLACE FUNCTION mixture(
5424 p UUID, x random_variable, y random_variable)
5425 RETURNS random_variable AS
5426$$
5427DECLARE
5428 token UUID;
5429 p_kind provsql.PROVENANCE_GATE;
5430 x_uuid UUID;
5431 y_uuid UUID;
5432 x_kind provsql.PROVENANCE_GATE;
5433 y_kind provsql.PROVENANCE_GATE;
5434BEGIN
5435 p_kind := provsql.get_gate_type(p);
5436 IF p_kind NOT IN ('input','mulinput','update',
5437 'plus','times','monus',
5438 'project','eq','cmp',
5439 'zero','one') THEN
5440 RAISE EXCEPTION 'provsql.mixture: p must be a Boolean gate '
5441 '(input/mulinput/update/plus/times/monus/project/eq/cmp/zero/one), got %', p_kind
5442 USING ERRCODE = 'feature_not_supported',
5443 DETAIL = 'provsql-reason: mixture-argument-kind; scope: out-of-scope';
5444 END IF;
5445
5446 x_uuid := (x)::UUID;
5447 y_uuid := (y)::UUID;
5448 x_kind := provsql.get_gate_type(x_uuid);
5449 y_kind := provsql.get_gate_type(y_uuid);
5450 IF x_kind NOT IN ('rv','value','arith','mixture') THEN
5451 RAISE EXCEPTION 'provsql.mixture: x must be a scalar RV root (rv / value / arith / mixture), got %', x_kind
5452 USING ERRCODE = 'feature_not_supported',
5453 DETAIL = 'provsql-reason: mixture-argument-kind; scope: out-of-scope';
5454 END IF;
5455 IF y_kind NOT IN ('rv','value','arith','mixture') THEN
5456 RAISE EXCEPTION 'provsql.mixture: y must be a scalar RV root (rv / value / arith / mixture), got %', y_kind
5457 USING ERRCODE = 'feature_not_supported',
5458 DETAIL = 'provsql-reason: mixture-argument-kind; scope: out-of-scope';
5459 END IF;
5460
5461 token := public.uuid_generate_v5(
5462 provsql.uuid_ns_provsql(),
5463 concat('mixture', p, x_uuid, y_uuid));
5464 PERFORM provsql.create_gate(token, 'mixture', ARRAY[p, x_uuid, y_uuid]);
5465 RETURN provsql.random_variable_make(token);
5467$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
5468
5469/**
5470 * @brief Ad-hoc mixture constructor that mints a fresh anonymous
5471 * @c gate_input Bernoulli with probability @p p_value.
5472 *
5473 * Sugar over the @c mixture(UUID, x, y) form: when the caller doesn't
5474 * care about reusing the Bernoulli token elsewhere in the circuit
5475 * (which is the common case &ndash; "give me a 0.3 / 0.7 weighted GMM,
5476 * I don't need to share the coin"), this overload creates the
5477 * underlying @c gate_input on the fly with a fresh
5478 * @c uuid_generate_v4() token, pins @p p_value via @c set_prob, and
5479 * threads everything into the UUID-keyed constructor.
5480 *
5481 * Each call mints a NEW Bernoulli, so two calls to
5482 * <tt>mixture(0.5, X, Y)</tt> are *independent* mixtures whose branch
5483 * selections are uncorrelated. When coupling is desired (e.g. two
5484 * mixtures sharing a coin), use the @c mixture(UUID, x, y) form with a
5485 * user-managed @c gate_input token.
5486 *
5487 * @warning <tt>VOLATILE</tt> is load-bearing for the same reason as
5488 * @ref normal and the other RV constructors -- folding under
5489 * @c STABLE / @c IMMUTABLE would collapse two independent draws into
5490 * one shared gate.
5491 *
5492 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
5494CREATE OR REPLACE FUNCTION mixture(
5495 p_value double precision,
5496 x random_variable,
5497 y random_variable)
5498 RETURNS random_variable AS
5499$$
5500DECLARE
5501 p_token UUID;
5502BEGIN
5503 IF p_value IS NULL OR p_value <> p_value OR p_value < 0 OR p_value > 1 THEN
5504 RAISE EXCEPTION 'provsql.mixture: probability must be in [0,1] (got %)', p_value;
5505 END IF;
5506 p_token := public.uuid_generate_v4();
5507 PERFORM provsql.create_gate(p_token, 'input');
5508 PERFORM provsql.set_prob(p_token, p_value);
5509 RETURN provsql.mixture(p_token, x, y);
5510END
5511$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5512
5513/**
5514 * @brief Categorical-RV constructor over explicit (probabilities,
5515 * values) arrays.
5516 *
5517 * Builds a categorical-form @c gate_mixture directly: a fresh
5518 * @c gate_input "key" anchor and one @c gate_mulinput per outcome with
5519 * positive mass, all sharing the key. The wires
5520 * <tt>[key, mul_1, ..., mul_n]</tt> are what downstream evaluators
5521 * (@c Expectation, @c MonteCarloSampler, @c AnalyticEvaluator,
5522 * @c RangeCheck) recognise via @c isCategoricalMixture and treat as a
5523 * scalar RV with the categorical distribution @p probs over
5524 * @p outcomes.
5526 * Validation:
5527 * - @p probs and @p outcomes must be non-null, same length, length &ge; 1.
5528 * - Each @c probs[i] must be finite, in <tt>[0, 1]</tt>, and the array
5529 * must sum to 1 within @c 1e-9.
5530 * - Each @c outcomes[i] must be finite.
5531 *
5532 * Each call mints a fresh key gate and a fresh set of mulinputs, so
5533 * two calls to @c categorical with the same arrays are *independent*
5534 * categorical RVs. The marking is @c VOLATILE accordingly.
5535 *
5536 * Degenerate case: a categorical with exactly one positive-mass
5537 * outcome reduces to @c as_random(v) at construction (the block would
5538 * just be a single mulinput, which is operationally a Dirac point
5539 * mass). Two such calls share the @c gate_value UUID via the v5
5540 * convention @c as_random already uses.
5542 * @sa @c mixture for the Bernoulli-weighted choice constructor.
5543 * @sa <a href="https://en.wikipedia.org/wiki/Categorical_distribution">Wikipedia: Categorical distribution</a>
5544 */
5545CREATE OR REPLACE FUNCTION categorical(
5546 probs double precision[],
5547 outcomes double precision[])
5548 RETURNS random_variable AS
5549$$
5550DECLARE
5551 n INTEGER;
5552 p_sum double precision := 0.0;
5553 i INTEGER;
5554 key_token UUID;
5555 mix_token UUID;
5556 mul_token UUID;
5557 mul_tokens UUID[] := ARRAY[]::UUID[];
5558 mix_wires UUID[];
5559 pi_i double precision;
5560 vi_i double precision;
5561BEGIN
5562 IF probs IS NULL OR outcomes IS NULL THEN
5563 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must be non-null';
5564 END IF;
5565 n := array_length(probs, 1);
5566 IF n IS NULL OR n < 1 THEN
5567 RAISE EXCEPTION 'provsql.categorical: probs must be non-empty';
5568 END IF;
5569 IF array_length(outcomes, 1) <> n THEN
5570 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must have the same length (got % and %)',
5571 n, array_length(outcomes, 1);
5572 END IF;
5573
5574 FOR i IN 1..n LOOP
5575 pi_i := probs[i];
5576 vi_i := outcomes[i];
5577 -- PostgreSQL diverges from IEEE 754: NaN = NaN is TRUE there, so
5578 -- the canonical x <> x NaN test doesn't fire. Compare against the
5579 -- literal 'NaN'::float8 instead, and reject ±Infinity for outcomes
5580 -- explicitly.
5581 IF pi_i IS NULL OR pi_i = 'NaN'::float8 OR pi_i < 0 OR pi_i > 1 THEN
5582 RAISE EXCEPTION 'provsql.categorical: probs[%] must be in [0,1] (got %)', i, pi_i;
5583 END IF;
5584 IF vi_i IS NULL OR vi_i = 'NaN'::float8
5585 OR vi_i = 'Infinity'::float8 OR vi_i = '-Infinity'::float8 THEN
5586 RAISE EXCEPTION 'provsql.categorical: outcomes[%] must be finite (got %)', i, vi_i;
5587 END IF;
5588 p_sum := p_sum + pi_i;
5589 END LOOP;
5590 IF abs(p_sum - 1.0) > 1e-9 THEN
5591 RAISE EXCEPTION 'provsql.categorical: probs must sum to 1 within 1e-9 (got %)', p_sum;
5592 END IF;
5593
5594 -- Degenerate case: exactly one positive-mass outcome (the rest are
5595 -- zero). The "categorical" is then a Dirac point mass; skip the
5596 -- block-allocation entirely and return @c as_random(v), which yields
5597 -- a shared, v5-keyed gate_value -- exactly what downstream
5598 -- evaluators (rv_moment, AnalyticEvaluator, rv_support) treat
5599 -- specially. Saves a key gate and a mulinput per call, and lets
5600 -- two calls to @c categorical({1.0}, {v}) collide on the same
5601 -- gate_value UUID instead of producing distinct anonymous blocks.
5602 DECLARE
5603 nb_positive INTEGER := 0;
5604 only_idx INTEGER := 0;
5605 BEGIN
5606 FOR i IN 1..n LOOP
5607 IF probs[i] > 0.0 THEN
5608 nb_positive := nb_positive + 1;
5609 only_idx := i;
5610 END IF;
5611 END LOOP;
5612 IF nb_positive = 1 THEN
5613 RETURN provsql.as_random(outcomes[only_idx]);
5614 END IF;
5615 END;
5616
5617 -- Mint the block's key anchor. Probability 1.0 matches the
5618 -- joint-table convention: the categorical mass lives on the
5619 -- mulinputs, the key just identifies the block.
5620 key_token := public.uuid_generate_v4();
5621 PERFORM provsql.create_gate(key_token, 'input');
5622 PERFORM provsql.set_prob(key_token, 1.0);
5623
5624 -- One mulinput per positive-probability outcome. Zero-probability
5625 -- entries contribute no mass and are skipped: the gate_mixture's
5626 -- wire vector is otherwise polluted with no-op leaves.
5627 FOR i IN 1..n LOOP
5628 pi_i := probs[i];
5629 IF pi_i <= 0.0 THEN CONTINUE; END IF;
5630 mul_token := public.uuid_generate_v4();
5631 PERFORM provsql.create_gate(mul_token, 'mulinput', ARRAY[key_token],
5632 i - 1, NULL, outcomes[i]::TEXT);
5633 PERFORM provsql.set_prob(mul_token, pi_i);
5634 mul_tokens := mul_tokens || mul_token;
5635 END LOOP;
5636
5637 mix_wires := ARRAY[key_token] || mul_tokens;
5638 mix_token := public.uuid_generate_v4();
5639 PERFORM provsql.create_gate(mix_token, 'mixture', mix_wires);
5640 RETURN provsql.random_variable_make(mix_token);
5641END
5642$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5643
5644/**
5645 * @brief Gaussian-mixture-model (GMM) constructor.
5647 * Packages the common fitted-density pattern -- a categorical choice
5648 * among Normal components -- into one call:
5649 *
5650 * @code
5651 * provsql.gmm(weights => ARRAY[0.3, 0.5, 0.2],
5652 * means => ARRAY[120.0, 380.0, 1200.0],
5653 * stddevs => ARRAY[40.0, 90.0, 250.0])
5654 * @endcode
5655 *
5656 * No new gate: the mixture decomposes into a stick-breaking cascade of
5657 * Bernoulli @c gate_mixture nodes over @c gate_rv Normal leaves
5658 * (component @c i is selected with conditional probability
5659 * @c w_i / (w_i + ... + w_n), so the joint selection probabilities are
5660 * exactly @p weights), which every evaluator already handles: moments
5661 * are closed-form through the mixture recursion, sampling is exact,
5662 * and comparisons ride the existing mixture machinery. Zero-weight
5663 * components are skipped; a single positive-weight component returns
5664 * its Normal directly (no mixture node).
5665 *
5666 * Validation mirrors @c categorical: same-length non-empty arrays,
5667 * weights finite in <tt>[0, 1]</tt> summing to 1 within @c 1e-9; the
5668 * component parameters are validated by @c provsql.normal (finite
5669 * @c mu, non-negative @c sigma; @c sigma @c = @c 0 degenerates to a
5670 * Dirac component).
5671 *
5672 * @sa @c mixture, @c categorical, @c normal
5673 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_model">Wikipedia: Mixture model</a>
5674 */
5675CREATE OR REPLACE FUNCTION gmm(
5676 weights double precision[],
5677 means double precision[],
5678 stddevs double precision[])
5679 RETURNS random_variable AS
5680$$
5681DECLARE
5682 n INTEGER;
5683 w_sum double precision := 0.0;
5684 i INTEGER;
5685 acc random_variable := NULL;
5686 remaining double precision := 0.0;
5687BEGIN
5688 IF weights IS NULL OR means IS NULL OR stddevs IS NULL THEN
5689 RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must be non-null';
5690 END IF;
5691 n := array_length(weights, 1);
5692 IF n IS NULL OR n < 1 THEN
5693 RAISE EXCEPTION 'provsql.gmm: weights must be non-empty';
5694 END IF;
5695 IF array_length(means, 1) <> n OR array_length(stddevs, 1) <> n THEN
5696 RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must have the same length (got %, %, %)',
5697 n, array_length(means, 1), array_length(stddevs, 1);
5698 END IF;
5699 FOR i IN 1..n LOOP
5700 IF weights[i] IS NULL OR weights[i] = 'NaN'::float8
5701 OR weights[i] < 0 OR weights[i] > 1 THEN
5702 RAISE EXCEPTION 'provsql.gmm: weights[%] must be in [0,1] (got %)',
5703 i, weights[i];
5704 END IF;
5705 w_sum := w_sum + weights[i];
5706 END LOOP;
5707 IF abs(w_sum - 1.0) > 1e-9 THEN
5708 RAISE EXCEPTION 'provsql.gmm: weights must sum to 1 within 1e-9 (got %)', w_sum;
5709 END IF;
5710
5711 -- Stick-breaking, built back to front: acc holds the mixture of
5712 -- components i+1..n, and prepending component i selects it with
5713 -- conditional probability w_i / (w_i + ... + w_n).
5714 FOR i IN REVERSE n..1 LOOP
5715 IF weights[i] <= 0.0 THEN
5716 CONTINUE;
5717 END IF;
5718 IF acc IS NULL THEN
5719 acc := provsql.normal(means[i], stddevs[i]);
5720 remaining := weights[i];
5721 ELSE
5722 remaining := remaining + weights[i];
5723 acc := provsql.mixture(least(1.0, weights[i] / remaining),
5724 provsql.normal(means[i], stddevs[i]), acc);
5725 END IF;
5726 END LOOP;
5727 RETURN acc;
5728END
5729$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5730
5731/**
5732 * @brief Empirical-samples constructor: the ecdf of a sample bundle as
5733 * a @c random_variable.
5734 *
5735 * Loads a Monte Carlo / MCMC / bootstrap sample array as the discrete
5736 * distribution putting mass @c 1/n on each draw (duplicates merge, so a
5737 * value drawn @c k times carries @c k/n) -- the standard empirical
5738 * distribution. Reduces entirely to @ref categorical, so the whole
5739 * exact discrete surface applies: moments are the sample moments,
5740 * comparisons against constants are decided analytically ("fraction of
5741 * samples below c"), and quantiles are the exact empirical quantiles.
5742 *
5743 * @code
5744 * -- Bulk load via array_agg over a sample table
5745 * INSERT INTO model_posteriors
5746 * SELECT param, provsql.empirical_samples(array_agg(value))
5747 * FROM mcmc_chain GROUP BY param;
5748 * @endcode
5749 *
5750 * At most 10000 distinct values (the categorical block cap): thin the
5751 * chain or bin the samples (e.g. with @c width_bucket) beyond that.
5752 *
5753 * @sa @ref categorical, @ref empirical_cdf
5754 * @sa <a href="https://en.wikipedia.org/wiki/Empirical_distribution_function">Wikipedia: Empirical distribution function</a>
5755 */
5756CREATE OR REPLACE FUNCTION empirical_samples(samples double precision[])
5757 RETURNS random_variable AS
5758$$
5759DECLARE
5760 n INTEGER;
5761 sorted double precision[];
5762 outcomes double precision[] := '{}';
5763 probs double precision[] := '{}';
5764 v double precision;
5765 prev double precision;
5766 run INTEGER := 0;
5767 started BOOLEAN := false;
5768BEGIN
5769 n := array_length(samples, 1);
5770 IF n IS NULL OR n < 1 THEN
5771 RAISE EXCEPTION 'provsql.empirical_samples: samples must be non-empty';
5772 END IF;
5773 sorted := ARRAY(SELECT s FROM unnest(samples) AS s ORDER BY 1);
5774 FOREACH v IN ARRAY sorted LOOP
5775 IF v IS NULL OR v = 'NaN'::float8
5776 OR v = 'Infinity'::float8 OR v = '-Infinity'::float8 THEN
5777 RAISE EXCEPTION
5778 'provsql.empirical_samples: samples must be finite (got %)', v;
5779 END IF;
5780 IF started AND v = prev THEN
5781 run := run + 1;
5782 ELSE
5783 IF started THEN
5784 outcomes := outcomes || prev;
5785 probs := probs || (run::double precision / n);
5786 END IF;
5787 prev := v;
5788 run := 1;
5789 started := true;
5790 END IF;
5791 END LOOP;
5792 outcomes := outcomes || prev;
5793 probs := probs || (run::double precision / n);
5794 IF array_length(outcomes, 1) > 10000 THEN
5795 RAISE EXCEPTION
5796 'provsql.empirical_samples: at most 10000 distinct values are '
5797 'supported (got %); thin the chain or bin the samples (e.g. with '
5798 'width_bucket)', array_length(outcomes, 1);
5799 END IF;
5800 RETURN provsql.categorical(probs, outcomes);
5801END
5802$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5803
5804/**
5805 * @brief Empirical-CDF constructor: a piecewise-linear CDF table as a
5806 * @c random_variable.
5807 *
5808 * Loads a tabulated CDF -- simulation output percentile tables, risk
5809 * models, expert-elicited forecasts -- as the distribution whose CDF is
5810 * @c cdf[i] at @c grid[i], linear in between: mass
5811 * @c cdf[i+1] @c - @c cdf[i] spread uniformly over
5812 * <tt>(grid[i], grid[i+1])</tt>, plus (when @c cdf[1] @c > @c 0) an
5813 * atom of mass @c cdf[1] at @c grid[1] for the probability at or below
5814 * the grid start. Packaged, like @ref gmm, as a stick-breaking cascade
5815 * of Bernoulli @ref mixture nodes over @ref uniform components (and the
5816 * optional @ref as_random atom), so moments and sampling are exact
5817 * through the existing mixture machinery; comparisons ride Monte Carlo.
5818 *
5819 * @code
5820 * provsql.empirical_cdf(
5821 * grid => ARRAY[0.0, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0],
5822 * cdf => ARRAY[0.32, 0.51, 0.67, 0.82, 0.94, 0.99, 1.0])
5823 * @endcode
5825 * Validation: same-length arrays of at least two entries, @p grid
5826 * strictly increasing and finite, @p cdf non-decreasing within
5827 * <tt>[0, 1]</tt> and ending at @c 1 within @c 1e-9.
5828 *
5829 * @sa @ref gmm, @ref empirical_samples
5830 * @sa <a href="https://en.wikipedia.org/wiki/Cumulative_distribution_function">Wikipedia: Cumulative distribution function</a>
5831 */
5832CREATE OR REPLACE FUNCTION empirical_cdf(grid double precision[],
5833 cdf double precision[])
5834 RETURNS random_variable AS
5835$$
5836DECLARE
5837 n INTEGER;
5838 i INTEGER;
5839 acc random_variable := NULL;
5840 remaining double precision := 0.0;
5841 w double precision;
5842 comp random_variable;
5843BEGIN
5844 n := array_length(grid, 1);
5845 IF n IS NULL OR n < 2 THEN
5846 RAISE EXCEPTION 'provsql.empirical_cdf: grid must have at least two entries';
5847 END IF;
5848 IF array_length(cdf, 1) <> n THEN
5849 RAISE EXCEPTION 'provsql.empirical_cdf: grid and cdf must have the same length (got % and %)',
5850 n, array_length(cdf, 1);
5851 END IF;
5852 IF n > 10000 THEN
5853 RAISE EXCEPTION 'provsql.empirical_cdf: at most 10000 grid points are supported (got %)', n;
5854 END IF;
5855 FOR i IN 1..n LOOP
5856 IF grid[i] IS NULL OR grid[i] = 'NaN'::float8
5857 OR grid[i] = 'Infinity'::float8 OR grid[i] = '-Infinity'::float8 THEN
5858 RAISE EXCEPTION 'provsql.empirical_cdf: grid[%] must be finite (got %)', i, grid[i];
5859 END IF;
5860 IF i > 1 AND NOT grid[i] > grid[i-1] THEN
5861 RAISE EXCEPTION 'provsql.empirical_cdf: grid must be strictly increasing (grid[%] = %, grid[%] = %)',
5862 i-1, grid[i-1], i, grid[i];
5863 END IF;
5864 IF cdf[i] IS NULL OR cdf[i] = 'NaN'::float8 OR cdf[i] < 0 OR cdf[i] > 1 THEN
5865 RAISE EXCEPTION 'provsql.empirical_cdf: cdf[%] must be in [0,1] (got %)', i, cdf[i];
5866 END IF;
5867 IF i > 1 AND cdf[i] < cdf[i-1] THEN
5868 RAISE EXCEPTION 'provsql.empirical_cdf: cdf must be non-decreasing (cdf[%] = %, cdf[%] = %)',
5869 i-1, cdf[i-1], i, cdf[i];
5870 END IF;
5871 END LOOP;
5872 IF abs(cdf[n] - 1.0) > 1e-9 THEN
5873 RAISE EXCEPTION 'provsql.empirical_cdf: cdf must end at 1 within 1e-9 (got %)', cdf[n];
5874 END IF;
5875
5876 -- Stick-breaking cascade, back to front: component i = 1 is the atom
5877 -- at the grid start (mass cdf[1]); component i >= 2 is
5878 -- uniform(grid[i-1], grid[i]) with mass cdf[i] - cdf[i-1].
5879 FOR i IN REVERSE n..1 LOOP
5880 w := CASE WHEN i = 1 THEN cdf[1] ELSE cdf[i] - cdf[i-1] END;
5881 IF w <= 0.0 THEN
5882 CONTINUE;
5883 END IF;
5884 comp := CASE WHEN i = 1 THEN provsql.as_random(grid[1])
5885 ELSE provsql.uniform(grid[i-1], grid[i]) END;
5886 IF acc IS NULL THEN
5887 acc := comp;
5888 remaining := w;
5889 ELSE
5890 remaining := remaining + w;
5891 acc := provsql.mixture(least(1.0, w / remaining), comp, acc);
5892 END IF;
5893 END LOOP;
5894 RETURN acc;
5895END
5896$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
5897
5899 * @brief Lift a deterministic constant into a random_variable
5900 *
5901 * Creates a <tt>gate_value</tt> carrying the constant's TEXT form so
5902 * that comparisons against a <tt>random_variable</tt> column produce
5903 * the same circuit shape regardless of whether the operand is an
5904 * actual RV or a literal constant.
5905 *
5906 * Marked <tt>IMMUTABLE</tt>: the gate UUID is derived deterministically
5907 * from the constant via the same v5 convention as <tt>provenance_semimod</tt>'s
5908 * inline value gate (<tt>concat('value', CAST(c AS VARCHAR))</tt>), so
5909 * <tt>as_random(2)</tt> always resolves to the same gate, and any other
5910 * code path that already creates a value gate for the same constant
5911 * (e.g. <tt>provenance_semimod</tt>) shares the UUID.
5912 * <tt>create_gate</tt> is idempotent on already-mapped tokens, so
5913 * repeat invocations are harmless.
5914 *
5915 * @sa <a href="https://en.wikipedia.org/wiki/Degenerate_distribution">Wikipedia: Degenerate distribution (Dirac point mass)</a>
5916 */
5917CREATE OR REPLACE FUNCTION as_random(c double precision)
5918 RETURNS random_variable AS
5919$$
5920DECLARE
5921 -- Canonicalise -0.0 to +0.0: IEEE 754 defines x + 0.0 = +0.0 for
5922 -- both signed zeros, and is identity for finite, NaN, and ±Infinity.
5923 -- Without this, as_random(-0.0) and as_random(+0.0) would produce
5924 -- different gate UUIDs (their CAST AS VARCHAR TEXT representations
5925 -- differ: '-0' vs '0') even though they denote the same constant.
5926 c_canon double precision := c + 0.0;
5927 c_text varchar := CAST(c_canon AS VARCHAR);
5928 token UUID := public.uuid_generate_v5(
5929 provsql.uuid_ns_provsql(), concat('value', c_text));
5930BEGIN
5931 PERFORM provsql.create_gate(token, 'value', NULL, NULL, NULL, c_text);
5932 RETURN provsql.random_variable_make(token);
5933END
5934$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
5935
5936/**
5937 * @brief Implicit cast double precision -> random_variable (lifts a
5938 * scalar literal to a constant RV).
5939 *
5940 * Lets users write <tt>WHERE reading > 2.5::float8</tt> instead of
5941 * <tt>WHERE reading > provsql.as_random(2.5)</tt>; the planner-hook
5942 * rewriter then sees a uniform <tt>random_variable</tt> on both sides.
5943 * Sibling casts below cover @c INTEGER and @c NUMERIC literals so
5944 * plain <tt>WHERE reading > 2</tt> and <tt>WHERE reading > 2.5</tt>
5945 * also work; PostgreSQL's operator resolution does not chain casts
5946 * across more than one step, so each NUMERIC-source type needs its
5947 * own direct cast.
5948 */
5949CREATE CAST (double precision AS random_variable)
5950 WITH FUNCTION as_random(double precision) AS IMPLICIT;
5951
5952/** @brief @c as_random for @c INTEGER (delegates to the @c float8 form). */
5953CREATE OR REPLACE FUNCTION as_random(c INTEGER)
5954 RETURNS random_variable AS
5955$$ SELECT provsql.as_random(c::double precision); $$
5956LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
5957
5958/** @brief @c as_random for @c NUMERIC (delegates to the @c float8 form). */
5959CREATE OR REPLACE FUNCTION as_random(c NUMERIC)
5960 RETURNS random_variable AS
5961$$ SELECT provsql.as_random(c::double precision); $$
5962LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
5963
5964/** @brief Implicit cast INTEGER -> random_variable. */
5965CREATE CAST (INTEGER AS random_variable)
5966 WITH FUNCTION as_random(INTEGER) AS IMPLICIT;
5967
5968/** @brief Implicit cast NUMERIC -> random_variable. */
5969CREATE CAST (NUMERIC AS random_variable)
5970 WITH FUNCTION as_random(NUMERIC) AS IMPLICIT;
5971
5972/**
5973 * @name Arithmetic and comparison on random_variable
5974 *
5975 * Each binary operator below is declared on @c (random_variable,
5976 * random_variable) only; mixed shapes such as <tt>rv + 2</tt> or
5977 * <tt>2.5 > rv</tt> resolve through the implicit casts from
5978 * @c INTEGER / @c NUMERIC / @c double @c precision to
5979 * @c random_variable declared above. This avoids the resolution
5980 * ambiguity that would arise if both <tt>(rv, NUMERIC)</tt> and
5981 * <tt>(rv, rv)</tt> overloads were declared while implicit casts also
5982 * existed.
5983 *
5984 * Arithmetic operators build a @c gate_arith via @c provenance_arith
5985 * and return a new @c random_variable wrapping its UUID.
5986 *
5987 * Comparison operators are placeholders that return @c BOOLEAN and
5988 * raise if executed -- the @c BOOLEAN return type is required so that
5989 * PostgreSQL accepts <tt>WHERE rv > 2</tt> at parse-analyze. The
5990 * planner hook intercepts every such @c OpExpr (matched by
5991 * @c opfuncid against @c constants_t::OID_FUNCTION_RV_CMP) and rewrites
5992 * it into a @c provenance_cmp call whose UUID is conjoined into the
5993 * tuple's @c provsql column via @c provenance_times. Code that needs
5994 * a @c gate_cmp UUID directly (without going through the planner hook)
5995 * uses the @c rv_cmp_* family below, which call @c provenance_cmp
5996 * with the matching float8-comparator OID.
5997 *
5998 * @{
5999 */
6000
6001/** @brief @c random_variable + @c random_variable (gate_arith PLUS). */
6002CREATE OR REPLACE FUNCTION random_variable_plus(
6003 a random_variable, b random_variable)
6004 RETURNS random_variable AS
6005$$
6006 SELECT provsql.random_variable_make(
6007 provsql.provenance_arith(
6008 0, -- PROVSQL_ARITH_PLUS
6009 ARRAY[(a)::UUID,
6010 (b)::UUID]));
6011$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6012
6013/** @brief @c random_variable - @c random_variable (gate_arith MINUS). */
6014CREATE OR REPLACE FUNCTION random_variable_minus(
6015 a random_variable, b random_variable)
6016 RETURNS random_variable AS
6017$$
6018 SELECT provsql.random_variable_make(
6019 provsql.provenance_arith(
6020 2, -- PROVSQL_ARITH_MINUS
6021 ARRAY[(a)::UUID,
6022 (b)::UUID]));
6023$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6024
6025/** @brief @c random_variable * @c random_variable (gate_arith TIMES). */
6026CREATE OR REPLACE FUNCTION random_variable_times(
6027 a random_variable, b random_variable)
6028 RETURNS random_variable AS
6029$$
6030 SELECT provsql.random_variable_make(
6031 provsql.provenance_arith(
6032 1, -- PROVSQL_ARITH_TIMES
6033 ARRAY[(a)::UUID,
6034 (b)::UUID]));
6035$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6036
6037/** @brief @c random_variable / @c random_variable (gate_arith DIV). */
6038CREATE OR REPLACE FUNCTION random_variable_div(
6039 a random_variable, b random_variable)
6040 RETURNS random_variable AS
6041$$
6042 SELECT provsql.random_variable_make(
6043 provsql.provenance_arith(
6044 3, -- PROVSQL_ARITH_DIV
6045 ARRAY[(a)::UUID,
6046 (b)::UUID]));
6047$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6048
6049/** @brief Unary @c -random_variable (gate_arith NEG). */
6050CREATE OR REPLACE FUNCTION random_variable_neg(a random_variable)
6051 RETURNS random_variable AS
6052$$
6053 SELECT provsql.random_variable_make(
6054 provsql.provenance_arith(
6055 4, -- PROVSQL_ARITH_NEG
6056 ARRAY[(a)::UUID]));
6057$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6058
6059/**
6060 * @brief @c random_variable ^ @c random_variable (gate_arith POW).
6061 *
6062 * Real-valued branch only: evaluation raises if a negative base is
6063 * drawn together with a non-INTEGER exponent (write
6064 * <tt>pow(greatest(x, 0), p)</tt> for the non-negative branch).
6065 */
6066CREATE OR REPLACE FUNCTION random_variable_pow(
6067 a random_variable, b random_variable)
6068 RETURNS random_variable AS
6069$$
6070 SELECT provsql.random_variable_make(
6071 provsql.provenance_arith(
6072 7, -- PROVSQL_ARITH_POW
6073 ARRAY[(a)::UUID,
6074 (b)::UUID]));
6075$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6076
6077/**
6078 * @brief Natural logarithm of a @c random_variable (gate_arith LN).
6079 *
6080 * Defined on @c [0, +Infinity): evaluation raises if a negative value
6081 * is drawn (restrict the argument's support); a draw of exactly @c 0
6082 * yields @c -Infinity.
6083 */
6084CREATE OR REPLACE FUNCTION ln(a random_variable)
6085 RETURNS random_variable AS
6086$$
6087 SELECT provsql.random_variable_make(
6088 provsql.provenance_arith(
6089 8, -- PROVSQL_ARITH_LN
6090 ARRAY[(a)::UUID]));
6091$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6093/** @brief @c e^x for a @c random_variable (gate_arith EXP). Total. */
6094CREATE OR REPLACE FUNCTION exp(a random_variable)
6095 RETURNS random_variable AS
6096$$
6097 SELECT provsql.random_variable_make(
6098 provsql.provenance_arith(
6099 9, -- PROVSQL_ARITH_EXP
6100 ARRAY[(a)::UUID]));
6101$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6102
6103/**
6104 * @brief @c pow / @c power spellings of the @c ^ operator, mirroring
6105 * PostgreSQL's NUMERIC surface. Scalar exponents resolve
6106 * through the implicit NUMERIC-to-rv casts:
6107 * <tt>pow(x, 0.5)</tt> is <tt>x ^ 0.5</tt>.
6108 */
6109CREATE OR REPLACE FUNCTION pow(a random_variable, b random_variable)
6110 RETURNS random_variable AS
6111$$
6112 SELECT provsql.random_variable_pow(a, b);
6113$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6114
6115CREATE OR REPLACE FUNCTION power(a random_variable, b random_variable)
6116 RETURNS random_variable AS
6117$$
6118 SELECT provsql.random_variable_pow(a, b);
6119$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6120
6121/**
6122 * @brief Square root of a @c random_variable: sugar for
6123 * <tt>x ^ 0.5</tt> (no gate or opcode of its own). Evaluation
6124 * raises on a negative draw, like any non-INTEGER exponent.
6125 */
6126CREATE OR REPLACE FUNCTION sqrt(a random_variable)
6127 RETURNS random_variable AS
6128$$
6129 SELECT provsql.random_variable_pow(a, provsql.as_random(0.5));
6130$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6131
6133 * @brief Internal helper: float8-comparator OID for a given symbol.
6134 *
6135 * Wraps the @c '&lt;sym&gt;(double precision,double precision)'::regoperator
6136 * lookup so the per-comparator functions read uniformly. Marked
6137 * @c IMMUTABLE because the resolved OID is fixed at catalog level
6138 * (the float8 comparators are core PG and never re-installed).
6139 */
6140CREATE OR REPLACE FUNCTION random_variable_cmp_oid(sym TEXT)
6141 RETURNS oid AS
6142$$
6143 SELECT (sym || '(double precision,double precision)')::regoperator::oid;
6144$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6145
6146/* The six @c random_variable_{lt,le,eq,ne,ge,gt} functions below are
6147 * BOOLEAN placeholders -- they exist only so the @c (rv, rv) operators
6148 * can be declared at all (PostgreSQL needs a procedure to bind to the
6149 * operator definition, and a procedure returning anything but @c BOOLEAN
6150 * would be rejected by parse-analyze in a WHERE position). They MUST
6151 * NOT be invoked directly: the planner hook in @c src/provsql.c
6152 * intercepts every @c OpExpr whose @c opfuncid matches one of these and
6153 * rewrites it into a @c provenance_cmp() call against the row's
6154 * provenance. If the executor ever reaches one of these, it means the
6155 * planner hook was bypassed (e.g. @c provsql.active was off), in which
6156 * case raising is the right behaviour. */
6157
6158/** @brief Placeholder body shared by every <tt>random_variable_*</tt>
6159 * comparison procedure. Raises with a uniform message. */
6160CREATE OR REPLACE FUNCTION random_variable_cmp_placeholder(
6161 a random_variable, b random_variable)
6162 RETURNS BOOLEAN AS
6163$$
6164BEGIN
6165 RAISE EXCEPTION 'random_variable comparison must be rewritten by the '
6166 'ProvSQL planner hook (is provsql.active off?)'
6167 USING ERRCODE = 'feature_not_supported',
6168 DETAIL = 'provsql-reason: rv-operator-not-rewritten; scope: out-of-scope';
6169END
6170$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
6171
6172CREATE OR REPLACE FUNCTION random_variable_lt(
6173 a random_variable, b random_variable) RETURNS BOOLEAN AS
6174$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
6175LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6176
6177CREATE OR REPLACE FUNCTION random_variable_le(
6178 a random_variable, b random_variable) RETURNS BOOLEAN AS
6179$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
6180LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6181
6182CREATE OR REPLACE FUNCTION random_variable_eq(
6183 a random_variable, b random_variable) RETURNS BOOLEAN AS
6184$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
6185LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6186
6187CREATE OR REPLACE FUNCTION random_variable_ne(
6188 a random_variable, b random_variable) RETURNS BOOLEAN AS
6189$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
6190LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6191
6192CREATE OR REPLACE FUNCTION random_variable_ge(
6193 a random_variable, b random_variable) RETURNS BOOLEAN AS
6194$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
6195LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6197CREATE OR REPLACE FUNCTION random_variable_gt(
6198 a random_variable, b random_variable) RETURNS BOOLEAN AS
6199$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
6200LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6201
6202/* Direct UUID constructors -- used by tests and any caller that wants
6203 * a @c gate_cmp without going through the planner hook (e.g. building
6204 * a circuit fragment in a SELECT list). Each delegates to
6205 * @c provenance_cmp with the matching float8-comparator OID. */
6206
6207/** @brief Build a @c gate_cmp for <tt>a &lt; b</tt> and return its UUID. */
6208CREATE OR REPLACE FUNCTION rv_cmp_lt(
6209 a random_variable, b random_variable) RETURNS UUID AS
6210$$
6211 SELECT provsql.provenance_cmp(
6212 (a)::UUID,
6213 provsql.random_variable_cmp_oid('<'),
6214 (b)::UUID);
6215$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6216
6217/** @brief Build a @c gate_cmp for <tt>a &le; b</tt> and return its UUID. */
6218CREATE OR REPLACE FUNCTION rv_cmp_le(
6219 a random_variable, b random_variable) RETURNS UUID AS
6220$$
6221 SELECT provsql.provenance_cmp(
6222 (a)::UUID,
6223 provsql.random_variable_cmp_oid('<='),
6224 (b)::UUID);
6225$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6226
6227/** @brief Build a @c gate_cmp for <tt>a = b</tt> and return its UUID. */
6228CREATE OR REPLACE FUNCTION rv_cmp_eq(
6229 a random_variable, b random_variable) RETURNS UUID AS
6230$$
6231 SELECT provsql.provenance_cmp(
6232 (a)::UUID,
6233 provsql.random_variable_cmp_oid('='),
6234 (b)::UUID);
6235$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6236
6237/** @brief Build a @c gate_cmp for <tt>a &lt;&gt; b</tt> and return its UUID. */
6238CREATE OR REPLACE FUNCTION rv_cmp_ne(
6239 a random_variable, b random_variable) RETURNS UUID AS
6240$$
6241 SELECT provsql.provenance_cmp(
6242 (a)::UUID,
6243 provsql.random_variable_cmp_oid('<>'),
6244 (b)::UUID);
6245$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6246
6247/** @brief Build a @c gate_cmp for <tt>a &ge; b</tt> and return its UUID. */
6248CREATE OR REPLACE FUNCTION rv_cmp_ge(
6249 a random_variable, b random_variable) RETURNS UUID AS
6250$$
6251 SELECT provsql.provenance_cmp(
6252 (a)::UUID,
6253 provsql.random_variable_cmp_oid('>='),
6254 (b)::UUID);
6255$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6256
6257/** @brief Build a @c gate_cmp for <tt>a &gt; b</tt> and return its UUID. */
6258CREATE OR REPLACE FUNCTION rv_cmp_gt(
6259 a random_variable, b random_variable) RETURNS UUID AS
6260$$
6261 SELECT provsql.provenance_cmp(
6262 (a)::UUID,
6263 provsql.random_variable_cmp_oid('>'),
6264 (b)::UUID);
6265$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6266
6267CREATE OPERATOR + (
6268 LEFTARG = random_variable,
6269 RIGHTARG = random_variable,
6270 PROCEDURE = random_variable_plus,
6271 COMMUTATOR = +
6272);
6273
6274CREATE OPERATOR - (
6275 LEFTARG = random_variable,
6276 RIGHTARG = random_variable,
6277 PROCEDURE = random_variable_minus
6278);
6279
6280CREATE OPERATOR * (
6281 LEFTARG = random_variable,
6282 RIGHTARG = random_variable,
6283 PROCEDURE = random_variable_times,
6284 COMMUTATOR = *
6285);
6286
6287CREATE OPERATOR / (
6288 LEFTARG = random_variable,
6289 RIGHTARG = random_variable,
6290 PROCEDURE = random_variable_div
6291);
6292
6293/** @brief Prefix unary minus on @c random_variable. */
6294CREATE OPERATOR - (
6295 RIGHTARG = random_variable,
6296 PROCEDURE = random_variable_neg
6297);
6298
6299CREATE OPERATOR ^ (
6300 LEFTARG = random_variable,
6301 RIGHTARG = random_variable,
6302 PROCEDURE = random_variable_pow
6303);
6304
6305CREATE OPERATOR < (
6306 LEFTARG = random_variable,
6307 RIGHTARG = random_variable,
6308 PROCEDURE = random_variable_lt,
6309 COMMUTATOR = >,
6310 NEGATOR = >=
6311);
6312
6313CREATE OPERATOR <= (
6314 LEFTARG = random_variable,
6315 RIGHTARG = random_variable,
6316 PROCEDURE = random_variable_le,
6317 COMMUTATOR = >=,
6318 NEGATOR = >
6319);
6320
6321CREATE OPERATOR = (
6322 LEFTARG = random_variable,
6323 RIGHTARG = random_variable,
6324 PROCEDURE = random_variable_eq,
6325 COMMUTATOR = =,
6326 NEGATOR = <>
6327);
6328
6329CREATE OPERATOR <> (
6330 LEFTARG = random_variable,
6331 RIGHTARG = random_variable,
6332 PROCEDURE = random_variable_ne,
6333 COMMUTATOR = <>,
6334 NEGATOR = =
6335);
6336
6337CREATE OPERATOR >= (
6338 LEFTARG = random_variable,
6339 RIGHTARG = random_variable,
6340 PROCEDURE = random_variable_ge,
6341 COMMUTATOR = <=,
6342 NEGATOR = <
6343);
6344
6345/**
6346 * @brief Order @c AGG_TOKEN values by the value each carries.
6347 *
6348 * With a cast of an aggregate carried rather than frozen, a column of
6349 * @c AGG_TOKEN is what a query sorts, groups or takes the DISTINCT of, and
6350 * those need an ordering. It is the ordering of the values on the data as it
6351 * is -- the one ProvSQL already documents for @c ORDER @c BY on an aggregate
6352 * -- and the comparison says so, once for the statement.
6353 */
6354CREATE OR REPLACE FUNCTION agg_token_btree_cmp(a AGG_TOKEN, b AGG_TOKEN)
6355 RETURNS INTEGER AS 'MODULE_PATHNAME', 'agg_token_btree_cmp'
6356 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
6358CREATE OPERATOR CLASS agg_token_ops
6359 DEFAULT FOR TYPE AGG_TOKEN USING btree AS
6360 OPERATOR 1 <,
6361 OPERATOR 2 <=,
6362 OPERATOR 3 =,
6363 OPERATOR 4 >=,
6364 OPERATOR 5 >,
6365 FUNCTION 1 agg_token_btree_cmp(AGG_TOKEN, AGG_TOKEN);
6366
6367
6368CREATE OPERATOR > (
6369 LEFTARG = random_variable,
6370 RIGHTARG = random_variable,
6371 PROCEDURE = random_variable_gt,
6372 COMMUTATOR = <,
6373 NEGATOR = <=
6374);
6376/**
6377 * @brief btree comparison support for @c random_variable -- always an error.
6378 *
6379 * A @c random_variable is a distribution, not a scalar, so it has no total
6380 * order: sorting (@c ORDER @c BY), de-duplicating (@c DISTINCT), grouping, and
6381 * the built-in @c GREATEST / @c LEAST all reduce to this btree comparison
6382 * proc, which raises a clear diagnostic rather than a placeholder message.
6383 *
6384 * The proc exists only so a DEFAULT btree operator class can be declared for
6385 * @c random_variable -- which is what lets PostgreSQL's @c GREATEST / @c LEAST
6386 * grammar parse over random variables so the planner hook can lift it into a
6387 * @c gate_arith @c MAX / @c MIN order statistic. When the hook is active the
6388 * @c GREATEST / @c LEAST node is rewritten before it ever calls this proc.
6389 */
6390CREATE OR REPLACE FUNCTION random_variable_btree_cmp(
6391 a random_variable, b random_variable) RETURNS INTEGER AS
6392$$
6393BEGIN
6394 RAISE EXCEPTION 'comparison or ordering of random_variable values is '
6395 'meaningless: a random_variable is a distribution, not a scalar'
6396 USING HINT =
6397 'Compare them as a probabilistic event -- in a WHERE / JOIN clause or '
6398 'with probability(x > y); take order statistics with provsql.greatest / '
6399 'provsql.least (or the min / max aggregates); summarise numerically with '
6400 'expected / variance / support.',
6401 DETAIL = 'provsql-reason: rv-comparison; scope: out-of-scope';
6402END
6403$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
6404
6405-- DEFAULT btree operator class over the (planner-hook-lifted) comparison
6406-- operators. Its only purpose is to make GREATEST / LEAST over random_variable
6407-- parse; every actual comparison it would drive (ORDER BY, DISTINCT, an
6408-- un-rewritten GREATEST) funnels through random_variable_btree_cmp above and
6409-- raises the "meaningless" diagnostic.
6410CREATE OPERATOR CLASS random_variable_ops
6411 DEFAULT FOR TYPE random_variable USING btree AS
6412 OPERATOR 1 <,
6413 OPERATOR 2 <=,
6414 OPERATOR 3 =,
6415 OPERATOR 4 >=,
6416 OPERATOR 5 >,
6417 FUNCTION 1 random_variable_btree_cmp(random_variable, random_variable);
6418
6419/**
6420 * @brief Condition a random variable on an event: @c "X | C".
6421 *
6422 * Returns a conditioned distribution that flows onward like any other
6423 * @c random_variable: it can be stored, re-conditioned, and queried with
6424 * @c expected / @c variance / @c moment / @c support, which then report the
6425 * conditional distribution. @p cond is a Boolean-event provenance token,
6426 * typically a comparison over the variable itself (@c "X | rv_cmp_gt(X,
6427 * as_random(3))" -- a truncation) or any external event.
6428 *
6429 * Unlike the UUID carrier's terminal @c cond, the random-variable form is a
6430 * composable two-child @c gate_conditioned @c [target, condition]: the moment
6431 * / support dispatchers unpack it and route through the existing conditional
6432 * evaluator (@c rv_moment over the joint of the target and the condition).
6433 * Nested conditioning folds: @c "(X|A)|B = X|(A∧B)".
6434 */
6435CREATE OR REPLACE FUNCTION random_variable_cond(rv random_variable, cond UUID)
6436 RETURNS random_variable AS
6437$$
6438DECLARE
6439 tgt UUID;
6440 ev UUID;
6441 result UUID;
6442 ch UUID[];
6443BEGIN
6444 IF cond IS NULL OR cond = gate_one() THEN
6445 RETURN rv;
6446 END IF;
6447
6448 -- A point-equality "Y = c" on a bare random-variable leaf is an
6449 -- OBSERVATION, not a truncation: rewrite it to the internal likelihood-
6450 -- weighting evidence (its density / mass at c). This is what lets
6451 -- "X | (normal(mu,1) = 8)" (a continuous point event, measure-zero as a
6452 -- Boolean selection) condition as the disintegration rather than fold to
6453 -- an infeasible event.
6454 cond := provsql.evidence_as_observation(cond);
6455
6456 tgt := (rv)::UUID;
6457 IF get_gate_type(tgt) = 'conditioned'
6458 AND array_length(get_children(tgt), 1) = 2 THEN
6459 -- Fold (X|A)|B = X|(A∧B): the rv-carrier conditioned gate is the
6460 -- two-child [target, condition] shape; accumulate the new event.
6461 ch := get_children(tgt);
6462 tgt := ch[1];
6463 ev := provenance_times(ch[2], cond);
6464 ELSE
6465 ev := cond;
6466 END IF;
6467
6468 result := public.uuid_generate_v5(uuid_ns_provsql(),
6469 concat('conditioned', tgt, ev));
6470 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev]);
6471 RETURN (result)::random_variable;
6473$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
6474 SECURITY DEFINER PARALLEL SAFE;
6475
6476CREATE OPERATOR | (
6477 LEFTARG = random_variable,
6478 RIGHTARG = UUID,
6479 PROCEDURE = random_variable_cond
6480);
6481
6482/**
6483 * @brief Placeholder for @c "X | (predicate)" -- conditioning a random
6484 * variable on a Boolean comparison written naturally.
6485 *
6486 * Lets one write @c "X | (X > 3)" instead of
6487 * @c "X | rv_cmp_gt(X, as_random(3))". Never executes: the ProvSQL planner
6488 * hook rewrites the Boolean operand (a combination of random_variable
6489 * comparisons) into the corresponding condition gate and emits
6490 * @c random_variable_cond. Reaching it at runtime means the rewriter was
6491 * inactive or the predicate was not a random_variable comparison.
6492 */
6493CREATE OR REPLACE FUNCTION random_variable_cond_predicate(
6494 rv random_variable, predicate BOOLEAN) RETURNS random_variable AS
6495$$
6496BEGIN
6497 RAISE EXCEPTION 'random_variable | (predicate) must be rewritten by the '
6498 'ProvSQL planner hook: the right operand must be a Boolean combination '
6499 'of random_variable comparisons (is provsql.active off?)'
6500 USING ERRCODE = 'feature_not_supported',
6501 DETAIL = 'provsql-reason: rv-operator-not-rewritten; scope: out-of-scope';
6502END
6503$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
6504
6505CREATE OPERATOR | (
6506 LEFTARG = random_variable,
6507 RIGHTARG = BOOLEAN,
6508 PROCEDURE = random_variable_cond_predicate
6509);
6510
6511/**
6512 * @brief Unpack the target of a random-variable conditioning gate.
6513 *
6514 * For a two-child @c gate_conditioned @c [target, condition] (the @c "X | C"
6515 * shape) returns @p target; for any other token returns it unchanged. Used
6516 * by the moment / support dispatchers to route a conditioned distribution
6517 * through the existing conditional evaluator.
6519CREATE OR REPLACE FUNCTION rv_conditioned_target(token UUID) RETURNS UUID AS
6520$$
6521 SELECT CASE
6522 WHEN provsql.get_gate_type(token) = 'conditioned'
6523 AND array_length(provsql.get_children(token), 1) = 2
6524 THEN (provsql.get_children(token))[1]
6525 ELSE token
6526 END;
6527$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
6528
6529/**
6530 * @brief Combine a conditioning gate's event with an explicit @p prov.
6531 *
6532 * For a two-child @c gate_conditioned @c [target, condition] returns
6533 * @c "condition ∧ prov"; otherwise returns @p prov unchanged. Lets a stored
6534 * @c "X | C" be queried as @c expected(X|C) (prov defaulting to one) or have
6535 * an extra condition conjoined as @c expected(X|C, extra_prov).
6537CREATE OR REPLACE FUNCTION rv_conditioned_prov(token UUID, prov UUID)
6538 RETURNS UUID AS
6539$$
6540 SELECT CASE
6541 WHEN provsql.get_gate_type(token) = 'conditioned'
6542 AND array_length(provsql.get_children(token), 1) = 2
6543 THEN provsql.provenance_times((provsql.get_children(token))[2], prov)
6544 ELSE prov
6545 END;
6546$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
6547
6548/*
6549 * Latent-variable posterior inference.
6550 *
6551 * Likelihood weighting (self-normalised importance sampling): bind an
6552 * observed datum to a latent-dependent random-variable leaf with observe,
6553 * conjoin the per-observation evidence with and_agg into a single evidence
6554 * token, and pass it as the prov conditioning argument of any moment /
6555 * quantile / sample readout. Latents are drawn from the prior (the
6556 * existing forward recursion) and each draw is weighted by the observed
6557 * leaves' densities at the data; the readouts then report the posterior.
6558 * It is the continuous generalisation of the rejection-based conditioning:
6559 * a Boolean event in the evidence contributes a 0/1 weight, an observe
6560 * contributes a pdf weight -- same evidence conjunction, same
6561 * "P(query AND evidence)/P(evidence)" normaliser, now weighted.
6562 */
6563
6564/**
6565 * @brief Internal: rewrite a point-equality conditioning event into an
6566 * observation. If @p ev is a @c gate_cmp with the @c "=" operator,
6567 * one side a bare @c gate_rv leaf and the other a constant, return
6568 * @c observe(leaf, const); otherwise return @p ev unchanged.
6570 * This is the bridge that makes the natural equality form the surface for
6571 * likelihood-weighting conditioning: @c "X | (Y = c)" and @c "given(Y = c)"
6572 * both produce a @c gate_cmp, which this turns into density evidence. A
6573 * point event on a bare leaf is only meaningful as an observation (a
6574 * continuous @c "Y = c" is measure-zero as a Boolean selection), so the
6575 * rewrite is unambiguous. Non-equality / non-leaf events pass through as
6576 * ordinary Boolean conditioning.
6577 */
6578CREATE OR REPLACE FUNCTION evidence_as_observation(ev UUID) RETURNS UUID AS
6579$$
6580DECLARE
6581 ch UUID[];
6582 i1 INTEGER;
6583 leaf UUID;
6584 datum_gate UUID;
6585BEGIN
6586 IF ev IS NULL OR provsql.get_gate_type(ev) <> 'cmp' THEN
6587 RETURN ev;
6588 END IF;
6589 ch := provsql.get_children(ev);
6590 IF array_length(ch, 1) <> 2 THEN
6591 RETURN ev;
6592 END IF;
6593 -- The cmp stores the comparison OPERATOR's OID in info1; match on its name
6594 -- '=' the same way the C-side cmpOpFromOid does (get_opname), rather than a
6595 -- fixed operator OID (which varies per install / carrier type).
6596 SELECT info1 INTO i1 FROM provsql.get_infos(ev);
6597 IF (SELECT oprname FROM pg_catalog.pg_operator WHERE oid = i1) IS DISTINCT FROM '=' THEN
6598 RETURN ev; -- not an equality
6599 END IF;
6600 IF provsql.get_gate_type(ch[1]) = 'rv'
6601 AND provsql.get_gate_type(ch[2]) = 'value' THEN
6602 leaf := ch[1]; datum_gate := ch[2];
6603 ELSIF provsql.get_gate_type(ch[2]) = 'rv'
6604 AND provsql.get_gate_type(ch[1]) = 'value' THEN
6605 leaf := ch[2]; datum_gate := ch[1];
6606 ELSE
6607 RETURN ev; -- not a bare-leaf-vs-constant point event
6608 END IF;
6609 RETURN provsql.observe((leaf)::random_variable,
6610 provsql.get_extra(datum_gate)::double precision);
6611END
6612$$ LANGUAGE plpgsql VOLATILE
6613 SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
6614
6616 * @brief Internal: bind an observed datum to a random-variable leaf --
6617 * the likelihood-weighting evidence behind @c "X | (Y = d)".
6618 *
6619 * @p x MUST be a bare @c gate_rv leaf (typically a latent-parameterised
6620 * one, e.g. @c normal(mu, 1) sharing a latent @c mu across rows).
6621 * Returns an @b evidence UUID -- a @c gate_observe wrapping the leaf with
6622 * the datum in @c extra -- that composes with other evidence through
6623 * @c and_agg (a @c gate_times conjunction) and is consumed by the
6624 * importance-sampling weight walk, contributing the factor @c f_X(d).
6625 *
6626 * Internal: the user-facing surface is the equality form @c "X | (Y = d)"
6627 * (single conditioning) and @c "given(Y = d)" (per-row evidence for
6628 * @c and_agg), both of which route here through @c evidence_as_observation.
6629 *
6630 * A fresh gate is minted per call (each observation is a distinct
6631 * evidence atom, so a repeated @c (leaf, datum) contributes its density
6632 * factor once per row -- and each is a separate Shapley atom). Observing
6633 * a derived quantity (@c observe(X+Y, d)) is out of scope: it needs a
6634 * change-of-variables density; a non-leaf argument is refused.
6635 */
6636CREATE OR REPLACE FUNCTION observe(x random_variable, datum double precision)
6637 RETURNS UUID AS
6638$$
6639DECLARE
6640 leaf UUID := (x)::UUID;
6641 result UUID;
6642BEGIN
6643 IF provsql.get_gate_type(leaf) <> 'rv' THEN
6644 RAISE EXCEPTION 'provsql.observe: the argument must be a bare '
6645 'random-variable leaf (a gate_rv), got a % gate', provsql.get_gate_type(leaf)
6646 USING HINT = 'observe binds a datum to a single distribution leaf; '
6647 'observing a derived quantity (a sum, product, or comparison) needs '
6648 'a change-of-variables density and is out of scope.',
6649 DETAIL = 'provsql-reason: observe-argument-kind; scope: out-of-scope';
6650 END IF;
6651 IF NOT provsql.is_finite_float8(datum) THEN
6652 RAISE EXCEPTION 'provsql.observe: datum must be finite (got %)', datum;
6653 END IF;
6654 result := public.uuid_generate_v4();
6655 PERFORM provsql.create_gate(result, 'observe', ARRAY[leaf], NULL, NULL, datum::TEXT);
6656 RETURN result;
6657END
6658$$ LANGUAGE plpgsql VOLATILE
6659 SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
6660
6661/**
6662 * @brief Conjunction state function for @c and_agg (evidence @c gate_times).
6663 *
6664 * Not @c STRICT: @c provenance_times maps a @c NULL operand to the times
6665 * neutral, so an empty group leaves the state @c NULL (no evidence) and a
6666 * first row seeds it with that row's evidence.
6667 */
6668CREATE OR REPLACE FUNCTION and_agg_sfunc(state UUID, ev UUID)
6669 RETURNS UUID AS
6670$$
6671 SELECT provsql.provenance_times(state, ev);
6672$$ LANGUAGE sql PARALLEL SAFE;
6673
6674/**
6675 * @brief Conjoin per-row evidence tokens into one evidence circuit.
6676 *
6677 * The evidence-conjunction counterpart used to fold one @c observe (or any
6678 * Boolean conditioning event) per row into a single @c gate_times root, to
6679 * be passed as the @c prov argument of the moment / quantile / sample
6680 * readouts. An empty group yields @c NULL (no evidence).
6681 */
6682CREATE AGGREGATE and_agg(UUID) (
6683 SFUNC = and_agg_sfunc,
6684 STYPE = UUID
6686
6687/**
6688 * @brief Marginal likelihood @c P(data) of an evidence circuit.
6689 *
6690 * The mean raw importance weight over @c provsql.rv_mc_samples prior draws
6691 * -- the same quantity rejection conditioning computes as @c P(C), now the
6692 * product of the observations' densities. @p evidence is an @c and_agg
6693 * conjunction of @c observe tokens (and/or Boolean events).
6694 */
6695CREATE OR REPLACE FUNCTION evidence(evidence UUID)
6696 RETURNS double precision
6697 AS 'provsql','rv_evidence' LANGUAGE C STRICT PARALLEL SAFE;
6698
6699/**
6700 * @brief The @c observe atoms of an evidence circuit.
6701 *
6702 * Collects every @c gate_observe leaf reachable through the @c gate_times
6703 * conjunction spine (the shape @c and_agg builds -- a possibly left-nested
6704 * tree, since @c provenance_times does not flatten). Used by
6705 * @c shapley_observe to recover the flat observation set regardless of the
6706 * conjunction's nesting.
6707 */
6708CREATE OR REPLACE FUNCTION observe_atoms(evidence UUID)
6709 RETURNS UUID[] AS
6710$$
6711 WITH RECURSIVE walk(tok) AS (
6712 SELECT evidence
6713 UNION
6714 SELECT c
6715 FROM walk, LATERAL unnest(provsql.get_children(walk.tok)) AS c
6716 WHERE provsql.get_gate_type(walk.tok) = 'times'
6717 )
6718 SELECT array_agg(tok ORDER BY tok)
6719 FROM walk
6720 WHERE provsql.get_gate_type(tok) = 'observe';
6721$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
6722
6723/**
6724 * @brief Shapley attribution of each observation to a posterior moment.
6725 *
6726 * "Which observation most shifted my posterior?" Because the importance
6727 * weight is a product of per-observation density factors, dropping an
6728 * observation is dropping one factor: the classical Shapley value of each
6729 * @c gate_observe atom over the coalitional value function
6730 * @c "v(S) = payoff(target | observations in S)" is the attribution, a
6731 * byproduct of the same likelihood-weighting machinery (see the
6732 * explainable-inference angle in the continuous-distributions notes).
6733 *
6734 * @p target is the latent (its @c UUID); @p evidence is the @c and_agg
6735 * conjunction of @c observe atoms; @p payoff is @c 'expected' or
6736 * @c 'variance'. Returns each observation atom with its Shapley value; the
6737 * values sum to @c "payoff(target | all data) - payoff(target)" (Shapley
6738 * efficiency: the total shift from prior to posterior).
6739 *
6740 * Exact enumeration over the @c 2^n observation subsets, so it is capped at
6741 * @c n = 12 observations (sampling-based attribution for larger sets is
6742 * future work); pin @c provsql.monte_carlo_seed so the coalitional value
6743 * functions share common random numbers (lower-variance differences).
6744 */
6745CREATE OR REPLACE FUNCTION shapley_observe(
6746 target UUID, evidence UUID, payoff TEXT DEFAULT 'expected')
6747 RETURNS TABLE(observation UUID, value double precision) AS
6748$$
6749DECLARE
6750 atoms UUID[];
6751 n INT;
6752 nmasks INT;
6753 pv double precision[];
6754 popc INT[];
6755 fact double precision[];
6756 mask INT;
6757 i INT;
6758 j INT;
6759 cnt INT;
6760 subset UUID[];
6761 ev_s UUID;
6762 sh double precision;
6763 bit INT;
6764 s_size INT;
6765BEGIN
6766 IF payoff NOT IN ('expected', 'variance') THEN
6767 RAISE EXCEPTION 'provsql.shapley_observe: payoff must be ''expected'' or '
6768 '''variance'' (got %)', payoff;
6769 END IF;
6770 atoms := provsql.observe_atoms(evidence);
6771 n := coalesce(array_length(atoms, 1), 0);
6772 IF n = 0 THEN
6773 RAISE EXCEPTION 'provsql.shapley_observe: evidence contains no observe() '
6774 'atoms (got a % gate)', provsql.get_gate_type(evidence)
6775 USING ERRCODE = 'feature_not_supported',
6776 DETAIL = 'provsql-reason: shapley-observe-no-evidence; scope: out-of-scope';
6777 END IF;
6778 IF n > 12 THEN
6779 RAISE EXCEPTION 'provsql.shapley_observe: exact attribution over % '
6780 'observations is exponential; capped at 12 (sampling-based '
6781 'attribution is future work)', n
6782 USING ERRCODE = 'feature_not_supported',
6783 DETAIL = 'provsql-reason: shapley-observe-too-many; scope: out-of-scope';
6784 END IF;
6785
6786 -- factorials 0!..n! (fact[k+1] = k!)
6787 fact := ARRAY[1::double precision];
6788 FOR i IN 1..n LOOP fact := fact || (fact[i] * i); END LOOP;
6789
6790 nmasks := (1 << n);
6791 pv := array_fill(NULL::double precision, ARRAY[nmasks]);
6792 popc := array_fill(0, ARRAY[nmasks]);
6793
6794 -- Payoff value function for every subset of observations.
6795 FOR mask IN 0 .. nmasks - 1 LOOP
6796 subset := ARRAY[]::UUID[];
6797 cnt := 0;
6798 FOR i IN 0 .. n - 1 LOOP
6799 IF (mask >> i) & 1 = 1 THEN
6800 subset := subset || atoms[i + 1];
6801 cnt := cnt + 1;
6802 END IF;
6803 END LOOP;
6804 popc[mask + 1] := cnt;
6805 IF cnt = 0 THEN
6806 ev_s := provsql.gate_one(); -- prior (no evidence)
6807 ELSE
6808 ev_s := provsql.provenance_times(VARIADIC subset);
6809 END IF;
6810 IF payoff = 'expected' THEN
6811 pv[mask + 1] := provsql.rv_moment(target, 1, false, ev_s);
6812 ELSE
6813 pv[mask + 1] := provsql.rv_moment(target, 2, true, ev_s);
6814 END IF;
6815 END LOOP;
6816
6817 -- Shapley value of each observation atom.
6818 FOR i IN 0 .. n - 1 LOOP
6819 sh := 0;
6820 bit := (1 << i);
6821 FOR mask IN 0 .. nmasks - 1 LOOP
6822 IF (mask >> i) & 1 = 0 THEN -- subsets S not containing i
6823 s_size := popc[mask + 1];
6824 -- weight |S|! (n-|S|-1)! / n!
6825 sh := sh + (fact[s_size + 1] * fact[n - s_size] / fact[n + 1])
6826 * (pv[(mask | bit) + 1] - pv[mask + 1]);
6827 END IF;
6828 END LOOP;
6829 observation := atoms[i + 1];
6830 value := sh;
6831 RETURN NEXT;
6832 END LOOP;
6833END
6834$$ LANGUAGE plpgsql VOLATILE
6835 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6836
6837/**
6838 * @name Order statistics over random_variable
6839 *
6840 * Same-row @c greatest / @c least over @c random_variable arguments: the
6841 * order-statistic counterpart of the element-wise @c "+ - * /" operators.
6842 * They lower to a single @c gate_arith with the @c MAX / @c MIN opcode over
6843 * the argument circuits, the same n-ary shape the @c max / @c min aggregates
6844 * build. Evaluation is Monte-Carlo-correct out of the box (@c std::max /
6845 * @c std::min over the jointly-sampled children, so shared base RVs stay
6846 * coupled); closed forms for i.i.d. families come from the analytic
6847 * order-statistic pass.
6848 *
6849 * PostgreSQL's built-in @c GREATEST / @c LEAST are dedicated syntax (a
6850 * @c MinMaxExpr requiring a btree comparison), not overloadable functions, so
6851 * the surface is the schema-qualified @c provsql.greatest(...) /
6852 * @c provsql.least(...). @c NULL arguments are ignored, matching the built-in
6853 * (an all-@c NULL / empty call returns @c NULL).
6854 * @{
6855 */
6856
6857-- "greatest" / "least" are col_name keywords, so the CREATE FUNCTION name
6858-- must be quoted; callers reach them qualified as provsql.greatest(...).
6859-- Idempotence: max / min ignore repeats, so identical children (same gate)
6860-- are de-duplicated -- greatest(x, x, y) == greatest(x, y) -- and a single
6861-- surviving child collapses to itself -- greatest(x) == x. DISTINCT also sorts
6862-- the children, so the argument order does not matter for gate sharing. (Two
6863-- independent draws of the same distribution are distinct gates and are NOT
6864-- de-duplicated.)
6865CREATE OR REPLACE FUNCTION "greatest"(VARIADIC args random_variable[])
6866 RETURNS random_variable AS
6867$$
6868DECLARE
6869 children UUID[];
6870BEGIN
6871 IF args IS NULL THEN
6872 RETURN NULL;
6873 END IF;
6874 SELECT array_agg(DISTINCT (a)::UUID) INTO children
6875 FROM unnest(args) a WHERE a IS NOT NULL;
6876 IF children IS NULL OR array_length(children, 1) IS NULL THEN
6877 RETURN NULL;
6878 END IF;
6879 IF array_length(children, 1) = 1 THEN
6880 RETURN provsql.random_variable_make(children[1]);
6881 END IF;
6882 RETURN provsql.random_variable_make(
6883 provsql.provenance_arith(5, children)); -- 5 = PROVSQL_ARITH_MAX
6884END
6885$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6886
6887CREATE OR REPLACE FUNCTION "least"(VARIADIC args random_variable[])
6888 RETURNS random_variable AS
6889$$
6890DECLARE
6891 children UUID[];
6892BEGIN
6893 IF args IS NULL THEN
6894 RETURN NULL;
6895 END IF;
6896 SELECT array_agg(DISTINCT (a)::UUID) INTO children
6897 FROM unnest(args) a WHERE a IS NOT NULL;
6898 IF children IS NULL OR array_length(children, 1) IS NULL THEN
6899 RETURN NULL;
6900 END IF;
6901 IF array_length(children, 1) = 1 THEN
6902 RETURN provsql.random_variable_make(children[1]);
6903 END IF;
6904 RETURN provsql.random_variable_make(
6905 provsql.provenance_arith(6, children)); -- 6 = PROVSQL_ARITH_MIN
6906END
6907$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6908
6909/**
6910 * @brief Build a @c random_variable from a guarded-selection @c gate_case.
6911 *
6912 * Thin @c random_variable wrapper over @c provenance_case (defined with the
6913 * other gate builders, since it is UUID-only), the target of the planner-hook
6914 * @c CASE-over-RV rewrite: the hook flattens the branches into
6915 * @c [guard_1, value_1, ..., default] and emits this call so an RV-typed
6916 * @c CASE surfaces as a first-class @c random_variable.
6917 */
6918CREATE OR REPLACE FUNCTION rv_case(
6919 children UUID[]
6920)
6921RETURNS random_variable AS
6922$$
6923 SELECT provsql.random_variable_make(provsql.provenance_case(children));
6924$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6925
6926/**
6927 * @brief Build an @c AGG_TOKEN from a guarded-selection @c gate_case.
6928 *
6929 * The aggregate-carrier analogue of @c rv_case: a thin @c AGG_TOKEN wrapper
6930 * over the carrier-agnostic @c provenance_case, the target of the planner-hook
6931 * lowering of a searched @c CASE whose guards are aggregate comparisons and
6932 * whose branches are aggregates. The branches (and default) are already
6933 * flattened into @c [guard_1, value_1, ..., default] UUIDs. The display cell
6934 * carries the actual-world CASE value -- the branch selected on the actual
6935 * data, resolved by @c agg_gate_value, exactly as a bare aggregate's cell
6936 * carries its actual-world value. The probabilistic result is produced by
6937 * the measure evaluators (``expected`` / ``probability`` / possible-worlds /
6938 * Monte Carlo) from the gate, not the token's cell.
6939 */
6940CREATE OR REPLACE FUNCTION agg_case(
6941 children UUID[]
6942)
6943RETURNS AGG_TOKEN AS
6944$$
6945 SELECT format('( %s , %s )', t::TEXT,
6946 coalesce(provsql.agg_gate_value(t)::TEXT, ''))::provsql.AGG_TOKEN
6947 FROM (SELECT provsql.provenance_case(children) AS t) AS s;
6948$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6949
6950/** @} */
6951
6952/**
6953 * @name Aggregates over random_variable
6954 *
6955 * An overload of the standard
6956 * @c sum aggregate that takes a @c random_variable per row and returns
6957 * the @c random_variable representing the (provenance-weighted) sum.
6958 * Lives in the @c provsql schema so a @c sum(random_variable) call
6959 * resolves to it without colliding with the built-in NUMERIC @c sum
6960 * overloads in @c pg_catalog.
6961 *
6962 * Direct calls outside a provenance-tracked query treat each row's
6963 * contribution unconditionally (no per-row Boolean selector). When
6964 * the planner hook sees a @c provsql.sum @c Aggref over a
6965 * provenance-tracked query, it wraps the per-row argument @c x in
6966 * <tt>provsql.mixture(prov_token, x, provsql.as_random(0))</tt> so the
6967 * aggregate's effective semantics become
6968 * @f$\mathrm{SUM}(x) = \sum_i \mathbf{1}\{\varphi_i\} \cdot X_i@f$,
6969 * the natural extension of semimodule-provenance to RV-valued M.
6970 *
6971 * The internal state is the array of UUIDs of the per-row mixtures.
6972 * The final function builds a single @c gate_arith @c PLUS over them
6973 * (or returns @c as_random(0) for an empty group, the additive
6974 * identity). Sharing on @c provenance_arith's v5 hash means two
6975 * @c sum invocations over the same set of rows collide on the same
6976 * gate.
6977 *
6978 * @{
6979 */
6980
6981/**
6982 * @brief Per-row helper: wrap an RV in @c mixture(prov, rv, as_random(0)).
6983 *
6984 * Internal helper used by the planner-hook rewriter to lift a
6985 * @c sum(random_variable) argument into its provenance-aware form.
6986 * Encodes one row's contribution to the SUM as a Bernoulli mixture
6987 * over the row's provenance: with probability @c P(prov) the mixture
6988 * samples @c rv, otherwise it samples the additive identity
6989 * @c as_random(0). Exposed as a regular SQL function so the planner
6990 * can construct a @c FuncExpr by name without needing to disambiguate
6991 * @c mixture / @c as_random overloads at OID-lookup time.
6992 */
6993CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
6994 prov UUID, rv random_variable)
6995 RETURNS random_variable AS
6996$$
6997 SELECT provsql.mixture(prov, rv, provsql.as_random(0::double precision));
6998$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6999
7001 * @brief Identity-parameterised per-row wrap for an RV-returning aggregate.
7002 *
7003 * Generalises the two-argument @ref rv_aggregate_semimod. The else-branch
7004 * (a row's contribution when its provenance is false) is
7005 * @c as_random(@p identity) instead of the additive @c as_random(0). The
7006 * planner-hook rewrite bakes each aggregate's own identity element into the
7007 * wrap -- @c 1 for @c product, @f$-\infty@f$ / @f$+\infty@f$ for @c max /
7008 * @c min -- so the aggregate's final function is a plain fold over the
7009 * per-row mixtures with no gate inspection. @c sum keeps the two-argument
7010 * form (@c identity @c = @c 0).
7011 */
7012CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
7013 prov UUID, rv random_variable, identity double precision)
7014 RETURNS random_variable AS
7015$$
7016 SELECT provsql.mixture(prov, rv, provsql.as_random(identity));
7017$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
7018
7019/**
7020 * @brief Per-row denominator wrap for @c avg(random_variable): the
7021 * provenance indicator @f$\mathbf{1}\{\varphi\}@f$.
7022 *
7023 * The row contributes @c 1 to the running count when present and @c 0 when
7024 * absent, so @c sum over these wraps is the provenance-weighted count
7025 * @f$\sum_i \mathbf{1}\{\varphi_i\}@f$. The planner-hook rewrites
7026 * @c avg(x) into @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
7027 * @c sum(rv_aggregate_indicator(prov)) -- the "@c AVG @c = @c SUM @c /
7028 * @c COUNT" identity lifted into the @c random_variable algebra -- so
7029 * @c avg rides entirely on @c sum's fold and never inspects a gate.
7030 */
7031CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov UUID)
7032 RETURNS random_variable AS
7033$$
7034 SELECT provsql.rv_aggregate_semimod(prov, provsql.as_random(1::double precision));
7035$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
7036
7037/**
7038 * @brief Value-aware presence indicator: NULL when the row's aggregated
7039 * value is NULL.
7040 *
7041 * SQL aggregates skip NULL inputs, so a NULL @c random_variable cell must
7042 * not count in @c avg's denominator: the wrap yields NULL (which the
7043 * @c sum fold skips) exactly when the value is NULL, and the plain
7044 * one-argument indicator otherwise. The planner-hook @c avg rewrite
7045 * emits this form; the one-argument indicator remains for the internal
7046 * public-form defaults.
7047 */
7048CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov UUID, rv random_variable)
7049 RETURNS random_variable AS
7050$$
7051 SELECT CASE WHEN rv IS NULL THEN NULL
7052 ELSE provsql.rv_aggregate_indicator(prov) END;
7053$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7054
7055/**
7056 * @brief State-transition function for @c sum(random_variable).
7057 *
7058 * Appends the input RV's UUID to the running array. NULL inputs are
7059 * skipped (matching standard SUM semantics). The aggregate's INITCOND
7060 * is @c '{}' so the FINALFUNC always runs and can tell an empty group
7061 * (state @c '{}') apart from a group whose every input was NULL -- both
7062 * of which SQL reports as @c NULL.
7063 */
7064CREATE OR REPLACE FUNCTION sum_rv_sfunc(
7065 state UUID[], rv random_variable)
7066 RETURNS UUID[] AS
7067$$
7068 SELECT CASE
7069 WHEN rv IS NULL THEN state
7070 ELSE array_append(state, (rv)::UUID)
7071 END;
7072$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7074/**
7075 * @brief Final function for @c sum(random_variable): build a
7076 * @c gate_arith PLUS root.
7077 *
7078 * Empty group (@c state = @c '{}'): return @c NULL, as SQL's @c sum
7079 * does over zero rows -- the same answer the @c AGG_TOKEN path gives
7080 * for @c sum over an empty aggregation.
7081 *
7082 * Singleton group: return the single child directly without minting a
7083 * useless single-child @c gate_arith.
7084 *
7085 * Otherwise: build @c gate_arith(PLUS, state) via @c provenance_arith.
7086 */
7087CREATE OR REPLACE FUNCTION sum_rv_ffunc(state UUID[])
7088 RETURNS random_variable AS
7089$$
7090DECLARE
7091 arith_token UUID;
7092BEGIN
7093 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7094 RETURN NULL;
7095 END IF;
7096 IF array_length(state, 1) = 1 THEN
7097 RETURN provsql.random_variable_make(state[1]);
7098 END IF;
7099 arith_token := provsql.provenance_arith(0, state); -- 0 = PROVSQL_ARITH_PLUS
7100 RETURN provsql.random_variable_make(arith_token);
7101END
7102$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
7103
7104CREATE AGGREGATE sum(random_variable) (
7105 SFUNC = sum_rv_sfunc,
7106 STYPE = UUID[],
7107 INITCOND = '{}',
7108 FINALFUNC = sum_rv_ffunc
7109);
7110
7111/**
7112 * @brief Numerator final function for the @c avg rewrite: @c sum,
7113 * @c NULL on an empty group.
7114 *
7115 * Behaviourally identical to @ref sum_rv_ffunc; kept as a separate
7116 * catalog entry because the @c avg rewrite names it explicitly. The
7117 * planner-hook @c avg rewrite emits
7118 * @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
7119 * @c sum(rv_aggregate_indicator(prov)); @c random_variable_div is
7120 * @c STRICT, so an empty group propagates the numerator's @c NULL and
7121 * @c avg is @c NULL -- the standard SQL @c AVG convention -- while a
7122 * non-empty group behaves exactly like @c sum.
7123 */
7124CREATE OR REPLACE FUNCTION rv_sum_or_null_ffunc(state UUID[])
7125 RETURNS random_variable AS
7126$$
7127BEGIN
7128 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7129 RETURN NULL;
7130 END IF;
7131 IF array_length(state, 1) = 1 THEN
7132 RETURN provsql.random_variable_make(state[1]);
7133 END IF;
7134 RETURN provsql.random_variable_make(
7135 provsql.provenance_arith(0, state)); -- 0 = PROVSQL_ARITH_PLUS
7136END
7137$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
7138
7139CREATE AGGREGATE rv_sum_or_null(random_variable) (
7140 SFUNC = sum_rv_sfunc,
7141 STYPE = UUID[],
7142 INITCOND = '{}',
7143 FINALFUNC = rv_sum_or_null_ffunc
7144);
7145
7146/**
7147 * @brief Final function for @c avg(random_variable).
7148 *
7149 * @c avg lifts the "@c AVG @c = @c SUM @c / @c COUNT" identity into the
7150 * @c random_variable algebra:
7151 * @f[
7152 * \mathrm{AVG}(x) \;=\; \frac{\sum_i \mathbf{1}\{\varphi_i\} \cdot X_i}
7153 * {\sum_i \mathbf{1}\{\varphi_i\}}.
7154 * @f]
7155 * In a provenance-tracked query the planner-hook rewrites @c avg(x) into
7156 * @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
7157 * @c sum(rv_aggregate_indicator(prov)) (see
7158 * @c make_rv_aggregate_expression), so both the numerator and the
7159 * provenance-weighted count denominator are built by @c sum's fold and no
7160 * gate is inspected. This FFUNC is therefore reached only on an
7161 * @em untracked call, where every row is unconditionally present: the
7162 * numerator is @c sum over the raw per-row RVs and the denominator is the
7163 * plain row count @c n (each row contributing @c as_random(1)).
7164 *
7165 * Empty group: returns @c NULL, matching standard SQL @c AVG (and unlike
7166 * @c sum, whose empty group is the additive identity @c as_random(0)):
7167 * the caller cannot otherwise disambiguate "0 rows" from "rows summing
7168 * to 0".
7169 */
7170CREATE OR REPLACE FUNCTION avg_rv_ffunc(state UUID[])
7171 RETURNS random_variable AS
7172$$
7173DECLARE
7174 n INTEGER;
7175 i INTEGER;
7176 num_token UUID;
7177 denom_token UUID;
7178 denom_state UUID[] := '{}';
7179 one_uuid UUID;
7180BEGIN
7181 IF state IS NULL THEN
7182 RETURN NULL;
7183 END IF;
7184 n := array_length(state, 1);
7185 IF n IS NULL THEN
7186 RETURN NULL;
7187 END IF;
7188
7189 one_uuid := (provsql.as_random(1::double precision))::UUID;
7190 FOR i IN 1..n LOOP
7191 denom_state := array_append(denom_state, one_uuid);
7192 END LOOP;
7193
7194 IF n = 1 THEN
7195 num_token := state[1];
7196 denom_token := denom_state[1];
7197 ELSE
7198 num_token := provsql.provenance_arith(0, state); -- 0 = PLUS
7199 denom_token := provsql.provenance_arith(0, denom_state); -- 0 = PLUS
7200 END IF;
7201
7202 RETURN provsql.random_variable_make(
7203 provsql.provenance_arith(
7204 3, -- 3 = PROVSQL_ARITH_DIV
7205 ARRAY[num_token, denom_token]));
7206END
7207$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
7208
7209CREATE AGGREGATE avg(random_variable) (
7210 SFUNC = sum_rv_sfunc,
7211 STYPE = UUID[],
7212 INITCOND = '{}',
7213 FINALFUNC = avg_rv_ffunc
7215
7216/**
7217 * @brief Final function for @c product(random_variable): fold a
7218 * @c gate_arith TIMES root over the per-row contributions.
7219 *
7220 * Multiplicative analogue of @c sum(random_variable):
7221 * @f[
7222 * \mathrm{PRODUCT}(x) \;=\; \prod_i \big(\mathbf{1}\{\varphi_i\} \cdot X_i
7223 * + \mathbf{1}\{\neg\varphi_i\} \cdot 1\big)
7224 * \;=\; \prod_{i : \varphi_i} X_i.
7225 * @f]
7226 * Each per-row contribution already carries the multiplicative identity
7227 * as its absent-row value: a provenance-tracked query wraps the argument
7228 * as @c mixture(prov_i, X_i, as_random(1)) (identity baked in by the
7229 * three-argument @ref rv_aggregate_semimod), and an untracked call passes
7230 * the raw RV through. So the FFUNC is a plain fold with no gate
7231 * inspection: @c gate_arith(TIMES, state).
7232 *
7233 * Reuses @c sum_rv_sfunc as the state-transition function. Empty group:
7234 * @c NULL, by symmetry with @c sum / @c avg / @c min / @c max, which take
7235 * it from their standard-SQL counterparts. The multiplicative identity
7236 * @c as_random(1) stays the absent-row value inside the fold, where it
7237 * belongs; a group containing no row has no product to report.
7238 * Singleton group: the single child directly, without a one-child TIMES
7239 * root.
7240 */
7241CREATE OR REPLACE FUNCTION product_rv_ffunc(state UUID[])
7242 RETURNS random_variable AS
7243$$
7244BEGIN
7245 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7246 RETURN NULL;
7247 END IF;
7248 IF array_length(state, 1) = 1 THEN
7249 RETURN provsql.random_variable_make(state[1]);
7250 END IF;
7251 RETURN provsql.random_variable_make(
7252 provsql.provenance_arith(1, state)); -- 1 = PROVSQL_ARITH_TIMES
7253END
7254$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
7255
7256CREATE AGGREGATE product(random_variable) (
7257 SFUNC = sum_rv_sfunc,
7258 STYPE = UUID[],
7259 INITCOND = '{}',
7260 FINALFUNC = product_rv_ffunc
7261);
7262
7263/**
7264 * @brief Final function for @c max(random_variable) / @c min(random_variable):
7265 * fold a @c gate_arith MAX / MIN root over the per-row contributions.
7266 *
7267 * The order-statistic analogues of @c sum / @c product:
7268 * @f[
7269 * \mathrm{MAX}(x) = \max_{i : \varphi_i} X_i, \qquad
7270 * \mathrm{MIN}(x) = \min_{i : \varphi_i} X_i.
7271 * @f]
7272 * A row absent in a world (its provenance @f$\varphi_i@f$ false) must not
7273 * perturb the extremum, so it contributes the order-statistic identity
7274 * @f$\mp\infty@f$. That identity is baked into each per-row contribution
7275 * upstream: a provenance-tracked query wraps the argument as
7276 * @c mixture(prov_i, X_i, as_random(∓∞)) (via the three-argument
7277 * @ref rv_aggregate_semimod), and an untracked call passes the raw RV
7278 * through. So the FFUNC is a plain fold with no gate inspection:
7279 * @c gate_arith(@p op, state).
7280 *
7281 * Empty group: @c NULL, as SQL's @c min / @c max report over zero rows.
7282 * @p identity belongs to the catalog signature and describes the per-row
7283 * absent contribution baked in upstream; the empty group does not consult
7284 * it, since @f$\mp\infty@f$ is an artefact of the fold rather than a value
7285 * the group actually contains.
7286 * Singleton group: the single child directly.
7287 */
7288CREATE OR REPLACE FUNCTION extremum_rv_ffunc(
7289 state UUID[], op INTEGER, identity double precision)
7290 RETURNS random_variable AS
7291$$
7292BEGIN
7293 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7294 RETURN NULL;
7295 END IF;
7296 IF array_length(state, 1) = 1 THEN
7297 RETURN provsql.random_variable_make(state[1]);
7298 END IF;
7299 RETURN provsql.random_variable_make(
7300 provsql.provenance_arith(op, state));
7301END
7302$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
7303
7304CREATE OR REPLACE FUNCTION max_rv_ffunc(state UUID[])
7305 RETURNS random_variable AS
7306$$
7307 -- 5 = PROVSQL_ARITH_MAX; empty-group / row-absent identity -inf.
7308 SELECT provsql.extremum_rv_ffunc(state, 5, '-Infinity'::double precision);
7309$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7310
7311CREATE OR REPLACE FUNCTION min_rv_ffunc(state UUID[])
7312 RETURNS random_variable AS
7313$$
7314 -- 6 = PROVSQL_ARITH_MIN; empty-group / row-absent identity +inf.
7315 SELECT provsql.extremum_rv_ffunc(state, 6, 'Infinity'::double precision);
7316$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7317
7318CREATE AGGREGATE max(random_variable) (
7319 SFUNC = sum_rv_sfunc,
7320 STYPE = UUID[],
7321 INITCOND = '{}',
7322 FINALFUNC = max_rv_ffunc
7323);
7324
7325CREATE AGGREGATE min(random_variable) (
7326 SFUNC = sum_rv_sfunc,
7327 STYPE = UUID[],
7328 INITCOND = '{}',
7329 FINALFUNC = min_rv_ffunc
7330);
7331
7332-- ---------------------------------------------------------------------
7333-- SQL-standard statistic aggregates over random_variable rows:
7334-- covar_pop / covar_samp / corr (two-argument), stddev_pop / stddev_samp
7335-- (one-argument), and the ordered-set percentile_cont.
7336--
7337-- Row presence is carried by a per-row 0/1 indicator RV: the public
7338-- aggregates use the certain indicator as_random(1) (every row present),
7339-- and a provenance-tracked query is rewritten by the planner hook
7340-- (make_rv_aggregate_expression) to the rv_*_impl aggregates whose extra
7341-- leading argument is rv_aggregate_indicator(prov), so a row absent in a
7342-- world drops out of every sum, the count, and the percentile member set.
7343-- The moment statistics are built from indicator-weighted power sums with
7344-- existing gate_arith opcodes (e.g. covar_pop = SXY/N - (SX/N)(SY/N)); a
7345-- world where the statistic is undefined (N = 0, or N = 1 for the sample
7346-- forms) evaluates to NaN, the established undefined-world convention the
7347-- moment estimators skip. percentile_cont is the one gate the arithmetic
7348-- cannot express: it mints the PROVSQL_ARITH_PERCENTILE gate_arith
7349-- (interleaved [ind_1, x_1, ...] wires, fraction in extra) that the Monte
7350-- Carlo sampler evaluates by sorting each draw's present values and
7351-- interpolating.
7352-- ---------------------------------------------------------------------
7353
7354/** @brief State transition for the one-argument RV statistic aggregates
7355 * (@c stddev_pop / @c stddev_samp): append the certain indicator and the
7356 * row's RV as a pair. NULL rows are skipped (standard SQL). */
7357CREATE OR REPLACE FUNCTION rv_stat1_sfunc(state UUID[], x random_variable)
7358 RETURNS UUID[] AS
7359$$
7360 SELECT CASE
7361 WHEN x IS NULL THEN state
7362 ELSE state || ARRAY[(provsql.as_random(1::double precision))::UUID,
7363 (x)::UUID]
7364 END;
7365$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7366
7367/** @brief State transition for the two-argument RV statistic aggregates
7368 * (@c covar_pop / @c covar_samp / @c corr): append the certain indicator
7369 * and the row's RV pair as a triple. Rows with either side NULL are
7370 * skipped (standard SQL covariance semantics). */
7371CREATE OR REPLACE FUNCTION rv_stat2_sfunc(
7372 state UUID[], x random_variable, y random_variable)
7373 RETURNS UUID[] AS
7374$$
7375 SELECT CASE
7376 WHEN x IS NULL OR y IS NULL THEN state
7377 ELSE state || ARRAY[(provsql.as_random(1::double precision))::UUID,
7378 (x)::UUID, (y)::UUID]
7379 END;
7380$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7381
7382/** @brief Indicator-carrying state transition for the one-argument
7383 * @c rv_*_impl statistic aggregates: the planner-hook rewrite passes the
7384 * row's provenance indicator @c rv_aggregate_indicator(prov) as @p ind. */
7385CREATE OR REPLACE FUNCTION rv_stat1_impl_sfunc(
7386 state UUID[], ind random_variable, x random_variable)
7387 RETURNS UUID[] AS
7388$$
7389 SELECT CASE
7390 WHEN x IS NULL THEN state
7391 ELSE state || ARRAY[coalesce((ind)::UUID,
7392 (provsql.as_random(1::double precision))::UUID),
7393 (x)::UUID]
7394 END;
7395$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7396
7397/** @brief Indicator-carrying state transition for the two-argument
7398 * @c rv_*_impl statistic aggregates. */
7399CREATE OR REPLACE FUNCTION rv_stat2_impl_sfunc(
7400 state UUID[], ind random_variable, x random_variable, y random_variable)
7401 RETURNS UUID[] AS
7402$$
7403 SELECT CASE
7404 WHEN x IS NULL OR y IS NULL THEN state
7405 ELSE state || ARRAY[coalesce((ind)::UUID,
7406 (provsql.as_random(1::double precision))::UUID),
7407 (x)::UUID, (y)::UUID]
7408 END;
7409$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7410
7411/**
7412 * @brief Mint the indicator-weighted power-sum gates shared by the
7413 * covariance / stddev final functions.
7414 *
7415 * @p state is the flat interleaved aggregate state -- pairs
7416 * @c [ind, x, ...] (@p stride 2) or triples @c [ind, x, y, ...]
7417 * (@p stride 3). Emits @c gate_arith tokens for
7418 * @f$N = \sum_i \mathbf{1}_i@f$, @f$SX = \sum_i \mathbf{1}_i x_i@f$,
7419 * @f$SXX = \sum_i \mathbf{1}_i x_i^2@f$ and, at stride 3, @f$SY@f$,
7420 * @f$SXY@f$, @f$SYY@f$. The per-row indicator gate is shared between
7421 * @f$N@f$ and every product it weighs, so the Monte Carlo per-iteration
7422 * cache keeps the row's presence coupled across all the sums (and a
7423 * repeated child @c [ind, x, x] reuses the same draw of @c x, giving
7424 * @f$x^2@f$, not two independent draws).
7425 */
7426CREATE OR REPLACE FUNCTION rv_stat_sum_tokens(
7427 state UUID[], stride INTEGER,
7428 OUT n_tok UUID, OUT sx_tok UUID, OUT sxx_tok UUID,
7429 OUT sy_tok UUID, OUT sxy_tok UUID, OUT syy_tok UUID)
7430AS
7431$$
7432DECLARE
7433 nrows INTEGER := coalesce(array_length(state, 1), 0) / stride;
7434 inds UUID[] := '{}';
7435 xs UUID[] := '{}';
7436 xxs UUID[] := '{}';
7437 ys UUID[] := '{}';
7438 xys UUID[] := '{}';
7439 yys UUID[] := '{}';
7440 ind UUID;
7441 x UUID;
7442 y UUID;
7443BEGIN
7444 FOR i IN 1..nrows LOOP
7445 ind := state[(i-1) * stride + 1];
7446 x := state[(i-1) * stride + 2];
7447 inds := array_append(inds, ind);
7448 xs := array_append(xs, provenance_arith(1, ARRAY[ind, x]));
7449 xxs := array_append(xxs, provenance_arith(1, ARRAY[ind, x, x]));
7450 IF stride = 3 THEN
7451 y := state[(i-1) * stride + 3];
7452 ys := array_append(ys, provenance_arith(1, ARRAY[ind, y]));
7453 xys := array_append(xys, provenance_arith(1, ARRAY[ind, x, y]));
7454 yys := array_append(yys, provenance_arith(1, ARRAY[ind, y, y]));
7455 END IF;
7456 END LOOP;
7457 n_tok := provenance_arith(0, inds);
7458 sx_tok := provenance_arith(0, xs);
7459 sxx_tok := provenance_arith(0, xxs);
7460 IF stride = 3 THEN
7461 sy_tok := provenance_arith(0, ys);
7462 sxy_tok := provenance_arith(0, xys);
7463 syy_tok := provenance_arith(0, yys);
7464 END IF;
7465END
7466$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7467 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7468
7469/** @brief Population-variance gate @f$SXX/N - (SX/N)^2@f$ from the
7470 * power-sum tokens. */
7471CREATE OR REPLACE FUNCTION rv_stat_var_pop_token(
7472 n_tok UUID, s_tok UUID, ss_tok UUID)
7473 RETURNS UUID AS
7474$$
7475 SELECT provsql.provenance_arith(2, ARRAY[
7476 provsql.provenance_arith(3, ARRAY[ss_tok, n_tok]),
7477 provsql.provenance_arith(1, ARRAY[
7478 provsql.provenance_arith(3, ARRAY[s_tok, n_tok]),
7479 provsql.provenance_arith(3, ARRAY[s_tok, n_tok])])]);
7480$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
7481
7482/** @brief Sample-variance gate @f$(SXX - SX^2/N) / (N - 1)@f$ from the
7483 * power-sum tokens (NaN in a world with @f$N \le 1@f$, the undefined-world
7484 * convention). */
7485CREATE OR REPLACE FUNCTION rv_stat_var_samp_token(
7486 n_tok UUID, s_tok UUID, ss_tok UUID)
7487 RETURNS UUID AS
7488$$
7489 SELECT provsql.provenance_arith(3, ARRAY[
7490 provsql.provenance_arith(2, ARRAY[
7491 ss_tok,
7492 provsql.provenance_arith(3, ARRAY[
7493 provsql.provenance_arith(1, ARRAY[s_tok, s_tok]), n_tok])]),
7494 provsql.provenance_arith(2, ARRAY[
7495 n_tok, (provsql.as_random(1::double precision))::UUID])]);
7496$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
7497
7498/** @brief @f$\sqrt{\max(v, 0)}@f$ gate over a variance token: the max-clamp
7499 * removes the tiny negative values float error can produce (variance is
7500 * mathematically non-negative), so the POW domain guard never fires. */
7501CREATE OR REPLACE FUNCTION rv_stat_sqrt_token(v_tok UUID)
7502 RETURNS UUID AS
7503$$
7504 SELECT provsql.provenance_arith(7, ARRAY[
7505 provsql.provenance_arith(5, ARRAY[
7506 v_tok, (provsql.as_random(0::double precision))::UUID]),
7507 (provsql.as_random(0.5::double precision))::UUID]);
7508$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
7509
7510/** @brief Population-covariance gate @f$SXY/N - (SX/N)(SY/N)@f$ from the
7511 * power-sum tokens. */
7512CREATE OR REPLACE FUNCTION rv_stat_covar_pop_token(
7513 n_tok UUID, sx_tok UUID, sy_tok UUID, sxy_tok UUID)
7514 RETURNS UUID AS
7515$$
7516 SELECT provsql.provenance_arith(2, ARRAY[
7517 provsql.provenance_arith(3, ARRAY[sxy_tok, n_tok]),
7518 provsql.provenance_arith(1, ARRAY[
7519 provsql.provenance_arith(3, ARRAY[sx_tok, n_tok]),
7520 provsql.provenance_arith(3, ARRAY[sy_tok, n_tok])])]);
7521$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
7522
7523/** @brief Final function for @c covar_pop(random_variable, random_variable). */
7524CREATE OR REPLACE FUNCTION covar_pop_rv_ffunc(state UUID[])
7525 RETURNS random_variable AS
7526$$
7527DECLARE
7528 t RECORD;
7529BEGIN
7530 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7531 RETURN NULL;
7532 END IF;
7533 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
7534 RETURN random_variable_make(
7535 rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok));
7536END
7537$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7538 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7539
7540/** @brief Final function for @c covar_samp(random_variable, random_variable):
7541 * @f$(SXY - SX\,SY/N) / (N-1)@f$. */
7542CREATE OR REPLACE FUNCTION covar_samp_rv_ffunc(state UUID[])
7543 RETURNS random_variable AS
7544$$
7545DECLARE
7546 t RECORD;
7547BEGIN
7548 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7549 RETURN NULL;
7550 END IF;
7551 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
7552 RETURN random_variable_make(
7553 provenance_arith(3, ARRAY[
7554 provenance_arith(2, ARRAY[
7555 t.sxy_tok,
7556 provenance_arith(3, ARRAY[
7557 provenance_arith(1, ARRAY[t.sx_tok, t.sy_tok]), t.n_tok])]),
7558 provenance_arith(2, ARRAY[
7559 t.n_tok, (as_random(1::double precision))::UUID])]));
7560END
7561$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7562 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7563
7564/** @brief Final function for @c corr(random_variable, random_variable):
7565 * @f$\mathrm{covar\_pop} / \sqrt{\max(v_x v_y, 0)}@f$ (a zero-variance
7566 * world divides to @f$\pm\infty@f$ / NaN, the undefined-world convention,
7567 * matching SQL's NULL for a zero-stddev input). */
7568CREATE OR REPLACE FUNCTION corr_rv_ffunc(state UUID[])
7569 RETURNS random_variable AS
7570$$
7571DECLARE
7572 t RECORD;
7573 vx UUID;
7574 vy UUID;
7575BEGIN
7576 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7577 RETURN NULL;
7578 END IF;
7579 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
7580 vx := rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok);
7581 vy := rv_stat_var_pop_token(t.n_tok, t.sy_tok, t.syy_tok);
7582 RETURN random_variable_make(
7583 provenance_arith(3, ARRAY[
7584 rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok),
7585 rv_stat_sqrt_token(provenance_arith(1, ARRAY[vx, vy]))]));
7586END
7587$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7588 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7589
7590/** @brief Final function for @c stddev_pop(random_variable). */
7591CREATE OR REPLACE FUNCTION stddev_pop_rv_ffunc(state UUID[])
7592 RETURNS random_variable AS
7593$$
7594DECLARE
7595 t RECORD;
7596BEGIN
7597 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7598 RETURN NULL;
7599 END IF;
7600 SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
7601 RETURN random_variable_make(
7602 rv_stat_sqrt_token(
7603 rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok)));
7604END
7605$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7606 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7607
7608/** @brief Final function for @c stddev_samp(random_variable). */
7609CREATE OR REPLACE FUNCTION stddev_samp_rv_ffunc(state UUID[])
7610 RETURNS random_variable AS
7611$$
7612DECLARE
7613 t RECORD;
7614BEGIN
7615 IF state IS NULL OR array_length(state, 1) IS NULL THEN
7616 RETURN NULL;
7617 END IF;
7618 SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
7619 RETURN random_variable_make(
7620 rv_stat_sqrt_token(
7621 rv_stat_var_samp_token(t.n_tok, t.sx_tok, t.sxx_tok)));
7622END
7623$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7624 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7625
7626CREATE AGGREGATE covar_pop(random_variable, random_variable) (
7627 SFUNC = rv_stat2_sfunc,
7628 STYPE = UUID[],
7629 INITCOND = '{}',
7630 FINALFUNC = covar_pop_rv_ffunc
7631);
7632
7633CREATE AGGREGATE covar_samp(random_variable, random_variable) (
7634 SFUNC = rv_stat2_sfunc,
7635 STYPE = UUID[],
7636 INITCOND = '{}',
7637 FINALFUNC = covar_samp_rv_ffunc
7638);
7639
7640CREATE AGGREGATE corr(random_variable, random_variable) (
7641 SFUNC = rv_stat2_sfunc,
7642 STYPE = UUID[],
7643 INITCOND = '{}',
7644 FINALFUNC = corr_rv_ffunc
7645);
7646
7647CREATE AGGREGATE stddev_pop(random_variable) (
7648 SFUNC = rv_stat1_sfunc,
7649 STYPE = UUID[],
7650 INITCOND = '{}',
7651 FINALFUNC = stddev_pop_rv_ffunc
7652);
7653
7654CREATE AGGREGATE stddev_samp(random_variable) (
7655 SFUNC = rv_stat1_sfunc,
7656 STYPE = UUID[],
7657 INITCOND = '{}',
7658 FINALFUNC = stddev_samp_rv_ffunc
7659);
7660
7661-- The indicator-carrying rewrite targets (planner hook only; never called
7662-- directly by users).
7663
7664CREATE AGGREGATE rv_covar_pop_impl(
7665 random_variable, random_variable, random_variable) (
7666 SFUNC = rv_stat2_impl_sfunc,
7667 STYPE = UUID[],
7668 INITCOND = '{}',
7669 FINALFUNC = covar_pop_rv_ffunc
7670);
7671
7672CREATE AGGREGATE rv_covar_samp_impl(
7673 random_variable, random_variable, random_variable) (
7674 SFUNC = rv_stat2_impl_sfunc,
7675 STYPE = UUID[],
7676 INITCOND = '{}',
7677 FINALFUNC = covar_samp_rv_ffunc
7678);
7679
7680CREATE AGGREGATE rv_corr_impl(
7681 random_variable, random_variable, random_variable) (
7682 SFUNC = rv_stat2_impl_sfunc,
7683 STYPE = UUID[],
7684 INITCOND = '{}',
7685 FINALFUNC = corr_rv_ffunc
7686);
7687
7688CREATE AGGREGATE rv_stddev_pop_impl(random_variable, random_variable) (
7689 SFUNC = rv_stat1_impl_sfunc,
7690 STYPE = UUID[],
7691 INITCOND = '{}',
7692 FINALFUNC = stddev_pop_rv_ffunc
7693);
7694
7695CREATE AGGREGATE rv_stddev_samp_impl(random_variable, random_variable) (
7696 SFUNC = rv_stat1_impl_sfunc,
7697 STYPE = UUID[],
7698 INITCOND = '{}',
7699 FINALFUNC = stddev_samp_rv_ffunc
7700);
7701
7702/**
7703 * @brief Mint the @c PROVSQL_ARITH_PERCENTILE gate: the continuous
7704 * percentile (SQL @c percentile_cont) over a group of RV rows.
7705 *
7706 * @p pairs is the interleaved wire list @c [ind_1, x_1, ..., ind_n, x_n]
7707 * (each @p ind_i a 0/1 presence-indicator RV). The @p fraction is
7708 * TEXT-encoded in the gate's @c extra and participates in the token UUID
7709 * (two percentiles of the same group at different fractions are distinct
7710 * gates). Per Monte Carlo draw, the sampler collects the values whose
7711 * indicator draws 1, sorts them, and linearly interpolates at the
7712 * fraction; a draw with no present row is NaN (undefined world).
7713 */
7714CREATE OR REPLACE FUNCTION rv_percentile_make(fraction double precision,
7715 pairs UUID[])
7716 RETURNS random_variable AS
7717$$
7718DECLARE
7719 token UUID;
7720BEGIN
7721 IF fraction IS NULL THEN
7722 RETURN NULL;
7723 END IF;
7724 IF fraction < 0 OR fraction > 1 THEN
7725 RAISE EXCEPTION
7726 'percentile_cont: fraction must be between 0 and 1 (got %)', fraction;
7727 END IF;
7728 token := public.uuid_generate_v5(
7729 uuid_ns_provsql(),
7730 concat('arith', '10', pairs::TEXT, fraction::TEXT));
7731 -- 10 = PROVSQL_ARITH_PERCENTILE
7732 PERFORM create_gate(token, 'arith', pairs, 10, NULL, fraction::TEXT);
7733 RETURN random_variable_make(token);
7734END
7735$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
7736 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7737
7738/** @brief State transition for the public ordered-set
7739 * @c percentile_cont(float8) WITHIN GROUP (ORDER BY random_variable):
7740 * append the certain indicator and the row's RV. Only reachable on
7741 * untracked input (a provenance-tracked query is rewritten to
7742 * @c rv_percentile_impl before planning), where the sort over
7743 * @c random_variable raises the ordering-is-meaningless diagnostic
7744 * first -- so in practice this runs only for empty input. */
7745CREATE OR REPLACE FUNCTION percentile_cont_rv_sfunc(
7746 state UUID[], x random_variable)
7747 RETURNS UUID[] AS
7748$$
7749 SELECT provsql.rv_stat1_sfunc(state, x);
7750$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7751
7752/** @brief Final function for the public ordered-set @c percentile_cont:
7753 * receives the direct @p fraction argument after the state. */
7754CREATE OR REPLACE FUNCTION percentile_cont_rv_ffunc(
7755 state UUID[], fraction double precision)
7756 RETURNS random_variable AS
7757$$
7758 SELECT CASE
7759 WHEN state IS NULL OR array_length(state, 1) IS NULL THEN NULL
7760 ELSE provsql.rv_percentile_make(fraction, state)
7761 END;
7762$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7763
7764CREATE AGGREGATE percentile_cont(double precision ORDER BY random_variable) (
7765 SFUNC = percentile_cont_rv_sfunc,
7766 STYPE = UUID[],
7767 INITCOND = '{}',
7768 FINALFUNC = percentile_cont_rv_ffunc
7769);
7770
7771/** @brief Transition state for @c rv_percentile_impl: the fraction (from
7772 * the first row) plus the interleaved indicator/value token pairs. */
7773CREATE TYPE rv_percentile_state AS (
7774 fraction double precision,
7775 tokens UUID[]
7776);
7777
7778/** @brief State transition for @c rv_percentile_impl, the planner-hook
7779 * rewrite target of a provenance-tracked @c percentile_cont: stashes the
7780 * (group-constant) fraction and appends the indicator/value pair. */
7781CREATE OR REPLACE FUNCTION rv_percentile_impl_sfunc(
7782 state rv_percentile_state, fraction double precision,
7783 ind random_variable, x random_variable)
7784 RETURNS rv_percentile_state AS
7785$$
7786 SELECT ROW(
7787 coalesce((state).fraction, fraction),
7788 CASE
7789 WHEN x IS NULL THEN (state).tokens
7790 ELSE (state).tokens ||
7791 ARRAY[coalesce((ind)::UUID,
7792 (provsql.as_random(1::double precision))::UUID),
7793 (x)::UUID]
7794 END)::provsql.rv_percentile_state;
7795$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7796
7797/** @brief Final function for @c rv_percentile_impl. */
7798CREATE OR REPLACE FUNCTION rv_percentile_impl_ffunc(state rv_percentile_state)
7799 RETURNS random_variable AS
7800$$
7801 SELECT CASE
7802 WHEN state IS NULL OR array_length((state).tokens, 1) IS NULL THEN NULL
7803 ELSE provsql.rv_percentile_make((state).fraction, (state).tokens)
7804 END;
7805$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7806
7807CREATE AGGREGATE rv_percentile_impl(
7808 double precision, random_variable, random_variable) (
7809 SFUNC = rv_percentile_impl_sfunc,
7810 STYPE = rv_percentile_state,
7811 INITCOND = '(,"{}")',
7812 FINALFUNC = rv_percentile_impl_ffunc
7813);
7814
7815/** @} */
7816
7817/** @} */
7818
7819/** @} */
7820
7821/** @defgroup aggregate_provenance Aggregate provenance
7822 * Functions for building and evaluating aggregate (GROUP BY) provenance,
7823 * including the δ-semiring operator and semimodule multiplication.
7824 * @{
7825 */
7826
7827/**
7828 * @brief Create a δ-semiring gate wrapping a provenance token
7829 *
7830 * Used internally for aggregate provenance. Returns the token unchanged
7831 * if it is gate_zero() or gate_one(), and gate_one() if the token is NULL.
7832 *
7833 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
7834 * PL/pgSQL function it was, so that plans stay the same.
7835 */
7836CREATE OR REPLACE FUNCTION provenance_delta
7837 (token UUID)
7838 RETURNS UUID AS
7839 'provsql','provenance_delta' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
7840
7841/**
7842 * @brief Build an aggregate provenance gate from grouped tokens
7843 *
7844 * Called internally by the query rewriter for GROUP BY queries.
7845 * Creates an agg gate linking all contributing tokens and records
7846 * the aggregate function OID and the computed scalar value. The gate's
7847 * address hashes everything it records, result type and value included:
7848 * what is recorded is written once, and two aggregations that differ only
7849 * there (a polymorphic aggregate over two types with the same texts, a
7850 * floating-point sum rounded differently by two plans) must be two gates.
7851 *
7852 * @param aggfnoid OID of the SQL aggregate function
7853 * @param aggtype OID of the aggregate result type
7854 * @param val computed aggregate value
7855 * @param tokens array of provenance tokens being aggregated
7856 * @param is_scalar true for a scalar (no GROUP BY) aggregation, whose
7857 * output row exists even when no tuple is present; stored in the
7858 * high bit of info2
7859 *
7860 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
7861 * PL/pgSQL function it was, so that plans stay the same.
7862 */
7863CREATE OR REPLACE FUNCTION provenance_aggregate(
7864 aggfnoid INTEGER,
7865 aggtype INTEGER,
7866 val ANYELEMENT,
7867 tokens UUID[],
7868 is_scalar BOOLEAN DEFAULT false)
7869 RETURNS AGG_TOKEN AS
7870 'provsql','provenance_aggregate' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
7871
7872/**
7873 * @brief Create a semimodule scalar multiplication gate
7874 *
7875 * Pairs a scalar value with a provenance token, used internally by
7876 * the query rewriter for aggregate provenance.
7877 *
7878 * @param val the scalar value
7879 * @param token the provenance token to multiply
7880 *
7881 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
7882 * PL/pgSQL function it was, so that plans stay the same.
7883 */
7884CREATE OR REPLACE FUNCTION provenance_semimod(val ANYELEMENT, token UUID)
7885 RETURNS UUID AS
7886 'provsql','provenance_semimod' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
7887
7888/**
7889 * @brief The contribution of an aggregate result to an aggregate of another
7890 * kind over it (internal)
7891 *
7892 * For a row of token @p token whose value @p val is the result of an
7893 * aggregate of another kind than the one aggregating it (an @c avg of a
7894 * @c count, a @c max of a @c sum, an aggregate of an arithmetic expression
7895 * over aggregates): @c semimod(g, token), where @c g is the inner
7896 * aggregate's own gate. Its value is one per possible world, not one value
7897 * of the database, so the evaluators that read a value per world resolve it
7898 * and the closed forms decline it.
7899 */
7900CREATE FUNCTION provenance_semimod_nested(val AGG_TOKEN, token UUID)
7901 RETURNS UUID AS
7902 'provsql','provenance_semimod_nested' LANGUAGE C PARALLEL SAFE IMMUTABLE;
7903
7904/**
7905 * @brief The contributions of an aggregate result to an aggregate of the
7906 * same kind over it (internal)
7907 *
7908 * For a row of token @p token whose value @p val is the result of
7909 * @c sum / @c count / @c max / @c min over a group, aggregated again by
7910 * @c sum / @c max / @c min: the contributions @c semimod(v_i, token ⊗ k_i)
7911 * of the group's own contributions @c semimod(v_i, k_i), so that
7912 * @c sum(sum(x)) is @c sum(x) over the rows of the groups -- semimodule
7913 * scalar multiplication, in every semiring. Collected by
7914 * @c provenance_contributions_cat.
7915 */
7916CREATE FUNCTION provenance_semimod_flat(val AGG_TOKEN, token UUID)
7917 RETURNS UUID[] AS
7918 'provsql','provenance_semimod_flat' LANGUAGE C PARALLEL SAFE IMMUTABLE;
7919
7920/** @brief Concatenation of the arrays of contributions of
7921 * @c provenance_semimod_flat (internal) */
7922CREATE AGGREGATE provenance_contributions_cat(UUID[]) (
7923 SFUNC = array_cat,
7924 STYPE = UUID[],
7925 INITCOND = '{}'
7926);
7927
7928/**
7929 * @brief Semimodule gate for an aggregate that sees its NULL inputs
7930 *
7931 * Variant of provenance_semimod() used by the query rewriter for
7932 * <tt>array_agg</tt>, <tt>json_agg</tt> and the like, whose result lists
7933 * every input, NULLs included: a NULL value still yields a semimod gate,
7934 * over the constant value gate gate_null().
7935 *
7936 * @param val the scalar value, possibly NULL
7937 * @param token the provenance token to multiply
7938 *
7939 * Implemented in C (<tt>gate_builders.c</tt>), with the declared cost of the
7940 * PL/pgSQL function it was, so that plans stay the same.
7941 */
7942CREATE OR REPLACE FUNCTION provenance_semimod_nullable(val ANYELEMENT, token UUID)
7943 RETURNS UUID AS
7944 'provsql','provenance_semimod_nullable' LANGUAGE C COST 100 PARALLEL SAFE IMMUTABLE;
7945
7946/**
7947 * @brief The rank of a row standing for its row number (internal)
7948 *
7949 * Called by the query rewriter for <tt>row_number() OVER (…)</tt> over
7950 * provenance-tracked relations, which is tracked as <tt>rank()</tt>: the two
7951 * are equal when the <tt>ORDER BY</tt> of the window leaves no ties. Returns
7952 * @p rank, with a warning, once per statement, when @p row_number differs
7953 * from it.
7954 *
7955 * @param rank the AGG_TOKEN of the rank of the row
7956 * @param row_number the row number PostgreSQL gave the row
7957 */
7958CREATE OR REPLACE FUNCTION row_number_as_rank(rank AGG_TOKEN, row_number bigint)
7959 RETURNS AGG_TOKEN
7960 AS 'provsql','row_number_as_rank' LANGUAGE C VOLATILE STRICT PARALLEL SAFE;
7961
7962/**
7963 * @brief The contributions of the distinct values of a window frame
7964 * (internal)
7965 *
7966 * Called by the query rewriter for <tt>dense_rank() OVER (…)</tt> over
7967 * provenance-tracked relations, which counts the distinct ordering values
7968 * before the current row: one <tt>provenance_semimod(1, ⊕ tokens)</tt> per
7969 * distinct value of @p vals, the ⊕ of the tokens of the rows that have it,
7970 * which is present when one of them is. Values are compared with the
7971 * equality of their type, as the peers of a window are, NULLs being equal.
7972 *
7973 * @param vals the ordering values of the rows of the frame
7974 * @param tokens the provenance tokens of these rows, in the same order
7975 */
7976CREATE OR REPLACE FUNCTION window_distinct_tokens(vals anyarray, tokens UUID[])
7977 RETURNS UUID[] AS
7978$$
7979 SELECT array_agg(s ORDER BY s)
7980 FROM (SELECT provsql.provenance_semimod(1, provsql.provenance_plus(array_agg(tokens[i]))) AS s
7981 FROM generate_subscripts(vals, 1) AS i GROUP BY vals[i]) g
7982$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
7983
7984/**
7985 * @brief Mark a part of a query to be evaluated as plain SQL, not tracked
7986 *
7987 * Over provenance-tracked relations, <tt>ORDER BY … LIMIT k</tt> keeps, in
7988 * each possible world, the rows that fewer than @p k present rows precede:
7989 * every row that may be among them is output, annotated with that condition.
7990 * <tt>LIMIT plain(k)</tt> (<tt>FETCH FIRST plain(k) ROWS</tt>,
7991 * <tt>OFFSET plain(m)</tt>) instead truncates the result as computed on the
7992 * actual data, and each row kept carries its provenance in the full result.
7993 * The function returns its argument.
7994 *
7995 * @param value the marked value
7996 */
7997CREATE OR REPLACE FUNCTION plain(value ANYELEMENT)
7998 RETURNS ANYELEMENT AS
7999$$ SELECT value $$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
8000
8001/** @} */
8002
8003/** @defgroup probability Probability and Shapley values
8004 * Functions for computing probabilities, expected values, and
8005 * game-theoretic contribution measures (Shapley/Banzhaf values)
8006 * from provenance circuits.
8007 * @{
8008 */
8009
8010/**
8011 * @brief Compute the probability of a provenance token
8012 *
8013 * Compiles the provenance circuit to d-DNNF and evaluates the
8014 * probability. The compilation method can be selected explicitly.
8015 *
8016 * @ref probability() "probability" is a shorter alias bound to the same C symbol, so
8017 * @c probability(token) is exactly @c probability_evaluate(token); it is
8018 * usually preferable, and additionally carries a @c (BOOLEAN) predicate
8019 * overload (e.g. @c probability(x @c > @c y)).
8020 *
8021 * @param token provenance token to evaluate
8022 * @param method knowledge compilation method (NULL for default)
8023 * @param arguments additional arguments for the method
8024 */
8025CREATE OR REPLACE FUNCTION probability_evaluate(
8026 token UUID,
8027 method TEXT = NULL,
8028 arguments TEXT = NULL)
8029 RETURNS DOUBLE PRECISION AS
8030 'provsql','probability_evaluate' LANGUAGE C STABLE;
8031
8032/**
8033 * @brief Short alias of @ref probability_evaluate.
8034 *
8035 * Bound to the same C symbol as @ref probability_evaluate, so
8036 * @c probability(token) is exactly @c probability_evaluate(token).
8037 * Provided to match the concise polymorphic surface of @ref expected,
8038 * @ref variance, and @ref support "support": callers are not forced to
8039 * spell out @c probability_evaluate.
8040 *
8041 * @param token provenance token to evaluate
8042 * @param method knowledge compilation method (NULL for default)
8043 * @param arguments additional arguments for the method
8044 */
8045CREATE OR REPLACE FUNCTION probability(
8046 token UUID,
8047 method TEXT = NULL,
8048 arguments TEXT = NULL)
8049 RETURNS DOUBLE PRECISION AS
8050 'provsql','probability_evaluate' LANGUAGE C STABLE;
8051
8052/**
8053 * @brief Probability of a Boolean event over random variables.
8054 *
8055 * The @c (BOOLEAN) overload of @c probability lets a query ask for the
8056 * probability of an event with the natural infix grammar, e.g.
8057 * @c probability(x @c > @c y @c AND @c x @c < @c z). When the argument
8058 * carries a probabilistic (random_variable / aggregate) comparison, the
8059 * planner hook intercepts the call and rewrites it into
8060 * @c probability_evaluate over the argument's event token (a @c gate_cmp /
8061 * Boolean combination); the body below is then never reached.
8062 *
8063 * When the argument is a purely deterministic Boolean (no probabilistic
8064 * comparison) the hook leaves the call alone and the body runs, so the
8065 * probability of a definite event is simply @c 1 when it holds and @c 0 when
8066 * it does not (@c NULL propagates). This makes @c probability total over
8067 * Booleans -- @c probability(1 @c > @c 0) is @c 1, @c probability(region @c =
8068 * @c 'north') is a per-row @c 0/1 -- and it works even with
8069 * @c provsql.active off. @c NOT strict so a default-NULL @c method does not
8070 * short-circuit the cast.
8071 *
8072 * The predicate surface deliberately lives only on the short @c probability
8073 * name, not on @c probability_evaluate: a Boolean overload of the latter
8074 * would make @c probability_evaluate('<UUID-as-TEXT>') ambiguous (an unknown
8075 * literal matches both the @c UUID and the @c BOOLEAN overload), breaking
8076 * existing string-literal callers. @c probability is new, so it carries the
8077 * predicate overload without that hazard.
8078 */
8079CREATE OR REPLACE FUNCTION probability(
8080 predicate BOOLEAN,
8081 method TEXT = NULL,
8082 arguments TEXT = NULL)
8083 RETURNS DOUBLE PRECISION AS
8084$$
8085 SELECT predicate::INTEGER::double precision;
8086$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
8087
8088/**
8089 * @brief Cheap certified probability interval of a DNF-shaped circuit.
8090 *
8091 * Returns @c [lower,upper] with @c lower <= probability_evaluate(token) <=
8092 * @c upper, computed without compiling the circuit (the Olteanu-Huang d-tree
8093 * leaf bound). Errors when @p token is not a monotone DNF over input leaves.
8094 */
8095CREATE OR REPLACE FUNCTION probability_bounds(
8096 token UUID,
8097 OUT lower DOUBLE PRECISION,
8098 OUT upper DOUBLE PRECISION) AS
8099 'provsql','probability_bounds' LANGUAGE C STABLE;
8100
8101/**
8102 * @brief Compute the expected value of a probabilistic scalar
8103 *
8104 * Computes E[input | prov] for either an @c AGG_TOKEN (discrete
8105 * SUM/MIN/MAX aggregation over Boolean-input gate_agg circuits, with
8106 * @c prov as the Boolean conditioning event) or a @c random_variable
8107 * (continuous distribution, traversed by the analytical / MC
8108 * evaluator from @c Expectation.cpp).
8109 *
8110 * Implementation: thin wrapper over @c moment(input, 1, prov, method,
8111 * arguments). Both branches converge on the same machinery; the
8112 * AGG_TOKEN side computes E[X] as the @f$k=1@f$ instance of the
8113 * @f$n^k@f$-tuple enumeration in @c agg_raw_moment, the
8114 * random_variable side calls @c compute_expectation through
8115 * @c rv_moment.
8116 *
8117 * @param input aggregate expression or random variable to compute E[·] of
8118 * @param prov provenance condition (defaults to gate_one(), i.e., unconditional)
8119 * @param method knowledge compilation method (AGG_TOKEN path only)
8120 * @param arguments additional arguments for the method (AGG_TOKEN path only)
8121 */
8122CREATE OR REPLACE FUNCTION expected(
8123 input ANYELEMENT,
8124 prov UUID = gate_one(),
8125 method TEXT = NULL,
8126 arguments TEXT = NULL)
8127 RETURNS DOUBLE PRECISION AS $$
8128 SELECT moment(input, 1, prov, method, arguments);
8129$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
8130
8131/**
8132 * @brief Internal: shared C entry point for variance / moment / central_moment.
8133 *
8134 * The @c expected() SQL function reaches the Expectation evaluator
8135 * through @c provenance_evaluate_compiled(..., 'expectation', ...).
8136 * The variance / raw-moment / central-moment SQL functions need an
8137 * extra @p k INTEGER argument that does not fit that dispatcher's
8138 * signature, so they go through this dedicated entry point. Returns
8139 * E[X^k] when @p central is FALSE, or E[(X - E[X])^k] when TRUE.
8140 */
8141CREATE OR REPLACE FUNCTION rv_moment(
8142 token UUID, k INTEGER, central BOOLEAN,
8143 prov UUID DEFAULT gate_one())
8144 RETURNS double precision
8145 AS 'provsql','rv_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8146
8147/** @brief Exact E[AVG^k | COUNT >= 1] over independent rows (the joint
8148 * (sum, count) fold); NULL when the shape is out of scope (shared
8149 * leaves, compound contributors), signalling @c agg_raw_moment's avg
8150 * arm to fall back to the Monte-Carlo scalar path. */
8151CREATE OR REPLACE FUNCTION agg_avg_moment_exact(token UUID, k INTEGER)
8152 RETURNS double precision
8153 AS 'provsql','agg_avg_moment_exact' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8154
8155/** @brief Collapsed (Rao-Blackwellised) raw moment E[C^k] of a correlated
8156 * COUNT / SUM whose per-row selection events are coupled through a single
8157 * shared continuous latent: 1-D quadrature over the latent, closed-form
8158 * per-row CDF given it (O(G·n), exact up to the grid). NULL when the
8159 * circuit does not match the shared-latent pattern (caller falls back to
8160 * the exact n^k enumeration). k in {1, 2}. */
8161CREATE OR REPLACE FUNCTION agg_collapsed_moment(token UUID, k INTEGER)
8162 RETURNS double precision
8163 AS 'provsql','agg_collapsed_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8164
8165/** @brief Both collapsed raw moments {E[C], E[C^2]} of a correlated COUNT / SUM
8166 * from a single circuit load and plan build; NULL when the shared-latent
8167 * pattern does not match. @c variance() uses this so a mean+variance readout
8168 * traverses the circuit once rather than calling @c agg_collapsed_moment twice
8169 * (the load and O(n) plan build dominate once the grid loop is arithmetic). */
8170CREATE OR REPLACE FUNCTION agg_collapsed_moments(token UUID)
8171 RETURNS double precision[]
8172 AS 'provsql','agg_collapsed_moments' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8173
8174/**
8175 * @brief Boolean event "this aggregate-carrying gate's value is defined
8176 * (non-NULL) in the world".
8177 *
8178 * Backs the conditional-on-defined convention of the aggregate moment
8179 * readouts: @c sum / @c count (and constants) have a value in every
8180 * world -- the empty group is the real value @c 0 -- so their defined
8181 * event is @c gate_one(); @c min / @c max / @c avg (and any other
8182 * aggregate) are @c NULL on an empty group, so their defined event is
8183 * "some contributing row is present", the OR of the semimod children's
8184 * row tokens; a @c case gate's value is defined iff its first-match
8185 * selected branch's value is (the same region walk as the moment
8186 * evaluator, conjoined per branch). Anything else (an @c arith
8187 * composite, whose AGG_TOKEN running value is total) counts as always
8188 * defined.
8189 */
8190CREATE OR REPLACE FUNCTION agg_defined_event(token UUID)
8191 RETURNS UUID AS $$
8192DECLARE
8193 gt PROVENANCE_GATE := get_gate_type(token);
8194 fname varchar;
8195 toks UUID[];
8196 wires UUID[];
8197 nw INTEGER;
8198 m INTEGER;
8199 i INTEGER;
8200 running_neg UUID := gate_one();
8201 parts UUID[] := '{}';
8202BEGIN
8203 IF token = gate_null() THEN
8204 RETURN gate_zero(); -- the NULL value: never defined
8205 END IF;
8206 IF gt = 'agg' THEN
8207 SELECT proname INTO fname
8208 FROM pg_proc WHERE oid = (get_infos(token)).info1;
8209 -- A scalar COUNT has a row in every world, counting a real 0 over none;
8210 -- every other aggregate (a SUM over no row is SQL NULL, a grouped one has
8211 -- no row at all) is defined only where a contributing row is.
8212 IF fname = 'count' AND (get_infos(token)).info2 < 0 THEN
8213 RETURN gate_one();
8214 END IF;
8215 SELECT array_agg((get_children(c))[1]) INTO toks
8216 FROM unnest(get_children(token)) AS c;
8217 IF toks IS NULL THEN
8218 RETURN gate_zero(); -- structurally empty aggregate: never defined
8219 END IF;
8220 RETURN provenance_plus(toks);
8221 ELSIF gt = 'case' THEN
8222 wires := get_children(token);
8223 nw := array_length(wires, 1);
8224 m := (nw - 1) / 2;
8225 FOR i IN 1..m LOOP
8226 parts := parts || provenance_times(
8227 running_neg, wires[2 * i - 1],
8228 agg_defined_event(wires[2 * i]));
8229 running_neg := provenance_times(running_neg,
8230 provenance_not(wires[2 * i - 1]));
8231 END LOOP;
8232 parts := parts || provenance_times(running_neg,
8233 agg_defined_event(wires[nw]));
8234 RETURN provenance_plus(parts);
8235 ELSIF gt = 'arith' THEN
8236 -- Arithmetic is STRICT: a value exists where every operand's does. Saying
8237 -- gate_one here made a var_pop moment count the world where the group has
8238 -- no row. Its CASE guards a count against 0 and against 1, and a
8239 -- comparison over an aggregate that has no value holds in no world, so in
8240 -- the empty world neither guard fires and the FORMULA arm is the one
8241 -- selected -- an arith over the sums, whose operands have no value there.
8242 -- Declared defined, that world was counted, and expected(var_pop(x)) came
8243 -- out NaN for a one-row group whose value is 0 wherever it exists.
8244 SELECT array_agg(agg_defined_event(c)) INTO parts
8245 FROM unnest(get_children(token)) AS c;
8246 IF parts IS NULL OR array_length(parts, 1) IS NULL THEN
8247 RETURN gate_one();
8248 END IF;
8249 RETURN provenance_times(VARIADIC parts);
8250 END IF;
8251 -- value / anything else: a value exists in every world (gate_null, the one
8252 -- value that never does, is answered at the top).
8253 RETURN gate_one();
8254END
8255$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
8256 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
8257
8258/**
8259 * @brief Compute the raw moment E[X^k | prov] of an AGG_TOKEN aggregate
8260 *
8261 * Sister of @c expected() for the AGG_TOKEN side of the polymorphic
8262 * @c moment / @c variance / @c central_moment dispatch. Supports the
8263 * same aggregation functions as @c expected: SUM (which COUNT
8264 * normalises to at the gate level via @c Aggregation.cpp:322), MIN,
8265 * MAX, and AVG (exact over independent / laminar rows via the joint
8266 * (sum, count) distribution, Monte-Carlo scalar fallback otherwise).
8267 * MIN / MAX / AVG are NULL on an empty group, so their moments are
8268 * CONDITIONAL on the aggregate being defined -- NULL only when it never
8269 * is; SUM / COUNT treat the empty world as the real value 0.
8270 *
8271 * Strategy:
8272 * - <b>SUM</b>: with X = Σᵢ Iᵢ·vᵢ (Iᵢ the per-row inclusion indicator,
8273 * vᵢ the row's value), expanding X^k and taking expectation gives
8274 * @f$E[X^k] = \sum_{(i_1,\ldots,i_k) \in \{1..n\}^k} v_{i_1}\cdots v_{i_k}
8275 * \cdot P(\bigwedge_{i \in \TEXT{distinct}(i_1..i_k)} I_i)@f$.
8276 * We enumerate the @f$n^k@f$ tuples, conjoin the distinct inclusion
8277 * tokens (and @p prov when conditioning), and evaluate the
8278 * probability via @c probability_evaluate.
8279 * - <b>MIN / MAX</b>: replace @c v with @c v^k in the rank-based
8280 * enumeration that @c expected already uses; @c MAX is handled by
8281 * sign-flipping per the existing trick (negate vs. rerank), with
8282 * the outer multiplier becoming @f$(-1)^k@f$ instead of just @f$-1@f$.
8283 *
8284 * Cost: SUM is @f$O(n^k)@f$ probability evaluations -- tractable for
8285 * small @p k or small @p n; for larger sizes, prefer reaching for the
8286 * sampler. MIN / MAX stay linear in @p n.
8287 */
8288CREATE OR REPLACE FUNCTION agg_raw_moment(
8289 token AGG_TOKEN,
8290 k INTEGER,
8291 prov UUID = gate_one(),
8292 method TEXT = NULL,
8293 arguments TEXT = NULL)
8294 RETURNS DOUBLE PRECISION AS $$
8295DECLARE
8296 aggregation_function VARCHAR;
8297 child_pairs UUID[];
8298 pair_children UUID[];
8299 n INTEGER;
8300 i INTEGER;
8301 j INTEGER;
8302 vals float8[];
8303 toks UUID[];
8304 total float8;
8305 total_probability float8;
8306 tup INTEGER[];
8307 d INTEGER;
8308 prod_v float8;
8309 distinct_tok UUID[];
8310 conj_token UUID;
8311 prob float8;
8312 sign_max float8;
8313 is_scalar BOOLEAN;
8314 defined_tok UUID;
8315BEGIN
8316 IF token IS NULL OR k IS NULL THEN
8317 RETURN NULL;
8318 END IF;
8319 IF k < 0 THEN
8320 RAISE EXCEPTION 'agg_raw_moment(): k must be non-negative (got %)', k;
8321 END IF;
8322
8323 -- Aggregate-carrier CASE (a gate_case over aggregate branches): a first-match
8324 -- guarded selection. The moment is CONDITIONAL on the CASE's value being
8325 -- defined (NULL only when it never is, mirroring the MIN/MAX convention):
8326 -- E[pick^k | defined ∧ prov]
8327 -- = Σ_i P(region_i ∧ def_i) · E[value_i^k | region_i ∧ def_i]
8328 -- / Σ_i P(region_i ∧ def_i),
8329 -- where region_i = (¬g_1 ∧ … ∧ ¬g_{i-1}) ∧ g_i ∧ prov is the world set that
8330 -- selects branch i (the default's region is "all guards false") and def_i is
8331 -- the branch's defined event (agg_defined_event: gate_one for sum / count /
8332 -- constants, "some row present" for min / max / avg, recursive for a nested
8333 -- CASE). Both factors are exact: probability() over the region ∧ def event,
8334 -- and the conditional aggregate moment (a recursive agg_raw_moment on the
8335 -- branch aggregate, which conditions on its own definedness within the
8336 -- region, so the two factors weigh the same worlds). The regions are
8337 -- mutually exclusive, so the terms sum with no inclusion-exclusion, and
8338 -- correlation between a guard and its branch (shared input tuples) is
8339 -- carried by the conditioning, exactly as HAVING carries it. When every
8340 -- branch is defined everywhere, the defined mass equals P(prov) and the
8341 -- formula reduces to the plain region-weighted sum.
8342 IF get_gate_type(token) = 'case' THEN
8343 IF k = 0 THEN
8344 RETURN 1;
8345 END IF;
8346 DECLARE
8347 wires UUID[] := get_children(token);
8348 nw INTEGER := array_length(get_children(token), 1);
8349 m INTEGER := (array_length(get_children(token), 1) - 1) / 2;
8350 running_neg UUID := gate_one();
8351 region_full UUID;
8352 prov_p float8;
8353 p float8;
8354 total float8 := 0;
8355 def_mass float8 := 0;
8356 ci INTEGER;
8357 vuid UUID;
8358 bm float8;
8359 BEGIN
8360 prov_p := probability(prov);
8361 IF prov_p IS NULL OR prov_p <= 0 THEN
8362 RETURN NULL; -- impossible conditioning event
8363 END IF;
8364 -- Branches 1..m are the guarded WHENs; branch m+1 is the ELSE default,
8365 -- whose region is "all guards false".
8366 FOR ci IN 1 .. m + 1 LOOP
8367 IF ci <= m THEN
8368 region_full := provenance_times(running_neg, wires[2 * ci - 1], prov);
8369 vuid := wires[2 * ci];
8370 running_neg :=
8371 provenance_times(running_neg, provenance_not(wires[2 * ci - 1]));
8372 ELSE
8373 region_full := provenance_times(running_neg, prov);
8374 vuid := wires[nw];
8375 END IF;
8376 p := probability(provenance_times(region_full,
8377 agg_defined_event(vuid)));
8378 IF p > 0 THEN
8379 -- E[value_i^k | region_i ∧ def_i]: a constant branch is a Dirac
8380 -- (c^k, exact); a single aggregate or nested CASE is exact via
8381 -- agg_raw_moment (whose MIN/MAX/CASE arms condition on their own
8382 -- definedness within the region); an arithmetic / composite branch
8383 -- takes the Monte-Carlo scalar path (which composes with the
8384 -- aggregate leaves).
8385 IF get_gate_type(vuid) = 'value' THEN
8386 bm := power(CAST(get_extra(vuid) AS float8), k);
8387 ELSIF get_gate_type(vuid) IN ('agg', 'case') THEN
8388 bm := agg_raw_moment(agg_token_make(vuid, 0), k, region_full,
8389 method, arguments);
8390 ELSE
8391 bm := rv_moment(vuid, k, false, region_full);
8392 END IF;
8393 total := total + p * bm;
8394 def_mass := def_mass + p;
8395 END IF;
8396 END LOOP;
8397 IF def_mass <= 0 THEN
8398 RETURN NULL; -- the CASE's value is never defined under prov
8399 END IF;
8400 RETURN total / def_mass;
8401 END;
8402 END IF;
8403
8404 IF get_gate_type(token) <> 'agg' THEN
8405 IF get_gate_type(token) IN ('arith', 'conditioned') THEN
8406 -- An arithmetic combination of aggregates (SUM(x) + SUM(y), SUM(x) / 2),
8407 -- or a conditioning of one: the scalar evaluator, exact over the
8408 -- possible worlds of few inputs, sampled otherwise
8409 RETURN rv_moment((token)::UUID, k, false, prov);
8410 ELSE
8411 RAISE EXCEPTION USING MESSAGE='Wrong gate type for agg_raw_moment computation',
8412 DETAIL = 'provsql-reason: moment-not-an-aggregate; scope: deliberate';
8413 END IF;
8414 END IF;
8415 IF k = 0 THEN
8416 RETURN 1;
8417 END IF;
8418
8419 SELECT pp.proname::varchar FROM pg_proc pp
8420 WHERE oid=(get_infos(token)).info1
8421 INTO aggregation_function;
8422
8423 child_pairs := get_children(token);
8424 n := COALESCE(array_length(child_pairs, 1), 0);
8425
8426 -- A contribution whose value is itself an aggregate (an avg of a count, a
8427 -- max of a sum, an aggregate of an arithmetic expression over aggregates:
8428 -- provenance_semimod_nested) takes one value per possible world, not a
8429 -- constant read off its gate, so none of the closed forms below applies to
8430 -- it. The scalar evaluator reads such a value in every world.
8431 IF EXISTS (SELECT 1 FROM unnest(child_pairs) AS c
8432 WHERE get_gate_type((get_children(c))[2]) <> 'value') THEN
8433 RETURN rv_moment((token)::UUID, k, false, prov);
8434 END IF;
8435
8436 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
8437 -- count(*) and count(col) both keep the COUNT identity at the gate level,
8438 -- their value being the SUM of per-row 1 / 0-or-1 indicators, so their
8439 -- moments are computed exactly like SUM.
8440 --
8441 -- The value exists only where a contributing row does: SUM over no row is
8442 -- SQL NULL, and a grouped aggregation has no row there at all. So the
8443 -- moment conditions on that event, as the MIN / MAX arms do, and
8444 -- @c expected(sum(x)) of a group equals @c expected(sum(x), provenance()).
8445 -- A scalar COUNT is the exception: its row is always there and counts a
8446 -- real 0 over no row.
8447 is_scalar := (get_infos(token)).info2 < 0; -- the scalar flag, high bit
8448
8449 -- Extract per-child token + value arrays.
8450 vals := ARRAY[]::float8[];
8451 toks := ARRAY[]::UUID[];
8452 FOR i IN 1..n LOOP
8453 pair_children := get_children(child_pairs[i]);
8454 toks := toks || pair_children[1];
8455 vals := vals || CAST(get_extra(pair_children[2]) AS float8);
8456 END LOOP;
8457 defined_tok := CASE
8458 WHEN aggregation_function = 'count' AND is_scalar THEN gate_one()
8459 WHEN n = 0 THEN gate_zero()
8460 ELSE provenance_plus(toks) END;
8461
8462 IF n = 0 THEN
8463 -- No contributing row at all: a real 0 for a scalar COUNT, SQL NULL
8464 -- (never defined) otherwise.
8465 RETURN CASE WHEN defined_tok = gate_one() THEN 0 ELSE NULL END;
8466 END IF;
8467
8468 -- Collapsed fast path: a correlated COUNT / SUM whose per-row selection
8469 -- events share a single continuous latent has an O(G·n) 1-D quadrature,
8470 -- vastly cheaper than the O(n^k) tuple enumeration below (which is the
8471 -- O(n^2) pair-probability bottleneck for the variance). Only fires
8472 -- unconditionally (prov = one) and for k in {1, 2}; agg_collapsed_moment
8473 -- returns NULL when the shared-latent pattern does not match, and we
8474 -- fall through to the exact enumeration. Its moment counts the worlds
8475 -- without a row as 0, which contribute nothing, so it only needs the
8476 -- conditional normalisation below.
8477 IF prov = gate_one() AND k <= 2 THEN
8478 total := agg_collapsed_moment((token)::UUID, k);
8479 IF total IS NOT NULL THEN
8480 IF defined_tok = gate_one() THEN
8481 RETURN total;
8482 END IF;
8483 prob := probability_evaluate(defined_tok, method, arguments);
8484 IF prob IS NULL OR prob <= 0 THEN
8485 RETURN NULL;
8486 END IF;
8487 RETURN total / prob;
8488 END IF;
8489 END IF;
8490
8491 -- Enumerate all k-tuples (i_1, ..., i_k) in {1..n}^k. tup is the
8492 -- current tuple; we step through them in lexicographic order.
8493 total := 0;
8494 tup := array_fill(1, ARRAY[k]);
8495 LOOP
8496 prod_v := 1;
8497 FOR j IN 1..k LOOP
8498 prod_v := prod_v * vals[tup[j]];
8499 END LOOP;
8500
8501 SELECT array_agg(DISTINCT toks[idx]) INTO distinct_tok
8502 FROM unnest(tup) AS idx;
8503
8504 IF prov <> gate_one() THEN
8505 distinct_tok := distinct_tok || prov;
8506 END IF;
8507 conj_token := provenance_times(VARIADIC distinct_tok);
8508 prob := probability_evaluate(conj_token, method, arguments);
8509
8510 total := total + prod_v * prob;
8511
8512 d := k;
8513 WHILE d >= 1 AND tup[d] = n LOOP
8514 tup[d] := 1;
8515 d := d - 1;
8516 END LOOP;
8517 EXIT WHEN d = 0;
8518 tup[d] := tup[d] + 1;
8519 END LOOP;
8520
8521 -- Conditional on the value existing (and on prov): every k-tuple of the
8522 -- sum above names a row, so the worlds without one contribute 0 and only
8523 -- the normalisation is left.
8524 IF defined_tok <> gate_one() THEN
8525 IF prov <> gate_one() THEN
8526 defined_tok := provenance_times(prov, defined_tok);
8527 END IF;
8528 prob := probability_evaluate(defined_tok, method, arguments);
8529 IF prob IS NULL OR prob <= 0 THEN
8530 RETURN NULL; -- never defined: SQL NULL
8531 END IF;
8532 RETURN total / prob; -- already conditional; skip the generic norm
8533 END IF;
8534 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
8535 -- Rank enumeration: per distinct value v, P(MIN = v) is the
8536 -- probability that some t_i with v_i=v is true and all t_j with
8537 -- smaller v are false. For MAX we negate values so the same
8538 -- "smaller-than" rank logic computes MIN-of-negated, then flip.
8539 -- The outer multiplier picks up the right sign for the k-th moment
8540 -- of MAX: E[MAX^k] = (-1)^k * E[MIN(-v)^k], so sign_max = (-1)^k.
8541 sign_max := CASE
8542 WHEN aggregation_function = 'max'
8543 THEN power(-1::float8, k)
8544 ELSE 1
8545 END;
8546
8547 -- MIN/MAX over the empty input world are NULL (no elements), not ±Infinity:
8548 -- SQL returns one row with a NULL value. The moment is therefore CONDITIONAL
8549 -- on the aggregate being defined (non-empty) -- the empty world is excluded
8550 -- and the result renormalised by P(prov AND non-empty). (count, whose empty
8551 -- value 0 is a real value, keeps the empty world; sum keeps it too, as 0.)
8552 IF n = 0 THEN
8553 RETURN NULL; -- structurally empty: MIN/MAX undefined
8554 END IF;
8555
8556 -- Numerator E[MIN^k . 1{prov AND non-empty}] (the rank sum naturally omits
8557 -- the empty world, since every term requires a present token).
8558 WITH tok_value AS (
8559 SELECT (get_children(c))[1] AS tok,
8560 (CASE WHEN aggregation_function='max' THEN -1 ELSE 1 END)
8561 * CAST(get_extra((get_children(c))[2]) AS DOUBLE PRECISION) AS v
8562 FROM UNNEST(child_pairs) AS c
8563 ) SELECT sign_max * COALESCE(SUM(p * power(v, k)), 0) FROM (
8564 SELECT t1.v AS v,
8565 probability_evaluate(
8566 CASE WHEN prov = gate_one()
8567 THEN provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
8568 provenance_plus(ARRAY_AGG(t2.tok)))
8569 ELSE provenance_times(prov,
8570 provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
8571 provenance_plus(ARRAY_AGG(t2.tok)))) END,
8572 method, arguments) AS p
8573 FROM tok_value t1 LEFT OUTER JOIN tok_value t2 ON t1.v > t2.v
8574 GROUP BY t1.v) tmp
8575 INTO total;
8576
8577 -- Denominator P(prov AND non-empty) = P(prov (x) (+) tokens).
8578 SELECT probability_evaluate(
8579 CASE WHEN prov = gate_one()
8580 THEN provenance_plus(ARRAY_AGG(tok))
8581 ELSE provenance_times(prov, provenance_plus(ARRAY_AGG(tok))) END,
8582 method, arguments)
8583 FROM (SELECT (get_children(c))[1] AS tok FROM UNNEST(child_pairs) AS c) s
8584 INTO total_probability;
8585
8586 IF total_probability <= 0 THEN
8587 RETURN NULL; -- never defined under prov: MIN/MAX undefined
8588 END IF;
8589 RETURN total / total_probability; -- already conditional; skip generic norm
8590 ELSIF aggregation_function = 'avg' THEN
8591 -- AVG = SUM/COUNT is a ratio of two correlated world-dependent
8592 -- quantities, so the k-tuple expansion above does not apply. Like
8593 -- MIN/MAX, AVG over the empty world is NULL, so its moment conditions
8594 -- on the aggregate being defined (COUNT >= 1), NULL when it never is.
8595 -- Two routes:
8596 -- * EXACT (independent rows, unconditional): the joint (sum, count)
8597 -- PMF folded in C by agg_avg_moment_exact --
8598 -- E[AVG^k | COUNT>=1] = Σ_{(s,c), c>=1} (s/c)^k pmf(s,c) / P(c>=1).
8599 -- * Monte-Carlo scalar fallback otherwise (an outer conditioning
8600 -- event, shared leaves, compound contributors): rv_moment samples
8601 -- the agg gate per world; its NaN-skip on empty draws implements
8602 -- the same conditional-on-defined convention, at the
8603 -- provsql.rv_mc_samples budget (0 raises, per convention).
8604 IF n = 0 THEN
8605 RETURN NULL; -- structurally empty: AVG undefined
8606 END IF;
8607 -- Conditioning on an event that AVG being defined implies -- some row
8608 -- among a set holding AVG's rows present, as provenance() of a GROUP BY
8609 -- row is, the group possibly having rows whose value is NULL -- is what
8610 -- the moment already does: the exact route applies.
8611 IF prov <> gate_one() THEN
8612 DECLARE
8613 inner_prov UUID := prov;
8614 prov_toks UUID[];
8615 agg_toks UUID[];
8616 BEGIN
8617 IF get_gate_type(inner_prov) = 'delta' THEN
8618 inner_prov := (get_children(inner_prov))[1];
8619 END IF;
8620 IF get_gate_type(inner_prov) = 'plus' THEN
8621 prov_toks := get_children(inner_prov);
8622 ELSE
8623 prov_toks := ARRAY[inner_prov];
8624 END IF;
8625 SELECT array_agg((get_children(c))[1]) INTO agg_toks
8626 FROM unnest(child_pairs) AS c;
8627 IF agg_toks <@ prov_toks THEN
8628 prov := gate_one();
8629 END IF;
8630 END;
8631 END IF;
8632 IF prov = gate_one() THEN
8633 total := agg_avg_moment_exact((token)::UUID, k);
8634 IF total IS NOT NULL THEN
8635 RETURN total;
8636 END IF;
8637 END IF;
8638 RETURN rv_moment((token)::UUID, k, false, prov);
8639 ELSE
8640 RAISE EXCEPTION USING MESSAGE=
8641 'Cannot compute moment for aggregation function ' || aggregation_function,
8642 DETAIL = 'provsql-reason: moment-aggregate-kind; scope: gap';
8643 END IF;
8644
8645 -- Conditional normalisation: E[X^k · 1_A] / P(A) = E[X^k | A].
8646 IF prov <> gate_one()
8647 AND total <> 0
8648 AND total <> 'Infinity'::float8
8649 AND total <> '-Infinity'::float8 THEN
8650 total := total / probability_evaluate(prov, method, arguments);
8651 END IF;
8652
8653 RETURN total;
8654END
8655$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
8656
8657/**
8658 * @brief Compute the variance Var[X | prov] of a probabilistic scalar
8659 *
8660 * Polymorphic dispatcher that mirrors @c expected: @c random_variable
8661 * inputs go through the analytical / MC evaluator
8662 * (@c rv_moment(UUID, 2, true)); @c AGG_TOKEN inputs go through the
8663 * @c agg_raw_moment helper, computing
8664 * @f$\mathrm{Var}[X|A] = E[X^2|A] - E[X|A]^2@f$. Conditioning on
8665 * @c prov is supported for @c AGG_TOKEN (matching @c expected) but
8666 * not yet for @c random_variable.
8667 */
8668CREATE OR REPLACE FUNCTION variance(
8669 input ANYELEMENT,
8670 prov UUID = gate_one(),
8671 method TEXT = NULL,
8672 arguments TEXT = NULL)
8673 RETURNS DOUBLE PRECISION AS $$
8674DECLARE
8675 m1 float8;
8676 m2 float8;
8677BEGIN
8678 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
8679 IF input IS NULL THEN
8680 RETURN NULL;
8681 END IF;
8682 -- Conditioning on prov is handled inside rv_moment: when prov
8683 -- resolves to gate_one() (the default, or load-time
8684 -- simplification of any always-true sub-circuit) the
8685 -- unconditional analytical path runs unchanged; otherwise the
8686 -- joint-circuit loader unifies shared gate_rv leaves between
8687 -- input and prov, and the conditional path runs either
8688 -- truncated-distribution closed form or MC rejection.
8689 RETURN provsql.rv_moment(
8690 rv_conditioned_target((input::random_variable)::UUID), 2, true,
8691 rv_conditioned_prov((input::random_variable)::UUID, prov));
8692 END IF;
8693
8694 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
8695 IF input IS NULL THEN
8696 RETURN NULL;
8697 END IF;
8698 -- Collapsed fast path: E[C] and E[C^2] from a single circuit load and plan
8699 -- build, instead of two agg_raw_moment() calls that each reload. Mirrors
8700 -- the guard in agg_raw_moment (unconditional only, prov = one); on any
8701 -- mismatch agg_collapsed_moments returns NULL and we fall through to the
8702 -- generic per-order path (which handles conditioning, SUM enumeration, ...).
8703 IF rv_conditioned_prov(input::UUID, prov) = gate_one() THEN
8704 DECLARE ms float8[];
8705 BEGIN
8706 ms := agg_collapsed_moments(
8707 (agg_conditioned_target(input::AGG_TOKEN))::UUID);
8708 IF ms IS NOT NULL THEN
8709 RETURN ms[2] - ms[1] * ms[1];
8710 END IF;
8711 END;
8712 END IF;
8713 m1 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
8714 rv_conditioned_prov(input::UUID, prov), method, arguments);
8715 m2 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 2,
8716 rv_conditioned_prov(input::UUID, prov), method, arguments);
8717 IF m1 IS NULL OR m2 IS NULL THEN
8718 RETURN NULL;
8719 END IF;
8720 RETURN m2 - m1 * m1;
8721 END IF;
8722
8723 -- Bernoulli event token (see moment()): Var[X] = p(1 - p).
8724 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
8725 IF input IS NULL THEN
8726 RETURN NULL;
8727 END IF;
8728 m1 := provsql.probability_evaluate(provsql.cond(input::UUID, prov),
8729 method, arguments);
8730 RETURN m1 * (1 - m1);
8731 END IF;
8732
8733 RAISE EXCEPTION 'variance() is not yet supported for input type %', pg_typeof(input)
8734 USING ERRCODE = 'feature_not_supported',
8735 DETAIL = 'provsql-reason: moment-input-type; scope: gap';
8736END
8737$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
8738
8739/**
8740 * @brief Compute the raw moment E[X^k | prov] of a probabilistic scalar
8741 *
8742 * @c k must be a non-negative INTEGER. @c k = 0 returns 1; @c k = 1
8743 * is equivalent to @c expected(input). Polymorphic dispatcher: routes
8744 * @c random_variable through @c rv_moment (analytical / MC) and
8745 * @c AGG_TOKEN through @c agg_raw_moment (SUM via tuple enumeration,
8746 * MIN / MAX via rank enumeration, AVG via the joint (sum, count)
8747 * distribution over independent / laminar rows with a Monte-Carlo
8748 * fallback).
8749 */
8750CREATE OR REPLACE FUNCTION moment(
8751 input ANYELEMENT,
8752 k INTEGER,
8753 prov UUID = gate_one(),
8754 method TEXT = NULL,
8755 arguments TEXT = NULL)
8756 RETURNS DOUBLE PRECISION AS $$
8757BEGIN
8758 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
8759 IF input IS NULL OR k IS NULL THEN
8760 RETURN NULL;
8761 END IF;
8762 -- See variance() above: rv_moment handles the conditional/unconditional
8763 -- dispatch internally based on the resolved prov gate type.
8764 RETURN provsql.rv_moment(
8765 rv_conditioned_target((input::random_variable)::UUID), k, false,
8766 rv_conditioned_prov((input::random_variable)::UUID, prov));
8767 END IF;
8768
8769 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
8770 RETURN agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), k,
8771 rv_conditioned_prov(input::UUID, prov), method, arguments);
8772 END IF;
8773
8774 -- A bare provenance event token (a gate_cmp lifted from an RV comparison,
8775 -- e.g. expected(x <= c)) is a Bernoulli indicator: X in {0,1}, so every raw
8776 -- moment E[X^k] with k >= 1 equals P(event), and E[X^0] = 1. cond() applies
8777 -- the optional conditioning prov (a no-op for the default gate_one()).
8778 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
8779 IF input IS NULL OR k IS NULL THEN
8780 RETURN NULL;
8781 END IF;
8782 IF k = 0 THEN
8783 RETURN 1;
8784 END IF;
8785 RETURN provsql.probability_evaluate(provsql.cond(input::UUID, prov),
8786 method, arguments);
8787 END IF;
8788
8789 RAISE EXCEPTION 'moment() is not yet supported for input type %', pg_typeof(input)
8790 USING ERRCODE = 'feature_not_supported',
8791 DETAIL = 'provsql-reason: moment-input-type; scope: gap';
8792END
8793$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
8794
8795/**
8796 * @brief Internal: rv-side quantile computation.
8797 *
8798 * C entry point behind the polymorphic @c quantile dispatcher.
8799 * Closed-form inverse CDF where the family has one (Normal via
8800 * Beasley-Springer-Moro polished by Newton steps, Uniform and
8801 * Exponential by algebraic inversion), generic monotone-CDF bisection
8802 * otherwise (Erlang, Gamma), exact generalised inverse for categorical
8803 * mixtures, and the empirical Monte Carlo quantile for compound scalar
8804 * circuits. A non-trivial @p prov conditions (truncates) the
8805 * distribution first, in closed form when the event reduces to an
8806 * interval on a bare @c gate_rv.
8807 */
8808CREATE OR REPLACE FUNCTION rv_quantile(
8809 token UUID, p double precision,
8810 prov UUID DEFAULT gate_one())
8811 RETURNS double precision
8812 AS 'provsql','rv_quantile' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8813
8814/**
8815 * @brief Compute the p-quantile (inverse CDF) of a probabilistic scalar
8816 *
8817 * @f$F^{-1}(p) = \min\{x : P(X \le x) \ge p\}@f$ for @f$p \in [0,1]@f$:
8818 * medians (@c p = 0.5), percentiles, Value-at-Risk, and credible
8819 * intervals. @c p = 0 / @c p = 1 return the (possibly infinite)
8820 * support edges. Polymorphic dispatcher mirroring @c expected /
8821 * @c moment: @c random_variable routes through @c rv_quantile
8822 * (analytical inverse CDF / MC), plain numerics are their own quantile
8823 * (a Dirac's inverse CDF is constant), and the optional @p prov
8824 * argument conditions on a provenance event, e.g.
8825 * <tt>quantile(x | (x > 0), 0.5)</tt> for the median of a truncated
8826 * distribution.
8827 */
8828CREATE OR REPLACE FUNCTION quantile(
8829 input ANYELEMENT,
8830 p double precision,
8831 prov UUID = gate_one(),
8832 method TEXT = NULL,
8833 arguments TEXT = NULL)
8834 RETURNS DOUBLE PRECISION AS $$
8835BEGIN
8836 IF p IS NULL THEN
8837 RETURN NULL;
8838 END IF;
8839 IF p <> p OR p < 0 OR p > 1 THEN
8840 RAISE EXCEPTION 'quantile: p must be in [0, 1] (got %)', p;
8841 END IF;
8842
8843 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
8844 IF input IS NULL THEN
8845 RETURN NULL;
8846 END IF;
8847 -- See variance(): rv_quantile handles the conditional/unconditional
8848 -- dispatch internally based on the resolved prov gate type.
8849 RETURN provsql.rv_quantile(
8850 rv_conditioned_target((input::random_variable)::UUID), p,
8851 rv_conditioned_prov((input::random_variable)::UUID, prov));
8852 END IF;
8853
8854 IF pg_typeof(input) IN ('smallint'::REGTYPE, 'INTEGER'::REGTYPE,
8855 'bigint'::REGTYPE, 'NUMERIC'::REGTYPE,
8856 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
8857 -- A deterministic scalar is a Dirac: every quantile is the value.
8858 RETURN input::double precision;
8859 END IF;
8860
8861 RAISE EXCEPTION 'quantile() is not yet supported for input type %', pg_typeof(input)
8862 USING ERRCODE = 'feature_not_supported',
8863 DETAIL = 'provsql-reason: quantile-input-type; scope: gap';
8864END
8865$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
8866
8867/**
8868 * @brief Internal: rv-side support computation
8869 *
8870 * Lifts @c provsql.compute_support out of @c RangeCheck.cpp -- the
8871 * same interval-arithmetic propagation @c runRangeCheck uses to
8872 * decide @c gate_cmps. Returns @c [-Infinity, +Infinity] when the
8873 * tightest bound is the conservative all-real interval (e.g. for a
8874 * normal RV, or any sub-circuit that mixes a normal in).
8875 */
8876CREATE OR REPLACE FUNCTION rv_support(
8877 token UUID, prov UUID DEFAULT gate_one(),
8878 OUT lo float8, OUT hi float8)
8879 AS 'provsql','rv_support' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
8880
8881/**
8882 * @brief Compute the support interval @c [lo, hi] of a probabilistic
8883 * (or deterministic) scalar
8884 *
8885 * Polymorphic dispatcher mirroring @c expected / @c variance /
8886 * @c moment / @c central_moment, with two extra "free" branches:
8887 *
8888 * - <b>Plain NUMERIC</b> (@c smallint / @c INTEGER / @c bigint /
8889 * @c NUMERIC / @c real / @c double @c precision): degenerate
8890 * point support @f$[c, c]@f$. Lets callers ask for the support
8891 * of a literal without round-tripping through @c as_random.
8892 * - <b>@c random_variable / bare @c UUID</b> (any provenance gate
8893 * token; the @c random_variable branch reinterprets the value via
8894 * the binary-coercible @c random_variable @c -> @c UUID cast):
8895 * routes to @c rv_support, which propagates distribution
8896 * supports (uniform exact, exponential @c [0,+∞), normal
8897 * @c (-∞,+∞)) through @c gate_arith via interval arithmetic.
8898 * @c gate_value gives the same @f$[c, c]@f$ point support as the
8899 * NUMERIC branch; any non-scalar gate (Boolean gates, aggregates,
8900 * ...) safely falls back to the conservative all-real interval
8901 * without raising. Conditioning on @c prov is not yet supported.
8902 *
8903 * - @c AGG_TOKEN: closed-form per aggregation function:
8904 * - @c SUM : @f$[\sum_i \min(0,v_i), \sum_i \max(0,v_i)]@f$
8905 * (every row is independently in or out of the included set; the
8906 * extreme SUMs are reached by including only positive or only
8907 * negative-valued rows).
8908 * - @c MIN : @f$[\min_i v_i, \max_i v_i]@f$ in the non-empty
8909 * subsets, plus @c +Infinity if the empty subset has positive
8910 * probability under @c prov.
8911 * - @c MAX : symmetric -- @c -Infinity if empty has positive
8912 * probability under @c prov, otherwise @c min_i v_i; @c hi is
8913 * always @c max_i v_i.
8914 *
8915 * Other aggregation functions raise.
8916 *
8917 * Returns the composite RECORD @c (lo, hi) via the function's
8918 * @c OUT parameters, with @c -Infinity / @c +Infinity marking
8919 * unbounded ends.
8920 */
8921CREATE OR REPLACE FUNCTION support(
8922 input ANYELEMENT,
8923 prov UUID = gate_one(),
8924 method TEXT = NULL,
8925 arguments TEXT = NULL,
8926 OUT lo float8,
8927 OUT hi float8)
8928 AS $$
8929DECLARE
8930 aggregation_function VARCHAR;
8931 child_pairs UUID[];
8932 values_arr float8[];
8933 total_probability float8;
8934BEGIN
8935 IF input IS NULL THEN
8936 lo := NULL; hi := NULL; RETURN;
8937 END IF;
8938
8939 -- Plain NUMERIC: degenerate point support. Lets `support(2.5)` /
8940 -- `support(42)` / etc. return (2.5, 2.5) without making the user
8941 -- wrap in `as_random`.
8942 IF pg_typeof(input) IN (
8943 'smallint'::REGTYPE, 'INTEGER'::REGTYPE, 'bigint'::REGTYPE,
8944 'NUMERIC'::REGTYPE, 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
8945 lo := input::double precision;
8946 hi := input::double precision;
8947 RETURN;
8948 END IF;
8949
8950 -- random_variable is binary-coercible to UUID (explicit cast
8951 -- below), so a single rv_support call covers both shapes.
8952 -- rv_support handles
8953 -- gate_value (point), gate_rv (distribution), gate_arith
8954 -- (propagated), and falls back to the conservative all-real
8955 -- interval for any other gate kind. Conditioning on prov is not
8956 -- supported (would require restricting the underlying joint
8957 -- distribution by the indicator of prov, which has no closed form
8958 -- for the basic distributions we ship).
8959 IF pg_typeof(input) IN ('random_variable'::REGTYPE, 'UUID'::REGTYPE) THEN
8960 -- Conditional support: rv_support folds the AND-conjunct interval
8961 -- constraints from prov into the unconditional support. When
8962 -- prov is gate_one() the unconditional support is returned
8963 -- unchanged.
8964 SELECT r.lo, r.hi INTO lo, hi
8965 FROM provsql.rv_support(
8966 rv_conditioned_target(input::UUID),
8967 rv_conditioned_prov(input::UUID, prov)) r;
8968 RETURN;
8969 END IF;
8970
8971 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
8972 -- A conditioned aggregate SUM(x)|C: the value-range support is that of
8973 -- the target aggregate (conditioning can only tighten it; the
8974 -- conservative range stays valid), so unpack to the target gate.
8975 DECLARE
8976 atok AGG_TOKEN := agg_conditioned_target(input::AGG_TOKEN);
8977 BEGIN
8978 IF get_gate_type(atok) <> 'agg' THEN
8979 RAISE EXCEPTION USING MESSAGE='Wrong gate type for support computation',
8980 DETAIL = 'provsql-reason: support-not-an-aggregate; scope: deliberate';
8981 END IF;
8982 SELECT pp.proname::varchar FROM pg_proc pp
8983 WHERE oid=(get_infos(atok)).info1
8984 INTO aggregation_function;
8985 child_pairs := get_children(atok);
8986
8987 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
8988 -- count(col) is a SUM of per-row 0/1 indicators (empty group = 0), so its
8989 -- support is computed like SUM; count(*) arrives as 'sum'.
8990 -- Empty AGG_TOKEN: SUM is identically 0.
8991 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
8992 lo := 0; hi := 0; RETURN;
8993 END IF;
8994 SELECT sum(LEAST(v, 0::float8)), sum(GREATEST(v, 0::float8))
8995 INTO lo, hi
8996 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
8997 FROM unnest(child_pairs) AS c) sub;
8998 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
8999 -- MIN/MAX over the empty input world are NULL, not ±Infinity (matching the
9000 -- moment surface): the empty world carries no value, so the support is just
9001 -- the range of the per-row values [min(v), max(v)]. A structurally empty
9002 -- aggregate has no defined value at all -> NULL support.
9003 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
9004 lo := NULL; hi := NULL; RETURN;
9005 END IF;
9006
9007 SELECT min(v), max(v)
9008 INTO lo, hi
9009 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
9010 FROM UNNEST(child_pairs) AS c) sub;
9011 ELSE
9012 RAISE EXCEPTION USING MESSAGE=
9013 'Cannot compute support for aggregation function ' || aggregation_function,
9014 DETAIL = 'provsql-reason: support-aggregate-kind; scope: gap';
9015 END IF;
9016 RETURN;
9017 END;
9018 END IF;
9019
9020 RAISE EXCEPTION 'support() is not yet supported for input type %', pg_typeof(input)
9021 USING ERRCODE = 'feature_not_supported',
9022 DETAIL = 'provsql-reason: support-input-type; scope: gap';
9023END
9024$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
9025
9026/**
9027 * @brief Compute the central moment E[(X - E[X|prov])^k | prov]
9028 *
9029 * @c k = 0 returns 1; @c k = 1 returns 0; @c k = 2 is equivalent to
9030 * @c variance(input, prov, ...). Polymorphic dispatcher: routes
9031 * @c random_variable through @c rv_moment, and @c AGG_TOKEN through
9032 * the binomial expansion
9033 * @f$E[(X-\mu)^k|A] = \sum_{i=0}^{k} \binom{k}{i} (-\mu)^{k-i} E[X^i|A]@f$
9034 * with @f$\mu = E[X|A]@f$, where each @f$E[X^i|A]@f$ comes from
9035 * @c agg_raw_moment.
9036 */
9037CREATE OR REPLACE FUNCTION central_moment(
9038 input ANYELEMENT,
9039 k INTEGER,
9040 prov UUID = gate_one(),
9041 method TEXT = NULL,
9042 arguments TEXT = NULL)
9043 RETURNS DOUBLE PRECISION AS $$
9044DECLARE
9045 mu float8;
9046 total float8;
9047 i INTEGER;
9048 raw_i float8;
9049 binom float8;
9050 -- iterative binomial coefficient C(k, i)
9051 k_double float8;
9052BEGIN
9053 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
9054 IF input IS NULL OR k IS NULL THEN
9055 RETURN NULL;
9056 END IF;
9057 -- See variance() above: rv_moment handles the conditional/unconditional
9058 -- dispatch internally based on the resolved prov gate type.
9059 RETURN provsql.rv_moment(
9060 rv_conditioned_target((input::random_variable)::UUID), k, true,
9061 rv_conditioned_prov((input::random_variable)::UUID, prov));
9062 END IF;
9063
9064 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
9065 IF input IS NULL OR k IS NULL THEN
9066 RETURN NULL;
9067 END IF;
9068 IF k < 0 THEN
9069 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
9070 END IF;
9071 IF k = 0 THEN RETURN 1; END IF;
9072 IF k = 1 THEN RETURN 0; END IF;
9073
9074 mu := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
9075 rv_conditioned_prov(input::UUID, prov), method, arguments);
9076 IF mu IS NULL THEN RETURN NULL; END IF;
9077 -- mu may be ±Infinity for empty MIN / MAX with positive empty
9078 -- probability; central_moment is undefined in that case.
9079 IF mu = 'Infinity'::float8 OR mu = '-Infinity'::float8 THEN
9080 RETURN mu;
9081 END IF;
9082
9083 total := 0;
9084 binom := 1; -- C(k, 0)
9085 k_double := k;
9086 FOR i IN 0..k LOOP
9087 raw_i := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), i,
9088 rv_conditioned_prov(input::UUID, prov), method, arguments);
9089 IF raw_i IS NULL THEN RETURN NULL; END IF;
9090 total := total + binom * power(-mu, k - i) * raw_i;
9091 -- C(k, i+1) = C(k, i) * (k - i) / (i + 1)
9092 IF i < k THEN
9093 binom := binom * (k_double - i) / (i + 1);
9094 END IF;
9095 END LOOP;
9096 RETURN total;
9097 END IF;
9098
9099 -- Bernoulli event token (see moment()): with p = P(event),
9100 -- E[(X-p)^k] = (1-p)(-p)^k + p(1-p)^k; k = 0 -> 1, k = 1 -> 0.
9101 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
9102 IF input IS NULL OR k IS NULL THEN
9103 RETURN NULL;
9104 END IF;
9105 IF k < 0 THEN
9106 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
9107 END IF;
9108 IF k = 0 THEN RETURN 1; END IF;
9109 IF k = 1 THEN RETURN 0; END IF;
9110 mu := provsql.probability_evaluate(provsql.cond(input::UUID, prov),
9111 method, arguments);
9112 RETURN (1 - mu) * power(-mu, k) + mu * power(1 - mu, k);
9113 END IF;
9114
9115 RAISE EXCEPTION 'central_moment() is not yet supported for input type %', pg_typeof(input)
9116 USING ERRCODE = 'feature_not_supported',
9117 DETAIL = 'provsql-reason: moment-input-type; scope: gap';
9118END
9119$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
9120
9121/** @brief C entry point behind @ref covariance (UUID-level binding). */
9122CREATE OR REPLACE FUNCTION rv_covariance(x UUID, y UUID, prov UUID)
9123 RETURNS double precision
9124 AS 'provsql','rv_covariance' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
9125
9126/**
9127 * @brief Covariance Cov(X, Y) = E[XY] − E[X]·E[Y] of two random variables.
9128 *
9129 * The bivariate readout complementing the univariate moment surface
9130 * (@ref expected / @ref variance / @ref moment / @ref central_moment).
9131 * Exact tiers: an exact @c 0 when the two arguments' stochastic-leaf
9132 * footprints are structurally independent (given @p prov), a variance
9133 * readout when the two arguments coincide, and the closed-form
9134 * @c E[XY] − E[X]·E[Y] whenever every factor decomposes analytically.
9135 * When some factor has no closed form, a SINGLE coupled Monte-Carlo pass
9136 * over the joint circuit draws @c (x, y) pairs (shared leaves produce one
9137 * draw both observe) and returns the sample covariance -- the estimator's
9138 * noise then scales with the covariance signal itself, not with the
9139 * product of the means as the naive three-run E[XY] − E[X]·E[Y]
9140 * subtraction would.
9141 *
9142 * @param x the first random variable.
9143 * @param y the second random variable.
9144 * @param prov optional conditioning event (a provenance @c UUID); the
9145 * default @c gate_one() is the unconditional covariance. Conditioning
9146 * is applied jointly: the Monte-Carlo pass rejection-samples the pair on
9147 * @p prov, giving @c Cov(X, Y | prov).
9148 */
9149CREATE OR REPLACE FUNCTION covariance(
9150 x random_variable, y random_variable, prov UUID DEFAULT gate_one())
9151 RETURNS double precision AS $$
9152 SELECT provsql.rv_covariance((x)::UUID, (y)::UUID, prov);
9153$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
9154
9155/**
9156 * @brief Standard deviation σ(X) = √Var(X) of a random variable.
9157 *
9158 * A thin NUMERIC readout over @ref variance. The square root is taken on
9159 * the scalar @c double result, so no RV-level @c sqrt is involved and this
9160 * carries no dependency on RV function application (@c pow / @c sqrt).
9161 * @c NULL propagates from a @c NULL input; the order-2 central moment is
9162 * non-negative by construction, so the root is always real.
9163 *
9164 * @param x the random variable.
9165 * @param prov optional conditioning event; default @c gate_one()
9166 * (unconditional).
9167 */
9168CREATE OR REPLACE FUNCTION stddev(
9169 x random_variable, prov UUID DEFAULT gate_one())
9170 RETURNS double precision AS $$
9171 SELECT sqrt(provsql.variance(x, prov));
9172$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
9173
9174/** @brief C entry point behind @ref correlation (UUID-level binding). */
9175CREATE OR REPLACE FUNCTION rv_correlation(x UUID, y UUID, prov UUID)
9176 RETURNS double precision
9177 AS 'provsql','rv_correlation' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
9178
9179/**
9180 * @brief Pearson correlation ρ(X, Y) = Cov(X, Y) / (σ(X)·σ(Y)).
9181 *
9182 * Same exact tiers as @ref covariance; on the Monte-Carlo path the
9183 * covariance and BOTH standard deviations are read off the same coupled
9184 * pass, instead of stacking five independent estimates (three for the
9185 * covariance, one per standard deviation). Returns @c NULL when either
9186 * standard deviation is @c 0 (a degenerate / constant variable, for which
9187 * correlation is undefined) rather than raising a division-by-zero.
9188 *
9189 * @param x the first random variable.
9190 * @param y the second random variable.
9191 * @param prov optional conditioning event; default @c gate_one()
9192 * (unconditional).
9193 */
9194CREATE OR REPLACE FUNCTION correlation(
9195 x random_variable, y random_variable, prov UUID DEFAULT gate_one())
9196 RETURNS double precision AS $$
9197 SELECT provsql.rv_correlation((x)::UUID, (y)::UUID, prov);
9198$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
9199
9200/** @brief C entry point behind @ref entropy (UUID-level binding). */
9201CREATE OR REPLACE FUNCTION rv_entropy(token UUID, prov UUID)
9202 RETURNS double precision
9203 AS 'provsql','rv_entropy' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
9204
9205/**
9206 * @brief Entropy H(X) of a random variable, in nats.
9207 *
9208 * Shannon entropy for a discrete distribution (a categorical / discrete
9209 * count / constant -- a point mass has entropy @c 0), differential
9210 * entropy for a continuous one (quadrature of @c -f ln f over the
9211 * family's integration range; also exact through independent-arm
9212 * Bernoulli mixture trees such as @ref gmm's). Shapes with no
9213 * closed density (arithmetic composites) and the conditional form fall
9214 * back to a Monte Carlo histogram plug-in estimate at the
9215 * @c provsql.rv_mc_samples budget.
9216 *
9217 * @param x the random variable.
9218 * @param prov optional conditioning event; default @c gate_one()
9219 * (unconditional).
9220 */
9221CREATE OR REPLACE FUNCTION entropy(
9222 x random_variable, prov UUID DEFAULT gate_one())
9223 RETURNS double precision AS $$
9224 SELECT provsql.rv_entropy((x)::UUID, prov);
9225$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
9226
9227/** @brief C entry point behind @ref kl (UUID-level binding). */
9228CREATE OR REPLACE FUNCTION rv_kl(p UUID, q UUID)
9229 RETURNS double precision
9230 AS 'provsql','rv_kl' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
9231
9232/**
9233 * @brief Kullback-Leibler divergence KL(P || Q), in nats.
9234 *
9235 * Exact: the defining sum for two discrete distributions (matching
9236 * outcomes by value) and the defining integral (quadrature over P's
9237 * integration window) for two continuous ones, including
9238 * independent-arm mixture trees. Returns @c Infinity when P is not
9239 * absolutely continuous with respect to Q -- an outcome of P that Q
9240 * gives zero mass, mismatched kinds (discrete vs continuous), or a
9241 * region of P's support where Q's density (under)flows to zero. Both
9242 * arguments must resolve to closed-form densities; arithmetic
9243 * composites and conditioned variables raise.
9244 */
9245CREATE OR REPLACE FUNCTION kl(p random_variable, q random_variable)
9246 RETURNS double precision AS $$
9247 SELECT provsql.rv_kl((p)::UUID, (q)::UUID);
9248$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
9249
9250/** @brief C entry point behind @ref mutual_information (UUID-level
9251 * binding). */
9252CREATE OR REPLACE FUNCTION rv_mutual_information(x UUID, y UUID)
9253 RETURNS double precision
9254 AS 'provsql','rv_mutual_information' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
9255
9256/**
9257 * @brief Mutual information I(X; Y), in nats.
9258 *
9259 * Exactly @c 0 for structurally independent variables (disjoint
9260 * stochastic-leaf footprints, the same test the moment evaluators use);
9261 * @c H(X) for a discrete variable paired with itself and @c Infinity
9262 * for a continuous one (I(X;X) diverges). A genuinely correlated pair
9263 * (shared leaves) is estimated by a 2-D histogram plug-in over coupled
9264 * joint Monte Carlo draws -- both roots evaluated against the same
9265 * per-iteration cache, so shared leaves keep their joint law -- at the
9266 * @c provsql.rv_mc_samples budget.
9267 */
9268CREATE OR REPLACE FUNCTION mutual_information(
9269 x random_variable, y random_variable)
9270 RETURNS double precision AS $$
9271 SELECT provsql.rv_mutual_information((x)::UUID, (y)::UUID);
9272$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
9273
9274/**
9275 * @brief Compute the Shapley value of an input variable
9276 *
9277 * Measures the contribution of a specific input variable to the
9278 * truth of a provenance expression, using game-theoretic Shapley values.
9279 *
9280 * @param token provenance token to evaluate
9281 * @param variable UUID of the input variable
9282 * @param method knowledge compilation method
9283 * @param arguments additional arguments for the method
9284 * @param banzhaf if true, compute the Banzhaf value instead
9285 */
9286CREATE OR REPLACE FUNCTION shapley(
9287 token UUID,
9288 variable UUID,
9289 method TEXT = NULL,
9290 arguments TEXT = NULL,
9291 banzhaf BOOLEAN = 'f')
9292 RETURNS DOUBLE PRECISION AS
9293 'provsql','shapley' LANGUAGE C STABLE;
9294
9295/** @brief Compute Shapley values for all input variables at once */
9296CREATE OR REPLACE FUNCTION shapley_all_vars(
9297 IN token UUID,
9298 IN method TEXT = NULL,
9299 IN arguments TEXT = NULL,
9300 IN banzhaf BOOLEAN = 'f',
9301 OUT variable UUID,
9302 OUT value DOUBLE PRECISION)
9303 RETURNS SETOF RECORD AS
9304 'provsql', 'shapley_all_vars'
9305 LANGUAGE C STABLE;
9306
9307/** @brief Compute the Banzhaf power index of an input variable */
9308CREATE OR REPLACE FUNCTION banzhaf(
9309 token UUID,
9310 variable UUID,
9311 method TEXT = NULL,
9312 arguments TEXT = NULL)
9313 RETURNS DOUBLE PRECISION AS
9314 $$ SELECT provsql.shapley(token, variable, method, arguments, 't') $$
9315 LANGUAGE SQL;
9316
9317/** @brief Compute Banzhaf power indices for all input variables at once */
9318CREATE OR REPLACE FUNCTION banzhaf_all_vars(
9319 IN token UUID,
9320 IN method TEXT = NULL,
9321 IN arguments TEXT = NULL,
9322 OUT variable UUID,
9323 OUT value DOUBLE PRECISION)
9324 RETURNS SETOF RECORD AS
9325 $$ SELECT * FROM provsql.shapley_all_vars(token, method, arguments, 't') $$
9326 LANGUAGE SQL;
9327
9328/**
9329 * @brief Exact reachability probability over bounded-treewidth data
9330 * (columnar form)
9331 *
9332 * Computes the probability that @p target is reachable from @p source in
9333 * the probabilistic graph given by the parallel edge arrays
9334 * (two-terminal network reliability). Unlike
9335 * @c probability_evaluate(), which compiles the provenance circuit
9336 * built along the relational query plan, this compiles the query
9337 * along a tree decomposition of the *data* graph (in the spirit of the
9338 * provenance refinement of Courcelle's theorem), producing a d-DNNF
9339 * whose size is linear in the number of edges for data of bounded
9340 * treewidth. Exact, and linear-time, on cyclic data as well -- where
9341 * the recursive-query fixpoint cannot terminate structurally.
9342 *
9343 * Edges are independent events. Two array positions may share a token
9344 * only if they are mutual reverses (the natural encoding of an
9345 * undirected edge in a directed edge relation); they are then treated
9346 * as a single bidirectional edge. This is an internal/testing surface:
9347 * the user-facing route is a plain @c WITH @c RECURSIVE reachability
9348 * query under the 'absorptive' (or 'BOOLEAN') provenance class, which
9349 * the query rewriter compiles through @c eval_reachability() /
9350 * @c reachability_materialize().
9351 *
9352 * @param sources source vertex of each edge (dense INTEGER IDs)
9353 * @param destinations destination vertex of each edge
9354 * @param tokens provenance token of each edge tuple
9355 * @param probabilities probability of each edge tuple
9356 * @param source the vertex reachability starts from
9357 * @param target the vertex whose reachability is evaluated
9358 * @param directed if false, each edge can be traversed both ways
9359 */
9360CREATE OR REPLACE FUNCTION reachability_evaluate(
9361 sources INT[],
9362 destinations INT[],
9363 tokens UUID[],
9364 probabilities DOUBLE PRECISION[],
9365 source INT,
9366 target INT,
9367 directed BOOLEAN)
9368 RETURNS DOUBLE PRECISION AS
9369 'provsql','reachability_evaluate' LANGUAGE C IMMUTABLE PARALLEL SAFE;
9370
9371/**
9372 * @brief Reachability probability plus compilation statistics
9373 * (columnar form)
9374 *
9375 * Same compilation as @c reachability_evaluate(), returning the
9376 * probability together with the structural statistics that
9377 * substantiate the bounded-treewidth guarantee: the treewidth of the
9378 * min-fill decomposition of the data graph, its number of bags, the
9379 * maximum number of dynamic-programming states at any decomposition
9380 * node, and the size of the emitted d-DNNF (linear in the number of
9381 * edges for fixed data treewidth).
9382 *
9383 * @param sources source vertex of each edge (dense INTEGER IDs)
9384 * @param destinations destination vertex of each edge
9385 * @param tokens provenance token of each edge tuple
9386 * @param probabilities probability of each edge tuple
9387 * @param source the vertex reachability starts from
9388 * @param target the vertex whose reachability is evaluated
9389 * @param directed if false, each edge can be traversed both ways
9390 * @param[out] probability the reachability probability
9391 * @param[out] data_treewidth treewidth of the min-fill decomposition of the
9392 * data graph
9393 * @param[out] nb_bags number of bags in the decomposition
9394 * @param[out] max_states maximum number of dynamic-programming states at any
9395 * decomposition node
9396 * @param[out] nb_gates number of gates in the emitted d-DNNF
9397 * @param[out] nb_variables number of variables in the emitted d-DNNF
9398 */
9399CREATE OR REPLACE FUNCTION reachability_compile_stats(
9400 IN sources INT[],
9401 IN destinations INT[],
9402 IN tokens UUID[],
9403 IN probabilities DOUBLE PRECISION[],
9404 IN source INT,
9405 IN target INT,
9406 IN directed BOOLEAN,
9407 OUT probability DOUBLE PRECISION,
9408 OUT data_treewidth INT,
9409 OUT nb_bags BIGINT,
9410 OUT max_states BIGINT,
9411 OUT nb_gates BIGINT,
9412 OUT nb_variables BIGINT)
9413 AS 'provsql','reachability_compile_stats'
9414 LANGUAGE C IMMUTABLE PARALLEL SAFE;
9415
9416
9417
9418/**
9419 * @brief Boolean UCQ probability plus compilation statistics
9420 * (columnar form, internal)
9421 *
9422 * Same compilation as @c ucq_joint_compile_stats(query jsonb, ...),
9423 * returning the probability together with the three width columns that
9424 * substantiate thesis Prop. 4.2.11 empirically -- the adversarial family
9425 * has small data and circuit widths but large joint width -- and the
9426 * structural statistics.
9427 *
9428 * @param disjunct_nvars number of query variables of each disjunct
9429 * @param atom_disjunct disjunct index of each atom (parallel to @p atom_rel)
9430 * @param atom_rel relation id of each atom
9431 * @param atom_vars query-variable indices of all atom columns, concatenated
9432 * @param atom_arity number of columns of each atom (slices @p atom_vars)
9433 * @param fact_rel relation id of each fact
9434 * @param fact_elems element ids of all fact columns, concatenated
9435 * @param fact_arity number of columns of each fact (slices @p fact_elems)
9436 * @param fact_tokens provenance token of each fact
9437 * @param fact_probs probability of each fact
9438 * @param[out] probability the exact UCQ probability
9439 * @param[out] joint_treewidth width of the min-fill decomposition found
9440 * @param[out] data_treewidth_lb degeneracy lower bound of the data-only graph
9441 * @param[out] circuit_treewidth_lb degeneracy lower bound of the slice-only graph
9442 * @param[out] n_bags number of bags in the decomposition
9443 * @param[out] max_states peak number of DP states at any node
9444 * @param[out] dd_size number of gates in the emitted d-D
9445 * @param[out] n_enumerating maximum number of essential (enumerating) query
9446 * variables over the disjuncts -- the @c e of the @f$2^{O(k^e)}@f$
9447 * bound, with variables functionally determined by others (via FDs
9448 * mined from the data) removed
9449 */
9450CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
9451 IN disjunct_nvars INT[],
9452 IN atom_disjunct INT[],
9453 IN atom_rel INT[],
9454 IN atom_vars INT[],
9455 IN atom_arity INT[],
9456 IN fact_rel INT[],
9457 IN fact_elems INT[],
9458 IN fact_arity INT[],
9459 IN fact_tokens UUID[],
9460 IN fact_probs DOUBLE PRECISION[],
9461 OUT probability DOUBLE PRECISION,
9462 OUT joint_treewidth INT,
9463 OUT data_treewidth_lb INT,
9464 OUT circuit_treewidth_lb INT,
9465 OUT n_bags BIGINT,
9466 OUT max_states BIGINT,
9467 OUT dd_size BIGINT,
9468 OUT n_enumerating INT)
9469 AS 'provsql','ucq_joint_compile_stats'
9470 LANGUAGE C IMMUTABLE PARALLEL SAFE;
9471
9472
9473/**
9474 * @brief Boolean UCQ probability plus statistics from a JSON specification
9475 *
9476 * JSON-spec wrapper over the columnar @c ucq_joint_compile_stats()
9477 * (see @c ucq_joint_evaluate(query jsonb, ...) for the JSON format).
9478 */
9479CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
9480 IN query JSONB,
9481 IN fact_rel INT[],
9482 IN fact_elems INT[],
9483 IN fact_arity INT[],
9484 IN fact_tokens UUID[],
9485 IN fact_probs DOUBLE PRECISION[],
9486 OUT probability DOUBLE PRECISION,
9487 OUT joint_treewidth INT,
9488 OUT data_treewidth_lb INT,
9489 OUT circuit_treewidth_lb INT,
9490 OUT n_bags BIGINT,
9491 OUT max_states BIGINT,
9492 OUT dd_size BIGINT,
9493 OUT n_enumerating INT)
9494 AS $$
9495DECLARE
9496 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
9497 avars INT[] := '{}'; aarity INT[] := '{}';
9498 d JSONB; a JSONB; v TEXT; didx INT := 0;
9499BEGIN
9500 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
9501 dnv := dnv || (d->>'n_vars')::INT;
9502 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9503 adisj := adisj || didx;
9504 arel := arel || (a->>'rel')::INT;
9505 aarity := aarity || jsonb_array_length(a->'vars');
9506 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9507 avars := avars || v::INT;
9508 END LOOP;
9509 END LOOP;
9510 didx := didx + 1;
9511 END LOOP;
9512 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
9513 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
9514 s.n_enumerating
9515 INTO probability, joint_treewidth, data_treewidth_lb,
9516 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
9517 FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
9518 fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s;
9519END;
9520$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
9521
9522
9523
9524
9525
9526
9527
9528
9529
9530
9531
9532/**
9533 * @brief Correlated Boolean UCQ probability plus compilation statistics
9534 * (columnar form, internal)
9535 *
9536 * Same compilation as @c ucq_joint_evaluate_tracked(); the three width
9537 * columns substantiate thesis Prop. 4.2.11 on real correlated data (the
9538 * data-only and circuit-only degeneracy bounds can be small while the
9539 * joint width is large).
9540 */
9541CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
9542 IN disjunct_nvars INT[],
9543 IN atom_disjunct INT[],
9544 IN atom_rel INT[],
9545 IN atom_vars INT[],
9546 IN atom_arity INT[],
9547 IN fact_rel INT[],
9548 IN fact_elems INT[],
9549 IN fact_arity INT[],
9550 IN fact_tokens UUID[],
9551 OUT probability DOUBLE PRECISION,
9552 OUT joint_treewidth INT,
9553 OUT data_treewidth_lb INT,
9554 OUT circuit_treewidth_lb INT,
9555 OUT n_bags BIGINT,
9556 OUT max_states BIGINT,
9557 OUT dd_size BIGINT,
9558 OUT n_enumerating INT)
9559 AS 'provsql','ucq_joint_compile_stats_tracked'
9560 LANGUAGE C STABLE PARALLEL SAFE;
9561
9562
9563/**
9564 * @brief Correlated Boolean UCQ probability plus statistics from a JSON spec
9565 */
9566CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
9567 IN query JSONB,
9568 IN fact_rel INT[],
9569 IN fact_elems INT[],
9570 IN fact_arity INT[],
9571 IN fact_tokens UUID[],
9572 OUT probability DOUBLE PRECISION,
9573 OUT joint_treewidth INT,
9574 OUT data_treewidth_lb INT,
9575 OUT circuit_treewidth_lb INT,
9576 OUT n_bags BIGINT,
9577 OUT max_states BIGINT,
9578 OUT dd_size BIGINT,
9579 OUT n_enumerating INT)
9580 AS $$
9581DECLARE
9582 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
9583 avars INT[] := '{}'; aarity INT[] := '{}';
9584 d JSONB; a JSONB; v TEXT; didx INT := 0;
9585BEGIN
9586 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
9587 dnv := dnv || (d->>'n_vars')::INT;
9588 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9589 adisj := adisj || didx;
9590 arel := arel || (a->>'rel')::INT;
9591 aarity := aarity || jsonb_array_length(a->'vars');
9592 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9593 avars := avars || v::INT;
9594 END LOOP;
9595 END LOOP;
9596 didx := didx + 1;
9597 END LOOP;
9598 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
9599 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
9600 s.n_enumerating
9601 INTO probability, joint_treewidth, data_treewidth_lb,
9602 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
9603 FROM ucq_joint_compile_stats_tracked(dnv, adisj, arel, avars, aarity,
9604 fact_rel, fact_elems, fact_arity, fact_tokens) s;
9605END;
9606$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
9607
9608/**
9609 * @brief Compile a correlated UCQ and materialise its certified d-D,
9610 * returning the root provenance token (columnar form, internal)
9611 *
9612 * The architecturally-primary route: the compiler builds the
9613 * deterministic, decomposable circuit and materialises it as ordinary
9614 * @c plus / @c times / @c monus provenance gates (carrying the d-DNNF
9615 * certificate); the answer is then obtained through the standard entry
9616 * points on the returned token -- @c probability_evaluate(token),
9617 * @c shapley(token, ...), expectation -- so the joint-width path shares
9618 * the one evaluation pipeline. The token is the exact Boolean
9619 * provenance of the UCQ (no @c 'absorptive' marker).
9620 */
9621CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
9622 disjunct_nvars INT[],
9623 atom_disjunct INT[],
9624 atom_rel INT[],
9625 atom_vars INT[],
9626 atom_arity INT[],
9627 fact_rel INT[],
9628 fact_elems INT[],
9629 fact_arity INT[],
9630 fact_tokens UUID[])
9631 RETURNS UUID AS
9632 'provsql','ucq_joint_materialize_tracked' LANGUAGE C VOLATILE;
9633
9634/**
9635 * @brief Compile a correlated UCQ and materialise its certified d-D
9636 * from a JSON spec, returning the root provenance token
9637 *
9638 * JSON-spec wrapper over @c ucq_joint_materialize_tracked(). Evaluate
9639 * the answer with the standard surface, e.g.
9640 * @c probability_evaluate(ucq_joint_materialize_tracked(query, ...)).
9641 */
9642CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
9643 query JSONB,
9644 fact_rel INT[],
9645 fact_elems INT[],
9646 fact_arity INT[],
9647 fact_tokens UUID[])
9648 RETURNS UUID AS $$
9649DECLARE
9650 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
9651 avars INT[] := '{}'; aarity INT[] := '{}';
9652 d JSONB; a JSONB; v TEXT; didx INT := 0;
9653BEGIN
9654 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
9655 dnv := dnv || (d->>'n_vars')::INT;
9656 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9657 adisj := adisj || didx;
9658 arel := arel || (a->>'rel')::INT;
9659 aarity := aarity || jsonb_array_length(a->'vars');
9660 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9661 avars := avars || v::INT;
9662 END LOOP;
9663 END LOOP;
9664 didx := didx + 1;
9665 END LOOP;
9666 RETURN ucq_joint_materialize_tracked(dnv, adisj, arel, avars, aarity,
9667 fact_rel, fact_elems, fact_arity, fact_tokens);
9668END;
9669$$ LANGUAGE plpgsql VOLATILE;
9670
9671/**
9672 * @brief Compile a UCQ over named relations into a materialised certified
9673 * d-D, gathering the facts from the store -- the descriptor-driven engine
9674 *
9675 * The query-surface bridge for the joint-width compiler: instead of
9676 * hand-built columnar arrays, a JSON @p descriptor names the relations
9677 * and how their columns map to query variables, and this function
9678 * gathers the facts itself (the provenance rewriting is disabled around
9679 * the gather), builds the value-based element dictionary shared across
9680 * the relations (so equal join values get the same dense id), compiles
9681 * and materialises the certified d-D, and returns its provenance token.
9682 * The answer is then any standard evaluation on that token --
9683 * @c probability_evaluate(ucq_joint_provenance(...)),
9684 * @c shapley(...), expectation. This is also the engine the planner-time
9685 * query recogniser drives once it builds the descriptor from a query's
9686 * abstract syntax.
9687 *
9688 * Descriptor shape:
9689 * @verbatim
9690 * { "disjuncts": [ { "n_vars": k,
9691 * "atoms": [ {"rel": <relidx>, "vars": [..]}, ... ] }, ... ],
9692 * "relations": [ "schema.r", "schema.s", ... ], -- relidx -> relation
9693 * "elem_cols": [ ["x"], ["x","y"], ... ] } -- per relation: the
9694 * element columns, in
9695 * the atom's var order
9696 * @endverbatim
9697 *
9698 * @param descriptor the UCQ + the relations and their element columns
9699 * @param fallback token returned if the joint-width compiler declines
9700 * @return the materialised joint-width provenance token (NULL UUID-free
9701 * exact Boolean provenance of the UCQ)
9702 */
9703CREATE OR REPLACE FUNCTION ucq_joint_provenance(
9704 descriptor JSONB, fallback UUID DEFAULT NULL)
9705RETURNS UUID AS $$
9706DECLARE
9707 legs TEXT; sql TEXT; saved TEXT;
9708 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
9709 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
9710 avars INT[]:='{}'; aarity INT[]:='{}';
9711 d jsonb; a jsonb; v TEXT; didx INT:=0;
9712BEGIN
9713 -- Parse the UCQ structure into the columnar query arrays.
9714 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
9715 dnv := dnv || (d->>'n_vars')::INT;
9716 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9717 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
9718 aarity := aarity || jsonb_array_length(a->'vars');
9719 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9720 avars := avars || v::INT;
9721 END LOOP;
9722 END LOOP;
9723 didx := didx + 1;
9724 END LOOP;
9725
9726 -- One UNION ALL leg per relation: (relation index, TEXT element array,
9727 -- provenance token). No temp tables: a single gather query, with the
9728 -- value-based dense element dictionary built inline.
9729 SELECT string_agg(
9730 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
9731 rn - 1,
9732 (SELECT string_agg(format('(%I)::TEXT', c), ',')
9733 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
9734 rel,
9735 -- the lifted single-relation selection (a pre-filter), already
9736 -- deparsed to SQL by the recogniser; '' / absent = unfiltered.
9737 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
9738 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
9739 ELSE '' END),
9740 ' UNION ALL ')
9741 INTO legs
9742 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
9743
9744 sql := format($q$
9745 WITH facts(rel,elems,tok) AS (%s),
9746 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
9747 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
9748 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
9749 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
9750 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
9751 (SELECT array_agg(tok ORDER BY ord) FROM ord),
9752 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
9753 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
9754 JOIN dict dd ON dd.val = e.val)
9755 $q$, legs);
9756
9757 -- Read the raw rows with provenance rewriting disabled (we only read
9758 -- the existing provsql column; this internal gather is not tracked).
9759 saved := current_setting('provsql.active', true);
9760 PERFORM set_config('provsql.active','off', true);
9761 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
9762 PERFORM set_config('provsql.active', saved, true);
9763
9764 RETURN ucq_joint_materialize_tracked(dnv,adisj,arel,avars,aarity,
9765 fact_rel,fact_elems,fact_arity,fact_tokens);
9766EXCEPTION WHEN OTHERS THEN
9767 -- The joint-width compiler declined (unsupported gate type, joint
9768 -- width too large, ...): fall back to the normal provenance so the
9769 -- query never fails. Both give the same probability.
9770 RETURN fallback;
9771END;
9772$$ LANGUAGE plpgsql VOLATILE;
9773
9774-- ===========================================================================
9775-- Safe-UCQ Möbius-inversion route (mobius_evaluate.cpp).
9776--
9777-- The last missing exact route of the Dalvi-Suciu dichotomy: UCQs that are
9778-- safe only because the \#P-hard terms of their inclusion-exclusion expansion
9779-- carry a zero Möbius value on the CNF lattice and cancel (canonical witness:
9780-- QW / q9). Same TID gather as ucq_joint, then the lattice-walking compiler
9781-- materialises a gate_mobius-rooted circuit (a signed combination over
9782-- certified-independent islands), answered in PTIME data complexity by the
9783-- standard probability path.
9784-- ===========================================================================
9785
9786/**
9787 * @brief Materialise the safe-UCQ Möbius circuit and return its root token.
9788 * Columnar (TID) interface; see ucq_mobius_provenance for the gather.
9789 */
9790CREATE OR REPLACE FUNCTION ucq_mobius_materialize_tracked(
9791 disjunct_nvars INT[],
9792 atom_disjunct INT[],
9793 atom_rel INT[],
9794 atom_vars INT[],
9795 atom_arity INT[],
9796 fact_rel INT[],
9797 fact_elems INT[],
9798 fact_arity INT[],
9799 fact_tokens UUID[],
9800 lineage UUID DEFAULT NULL)
9801 RETURNS UUID AS
9802 'provsql','ucq_mobius_materialize_tracked' LANGUAGE C VOLATILE;
9803
9804/**
9805 * @brief Compile the Möbius circuit and return the lattice statistics plus the
9806 * probability (the demonstrability surface). @c cancelled_hard is the
9807 * single number that makes the mechanism legible: for q9 the 1 cancelled
9808 * element is \#P-hard, so the query is easy only because its hard part
9809 * cancels.
9810 */
9811CREATE OR REPLACE FUNCTION ucq_mobius_compile_stats(
9812 IN disjunct_nvars INT[],
9813 IN atom_disjunct INT[],
9814 IN atom_rel INT[],
9815 IN atom_vars INT[],
9816 IN atom_arity INT[],
9817 IN fact_rel INT[],
9818 IN fact_elems INT[],
9819 IN fact_arity INT[],
9820 IN fact_tokens UUID[],
9821 OUT probability DOUBLE PRECISION,
9822 OUT n_components INT,
9823 OUT n_cnf_conjuncts INT,
9824 OUT lattice_size INT,
9825 OUT n_nonzero INT,
9826 OUT n_cancelled INT,
9827 OUT cancelled_hard BOOLEAN,
9828 OUT dd_size BIGINT,
9829 OUT memo_hits BIGINT)
9830 AS 'provsql','ucq_mobius_compile_stats'
9831 LANGUAGE C VOLATILE;
9832
9833/**
9834 * @brief Pass a token through iff it is a @c gate_mobius, else return NULL.
9835 *
9836 * The Möbius-precedence dispatch (see @c make_provenance_expression) wraps the
9837 * Möbius call in this and then @c COALESCE\ s it before the joint-width call:
9838 * a Möbius *success* always roots a @c gate_mobius (the compiler wraps even a
9839 * thin selector around the lineage), so it short-circuits and the joint-width
9840 * compiler never runs; a Möbius *decline* returns the literal lineage (never a
9841 * @c gate_mobius), so this yields NULL and @c COALESCE falls through to
9842 * joint-width. The lineage token is a plain plus/times/input, so the test is
9843 * unambiguous.
9844 */
9845CREATE OR REPLACE FUNCTION mobius_or_null(tok UUID)
9846RETURNS UUID AS $$
9847 SELECT CASE WHEN tok IS NOT NULL AND provsql.get_gate_type(tok) = 'mobius'
9848 THEN tok END
9849$$ LANGUAGE sql STABLE;
9850
9851/**
9852 * @brief Möbius-route provenance from a descriptor (the planner-substituted
9853 * entry point, and the manual one). Same descriptor and TID gather as
9854 * @c ucq_joint_provenance; on any decline (unsafe shape, cap, not TID)
9855 * returns @p fallback, so a recognised query never fails.
9856 */
9857CREATE OR REPLACE FUNCTION ucq_mobius_provenance(
9858 descriptor JSONB, fallback UUID DEFAULT NULL)
9859RETURNS UUID AS $$
9860DECLARE
9861 legs TEXT; sql TEXT; saved TEXT;
9862 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
9863 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
9864 avars INT[]:='{}'; aarity INT[]:='{}';
9865 d jsonb; a jsonb; v TEXT; didx INT:=0;
9866BEGIN
9867 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
9868 dnv := dnv || (d->>'n_vars')::INT;
9869 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9870 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
9871 aarity := aarity || jsonb_array_length(a->'vars');
9872 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9873 avars := avars || v::INT;
9874 END LOOP;
9875 END LOOP;
9876 didx := didx + 1;
9877 END LOOP;
9878
9879 SELECT string_agg(
9880 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
9881 rn - 1,
9882 (SELECT string_agg(format('(%I)::TEXT', c), ',')
9883 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
9884 rel,
9885 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
9886 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
9887 ELSE '' END),
9888 ' UNION ALL ')
9889 INTO legs
9890 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
9891
9892 sql := format($q$
9893 WITH facts(rel,elems,tok) AS (%s),
9894 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
9895 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
9896 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
9897 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
9898 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
9899 (SELECT array_agg(tok ORDER BY ord) FROM ord),
9900 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
9901 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
9902 JOIN dict dd ON dd.val = e.val)
9903 $q$, legs);
9904
9905 saved := current_setting('provsql.active', true);
9906 PERFORM set_config('provsql.active','off', true);
9907 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
9908 PERFORM set_config('provsql.active', saved, true);
9909
9910 -- Pass the normal-provenance fallback as the lineage: it is carried on the
9911 -- gate_mobius so the token still answers Shapley / semiring / PROV on the
9912 -- literal lineage (the Möbius combination is a probability-only shortcut).
9913 RETURN ucq_mobius_materialize_tracked(dnv,adisj,arel,avars,aarity,
9914 fact_rel,fact_elems,fact_arity,fact_tokens, fallback);
9915EXCEPTION WHEN OTHERS THEN
9916 RETURN fallback;
9917END;
9918$$ LANGUAGE plpgsql VOLATILE;
9919
9920/**
9921 * @brief Möbius lattice statistics + probability from a descriptor: the
9922 * demonstrability SRF. Gathers
9923 * the same TID facts as @c ucq_mobius_provenance, then runs the columnar
9924 * @c ucq_mobius_compile_stats.
9925 */
9926CREATE OR REPLACE FUNCTION mobius_compile_stats(
9927 IN descriptor JSONB,
9928 OUT probability DOUBLE PRECISION,
9929 OUT n_components INT,
9930 OUT n_cnf_conjuncts INT,
9931 OUT lattice_size INT,
9932 OUT n_nonzero INT,
9933 OUT n_cancelled INT,
9934 OUT cancelled_hard BOOLEAN,
9935 OUT dd_size BIGINT,
9936 OUT memo_hits BIGINT)
9937RETURNS RECORD AS $$
9938DECLARE
9939 legs TEXT; sql TEXT; saved TEXT;
9940 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
9941 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
9942 avars INT[]:='{}'; aarity INT[]:='{}';
9943 d jsonb; a jsonb; v TEXT; didx INT:=0;
9944BEGIN
9945 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
9946 dnv := dnv || (d->>'n_vars')::INT;
9947 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
9948 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
9949 aarity := aarity || jsonb_array_length(a->'vars');
9950 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
9951 avars := avars || v::INT;
9952 END LOOP;
9953 END LOOP;
9954 didx := didx + 1;
9955 END LOOP;
9956
9957 SELECT string_agg(
9958 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
9959 rn - 1,
9960 (SELECT string_agg(format('(%I)::TEXT', c), ',')
9961 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
9962 rel,
9963 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
9964 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
9965 ELSE '' END),
9966 ' UNION ALL ')
9967 INTO legs
9968 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
9969
9970 sql := format($q$
9971 WITH facts(rel,elems,tok) AS (%s),
9972 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
9973 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
9974 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
9975 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
9976 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
9977 (SELECT array_agg(tok ORDER BY ord) FROM ord),
9978 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
9979 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
9980 JOIN dict dd ON dd.val = e.val)
9981 $q$, legs);
9982
9983 saved := current_setting('provsql.active', true);
9984 PERFORM set_config('provsql.active','off', true);
9985 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
9986 PERFORM set_config('provsql.active', saved, true);
9987
9988 SELECT s.probability, s.n_components, s.n_cnf_conjuncts, s.lattice_size,
9989 s.n_nonzero, s.n_cancelled, s.cancelled_hard, s.dd_size, s.memo_hits
9990 INTO probability, n_components, n_cnf_conjuncts, lattice_size,
9991 n_nonzero, n_cancelled, cancelled_hard, dd_size, memo_hits
9992 FROM ucq_mobius_compile_stats(dnv,adisj,arel,avars,aarity,
9993 fact_rel,fact_elems,fact_arity,fact_tokens) s;
9994END;
9995$$ LANGUAGE plpgsql VOLATILE;
9996
9997/**
9998 * @brief Internal gather for the per-answer joint route: parse @p descriptor
9999 * into the columnar UCQ arrays and gather every fact (relation index,
10000 * dense element ids, provenance token) with the value dictionary.
10001 *
10002 * Used only by the planner-substituted @c ucq_joint_provenance_answer (the C
10003 * single-DP entry point), which calls it ONCE per query and then computes all
10004 * answers in one sweep. No head pinning: the single DP discovers the answers.
10005 * @c val_by_id maps a dense element id back to its TEXT value (so an answer's
10006 * head ids can be matched to the @c GROUP @c BY head TEXT).
10007 */
10008CREATE OR REPLACE FUNCTION ucq_joint_gather(
10009 descriptor JSONB,
10010 OUT disjunct_nvars INT[], OUT atom_disjunct INT[], OUT atom_rel INT[],
10011 OUT atom_vars INT[], OUT atom_arity INT[],
10012 OUT fact_rel INT[], OUT fact_elems INT[], OUT fact_arity INT[],
10013 OUT fact_tokens UUID[], OUT val_by_id TEXT[])
10014AS $$
10015DECLARE
10016 legs TEXT; sql TEXT; saved TEXT; d jsonb; a jsonb; v TEXT; didx INT := 0;
10017BEGIN
10018 disjunct_nvars:='{}'; atom_disjunct:='{}'; atom_rel:='{}';
10019 atom_vars:='{}'; atom_arity:='{}';
10020 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
10021 disjunct_nvars := disjunct_nvars || (d->>'n_vars')::INT;
10022 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
10023 atom_disjunct := atom_disjunct || didx;
10024 atom_rel := atom_rel || (a->>'rel')::INT;
10025 atom_arity := atom_arity || jsonb_array_length(a->'vars');
10026 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
10027 atom_vars := atom_vars || v::INT;
10028 END LOOP;
10029 END LOOP;
10030 didx := didx + 1;
10031 END LOOP;
10032
10033 SELECT string_agg(
10034 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s', rn - 1,
10035 (SELECT string_agg(format('(%I)::TEXT', c), ',')
10036 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
10037 rel,
10038 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
10039 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
10040 ELSE '' END),
10041 ' UNION ALL ')
10042 INTO legs
10043 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
10044
10045 sql := format($q$
10046 WITH facts(rel,elems,tok) AS (%s),
10047 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
10048 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
10049 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
10050 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
10051 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
10052 (SELECT array_agg(tok ORDER BY ord) FROM ord),
10053 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
10054 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
10055 JOIN dict dd ON dd.val = e.val),
10056 (SELECT array_agg(val ORDER BY id) FROM dict)
10057 $q$, legs);
10058
10059 saved := current_setting('provsql.active', true);
10060 PERFORM set_config('provsql.active','off', true);
10061 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems, val_by_id;
10062 PERFORM set_config('provsql.active', saved, true);
10063END;
10064$$ LANGUAGE plpgsql VOLATILE;
10065
10066/**
10067 * @brief Per-answer joint-width provenance via the TOP-DOWN single DP
10068 * (planner-substituted, C).
10069 *
10070 * The transparent per-answer rewrite substitutes one call per output group.
10071 * On the FIRST call of a query the function gathers the facts once
10072 * (@c ucq_joint_gather), runs the single DP, and materialises EVERY answer's
10073 * certified d-D into the store, caching @c head_vals -> token in @c fn_extra;
10074 * each subsequent group call is an O(1) lookup -- so the whole GROUP BY costs
10075 * one gather + one decomposition + one sweep, not @p k of each. On any
10076 * decline (joint width too large) the @p fallback token (the normal
10077 * per-answer provenance) is returned, so the query never fails. The answer's
10078 * marginal / Shapley / expectation is then the standard evaluation on the
10079 * returned token -- one pipeline for the whole system.
10080 */
10081CREATE OR REPLACE FUNCTION ucq_joint_provenance_answer(
10082 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
10083RETURNS UUID AS 'provsql','ucq_joint_provenance_answer'
10084LANGUAGE C STABLE;
10085
10086/**
10087 * @brief Per-answer safe-UCQ Möbius provenance (planner-substituted): one
10088 * head-pinned Möbius circuit per output group. On the first call the
10089 * facts are gathered once (ucq_joint_gather) and cached; each group pins
10090 * @p head_vars to @p head_vals and compiles, caching head -> token. On
10091 * any decline returns @p fallback. STABLE: it caches per fn-call
10092 * context, so it is not re-evaluated within one scan.
10093 */
10094CREATE OR REPLACE FUNCTION ucq_mobius_provenance_answer(
10095 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
10096RETURNS UUID AS 'provsql','ucq_mobius_provenance_answer'
10097LANGUAGE C STABLE;
10098
10099
10100/**
10101 * @brief Compile and materialise the reachability provenance of every
10102 * vertex (columnar form, internal)
10103 *
10104 * All-targets variant of @c reachability_evaluate(): compiles, along a
10105 * tree decomposition of the data graph, one certified provenance
10106 * circuit per vertex reachable from some source in the all-edges-present
10107 * world, materialises the (shared, linear-size) circuits in the
10108 * provenance store -- @c plus / @c times gates carrying the d-DNNF
10109 * certificate, negated edges as @c monus(one, edge) -- and returns one
10110 * @c (vertex, token) row per such vertex. Sources form a possibly
10111 * *probabilistic source set*: each source arc is gated by the source
10112 * tuple's token, the nil UUID marking a certain (always present)
10113 * source. This is the engine behind the rewriter's
10114 * recursive-reachability route; the returned tokens are ordinary
10115 * provenance tokens usable with the whole evaluation surface, wrapped
10116 * in the 'absorptive' assumption marker (the compiled circuit is the
10117 * exact Boolean lineage but only the absorptive quotient of the
10118 * infinite recursive semiring provenance: probability and absorptive
10119 * semiring evaluations -- e.g. nonnegative min-plus -- are exact,
10120 * counting and why-provenance refuse).
10121 *
10122 * @param sources source vertex of each edge (dense INTEGER IDs)
10123 * @param destinations destination vertex of each edge
10124 * @param tokens provenance token of each edge tuple
10125 * @param probabilities probability of each edge tuple
10126 * @param block_keys per-edge BID key variable (nil UUID = independent
10127 * tuple; alternatives sharing a key are mutually exclusive, e.g.
10128 * from repair_key)
10129 * @param block_indices per-edge outcome index within its block
10130 * @param source_vertices the source vertices
10131 * @param source_tokens per-source provenance token (nil UUID = certain)
10132 * @param source_probabilities per-source probability
10133 * @param directed if false, each edge can be traversed both ways
10134 * @param[out] vertex a vertex reachable from some source
10135 * @param[out] token the materialised reachability provenance token of @c vertex
10136 */
10137CREATE OR REPLACE FUNCTION reachability_materialize(
10138 IN sources INT[],
10139 IN destinations INT[],
10140 IN tokens UUID[],
10141 IN probabilities DOUBLE PRECISION[],
10142 IN block_keys UUID[],
10143 IN block_indices INT[],
10144 IN source_vertices INT[],
10145 IN source_tokens UUID[],
10146 IN source_probabilities DOUBLE PRECISION[],
10147 IN directed BOOLEAN,
10148 OUT vertex INT,
10149 OUT token UUID)
10150 RETURNS SETOF RECORD AS
10151 'provsql','reachability_materialize' LANGUAGE C VOLATILE;
10152
10153
10154/**
10155 * @brief Bounded-hop variant of @c reachability_materialize() (internal)
10156 *
10157 * Compiles, along a tree decomposition of the data graph, one certified
10158 * provenance circuit per (vertex, walk length) pair achievable within
10159 * @p hop_bound edges -- the rows a hop-counting recursive CTE derives,
10160 * row @c (v,h) meaning "some *walk* of exactly @c h edges connects a
10161 * present source to @c v" -- and returns them as @c (vertex, hops,
10162 * token) with @p hop_seed added to the lengths (the CTE base arm's hop
10163 * constant). Also pre-creates, per vertex, the certified gate that a
10164 * hop-discarding query's deduplication will address, wired to the
10165 * compilation's native within-bound root, so the natural "within k
10166 * hops" probability evaluates through the linear certified route.
10167 *
10168 * @param sources source vertex of each edge (dense INTEGER IDs)
10169 * @param destinations destination vertex of each edge
10170 * @param tokens provenance token of each edge tuple
10171 * @param probabilities probability of each edge tuple
10172 * @param block_keys per-edge BID key variable (nil UUID = independent)
10173 * @param block_indices per-edge outcome index within its block
10174 * @param source_vertices the source vertices
10175 * @param source_tokens per-source provenance token (nil UUID = certain)
10176 * @param source_probabilities per-source probability
10177 * @param directed if false, each edge can be traversed both ways
10178 * @param hop_bound maximum walk length
10179 * @param hop_seed hop value of the base arm (added to reported lengths)
10180 * @param[out] vertex a reachable vertex
10181 * @param[out] hops the walk length at which @c vertex is reached
10182 * @param[out] token the materialised provenance token of the @c (vertex, hops) pair
10183 */
10184CREATE OR REPLACE FUNCTION reachability_materialize_hops(
10185 IN sources INT[],
10186 IN destinations INT[],
10187 IN tokens UUID[],
10188 IN probabilities DOUBLE PRECISION[],
10189 IN block_keys UUID[],
10190 IN block_indices INT[],
10191 IN source_vertices INT[],
10192 IN source_tokens UUID[],
10193 IN source_probabilities DOUBLE PRECISION[],
10194 IN directed BOOLEAN,
10195 IN hop_bound INT,
10196 IN hop_seed INT,
10197 OUT vertex INT,
10198 OUT hops INT,
10199 OUT token UUID)
10200 RETURNS SETOF RECORD AS
10201 'provsql','reachability_materialize_hops' LANGUAGE C VOLATILE;
10202
10203
10204/**
10205 * @brief Per-group "some member reachable" compilation (columnar form,
10206 * internal)
10207 *
10208 * For each distinct group in the parallel @p group_ids /
10209 * @p member_vertices arrays, compiles the certified circuit of "some
10210 * member vertex is reachable from a present source" along the data
10211 * decomposition -- the disjunction over the group's *correlated*
10212 * per-vertex reachability events, deterministic by construction
10213 * through the set-reachability state bit -- materialises it, and
10214 * returns one @c (group_id, token) row per group. Engine behind the
10215 * rewriter's cross-vertex aggregation planting.
10216 *
10217 * @param sources source vertex of each edge (dense INTEGER IDs)
10218 * @param destinations destination vertex of each edge
10219 * @param tokens provenance token of each edge tuple
10220 * @param probabilities probability of each edge tuple
10221 * @param block_keys per-edge BID key variable (nil UUID = independent)
10222 * @param block_indices per-edge outcome index within its block
10223 * @param source_vertices the source vertices
10224 * @param source_tokens per-source provenance token (nil UUID = certain)
10225 * @param source_probabilities per-source probability
10226 * @param directed if false, each edge can be traversed both ways
10227 * @param group_ids group identifier of each member row
10228 * @param member_vertices member vertex of each member row
10229 * @param[out] group_id a group whose every member is reachable
10230 * @param[out] token the materialised all-members-reachable provenance token of
10231 * @c group_id
10232 */
10233CREATE OR REPLACE FUNCTION reachability_materialize_any(
10234 IN sources INT[],
10235 IN destinations INT[],
10236 IN tokens UUID[],
10237 IN probabilities DOUBLE PRECISION[],
10238 IN block_keys UUID[],
10239 IN block_indices INT[],
10240 IN source_vertices INT[],
10241 IN source_tokens UUID[],
10242 IN source_probabilities DOUBLE PRECISION[],
10243 IN directed BOOLEAN,
10244 IN group_ids INT[],
10245 IN member_vertices INT[],
10246 OUT group_id INT,
10247 OUT token UUID)
10248 RETURNS SETOF RECORD AS
10249 'provsql','reachability_materialize_any' LANGUAGE C VOLATILE;
10250
10251/**
10252 * @brief Compile and materialise the "every member vertex reachable"
10253 * (k-terminal / coverage) circuit (columnar form, internal)
10254 *
10255 * Arguments as @c reachability_materialize_any() with a single member
10256 * set: compiles the certified circuit of "every member vertex is
10257 * reachable from a present source" -- the conjunction over the
10258 * members' *correlated* per-vertex events, deterministic by
10259 * construction through the pending rescuer-set congruence --
10260 * materialises it, and returns its token, wrapped in the
10261 * @c 'absorptive' assumption marker. Probability evaluation gives the
10262 * k-terminal reliability; nonnegative min-plus the cost of the
10263 * cheapest covering subgraph (directed Steiner cost), shared edges
10264 * paid once. A member vertex absent from the graph is unreachable:
10265 * the circuit is then constant false.
10266 *
10267 * @param sources source vertex of each edge (dense INTEGER IDs)
10268 * @param destinations destination vertex of each edge
10269 * @param tokens provenance token of each edge tuple
10270 * @param probabilities probability of each edge tuple
10271 * @param block_keys per-edge BID key variable (nil UUID = independent)
10272 * @param block_indices per-edge outcome index within its block
10273 * @param source_vertices the source vertices
10274 * @param source_tokens per-source provenance token (nil UUID = certain)
10275 * @param source_probabilities per-source probability
10276 * @param directed if false, each edge can be traversed both ways
10277 * @param member_vertices the member vertices (dense IDs)
10278 */
10279CREATE OR REPLACE FUNCTION reachability_materialize_cover(
10280 sources INT[],
10281 destinations INT[],
10282 tokens UUID[],
10283 probabilities DOUBLE PRECISION[],
10284 block_keys UUID[],
10285 block_indices INT[],
10286 source_vertices INT[],
10287 source_tokens UUID[],
10288 source_probabilities DOUBLE PRECISION[],
10289 directed BOOLEAN,
10290 member_vertices INT[])
10291 RETURNS UUID AS
10292 'provsql','reachability_materialize_cover' LANGUAGE C VOLATILE;
10293
10294/**
10295 * @brief Plant certified any-member-reachable gates for a grouped
10296 * reachability aggregation (internal)
10297 *
10298 * Called (at plan time, over SPI) by the recursive-CTE lowering when
10299 * the outer query aggregates a reachability working table by a column
10300 * of a joined, untracked member relation: @c GROUP @c BY collapses
10301 * each group's per-vertex reach tokens with @c provenance_plus, whose
10302 * disjuncts are correlated (they share edges) and would otherwise
10303 * leave the certified route. For each multi-member group this
10304 * pre-creates, at the canonical address of the group's token multiset,
10305 * a certified single-child plus over the group's native
10306 * any-member-reachable circuit (@c reachability_materialize_any), so
10307 * the natural aggregation stays on the linear evaluation route.
10308 * Best-effort: any failure leaves the generic path untouched (notice
10309 * under verbosity 10).
10310 *
10311 * @param work_name the lowered CTE's working table
10312 * @param node_attribute its vertex column
10313 * @param member_rel the joined member relation (must be untracked)
10314 * @param member_attribute the member relation's join column
10315 * @param group_attribute the member relation's grouping column
10316 * @param edge_rel the tracked edge relation (as for eval_reachability)
10317 * @param source_attribute name of the source-vertex column
10318 * @param destination_attribute name of the destination-vertex column
10319 * @param source_value the base arm's constant, as TEXT
10320 * @param directed if false, each edge can be traversed both ways
10321 * @param edge_quals optional deterministic filter over edge columns
10322 * @param source_rel source relation of a multi-source base arm
10323 * @param source_rel_attribute the source relation's vertex column
10324 * @param edge_sql deparsed edge subquery (join-defined edges)
10325 * @param member_quals optional deterministic filter over the member
10326 * relation's columns (table-qualified as @c t.column), restricting
10327 * which members participate in each group
10328 */
10329CREATE OR REPLACE FUNCTION plant_reach_any_groups(
10330 work_name TEXT,
10331 node_attribute TEXT,
10332 member_rel REGCLASS,
10333 member_attribute TEXT,
10334 group_attribute TEXT,
10335 edge_rel REGCLASS,
10336 source_attribute TEXT,
10337 destination_attribute TEXT,
10338 source_value TEXT,
10339 directed BOOLEAN,
10340 edge_quals TEXT DEFAULT NULL,
10341 source_rel REGCLASS DEFAULT NULL,
10342 source_rel_attribute TEXT DEFAULT NULL,
10343 edge_sql TEXT DEFAULT NULL,
10344 member_quals TEXT DEFAULT NULL)
10345 RETURNS VOID AS
10346$$
10347DECLARE
10348 e RECORD;
10349 grp RECORD;
10350 m RECORD;
10351 sv TEXT[];
10352 st UUID[];
10353 sp double precision[];
10354 gids INT[] := ARRAY[]::INT[];
10355 mids INT[] := ARRAY[]::INT[];
10356 vid INT;
10357 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
10358BEGIN
10359 BEGIN
10360 -- A tracked member relation would make the aggregated tokens
10361 -- per-row products, not the bare reach tokens: nothing to plant.
10362 IF EXISTS (SELECT 1 FROM pg_attribute
10363 WHERE attrelid = member_rel AND attname = 'provsql'
10364 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped) THEN
10365 RETURN;
10366 END IF;
10367
10368 IF source_rel IS NOT NULL THEN
10369 SELECT g.source_values, g.source_tokens, g.source_probabilities
10370 INTO sv, st, sp
10371 FROM provsql.gather_reachability_sources(source_rel,
10372 source_rel_attribute) g;
10373 IF sv IS NULL THEN
10374 sv := ARRAY[]::TEXT[];
10375 st := ARRAY[]::UUID[];
10376 sp := ARRAY[]::float8[];
10377 END IF;
10378 ELSE
10379 sv := ARRAY[source_value];
10380 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
10381 sp := ARRAY[1.0::float8];
10382 END IF;
10383
10384 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
10385 destination_attribute,
10386 sv, edge_quals, edge_sql);
10387
10388 -- The groups, replicating the user's join semantics: per group, the
10389 -- member vertices and the multiset of their reach tokens (with the
10390 -- multiplicity the join produces). Single-member groups need no
10391 -- planting (provenance_plus passes a single token through).
10392 -- Two steps: materialise the joined rows with their per-row tokens
10393 -- (tracked CTAS, then strip the automatic provsql column), and only
10394 -- then aggregate the now-plain table -- aggregating provenance()
10395 -- inside a grouped tracked query would be rewritten as a
10396 -- provenance-aware aggregation, which is not what the planting
10397 -- needs.
10398 DROP TABLE IF EXISTS provsql_reach_any_flat_tmp;
10399 EXECUTE format(
10400 'CREATE TEMP TABLE provsql_reach_any_flat_tmp AS '
10401 || 'SELECT w.%1$I::TEXT AS node_val, provsql.provenance() AS tok, '
10402 || ' t.%5$I AS grp_key '
10403 || 'FROM %2$I w JOIN %3$s t ON w.%1$I = t.%4$I'
10404 -- The member-relation filter restricts which members participate
10405 -- (deparsed table-qualified as t.column); the working table side
10406 -- carries no provenance distinction here.
10407 || coalesce(' WHERE ' || member_quals, ''),
10408 node_attribute, work_name, member_rel::TEXT, member_attribute,
10409 group_attribute);
10410 PERFORM provsql.remove_provenance('provsql_reach_any_flat_tmp');
10411 DROP TABLE IF EXISTS provsql_reach_any_groups_tmp;
10412 CREATE TEMP TABLE provsql_reach_any_groups_tmp AS
10413 SELECT (row_number() OVER ())::INT AS gid, members, toks FROM (
10414 SELECT array_agg(node_val) AS members, array_agg(tok) AS toks
10415 FROM provsql_reach_any_flat_tmp
10416 GROUP BY grp_key HAVING count(*) >= 2) g;
10417 DROP TABLE provsql_reach_any_flat_tmp;
10418
10419 FOR grp IN SELECT gid, members FROM provsql_reach_any_groups_tmp LOOP
10420 FOR m IN SELECT DISTINCT unnest(grp.members) AS val LOOP
10421 vid := array_position(e.vertices, m.val);
10422 IF vid IS NOT NULL THEN
10423 gids := gids || grp.gid;
10424 mids := mids || vid;
10425 END IF;
10426 END LOOP;
10427 END LOOP;
10428 IF cardinality(gids) = 0 THEN
10429 DROP TABLE provsql_reach_any_groups_tmp;
10430 RETURN;
10431 END IF;
10432
10433 FOR grp IN
10434 SELECT a.group_id, a.token AS any_token, t.toks
10435 FROM provsql.reachability_materialize_any(
10436 e.sources, e.destinations, e.tokens, e.probabilities,
10437 e.block_keys, e.block_indices, e.extra_ids, st, sp,
10438 directed, gids, mids) a
10439 JOIN provsql_reach_any_groups_tmp t ON t.gid = a.group_id
10440 LOOP
10441 PERFORM provsql.plant_canonical(work_name, 'plus', grp.toks,
10442 grp.any_token, 1);
10443 END LOOP;
10444 DROP TABLE provsql_reach_any_groups_tmp;
10445 IF verbosity >= 20 THEN
10446 -- Lift the function-level client_min_messages = warning for the
10447 -- one RAISE; the function-level SET restores the caller's value.
10448 PERFORM set_config('client_min_messages', 'notice', true);
10449 RAISE NOTICE 'ProvSQL: certified any-member gates planted for the aggregation of "%" by %.%',
10450 work_name, member_rel, group_attribute;
10451 PERFORM set_config('client_min_messages', 'warning', true);
10452 END IF;
10453 EXCEPTION WHEN OTHERS THEN
10454 IF verbosity >= 10 THEN
10455 PERFORM set_config('client_min_messages', 'notice', true);
10456 RAISE NOTICE 'ProvSQL: any-member planting for "%" skipped (%)',
10457 work_name, SQLERRM;
10458 PERFORM set_config('client_min_messages', 'warning', true);
10459 END IF;
10460 END;
10461END
10462-- No SET search_path: the deparsed edge subquery must resolve against
10463-- the caller's path; ProvSQL internals are schema-qualified.
10464$$ LANGUAGE plpgsql SET client_min_messages = warning;
10465
10466/**
10467 * @brief Plant the certified all-members-reachable gate for a
10468 * reachability self-join conjunction (internal)
10469 *
10470 * Called (at plan time, over SPI) by the recursive-CTE lowering when
10471 * the outer query self-joins a reachability working table with one
10472 * constant node binding per reference -- "are these k vertices all
10473 * reachable" -- whose row provenance @c provenance_times() computes as
10474 * the product of *correlated* per-vertex reach tokens (they share
10475 * edges). This pre-creates, at the times-canonical address of that
10476 * token multiset, a certified single-child times over the native
10477 * all-members-reachable circuit (@c reachability_materialize_cover),
10478 * so the natural conjunction stays on the linear certified route --
10479 * with the joint-worlds semantics: probability evaluation gives the
10480 * k-terminal reliability, and nonnegative min-plus the cost of the
10481 * cheapest covering subgraph (directed Steiner cost), shared edges
10482 * paid once where the raw product would pay them once per factor.
10483 * Best-effort: any failure leaves the generic path untouched (notice
10484 * under verbosity 10).
10485 *
10486 * @param work_name the lowered CTE's working table
10487 * @param node_attribute its vertex column
10488 * @param edge_rel the tracked edge relation (as for eval_reachability)
10489 * @param source_attribute name of the source-vertex column
10490 * @param destination_attribute name of the destination-vertex column
10491 * @param source_value the base arm's constant, as TEXT
10492 * @param directed if false, each edge can be traversed both ways
10493 * @param node_values the constant node bindings, as TEXT (multiset:
10494 * one per self-join reference)
10495 * @param edge_quals optional deterministic filter over edge columns
10496 * @param source_rel source relation of a multi-source base arm
10497 * @param source_rel_attribute the source relation's vertex column
10498 * @param edge_sql deparsed edge subquery (join-defined edges)
10499 */
10500CREATE OR REPLACE FUNCTION plant_reach_cover(
10501 work_name TEXT,
10502 node_attribute TEXT,
10503 edge_rel REGCLASS,
10504 source_attribute TEXT,
10505 destination_attribute TEXT,
10506 source_value TEXT,
10507 directed BOOLEAN,
10508 node_values TEXT[],
10509 edge_quals TEXT DEFAULT NULL,
10510 source_rel REGCLASS DEFAULT NULL,
10511 source_rel_attribute TEXT DEFAULT NULL,
10512 edge_sql TEXT DEFAULT NULL)
10513 RETURNS VOID AS
10514$$
10515DECLARE
10516 e RECORD;
10517 sv TEXT[];
10518 st UUID[];
10519 sp double precision[];
10520 val TEXT;
10521 vid INT;
10522 vids INT[] := ARRAY[]::INT[];
10523 tok UUID;
10524 toks UUID[] := ARRAY[]::UUID[];
10525 cover_token UUID;
10526 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
10527BEGIN
10528 BEGIN
10529 IF source_rel IS NOT NULL THEN
10530 SELECT g.source_values, g.source_tokens, g.source_probabilities
10531 INTO sv, st, sp
10532 FROM provsql.gather_reachability_sources(source_rel,
10533 source_rel_attribute) g;
10534 IF sv IS NULL THEN
10535 sv := ARRAY[]::TEXT[];
10536 st := ARRAY[]::UUID[];
10537 sp := ARRAY[]::float8[];
10538 END IF;
10539 ELSE
10540 sv := ARRAY[source_value];
10541 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
10542 sp := ARRAY[1.0::float8];
10543 END IF;
10544
10545 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
10546 destination_attribute,
10547 sv, edge_quals, edge_sql);
10548
10549 -- The bound vertices and their per-row reach tokens, with the
10550 -- multiplicity the self-join produces. A vertex absent from the
10551 -- graph, or from the working table, means the join is empty: no
10552 -- row will exist, nothing to plant.
10553 FOREACH val IN ARRAY node_values LOOP
10554 vid := array_position(e.vertices, val);
10555 IF vid IS NULL THEN
10556 RETURN;
10557 END IF;
10558 vids := vids || vid;
10559 EXECUTE format('SELECT provsql FROM %I WHERE %I::TEXT = $1',
10560 work_name, node_attribute)
10561 INTO tok USING val;
10562 IF tok IS NULL THEN
10563 RETURN;
10564 END IF;
10565 toks := toks || tok;
10566 END LOOP;
10567
10568 cover_token := provsql.reachability_materialize_cover(
10569 e.sources, e.destinations, e.tokens, e.probabilities,
10570 e.block_keys, e.block_indices, e.extra_ids, st, sp,
10571 directed, vids);
10572
10573 PERFORM provsql.plant_canonical(work_name, 'times', toks, cover_token, 1);
10574 IF verbosity >= 20 THEN
10575 -- Lift the function-level client_min_messages = warning for the
10576 -- one RAISE; the function-level SET restores the caller's value.
10577 PERFORM set_config('client_min_messages', 'notice', true);
10578 RAISE NOTICE 'ProvSQL: certified all-members gate planted for the self-join of "%"',
10579 work_name;
10580 PERFORM set_config('client_min_messages', 'warning', true);
10581 END IF;
10582 EXCEPTION WHEN OTHERS THEN
10583 IF verbosity >= 10 THEN
10584 PERFORM set_config('client_min_messages', 'notice', true);
10585 RAISE NOTICE 'ProvSQL: all-members planting for "%" skipped (%)',
10586 work_name, SQLERRM;
10587 PERFORM set_config('client_min_messages', 'warning', true);
10588 END IF;
10589 END;
10590END
10591-- No SET search_path: the deparsed edge subquery must resolve against
10592-- the caller's path; ProvSQL internals are schema-qualified.
10593$$ LANGUAGE plpgsql SET client_min_messages = warning;
10594
10595/**
10596 * @brief Input leaves of a conjunction-shaped provenance token (internal)
10597 *
10598 * Descends a token's circuit through the conjunctive gate types
10599 * (@c times, and the pass-through @c project / @c eq where-provenance
10600 * wrappers) down to @c input leaves. Returns the distinct leaves, or
10601 * NULL when the circuit contains any other gate type (a disjunctive or
10602 * aggregate shape, which is not a conjunction of independent tuples).
10603 * Used by the reachability gathering to accept join-defined edges:
10604 * a derived edge whose token is a pure conjunction of base tuples.
10605 *
10606 * @param token the provenance token
10607 */
10608CREATE OR REPLACE FUNCTION token_conjunctive_leaves(token UUID)
10609 RETURNS UUID[] AS
10610$$
10611WITH RECURSIVE walk(g) AS (
10612 SELECT token
10613 UNION
10614 SELECT c FROM walk w, unnest(provsql.get_children(w.g)) AS c
10615 WHERE provsql.get_gate_type(w.g) IN ('times', 'project', 'eq', 'annotation')
10616)
10617SELECT CASE WHEN bool_and(provsql.get_gate_type(g)
10618 IN ('times', 'project', 'eq', 'annotation', 'input'))
10619 THEN array_agg(DISTINCT g)
10620 FILTER (WHERE provsql.get_gate_type(g) = 'input')
10621 ELSE NULL END
10622FROM walk;
10623$$ LANGUAGE sql STABLE;
10624
10625/**
10626 * @brief Gather the edges of a tracked relation in the columnar form
10627 * expected by reachability_evaluate (internal)
10628 *
10629 * Materializes the edge relation with its provenance tokens and
10630 * probabilities, maps arbitrary vertex values (compared as TEXT) onto
10631 * dense INTEGER IDs, and checks that every edge tuple carries a base
10632 * input token (independent tuples): reachability compilation along the
10633 * data is only correct when the edges are independent events, so views
10634 * or query results with derived provenance are rejected.
10635 *
10636 * @param rel the provenance-tracked edge relation
10637 * @param source_attribute name of the source-vertex column
10638 * @param destination_attribute name of the destination-vertex column
10639 * @param extra_vertices vertex values (as TEXT) that must be part of
10640 * the dense ID space even when they touch no edge -- the source
10641 * set in particular; their IDs come back in @c extra_ids
10642 * (aligned with the input)
10643 * @param edge_quals optional deterministic filter over the edge
10644 * relation's columns (SQL TEXT, deparsed by the rewriter from
10645 * the recursive arm's WHERE clause), restricting which edges
10646 * participate
10647 * @param rel_sql deparsed edge subquery to gather from instead of
10648 * @p rel (join-defined edges); the tokens are then conjunctions
10649 * of base tuples, validated for shape and disjoint supports
10650 *
10651 * The @c vertices output maps the dense IDs back to the original
10652 * vertex values (as TEXT, 1-indexed), for callers that need to label
10653 * per-vertex results.
10654 *
10655 * @param[out] sources source vertex (dense ID) of each gathered edge
10656 * @param[out] destinations destination vertex (dense ID) of each edge
10657 * @param[out] tokens provenance token of each edge tuple
10658 * @param[out] probabilities probability of each edge tuple
10659 * @param[out] block_keys per-edge BID key variable (nil UUID = independent)
10660 * @param[out] block_indices per-edge outcome index within its block
10661 * @param[out] extra_ids dense IDs assigned to the @p extra_vertices
10662 * @param[out] vertices dense-ID-to-original-value map (TEXT, 1-indexed)
10663 */
10664CREATE OR REPLACE FUNCTION gather_reachability_edges(
10665 IN rel REGCLASS,
10666 IN source_attribute TEXT,
10667 IN destination_attribute TEXT,
10668 IN extra_vertices TEXT[],
10669 IN edge_quals TEXT DEFAULT NULL,
10670 IN rel_sql TEXT DEFAULT NULL,
10671 OUT sources INT[],
10672 OUT destinations INT[],
10673 OUT tokens UUID[],
10674 OUT probabilities DOUBLE PRECISION[],
10675 OUT block_keys UUID[],
10676 OUT block_indices INT[],
10677 OUT extra_ids INT[],
10678 OUT vertices TEXT[])
10679AS
10680$$
10681DECLARE
10682 tkind TEXT;
10683 bkey_expr TEXT;
10684 sel_probs TEXT;
10685 sel_bkeys TEXT;
10686 sel_bidx TEXT;
10687 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
10688BEGIN
10689 -- Consult the per-table characterisation registry (TID / BID / OPAQUE,
10690 -- maintained by add_provenance / repair_key and the CTAS lineage hook):
10691 -- a TID relation is certified all-independent-inputs, a BID relation
10692 -- holds input or mulinput rows with the block structure given by the
10693 -- registry's key columns. Derived (OPAQUE), unregistered, or
10694 -- subquery-defined edges take the fully dynamic per-token path.
10695 IF rel IS NOT NULL AND rel_sql IS NULL THEN
10696 tkind := (provsql.get_table_info(rel::oid)).kind;
10697 END IF;
10698 IF tkind NOT IN ('tid', 'bid') THEN
10699 tkind := NULL;
10700 END IF;
10701 IF tkind = 'bid' THEN
10702 SELECT string_agg(quote_ident(a.attname) || '::TEXT', ' || '','' || '
10703 ORDER BY k.ord)
10704 INTO bkey_expr
10705 FROM unnest((provsql.get_table_info(rel::oid)).block_key)
10706 WITH ORDINALITY AS k(attnum, ord)
10707 JOIN pg_attribute a ON a.attrelid = rel AND a.attnum = k.attnum;
10708 -- An empty registry key means the whole table is one block.
10709 bkey_expr := coalesce(bkey_expr, quote_literal(''));
10710 END IF;
10711 IF tkind IS NOT NULL AND verbosity >= 20 THEN
10712 -- The function-level client_min_messages = warning (which silences
10713 -- the CTAS / DROP TABLE chatter) would also swallow this notice;
10714 -- lift it for the one RAISE. The function-level SET restores the
10715 -- caller's value at exit regardless.
10716 PERFORM set_config('client_min_messages', 'notice', true);
10717 RAISE NOTICE 'ProvSQL: catalog characterises % as %', rel, upper(tkind);
10718 PERFORM set_config('client_min_messages', 'warning', true);
10719 END IF;
10720
10721 -- Materialize the edges with their tokens; the planner hook resolves
10722 -- provenance() over the tracked relation, and remove_provenance strips
10723 -- the automatic provsql column so the later aggregation is plain SQL.
10724 -- For a BID relation the synthetic per-block key (a v5 UUID over the
10725 -- registry key columns' values) is computed here, while the columns
10726 -- are in scope.
10727 DROP TABLE IF EXISTS provsql_reachability_edges_tmp;
10728 EXECUTE format(
10729 'CREATE TEMP TABLE provsql_reachability_edges_tmp AS '
10730 || 'SELECT %1$I::TEXT AS u, %2$I::TEXT AS v, '
10731 || 'provsql.strip_annotations(provsql.provenance()) AS token%5$s '
10732 || 'FROM %3$s WHERE %1$I IS NOT NULL AND %2$I IS NOT NULL%4$s',
10733 source_attribute, destination_attribute,
10734 CASE WHEN rel_sql IS NULL THEN rel::TEXT
10735 ELSE '(' || rel_sql || ') AS provsql_edge_subquery' END,
10736 CASE WHEN edge_quals IS NULL THEN ''
10737 ELSE ' AND (' || edge_quals || ')' END,
10738 CASE WHEN tkind = 'bid'
10739 THEN ', public.uuid_generate_v5(provsql.uuid_ns_provsql(), '
10740 || quote_literal('bidblock' || rel::TEXT || ':')
10741 || ' || ' || bkey_expr || ') AS bkey'
10742 ELSE ', NULL::UUID AS bkey' END);
10743 PERFORM provsql.remove_provenance('provsql_reachability_edges_tmp');
10744
10745 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
10746 IF tkind IS NULL THEN
10747 -- Dynamic path: validate the token shapes and, for conjunction-shaped
10748 -- (join-defined) tokens, the pairwise disjointness of their supports.
10749 IF EXISTS (SELECT 1 FROM provsql_reachability_edges_tmp
10750 WHERE provsql.get_gate_type(token) NOT IN ('input', 'mulinput', 'times',
10751 'project', 'eq')) THEN
10752 DROP TABLE provsql_reachability_edges_tmp;
10753 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input, repair_key, or conjunctive join tokens', coalesce(rel::TEXT, 'the edge query')
10754 USING ERRCODE = 'feature_not_supported',
10755 DETAIL = 'provsql-reason: reachability-provenance-shape; scope: gap';
10756 END IF;
10757 CREATE TEMP TABLE provsql_reachability_support_tmp AS
10758 SELECT t.token, l.leaf
10759 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp
10760 WHERE provsql.get_gate_type(token) IN ('times', 'project', 'eq')) t,
10761 LATERAL unnest(provsql.token_conjunctive_leaves(t.token)) AS l(leaf);
10762 IF EXISTS (SELECT 1
10763 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp) t
10764 WHERE provsql.get_gate_type(t.token) IN ('times', 'project', 'eq')
10765 AND provsql.token_conjunctive_leaves(t.token) IS NULL) THEN
10766 DROP TABLE provsql_reachability_support_tmp;
10767 DROP TABLE provsql_reachability_edges_tmp;
10768 RAISE EXCEPTION 'reachability: a join-defined edge token is not a pure conjunction of base tuples'
10769 USING ERRCODE = 'feature_not_supported',
10770 DETAIL = 'provsql-reason: reachability-edge-not-conjunction; scope: gap';
10771 END IF;
10772 IF EXISTS (SELECT 1 FROM (
10773 SELECT leaf FROM provsql_reachability_support_tmp
10774 UNION ALL
10775 SELECT DISTINCT token FROM provsql_reachability_edges_tmp
10776 WHERE provsql.get_gate_type(token) = 'input'
10777 ) all_leaves
10778 GROUP BY leaf HAVING count(*) > 1) THEN
10779 DROP TABLE provsql_reachability_support_tmp;
10780 DROP TABLE provsql_reachability_edges_tmp;
10781 RAISE EXCEPTION 'reachability: join-defined edges share base tuples (their supports overlap), so they are not independent'
10782 USING ERRCODE = 'feature_not_supported',
10783 DETAIL = 'provsql-reason: reachability-edges-share-tuples; scope: gap';
10784 END IF;
10785 END IF;
10786
10787 -- Per-kind classification expressions for the final aggregation: a TID
10788 -- relation needs no per-row gate introspection at all; a BID relation
10789 -- one get_gate_type per row (the input/mulinput split), block keys from
10790 -- the precomputed column-derived key and indices by numbering within
10791 -- the block; the dynamic path reads the gates.
10792 IF tkind = 'tid' THEN
10793 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
10794 sel_bkeys := $sql$'00000000-0000-0000-0000-000000000000'::UUID$sql$;
10795 sel_bidx := '0';
10796 ELSIF tkind = 'bid' THEN
10797 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
10798 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
10799 THEN e.bkey
10800 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
10801 sel_bidx := 'e.bidx';
10802 ELSE
10803 sel_probs := $sql$CASE WHEN provsql.get_gate_type(e.token) IN ('times','project','eq')
10804 THEN (SELECT CASE WHEN bool_or(coalesce(provsql.get_prob(s.leaf),1.0) = 0)
10805 THEN 0.0
10806 ELSE exp(sum(ln(coalesce(provsql.get_prob(s.leaf),1.0)))) END
10807 FROM provsql_reachability_support_tmp s
10808 WHERE s.token = e.token)
10809 ELSE coalesce(provsql.get_prob(e.token), 1.0) END$sql$;
10810 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
10811 THEN (provsql.get_children(e.token))[1]
10812 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
10813 sel_bidx := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
10814 THEN (provsql.get_infos(e.token)).info1 ELSE 0 END$sql$;
10815 END IF;
10816
10817 EXECUTE format(
10818 $sql$
10819 WITH verts AS (
10820 SELECT u AS x FROM provsql_reachability_edges_tmp
10821 UNION SELECT v FROM provsql_reachability_edges_tmp
10822 UNION SELECT unnest($1)),
10823 ids AS (
10824 SELECT x, (row_number() OVER (ORDER BY x))::INT AS id FROM verts)
10825 SELECT array_agg(iu.id), array_agg(iv.id),
10826 array_agg(e.token),
10827 array_agg(%s),
10828 array_agg(%s),
10829 array_agg(%s),
10830 (SELECT array_agg(i.id ORDER BY ev.ord)
10831 FROM unnest($1) WITH ORDINALITY AS ev(x, ord)
10832 JOIN ids i ON i.x = ev.x),
10833 (SELECT array_agg(x ORDER BY id) FROM ids)
10834 FROM (SELECT t.*,
10835 (row_number() OVER (PARTITION BY t.bkey))::INT AS bidx
10836 FROM provsql_reachability_edges_tmp t) e
10837 JOIN ids iu ON iu.x = e.u
10838 JOIN ids iv ON iv.x = e.v
10839 $sql$, sel_probs, sel_bkeys, sel_bidx)
10840 INTO sources, destinations, tokens, probabilities, block_keys,
10841 block_indices, extra_ids, vertices
10842 USING extra_vertices;
10843
10844 DROP TABLE provsql_reachability_edges_tmp;
10845 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
10846END
10847-- No SET search_path: the deparsed edge subquery (and the REGCLASS
10848-- rendering) must resolve against the caller's search_path; the ProvSQL
10849-- calls above are schema-qualified instead.
10850$$ LANGUAGE plpgsql SET client_min_messages = warning;
10851
10852
10853/**
10854 * @brief Gather a source relation's vertices, tokens and probabilities
10855 * (internal)
10856 *
10857 * For a provenance-tracked source relation, every tuple must carry a
10858 * base @c input token (a *probabilistic source set*); for an untracked
10859 * relation the sources are certain and the tokens come back as the nil
10860 * UUID. Vertex values are returned as TEXT, for the shared dense-ID
10861 * mapping of @c gather_reachability_edges().
10862 *
10863 * @param rel the source relation
10864 * @param source_attribute name of the vertex column
10865 * @param[out] source_values vertex value of each source tuple (as TEXT)
10866 * @param[out] source_tokens per-source base @c input token (nil UUID = certain)
10867 * @param[out] source_probabilities per-source probability
10868 */
10869CREATE OR REPLACE FUNCTION gather_reachability_sources(
10870 IN rel REGCLASS,
10871 IN source_attribute TEXT,
10872 OUT source_values TEXT[],
10873 OUT source_tokens UUID[],
10874 OUT source_probabilities DOUBLE PRECISION[])
10875AS
10876$$
10877DECLARE
10878 tracked BOOLEAN;
10879 tkind TEXT;
10880BEGIN
10881 SELECT EXISTS (
10882 SELECT 1 FROM pg_attribute
10883 WHERE attrelid = rel AND attname = 'provsql'
10884 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped)
10885 INTO tracked;
10886
10887 -- Registry consultation: a TID source relation is certified
10888 -- all-base-input, so the per-row gate check can be skipped; a BID one
10889 -- holds block-correlated tuples, which a probabilistic source set
10890 -- cannot model -- reject it before gathering anything.
10891 IF tracked THEN
10892 tkind := (get_table_info(rel::oid)).kind;
10893 IF tkind = 'bid' THEN
10894 RAISE EXCEPTION 'reachability: % is block-independent (repair_key); block-correlated source sets are not supported', rel
10895 USING ERRCODE = 'feature_not_supported',
10896 DETAIL = 'provsql-reason: reachability-block-correlated; scope: gap';
10897 END IF;
10898 END IF;
10899
10900 DROP TABLE IF EXISTS provsql_reachability_sources_tmp;
10901 IF tracked THEN
10902 EXECUTE format(
10903 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
10904 || 'SELECT %1$I::TEXT AS x, provenance() AS token '
10905 || 'FROM %2$s WHERE %1$I IS NOT NULL',
10906 source_attribute, rel);
10907 PERFORM remove_provenance('provsql_reachability_sources_tmp');
10908 IF tkind IS DISTINCT FROM 'tid'
10909 AND EXISTS (SELECT 1 FROM provsql_reachability_sources_tmp
10910 WHERE get_gate_type(token) <> 'input') THEN
10911 DROP TABLE provsql_reachability_sources_tmp;
10912 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input tokens (independent tuples); views or query results are not supported', rel
10913 USING ERRCODE = 'feature_not_supported',
10914 DETAIL = 'provsql-reason: reachability-not-base-inputs; scope: gap';
10915 END IF;
10916 SELECT array_agg(x), array_agg(token),
10917 array_agg(coalesce(get_prob(token), 1.0))
10918 INTO source_values, source_tokens, source_probabilities
10919 FROM provsql_reachability_sources_tmp;
10920 DROP TABLE provsql_reachability_sources_tmp;
10921 ELSE
10922 EXECUTE format(
10923 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
10924 || 'SELECT DISTINCT %1$I::TEXT AS x FROM %2$s WHERE %1$I IS NOT NULL',
10925 source_attribute, rel);
10926 SELECT array_agg(x),
10927 array_agg('00000000-0000-0000-0000-000000000000'::UUID),
10928 array_agg(1.0::float8)
10929 INTO source_values, source_tokens, source_probabilities
10930 FROM provsql_reachability_sources_tmp;
10931 DROP TABLE provsql_reachability_sources_tmp;
10932 END IF;
10933END
10934$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SET client_min_messages = warning;
10935
10936/**
10937 * @brief Fixpoint driver for the recursive reachability shape:
10938 * decomposition-aligned compilation with fallback to eval_recursive
10939 *
10940 * Called (at plan time, over SPI) by the recursive-CTE lowering when
10941 * the provenance class is 'absorptive' or 'BOOLEAN'
10942 * (@c provsql.provenance) and the CTE matches the linear
10943 * reachability shape over a tracked base edge relation. Attempts the
10944 * decomposition-aligned route -- gather the edges, compile every
10945 * reachable vertex's certified provenance circuit along a tree
10946 * decomposition of the data graph, materialise them, and fill the
10947 * working table with one tokenised row per reachable vertex. On any
10948 * failure (data treewidth above the cap, per-node state bound, edges
10949 * that are not independent base tuples...), falls back to the generic
10950 * @c eval_recursive() fixpoint, preserving its behaviour exactly.
10951 *
10952 * @param edge_rel the provenance-tracked edge relation
10953 * @param source_attribute name of the source-vertex column
10954 * @param destination_attribute name of the destination-vertex column
10955 * @param source_value the base arm's constant, as TEXT
10956 * @param directed if false, each edge can be traversed both ways
10957 * @param work_name name of the working temp table (the CTE name)
10958 * @param colnames comma-separated user column names (for the fallback)
10959 * @param coldef column definitions of the working table
10960 * @param coltype type of the CTE's single column
10961 * @param body_sql deparsed CTE body (for the fallback)
10962 * @param edge_quals optional deterministic filter over edge columns
10963 * (deparsed from the recursive arm's WHERE clause)
10964 * @param source_rel source relation of a multi-source base arm
10965 * (@c SELECT col FROM sources), NULL for the constant form;
10966 * tracked sources form a probabilistic source set, untracked
10967 * ones are certain
10968 * @param source_rel_attribute the source relation's vertex column
10969 * @param edge_sql deparsed edge subquery when the recursive arm joins a
10970 * derived (join-defined) edge relation instead of a base one;
10971 * NULL for the REGCLASS form
10972 * @param hop_bound maximum number of recursive steps for the
10973 * hop-counting CTE shape (NULL for plain reachability)
10974 * @param hop_seed the base arm's hop constant (hop-counting shape)
10975 * @param hops_position 1-based position of the hop column among the
10976 * CTE's two columns (hop-counting shape)
10977 */
10978CREATE OR REPLACE FUNCTION eval_reachability(
10979 edge_rel REGCLASS,
10980 source_attribute TEXT,
10981 destination_attribute TEXT,
10982 source_value TEXT,
10983 directed BOOLEAN,
10984 work_name TEXT,
10985 colnames TEXT,
10986 coldef TEXT,
10987 coltype TEXT,
10988 body_sql TEXT,
10989 edge_quals TEXT DEFAULT NULL,
10990 source_rel REGCLASS DEFAULT NULL,
10991 source_rel_attribute TEXT DEFAULT NULL,
10992 edge_sql TEXT DEFAULT NULL,
10993 hop_bound INT DEFAULT NULL,
10994 hop_seed INT DEFAULT NULL,
10995 hops_position INT DEFAULT NULL)
10996 RETURNS VOID AS
10997$$
10998DECLARE
10999 e RECORD;
11000 sv TEXT[];
11001 st UUID[];
11002 sp double precision[];
11003 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
11004BEGIN
11005 BEGIN
11006 IF source_rel IS NOT NULL THEN
11007 -- Multi-source: gather the source relation (probabilistic when
11008 -- tracked, certain otherwise).
11009 SELECT g.source_values, g.source_tokens, g.source_probabilities
11010 INTO sv, st, sp
11011 FROM provsql.gather_reachability_sources(source_rel,
11012 source_rel_attribute) g;
11013 IF sv IS NULL THEN
11014 sv := ARRAY[]::TEXT[];
11015 st := ARRAY[]::UUID[];
11016 sp := ARRAY[]::float8[];
11017 END IF;
11018 ELSE
11019 -- Constant base arm: one certain source.
11020 sv := ARRAY[source_value];
11021 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
11022 sp := ARRAY[1.0::float8];
11023 END IF;
11024
11025 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
11026 destination_attribute,
11027 sv, edge_quals, edge_sql);
11028 IF to_regclass(work_name) IS NOT NULL THEN
11029 EXECUTE format('DROP TABLE %I', work_name);
11030 END IF;
11031 /* Dropped with the statement, as the other drivers' tables are. */
11032 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID) ON COMMIT DROP',
11033 work_name, coldef);
11034 PERFORM provsql.planted_scope(work_name);
11035 IF hop_bound IS NULL THEN
11036 EXECUTE format(
11037 'INSERT INTO %I SELECT ($1::TEXT[])[m.vertex]::%s, m.token '
11038 || 'FROM provsql.reachability_materialize($2, $3, $4, $5, $6, $7, $8, $9, $10, $11) m',
11039 work_name, coltype)
11040 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
11041 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed;
11042 ELSE
11043 -- Hop-counting shape: one row per (vertex, walk length), the hop
11044 -- column in its CTE position.
11045 EXECUTE format(
11046 'INSERT INTO %I SELECT %s, m.token '
11047 || 'FROM provsql.reachability_materialize_hops($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) m',
11048 work_name,
11049 CASE WHEN hops_position = 1
11050 THEN format('m.hops, ($1::TEXT[])[m.vertex]::%s', coltype)
11051 ELSE format('($1::TEXT[])[m.vertex]::%s, m.hops', coltype) END)
11052 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
11053 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed,
11054 hop_bound, hop_seed;
11055 END IF;
11056 IF verbosity >= 20 THEN
11057 RAISE NOTICE 'ProvSQL: recursive CTE "%" compiled along a tree decomposition of %',
11058 regexp_replace(work_name, '^provsql_rec_[0-9]+a?_', ''),
11059 coalesce(edge_rel::TEXT, 'the join-defined edge query');
11060 END IF;
11061 EXCEPTION WHEN OTHERS THEN
11062 IF verbosity >= 10 THEN
11063 /* Named as the user named the CTE: the working table carries a name of
11064 ours (provsql_rec_<n>_<cte>), which is no business of a message. */
11065 RAISE NOTICE 'ProvSQL: reachability route for "%" fell back to the generic fixpoint (%)',
11066 regexp_replace(work_name, '^provsql_rec_[0-9]+a?_', ''), SQLERRM;
11067 END IF;
11068 PERFORM provsql.eval_recursive(body_sql, work_name, colnames, coldef);
11069 END;
11070END
11071$$ LANGUAGE plpgsql;
11072
11073
11074
11075/** @} */
11076
11077/** @defgroup provenance_output Provenance output
11078 * Functions for visualizing and exporting provenance circuits
11079 * in various formats.
11080 * @{
11081 */
11082
11083/**
11084 * @brief Return a DOT or TEXT visualization of the provenance circuit
11085 *
11086 * @param token root provenance token
11087 * @param token2desc mapping table for gate descriptions
11088 * @param dbg debug level (0 = normal)
11089 */
11090CREATE OR REPLACE FUNCTION view_circuit(
11091 token UUID,
11092 token2desc REGCLASS,
11093 dbg INT = 0)
11094 RETURNS TEXT AS
11095 'provsql','view_circuit' LANGUAGE C;
11096
11097/**
11098 * @brief Return a DOT visualisation of the d-DNNF compiled from the
11099 * provenance circuit
11100 *
11101 * Runs the requested external knowledge compiler and renders the
11102 * resulting d-DNNF as a GraphViz digraph.
11103 *
11104 * @param token root provenance token
11105 * @param compiler external compiler or in-process meta-route to invoke;
11106 * empty (the default) picks the highest-preference available compiler
11107 */
11108CREATE OR REPLACE FUNCTION compile_to_ddnnf_dot(
11109 token UUID,
11110 compiler TEXT = '')
11111 RETURNS TEXT AS
11112 'provsql','compile_to_ddnnf_dot' LANGUAGE C;
11113
11114/**
11115 * @brief Return the compiled d-DNNF of a provenance circuit in the
11116 * c2d / d4 ".nnf" TEXT interchange format.
11117 *
11118 * Companion to compile_to_ddnnf_dot (DOT, for viewing): this is the
11119 * machine-readable form, suitable for feeding to an external d-DNNF
11120 * reasoner / verifier or saving next to tseytin_cnf (same variable
11121 * numbering). Accepts the same compiler / meta-route names.
11122 *
11123 * @param token root provenance token
11124 * @param compiler compiler or in-process meta-route to use; empty (the
11125 * default) picks the highest-preference available compiler
11126 */
11127CREATE OR REPLACE FUNCTION compile_to_ddnnf(
11128 token UUID,
11129 compiler TEXT = '')
11130 RETURNS TEXT AS
11131 'provsql','compile_to_ddnnf' LANGUAGE C;
11132
11133/**
11134 * @brief Structural statistics of the d-DNNF a compiler produces for a
11135 * provenance circuit.
11136 *
11137 * Compiles the circuit with the given compiler / meta-route (same names
11138 * as compile_to_ddnnf_dot: d4, d4v2, c2d, minic2d, dsharp, panini-*,
11139 * tree-decomposition, interpret-as-dd, default) and returns a jsonb
11140 * object: nodes, edges, and / or / not / inputs counts, smooth, depth
11141 * (longest path), treewidth (null when not computable), and compile_ms.
11142 * Lets clients compare what each compiler produces on the same circuit.
11143 *
11144 * @param token root provenance token
11145 * @param compiler compiler or in-process meta-route to use; empty (the
11146 * default) picks the highest-preference available compiler
11147 */
11148CREATE OR REPLACE FUNCTION ddnnf_stats(
11149 token UUID,
11150 compiler TEXT = '')
11151 RETURNS jsonb AS
11152 'provsql','ddnnf_stats' LANGUAGE C;
11153
11154/**
11155 * @brief Return the DIMACS CNF (Tseytin transformation) of the provenance circuit
11156 *
11157 * Returns the same encoding the extension writes to a temp file before
11158 * invoking d4 / c2d / minic2d / dsharp. With @c weighted true (the
11159 * default), per-input probability weights are appended as @c w lines.
11160 *
11161 * @param token root provenance token
11162 * @param weighted include probability weights when true
11163 * @param mapping prepend "c input <var> <UUID> <prob>" comment lines
11164 * documenting which provenance input each variable stands for
11165 */
11166CREATE OR REPLACE FUNCTION tseytin_cnf(
11167 token UUID,
11168 weighted BOOLEAN = TRUE,
11169 mapping BOOLEAN = TRUE)
11170 RETURNS TEXT AS
11171 'provsql','tseytin_cnf' LANGUAGE C;
11172
11173/**
11174 * @brief Map each DIMACS variable of tseytin_cnf back to its
11175 * provenance input.
11176 *
11177 * Returns one row per input gate: the variable index (matching
11178 * tseytin_cnf and compile_to_ddnnf's NNF), the original-circuit UUID
11179 * of that input, and its probability. Lets a satisfying assignment or
11180 * weighted model count obtained from an external tool be read against
11181 * the provenance circuit.
11182 *
11183 * @param token root provenance token
11184 */
11185CREATE OR REPLACE FUNCTION tseytin_cnf_mapping_json(token UUID)
11186 RETURNS jsonb AS
11187 'provsql','tseytin_cnf_mapping_json' LANGUAGE C;
11188
11189CREATE OR REPLACE FUNCTION tseytin_cnf_mapping(token UUID)
11190 RETURNS TABLE(variable INT, gate UUID, probability FLOAT8) AS $$
11191 SELECT variable, gate, probability
11192 FROM jsonb_to_recordset(tseytin_cnf_mapping_json(token))
11193 AS x(variable INT, gate UUID, probability FLOAT8)
11194 ORDER BY variable
11195$$ LANGUAGE SQL STABLE;
11196
11197/**
11198 * @brief Return a DOT visualisation of the tree decomposition of the
11199 * provenance circuit
11200 *
11201 * Computes the min-fill decomposition used by the in-process
11202 * knowledge compiler. The first line of the output is a comment of
11203 * the form @c "// treewidth=<n>".
11204 *
11205 * @param token root provenance token
11206 */
11207CREATE OR REPLACE FUNCTION tree_decomposition_dot(
11208 token UUID)
11209 RETURNS TEXT AS
11210 'provsql','tree_decomposition_dot' LANGUAGE C;
11211
11212/**
11213 * @brief Report whether an external tool is on the backend's resolved PATH
11214 *
11215 * Uses the same @c find_external_tool() helper that the compilers
11216 * (d4 / c2d / minic2d / dsharp / panini), model counters (ganak /
11217 * sharpsat-td / dpmc via htb+dmc / weightmc), and visualisation
11218 * wrappers (graph-easy, dot) themselves consult, so the result
11219 * reflects exactly what a subsequent @c probability_evaluate or
11220 * @c view_circuit call would see, including the
11221 * @c provsql.tool_search_path GUC prepended to @c $PATH.
11222 *
11223 * Names with a slash are treated as paths and tested directly via
11224 * @c access(X_OK); bare names are resolved through @c /bin/sh's
11225 * @c command -v under the backend's PATH.
11226 *
11227 * @param name bare executable (e.g. @c 'd4') or an absolute path
11228 * @return true iff the tool resolves to an executable file
11229 */
11230CREATE OR REPLACE FUNCTION tool_available(name TEXT)
11231 RETURNS BOOLEAN AS
11232 'provsql','tool_available' LANGUAGE C STRICT;
11233
11234/* ----------------------------------------------------------------------
11235 * External-tool registry
11236 *
11237 * A catalog of the external tools ProvSQL can invoke (the knowledge
11238 * compilers, weighted model counters, and the graph-easy DOT renderer).
11239 * The default tools and their invocations are compiled in (seeded in C), so
11240 * out-of-the-box behaviour is unchanged with no configuration.
11241 *
11242 * Administrators may add / repoint / reorder / disable tools at run time;
11243 * those changes are persisted in the @c provsql.tool_overrides table below
11244 * and overlaid on the compiled seed, so they survive across sessions and
11245 * backends (and dump/restore). An empty overrides table means exactly the
11246 * compiled defaults. The mutators are superuser-only because a tool RECORD
11247 * names an executable run as the PostgreSQL OS user (the same trust level as
11248 * provsql.tool_search_path).
11249 * ---------------------------------------------------------------------- */
11250
11251/**
11252 * @brief Persistent overrides overlaid on the compiled-in tool seed.
11253 *
11254 * Each row is the complete desired RECORD for a tool (added or modified) keyed
11255 * by logical @c name, or a tombstone (@c removed = true) hiding a seeded
11256 * default. The effective registry is the compiled seed with tombstoned names
11257 * removed and the remaining rows upserted over it. Written only by the
11258 * superuser-only register_tool / unregister_tool / set_tool_* functions;
11259 * read back into each backend's in-memory registry on demand. Marked as a
11260 * configuration table so pg_dump carries an operator's registrations.
11261 */
11262CREATE TABLE IF NOT EXISTS tool_overrides(
11263 name TEXT PRIMARY KEY,
11264 removed BOOLEAN NOT NULL DEFAULT false,
11265 kind TEXT,
11266 executable TEXT,
11267 operations TEXT[],
11268 input_formats TEXT[],
11269 output_format TEXT,
11270 parser TEXT,
11271 preference INT,
11272 enabled BOOLEAN,
11273 dependencies TEXT[],
11274 argtpl TEXT,
11275 argtpl_circuit TEXT,
11276 endpoint TEXT
11277);
11278SELECT pg_catalog.pg_extension_config_dump('tool_overrides', '');
11279
11280/**
11281 * @brief Set-returning listing backing the @c provsql.tools view.
11282 *
11283 * @c operations / @c input_formats / @c output_format use the KCMCP
11284 * shared-registry names (see the KCMCP server protocol), so a CLI RECORD and
11285 * a future kcmcp-server RECORD are comparable; @c parser is the CLI-only tag
11286 * for how to decode the tool's raw output. @c argtpl is the command template
11287 * ({in}/{out}/... placeholders). @c available is true iff @c executable
11288 * (when set) and every dependency currently resolve on the backend's PATH.
11289 */
11290CREATE OR REPLACE FUNCTION tool_registry_list()
11291 RETURNS TABLE(name TEXT, kind TEXT, executable TEXT, operations TEXT[],
11292 input_formats TEXT[], output_format TEXT, parser TEXT,
11293 preference INT, enabled BOOLEAN, argtpl TEXT,
11294 argtpl_circuit TEXT, endpoint TEXT, available BOOLEAN) AS
11295 'provsql','tool_registry_list' LANGUAGE C STABLE;
11296
11297/**
11298 * @brief Read-only view of the registered tools.
11299 */
11300CREATE OR REPLACE VIEW tools AS
11301 SELECT name, kind, executable, operations, input_formats, output_format,
11302 parser, preference, enabled, argtpl, argtpl_circuit, endpoint,
11303 available
11304 FROM tool_registry_list();
11305
11306/**
11307 * @brief Register a tool, or replace the RECORD with the same logical name.
11308 *
11309 * @param name logical id (e.g. @c 'd4-jm62300'); also the value
11310 * @c provsql.fallback_compiler / the wmc tool selector use
11311 * @param executable executable to resolve on PATH (defaults to @c name)
11312 * @param kind @c 'cli' (spawn @c executable) or @c 'kcmcp' (talk to
11313 * the KCMCP server at @c endpoint)
11314 * @param operations capabilities (KCMCP names): @c 'compile' / @c 'wmc'
11315 * (and ProvSQL-local @c 'render')
11316 * @param input_formats accepted inputs (KCMCP names): @c 'dimacs-cnf',
11317 * @c 'circuit-bcs12' (listing @c 'circuit-bcs12' enables
11318 * the native-circuit fast path)
11319 * @param output_format result encoding (KCMCP names): @c 'ddnnf-nnf',
11320 * @c 'decimal', @c 'rational', ... (local @c 'panini-dd'
11321 * / @c 'ascii' where KCMCP has no code)
11322 * @param parser CLI-only decode tag: @c 'nnf' (the tolerant d4 / c2d
11323 * NNF reader), @c 'panini-dd', @c 'wmc-line',
11324 * @c 'weightmc', @c 'ascii'
11325 * @param argtpl command template; placeholders @c {in} / @c {out}
11326 * (and @c {binary} / @c {tmpdir} / @c {pivotAC}). When
11327 * it omits @c {binary}, the executable is prepended.
11328 * @param argtpl_circuit command used when the @c 'circuit-bcs12' input is
11329 * selected (a BC-S1.2 circuit rather than a CNF); only a
11330 * tool accepting that input needs it
11331 * @param preference ordering within an operation (higher first)
11332 * @param enabled whether the dispatchers may select it
11333 * @param endpoint for a @c 'kcmcp' RECORD, the server address:
11334 * @c 'unix:/path' or @c 'host:port'
11335 *
11336 * Superuser-only: a CLI RECORD runs an arbitrary command as the PostgreSQL
11337 * OS user, and a kcmcp RECORD names a socket the server connects to.
11338 */
11339CREATE OR REPLACE FUNCTION register_tool(
11340 name TEXT,
11341 executable TEXT DEFAULT NULL,
11342 kind TEXT DEFAULT 'cli',
11343 operations TEXT[] DEFAULT NULL,
11344 input_formats TEXT[] DEFAULT NULL,
11345 output_format TEXT DEFAULT NULL,
11346 parser TEXT DEFAULT NULL,
11347 argtpl TEXT DEFAULT NULL,
11348 argtpl_circuit TEXT DEFAULT NULL,
11349 preference INT DEFAULT 0,
11350 enabled BOOLEAN DEFAULT true,
11351 endpoint TEXT DEFAULT NULL)
11352 RETURNS VOID AS
11353 'provsql','tool_registry_register' LANGUAGE C;
11354
11355/** @brief Unregister a tool; errors on an unknown tool name. Superuser-only. */
11356CREATE OR REPLACE FUNCTION unregister_tool(name TEXT)
11357 RETURNS VOID AS
11358 'provsql','tool_registry_unregister' LANGUAGE C STRICT;
11359
11360/** @brief Enable/disable a tool; errors on an unknown tool name. Superuser-only. */
11361CREATE OR REPLACE FUNCTION set_tool_enabled(name TEXT, enabled BOOLEAN)
11362 RETURNS VOID AS
11363 'provsql','tool_registry_set_enabled' LANGUAGE C STRICT;
11364
11365/** @brief Set a tool's preference; errors on an unknown tool name. Superuser-only. */
11366CREATE OR REPLACE FUNCTION set_tool_preference(name TEXT, preference INT)
11367 RETURNS VOID AS
11368 'provsql','tool_registry_set_preference' LANGUAGE C STRICT;
11369
11370-- The mutators guard at the C level too, but revoke from PUBLIC so the
11371-- superuser requirement is visible in the catalog.
11372REVOKE ALL ON FUNCTION register_tool(TEXT, TEXT, TEXT, TEXT[], TEXT[], TEXT, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT) FROM PUBLIC;
11373REVOKE ALL ON FUNCTION unregister_tool(TEXT) FROM PUBLIC;
11374REVOKE ALL ON FUNCTION set_tool_enabled(TEXT, BOOLEAN) FROM PUBLIC;
11375REVOKE ALL ON FUNCTION set_tool_preference(TEXT, INT) FROM PUBLIC;
11376
11377/**
11378 * @brief Return an XML representation of the provenance circuit
11379 *
11380 * @param token root provenance token
11381 * @param token2desc optional mapping table for gate descriptions
11382 */
11383CREATE OR REPLACE FUNCTION to_provxml(
11384 token UUID,
11385 token2desc REGCLASS = NULL)
11386 RETURNS TEXT AS
11387 'provsql','to_provxml' LANGUAGE C;
11388
11389/** @brief Return the provenance token of the current query result tuple */
11390CREATE OR REPLACE FUNCTION provenance() RETURNS UUID AS
11391 'provsql', 'provenance' LANGUAGE C;
11392
11393/**
11394 * @brief Compute where-provenance for a result tuple
11395 *
11396 * Returns a TEXT representation showing which input columns
11397 * contributed to each output column.
11398 */
11399CREATE OR REPLACE FUNCTION where_provenance(token UUID)
11400 RETURNS TEXT AS
11401 'provsql','where_provenance' LANGUAGE C;
11402
11403/** @} */
11404
11405/** @defgroup circuit_init Circuit initialization
11406 * Functions and statements executed at extension load time to
11407 * reset internal caches and create the constant zero/one gates.
11408 * @{
11409 */
11410
11411/** @brief Reset the internal cache of OID constants used by the query rewriter */
11412CREATE OR REPLACE FUNCTION reset_constants_cache()
11413 RETURNS VOID AS
11414 'provsql', 'reset_constants_cache' LANGUAGE C;
11415
11416SELECT reset_constants_cache();
11417
11418SELECT create_gate(gate_zero(), 'zero');
11419SELECT create_gate(gate_one(), 'one');
11420SELECT create_gate(gate_null(), 'value', NULL, NULL, NULL, 'NULL');
11421
11422/** @} */
11423
11424/** @brief Types of update operations tracked for temporal provenance */
11425CREATE TYPE QUERY_TYPE_ENUM AS ENUM (
11426 'INSERT', -- Row was inserted
11427 'DELETE', -- Row was deleted
11428 'UPDATE', -- Row was updated
11429 'UNDO', -- Previous operation was undone
11430 'TRANSACTION', -- The transaction the statements below belong to
11431 'REPLACE' -- An update gate given a different probability
11432 );
11433
11434/** @defgroup compiled_semirings Compiled semirings
11435 * Definitions of compiled semirings
11436 * @{
11437 */
11438
11439/** @brief Evaluate provenance as a symbolic formula (e.g., "a ⊗ b ⊕ c") */
11440-- The mapping is optional (as for sr_boolexpr): formula renders whatever
11441-- circuit it is given, and the measure-carrier circuits it is most useful
11442-- on (random variables, arithmetic, mixtures) have no leaf mapping at all.
11443-- Without one, input leaves render as the semiring's 𝟙.
11444CREATE FUNCTION sr_formula(token ANYELEMENT, token2value REGCLASS = NULL)
11445 RETURNS VARCHAR AS
11446$$
11447BEGIN
11448 IF token IS NULL THEN
11449 RETURN NULL;
11450 END IF;
11451 RETURN provsql.provenance_evaluate_compiled(
11452 token,
11453 token2value,
11454 'formula',
11455 '𝟙'::VARCHAR
11456 );
11457END
11458$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
11459
11460/** @brief Evaluate provenance over the counting semiring (ℕ) */
11461CREATE FUNCTION sr_counting(token ANYELEMENT, token2value REGCLASS)
11462 RETURNS INT AS
11463$$
11464BEGIN
11465 RETURN provsql.provenance_evaluate_compiled(
11466 token,
11467 token2value,
11468 'counting',
11469 1
11470 );
11471END
11472$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11473
11474/** @brief Evaluate provenance as why-provenance (set of witness sets) */
11475CREATE FUNCTION sr_why(token ANYELEMENT, token2value REGCLASS)
11476 RETURNS VARCHAR AS
11477$$
11478BEGIN
11479 RETURN provsql.provenance_evaluate_compiled(
11480 token,
11481 token2value,
11482 'why',
11483 '{}'::VARCHAR
11484 );
11485END
11486$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11487
11488/** @brief Evaluate provenance as how-provenance (canonical polynomial provenance ℕ[X], universal commutative-semiring provenance) */
11489CREATE FUNCTION sr_how(token ANYELEMENT, token2value REGCLASS)
11490 RETURNS VARCHAR AS
11491$$
11492BEGIN
11493 RETURN provsql.provenance_evaluate_compiled(
11494 token,
11495 token2value,
11496 'how',
11497 '{}'::VARCHAR
11498 );
11499END
11500$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11501
11502/** @brief Evaluate provenance as which-provenance (lineage: a single set of contributing labels) */
11503CREATE FUNCTION sr_which(token ANYELEMENT, token2value REGCLASS)
11504 RETURNS VARCHAR AS
11505$$
11506BEGIN
11507 RETURN provsql.provenance_evaluate_compiled(
11508 token,
11509 token2value,
11510 'which',
11511 '{}'::VARCHAR
11512 );
11513END
11514$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11515
11516/** @brief Evaluate provenance as a Boolean expression
11517 *
11518 * The optional @p token2value mapping labels the leaves of the
11519 * formula: when omitted, leaves are rendered as bare @c x@<id@>
11520 * placeholders.
11521 */
11522CREATE FUNCTION sr_boolexpr(token ANYELEMENT, token2value REGCLASS = NULL)
11523 RETURNS VARCHAR AS
11524$$
11525BEGIN
11526 IF token IS NULL THEN
11527 RETURN NULL;
11528 END IF;
11529 RETURN provsql.provenance_evaluate_compiled(
11530 token,
11531 token2value,
11532 'boolexpr',
11533 '⊤'::VARCHAR
11534 );
11535END
11536$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
11537
11538/** @brief Evaluate provenance over the Boolean semiring (true/false)
11539 *
11540 * The optional @p token2value mapping gives the Boolean value of the
11541 * leaves; a leaf it does not map, and every leaf when it is omitted, is
11542 * true. Without a mapping, the result is whether the token holds in the
11543 * database as it is, every input tuple present.
11544 */
11545CREATE FUNCTION sr_boolean(token ANYELEMENT, token2value REGCLASS = NULL)
11546 RETURNS BOOLEAN AS
11547$$
11548BEGIN
11549 IF token IS NULL THEN
11550 RETURN NULL;
11551 END IF;
11552 RETURN provsql.provenance_evaluate_compiled(
11553 token,
11554 token2value,
11555 'BOOLEAN',
11556 TRUE
11557 );
11558END
11559$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
11560
11561/**
11562 * @brief Whether a token holds in the database as it is (internal)
11563 *
11564 * @c sr_boolean(token) without a mapping, with a fast path for leaves. The
11565 * rewriter filters by it the rows a displayed aggregate value reads, so that
11566 * the value is the one plain SQL computes on the same data.
11567 */
11568CREATE FUNCTION plain_truth(token UUID)
11569 RETURNS BOOLEAN AS
11570 'provsql', 'plain_truth' LANGUAGE C PARALLEL SAFE STABLE;
11571
11572/** @brief Structural universal-zero test (C backend of nonzero's default mode) */
11573CREATE FUNCTION true_nonzero(token UUID)
11574 RETURNS BOOLEAN AS
11575 'provsql', 'true_nonzero' LANGUAGE C PARALLEL SAFE STABLE;
11576
11577/**
11578 * @brief Test whether a provenance annotation is nonzero.
11579 *
11580 * Returns false only on a *proof* that the annotation is zero; true
11581 * otherwise, so filtering with <tt>WHERE nonzero(provenance())</tt> never
11582 * discards a row whose annotation could be nonzero.
11583 *
11584 * The default mode (@p semiring NULL) tests *universal* zero-ness: zero in
11585 * every (m-)semiring under every leaf valuation, decided by sound
11586 * structural rules (zero propagation through the gates; a comparison gate
11587 * whose satisfying-world set is empty). Filtering on it can never
11588 * contradict any downstream semiring evaluation.
11589 *
11590 * A named @p semiring evaluates the circuit there and tests against that
11591 * semiring's zero: 'BOOLEAN' is presence in the vanilla SQL answer on this
11592 * instance (the mode that filters, e.g., the null-padded arm of a
11593 * difference), 'counting' is bag multiplicity. An absent @p mapping reads
11594 * every leaf as the semiring's one (true / 1); with a mapping, leaves take
11595 * their mapped values.
11596 *
11597 * A NULL @p token reads as the neutral 1 (an untracked row): true.
11598 *
11599 * @param token provenance token to test
11600 * @param semiring NULL (universal zero test), 'BOOLEAN', or 'counting'
11601 * @param mapping optional mapping table from tokens to leaf values
11602 */
11603CREATE FUNCTION nonzero(token UUID,
11604 semiring TEXT DEFAULT NULL,
11605 mapping REGCLASS DEFAULT NULL)
11606 RETURNS BOOLEAN AS
11607$$
11608BEGIN
11609 IF token IS NULL THEN
11610 RETURN true;
11611 END IF;
11612 IF semiring IS NULL THEN
11613 RETURN provsql.true_nonzero(token);
11614 ELSIF semiring = 'BOOLEAN' THEN
11615 IF mapping IS NULL THEN
11616 /* Every leaf true: that truth is plain_truth, which walks the gates
11617 * whose truth follows from their children's and reads a comparison of
11618 * aggregate results off the values they RECORD. Reading such a
11619 * comparison over the worlds of what it aggregates, as the evaluation
11620 * below does, costs one term per subset of its contributions. */
11621 RETURN provsql.plain_truth(token);
11622 END IF;
11623 RETURN provsql.provenance_evaluate_compiled(token, mapping, 'BOOLEAN', TRUE);
11624 ELSIF semiring = 'counting' THEN
11625 RETURN provsql.provenance_evaluate_compiled(token, mapping, 'counting', 1) <> 0;
11626 ELSE
11627 RAISE EXCEPTION 'nonzero: unsupported semiring "%" (supported: BOOLEAN, counting; NULL for the universal zero test)', semiring
11628 USING ERRCODE = 'feature_not_supported',
11629 DETAIL = 'provsql-reason: nonzero-semiring; scope: gap';
11630 END IF;
11631END
11632$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
11633
11634/**
11635 * @brief Presence in the vanilla SQL answer on this instance.
11636 *
11637 * <tt>WHERE present(provenance())</tt> restores the result set the query
11638 * has without provenance tracking, filtering the zero-annotated extras
11639 * (antijoin arms, failed HAVING groups, unknown comparisons) that the
11640 * rewriting keeps visible.
11641 *
11642 * Shorthand for <tt>nonzero(token, 'BOOLEAN')</tt> with every leaf true,
11643 * which reads that truth off the gates (@c plain_truth).
11644 */
11645CREATE FUNCTION present(token UUID)
11646 RETURNS BOOLEAN AS
11647$$
11648 SELECT provsql.nonzero(token, 'BOOLEAN');
11649$$ LANGUAGE sql PARALLEL SAFE STABLE;
11650
11651/** @brief Evaluate provenance over the tropical (min-plus) m-semiring
11652 *
11653 * Inputs are read as %float8 cost values; the additive identity
11654 * is <tt>'Infinity'::%float8</tt> and the multiplicative identity is 0.
11655 * Returns the cost of the cheapest derivation.
11656 *
11657 * With @p nonnegative, input costs are checked nonnegative and the
11658 * semiring is *absorptive*: evaluation then also accepts circuits
11659 * carrying the @c 'absorptive' assumption marker -- notably cyclic
11660 * recursive queries truncated at the absorptive value fixpoint, giving
11661 * exact min-cost reachability on cyclic data.
11662 */
11663CREATE FUNCTION sr_tropical(token ANYELEMENT, token2value REGCLASS,
11664 nonnegative BOOLEAN = false)
11665 RETURNS FLOAT AS
11666$$
11667BEGIN
11668 RETURN provsql.provenance_evaluate_compiled(
11669 token,
11670 token2value,
11671 CASE WHEN nonnegative THEN 'tropical_nonneg' ELSE 'tropical' END,
11672 0::FLOAT
11673 );
11674END
11675$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11676
11677/** @brief Evaluate provenance over the Viterbi (max-times) m-semiring
11678 *
11679 * Inputs are read as %float8 probability values in @f$[0,1]@f$.
11680 * Returns the probability of the most likely derivation.
11681 */
11682CREATE FUNCTION sr_viterbi(token ANYELEMENT, token2value REGCLASS)
11683 RETURNS FLOAT AS
11684$$
11685BEGIN
11686 RETURN provsql.provenance_evaluate_compiled(
11687 token,
11688 token2value,
11689 'viterbi',
11690 1::FLOAT
11691 );
11692END
11693$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11694
11695/** @brief Evaluate provenance over the Łukasiewicz fuzzy m-semiring
11696 *
11697 * Inputs are read as %float8 graded-truth values in @f$[0,1]@f$.
11698 * Addition is @f$\max@f$; multiplication is the Łukasiewicz t-norm
11699 * @f$\max(a + b - 1, 0)@f$, which preserves crisp truth and avoids
11700 * the near-zero collapse of long product chains.
11701 */
11702CREATE FUNCTION sr_lukasiewicz(token ANYELEMENT, token2value REGCLASS)
11703 RETURNS FLOAT AS
11704$$
11705BEGIN
11706 RETURN provsql.provenance_evaluate_compiled(
11707 token,
11708 token2value,
11709 'lukasiewicz',
11710 1::FLOAT
11711 );
11712END
11713$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11714
11715/** @brief Evaluate provenance over the min-max m-semiring on a user ENUM
11716 *
11717 * Inputs are read as values of a user-defined ENUM carrier; addition
11718 * is ENUM-min, multiplication is ENUM-max. Bottom and top of the ENUM
11719 * are derived from @c pg_enum.enumsortorder. The third argument is a
11720 * sample value of the carrier ENUM, used only for type inference; its
11721 * value is ignored.
11722 *
11723 * The security shape: alternative derivations combine to the least
11724 * sensitive label, joins combine to the most sensitive label.
11725 *
11726 * @param token Provenance token to evaluate.
11727 * @param token2value Mapping from input gates to ENUM values.
11728 * @param element_one Sample value of the carrier ENUM (any value works).
11729 */
11730CREATE FUNCTION sr_minmax(token UUID, token2value REGCLASS, element_one ANYENUM)
11731 RETURNS ANYENUM AS
11732$$
11733BEGIN
11734 RETURN provsql.provenance_evaluate_compiled(
11735 token,
11736 token2value,
11737 'minmax',
11738 element_one
11739 );
11740END
11741$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11742
11743/** @brief Evaluate provenance over the max-min m-semiring on a user ENUM
11744 *
11745 * Dual of :sqlfunc:`sr_minmax`: addition is ENUM-max, multiplication
11746 * is ENUM-min. The fuzzy / availability / trust shape: alternatives
11747 * combine to the most permissive label, joins combine to the strictest
11748 * label. The third argument is a sample value of the carrier ENUM,
11749 * used only for type inference; its value is ignored.
11750 *
11751 * @param token Provenance token to evaluate.
11752 * @param token2value Mapping from input gates to ENUM values.
11753 * @param element_one Sample value of the carrier ENUM (any value works).
11754 */
11755CREATE FUNCTION sr_maxmin(token UUID, token2value REGCLASS, element_one ANYENUM)
11756 RETURNS ANYENUM AS
11757$$
11758BEGIN
11759 RETURN provsql.provenance_evaluate_compiled(
11760 token,
11761 token2value,
11762 'maxmin',
11763 element_one
11764 );
11765END
11766$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
11767
11768/** @} */
11769
11770/** @defgroup choose_aggregate choose aggregate
11771 * Choose one value among many, used in particular to code a mutually
11772 * exclusive choice as an aggregate.
11773 * @{
11774 */
11775
11776/** @brief Transition function for the choose aggregate (keeps first non-NULL value) */
11777CREATE FUNCTION choose_function(state ANYELEMENT, data ANYELEMENT)
11778 RETURNS ANYELEMENT AS
11779$$
11780BEGIN
11781 IF state IS NULL THEN
11782 RETURN data;
11783 ELSE
11784 RETURN state;
11785 END IF;
11786END
11787$$ LANGUAGE plpgsql PARALLEL SAFE IMMUTABLE;
11788
11789/** @brief Aggregate that returns an arbitrary non-NULL value from a group */
11790CREATE AGGREGATE choose(ANYELEMENT) (
11791 SFUNC = choose_function,
11792 STYPE = ANYELEMENT
11793);
11794
11795/** @brief Transition function of @c array_collect */
11796CREATE FUNCTION array_collect_step(state ANYARRAY, data ANYNONARRAY)
11797 RETURNS ANYARRAY AS
11798$$ SELECT array_append(state, data) $$
11799LANGUAGE sql PARALLEL SAFE IMMUTABLE;
11800
11801/** @brief @c array_agg, but the empty array over no rows, not NULL
11802 *
11803 * This is the value of @c ARRAY(SELECT ...): ProvSQL rewrites such a
11804 * subquery into this aggregate over the matching rows. */
11805CREATE AGGREGATE array_collect(ANYNONARRAY) (
11806 SFUNC = array_collect_step,
11807 STYPE = ANYARRAY,
11808 INITCOND = '{}'
11809);
11810
11811/** @brief Explodes a table column containing aggregated provenance into multiple rows.
11812 *
11813 * For each row in the input table, this function unnests the children of the
11814 * specified aggregate token column and produces one output row per child.
11815 * It reconstructs the corresponding value and provenance (`provsql`) for
11816 * each resulting row.
11817 *
11818 * The original table is replaced by the transformed table.
11819 *
11820 * @param _tbl Name of the table to transform.
11821 * @param AGG_TOKEN Name of the column containing the aggregate to explode.
11822 */
11823CREATE OR REPLACE FUNCTION explode_table(_tbl TEXT, AGG_TOKEN TEXT)
11824RETURNS VOID AS $$
11825DECLARE
11826 _nsp TEXT;
11827BEGIN
11828 -- Resolve the schema actually holding _tbl so the rebuilt table is
11829 -- recreated in place (the provsql helper functions are schema-qualified
11830 -- so this works whatever the caller's search_path is).
11831 SELECT n.nspname INTO _nsp
11832 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
11833 WHERE c.oid = _tbl::REGCLASS;
11834
11835 EXECUTE format('
11836 CREATE TABLE %1$I.temp_exploded AS
11837 SELECT
11838 %2$I.*,
11839 provsql.get_extra(children[2]) AS new_t,
11840 provsql.provenance_times(children[1], provsql) AS new_provsql
11841 FROM %1$I.%2$I,
11842 LATERAL (
11843 SELECT provsql.get_children(sm) AS children
11844 FROM UNNEST(provsql.agg_token_explode_children(%3$I)) AS sm
11845 ) AS sub', _nsp, _tbl, AGG_TOKEN);
11846 EXECUTE format('DROP TABLE %I.%I', _nsp, _tbl);
11847 EXECUTE format('ALTER TABLE %I.temp_exploded DROP COLUMN %I, DROP COLUMN provsql', _nsp, AGG_TOKEN);
11848 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_t TO %I', _nsp, AGG_TOKEN);
11849 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_provsql TO provsql', _nsp);
11850 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME TO %I', _nsp, _tbl);
11851END;
11852$$ LANGUAGE plpgsql;
11853
11854/** @} */
11855
11856/**
11857 * @brief Append @c provsql to this database's default search_path, if missing.
11858 *
11859 * ProvSQL's operators and functions live in the @c provsql schema and
11860 * are resolved through @c search_path. When @c provsql is absent from
11861 * the path some surfaces fail with a clear error (RV/AGG_TOKEN
11862 * arithmetic), but others can be silently misrouted by an implicit
11863 * cross-domain cast. This helper makes the common case painless: it
11864 * reads the current <em>database-level</em> search_path setting from
11865 * @c pg_db_role_setting, appends @c provsql if not already present
11866 * (never replacing or reordering the existing entries), and applies the
11867 * result with @c ALTER @c DATABASE. It is idempotent and emits a
11868 * @c NOTICE describing what it did.
11869 *
11870 * Only @b new sessions pick up the change; the calling session keeps its
11871 * current path. Role-level settings (if any) take precedence over the
11872 * database-level setting and are left untouched. The caller must be the
11873 * database owner or a superuser (the privilege model of @c ALTER
11874 * @c DATABASE). Returns the resulting search_path value.
11875 */
11876CREATE OR REPLACE FUNCTION setup_search_path()
11877 RETURNS TEXT
11878 LANGUAGE plpgsql AS $$
11879DECLARE
11880 db TEXT := current_database();
11881 cfg TEXT[];
11882 cur TEXT; -- existing database-level search_path value
11883 new_path TEXT;
11884BEGIN
11885 -- setrole = 0 selects the database-wide default, not a per-role override.
11886 SELECT s.setconfig INTO cfg
11887 FROM pg_db_role_setting s
11888 JOIN pg_database d ON d.oid = s.setdatabase
11889 WHERE d.datname = db AND s.setrole = 0;
11890
11891 IF cfg IS NOT NULL THEN
11892 SELECT substr(e, length('search_path=') + 1) INTO cur
11893 FROM unnest(cfg) AS e
11894 WHERE e LIKE 'search_path=%';
11895 END IF;
11896
11897 IF cur IS NULL THEN
11898 -- No database-level search_path at all: install the documented
11899 -- default with provsql appended.
11900 new_path := '"$user", public, provsql';
11901 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
11902 RAISE NOTICE 'ProvSQL: set search_path = % for database "%" (no previous database-level setting). Only new sessions are affected.',
11903 new_path, db;
11904 RETURN new_path;
11905 END IF;
11906
11907 -- Already contains provsql as a path element? Idempotent no-op.
11908 IF EXISTS (
11909 SELECT 1 FROM unnest(string_to[](cur, ',')) AS p
11910 WHERE btrim(btrim(p), '"') = 'provsql')
11911 THEN
11912 RAISE NOTICE 'ProvSQL: search_path for database "%" already contains provsql (= %); no change.',
11913 db, cur;
11914 RETURN cur;
11915 END IF;
11916
11917 new_path := cur || ', provsql';
11918 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
11919 RAISE NOTICE 'ProvSQL: appended provsql to search_path for database "%" (now: %). Only new sessions are affected.',
11920 db, new_path;
11921 RETURN new_path;
11922END;
11923$$;
11924
11925GRANT USAGE ON SCHEMA provsql TO PUBLIC;
11926
11927SET search_path TO public;
11928
11929-- Installation-time advisory: if provsql is not in the database's default
11930-- search_path, point the user at setup_search_path(). reset_val reflects
11931-- the configured session default (postgresql.conf / ALTER DATABASE / ALTER
11932-- ROLE), unaffected by the SET search_path statements this script ran.
11933-- CREATE EXTENSION raises client_min_messages to WARNING for the duration
11934-- of the script, so we lower it around the RAISE NOTICE. SET LOCAL only:
11935-- it unwinds by itself when CREATE EXTENSION's transaction ends. An
11936-- explicit save/restore here would capture the WARNING clamp (already in
11937-- force when this block runs) and restore *that* at session level,
11938-- leaving the whole installing session with NOTICEs suppressed.
11939DO $$
11940DECLARE
11941 rp TEXT;
11942 has_provsql BOOLEAN;
11943BEGIN
11944 SELECT reset_val INTO rp FROM pg_settings WHERE name = 'search_path';
11945 SELECT bool_or(btrim(btrim(p), '"') = 'provsql')
11946 INTO has_provsql
11947 FROM unnest(string_to[](coalesce(rp, ''), ',')) AS p;
11948 IF NOT coalesce(has_provsql, false) THEN
11949 SET LOCAL client_min_messages = notice;
11950 RAISE NOTICE 'ProvSQL: schema "provsql" is not in your default search_path (currently: %).', rp;
11951 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());
11952 END IF;
11953END;
11954$$;
11955
11956-- Final constants-cache refresh. The planned SELECT statements earlier in
11957-- this script (reset_constants_cache itself, the zero/one create_gate calls)
11958-- make the installing session memoize the OID constants *mid-script*, while
11959-- objects defined later (notably the choose aggregate, used by the
11960-- scalar-subquery decorrelation) do not exist yet. Their optional lookups
11961-- then stay InvalidOid for the rest of the session, silently disabling the
11962-- corresponding rewrites (e.g. IN/NOT IN over a tracked relation would raise
11963-- "Subqueries ... not supported") until a new connection. Refreshing here,
11964-- after every object exists, repairs the installing session's cache.
11965SELECT provsql.reset_constants_cache();
11966SET search_path TO provsql;
11967
11968/** @defgroup update_provenance Update provenance (PostgreSQL 14+)
11969 * Extended provenance tracking for INSERT, UPDATE, DELETE, and UNDO
11970 * operations, including temporal validity ranges.
11971 * @{
11972 */
11973
11974/**
11975 * @brief Table recording the history of INSERT, UPDATE, DELETE, and UNDO operations
11976 *
11977 * Each row records one provenance-tracked modification, linking the
11978 * operation's provenance token to metadata (query TEXT, type, user,
11979 * TIMESTAMP) and the temporal validity range of the affected rows.
11980 *
11981 * A row of type @c TRANSACTION stands for the transaction the statements
11982 * around it belong to (see @c provsql.transaction_token): @c xid is its
11983 * transaction id and its own @c tx_token is NULL, while every statement
11984 * row of that transaction carries the transaction's token in @c tx_token.
11985 * A modified tuple's provenance names both -- the effect is
11986 * @c times(tx_token, statement_token) -- so @c undo() reverses either one
11987 * statement or the whole transaction, and the temporal semiring reads the
11988 * transaction's validity through the shared factor.
11989 *
11990 * @c ts and the lower bound of @c valid_time are stamped at commit, not
11991 * at the statement: @c CURRENT_TIMESTAMP is the transaction's start time,
11992 * so two overlapping transactions could otherwise commit in the opposite
11993 * order of their recorded validity.
11994 */
11995CREATE TABLE update_provenance (
11996 provsql UUID,
11997 query TEXT,
11998 query_type QUERY_TYPE_ENUM,
11999 username TEXT,
12000 ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
12001 valid_time TSTZMULTIRANGE DEFAULT TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
12002 xid xid8,
12003 tx_token UUID
12004);
12005/**
12006 * @brief The update gate standing for the current transaction
12007 *
12008 * Each data-modification statement already mints an @c update gate of its
12009 * own, but nothing tied the statements of one transaction together: a
12010 * reader of @c update_provenance could not tell that two rows came from
12011 * the same transaction, and @c undo() could reverse a statement but not
12012 * "the transaction". This mints one gate per transaction, on the first
12013 * tracked modification, and hands the same one back for the rest of it.
12014 *
12015 * Where a transaction's gate lives is @c SET @c LOCAL, so it vanishes
12016 * when the transaction ends, whether it commits or rolls back -- and a
12017 * rolled-back transaction leaves no @c update_provenance row for it
12018 * either, since that row is an ordinary heap insert.
12019 */
12020CREATE OR REPLACE FUNCTION transaction_token()
12021RETURNS UUID
12022LANGUAGE plpgsql
12023AS $$
12024DECLARE
12025 tok TEXT;
12026 new_tok UUID;
12027 query_text TEXT;
12028BEGIN
12029 tok := current_setting('provsql.transaction_token', true);
12030 IF tok IS NOT NULL AND tok <> '' THEN
12031 RETURN tok::UUID;
12032 END IF;
12033
12034 new_tok := public.uuid_generate_v4();
12035 PERFORM create_gate(new_tok, 'update');
12036 PERFORM set_config('provsql.transaction_token', new_tok::TEXT, true);
12037
12038 -- A transaction has no query TEXT of its own: query is left NULL, so
12039 -- looking a statement up by its TEXT finds the statement and not the
12040 -- transaction that carried it.
12041 query_text := NULL;
12042
12043 -- The transaction's own validity is the universal range, the
12044 -- multiplicative identity of the temporal m-semiring: it is a factor of
12045 -- every effect of the transaction, and what a tuple is valid for is the
12046 -- statement's business, not the transaction's. Giving it a real
12047 -- interval would intersect it into every one of them.
12048 INSERT INTO update_provenance(provsql, query, query_type, username, ts,
12049 valid_time, xid)
12050 VALUES (new_tok, query_text, 'TRANSACTION', current_user,
12051 CURRENT_TIMESTAMP, '{(,)}'::TSTZMULTIRANGE,
12052 pg_current_xact_id());
12053
12054 RETURN new_tok;
12055END;
12056$$;
12057
12058/**
12059 * @brief Deferred trigger stamping a log row with its commit time
12060 *
12061 * @c CURRENT_TIMESTAMP is the transaction's *start* time, so two
12062 * overlapping transactions can commit in the opposite order of the
12063 * validity they recorded. This fires at commit -- it is a constraint
12064 * trigger declared @c DEFERRABLE @c INITIALLY @c DEFERRED -- and moves
12065 * the row's TIMESTAMP and the lower bound of its validity to
12066 * @c clock_timestamp(), which by then is the commit time to within the
12067 * commit itself.
12068 */
12069CREATE OR REPLACE FUNCTION stamp_commit_time()
12070 RETURNS trigger AS
12071$$
12072DECLARE
12073 now_ts TIMESTAMPTZ := clock_timestamp();
12074BEGIN
12075 UPDATE update_provenance
12076 SET ts = now_ts,
12077 valid_time = CASE WHEN query_type = 'TRANSACTION' THEN valid_time
12078 ELSE TSTZMULTIRANGE(tstzrange(now_ts, NULL)) END
12079 WHERE provsql = NEW.provsql;
12080 RETURN NULL;
12081END;
12082$$ LANGUAGE plpgsql;
12083
12084DO $$ BEGIN
12085 IF NOT EXISTS (SELECT 1 FROM pg_trigger
12086 WHERE tgrelid = 'provsql.update_provenance'::REGCLASS
12087 AND tgname = 'stamp_commit_time') THEN
12088 CREATE CONSTRAINT TRIGGER stamp_commit_time
12089 AFTER INSERT ON provsql.update_provenance
12090 DEFERRABLE INITIALLY DEFERRED
12091 FOR EACH ROW EXECUTE FUNCTION provsql.stamp_commit_time();
12092 END IF;
12093END $$;
12094
12095/** @cond INTERNAL */
12096/* Enable provenance tracking on an existing table (PostgreSQL 14+ version).
12097 * Overrides the common version; documented via add_provenance in provsql.common.sql. */
12098CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
12099 RETURNS VOID AS
12100$$
12101BEGIN
12102 -- Idempotence: a second add_provenance on an already-tracked table is
12103 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
12104 -- re-run freely.
12105 IF EXISTS (
12106 SELECT 1 FROM pg_attribute
12107 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
12108 ) THEN
12109 RAISE NOTICE 'table % already has provenance tracking', _tbl;
12110 RETURN;
12111 END IF;
12112 -- See the common-version body for the rationale of dropping the
12113 -- column DEFAULT and UNIQUE in favour of provenance_guard + a
12114 -- plain index.
12115 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
12116 EXECUTE format(
12117 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
12118 _tbl);
12119 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
12120 EXECUTE format(
12121 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
12122 'ON %s FOR EACH ROW EXECUTE FUNCTION provsql.provenance_guard()',
12123 _tbl);
12124
12125 EXECUTE format('CREATE TRIGGER insert_statement AFTER INSERT ON %s REFERENCING NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE FUNCTION provsql.insert_statement_trigger()', _tbl);
12126 EXECUTE format('CREATE TRIGGER delete_statement AFTER DELETE ON %s REFERENCING OLD TABLE AS OLD_TABLE FOR EACH STATEMENT EXECUTE FUNCTION provsql.delete_statement_trigger()', _tbl);
12127 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 FUNCTION provsql.update_statement_trigger()', _tbl);
12128
12129 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
12130 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
12131 -- A view defined before this call selected the columns the table had then,
12132 -- so it has no provsql column and never will: PostgreSQL resolved its
12133 -- "SELECT *" at definition time. A query over such a view is answered
12134 -- without provenance and without a warning -- the rewriting sees a relation
12135 -- that carries none -- so the one place where saying it is useful is here,
12136 -- where recreating the view is the remedy.
12137 DECLARE
12138 stale TEXT;
12139 BEGIN
12140 SELECT string_agg(DISTINCT v.rel::REGCLASS::TEXT, ', ') INTO stale
12141 FROM (SELECT r.ev_class AS rel
12142 FROM pg_catalog.pg_depend d
12143 JOIN pg_catalog.pg_rewrite r ON r.oid = d.objid
12144 WHERE d.classid = 'pg_catalog.pg_rewrite'::REGCLASS
12145 AND d.refclassid = 'pg_catalog.pg_class'::REGCLASS
12146 AND d.refobjid = _tbl
12147 AND r.ev_class <> _tbl) AS v
12148 WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_attribute a
12149 WHERE a.attrelid = v.rel AND a.attname = 'provsql'
12150 AND NOT a.attisdropped);
12151 IF stale IS NOT NULL THEN
12152 RAISE WARNING 'ProvSQL: % is read by views defined before it was '
12153 'tracked, which have no provenance column of their own, '
12154 'so a query over one of them is answered as plain SQL, '
12155 'not tracked: %',
12156 _tbl, stale
12157 USING HINT = 'recreate the view (CREATE OR REPLACE VIEW ... or DROP and '
12158 'CREATE) so that its definition reads the tracked table',
12159 DETAIL = 'provsql-reason: view-defined-before-tracking; '
12160 'scope: gap';
12161 END IF;
12162 END;
12163END
12164$$ LANGUAGE plpgsql SECURITY DEFINER;
12165/** @endcond */
12166
12167/** @cond INTERNAL */
12168/* Trigger function for DELETE statement provenance tracking (PostgreSQL 14+).
12169 * Overrides the common version; documented via delete_statement_trigger in provsql.common.sql. */
12170CREATE OR REPLACE FUNCTION delete_statement_trigger()
12171 RETURNS TRIGGER AS
12172$$
12173DECLARE
12174 query_text TEXT;
12175 delete_token UUID;
12176 old_token UUID;
12177 new_token UUID;
12178 r RECORD;
12179 tx_token UUID;
12180 enable_trigger BOOL;
12181BEGIN
12182 enable_trigger := current_setting('provsql.update_provenance', true);
12183 IF enable_trigger = 'f' THEN
12184 RETURN NULL;
12185 END IF;
12186 delete_token := public.uuid_generate_v4();
12187
12188 PERFORM create_gate(delete_token, 'update');
12189
12190 SELECT query
12191 INTO query_text
12192 FROM pg_stat_activity
12193 WHERE pid = pg_backend_pid();
12194
12195 tx_token := transaction_token();
12196
12197 INSERT INTO update_provenance (provsql, query, query_type, username, ts,
12198 valid_time, xid, tx_token)
12199 VALUES (delete_token, query_text, 'DELETE', current_user, CURRENT_TIMESTAMP,
12200 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
12201 pg_current_xact_id(), tx_token);
12202
12203 -- The effect this statement has on a row names both the statement and
12204 -- the transaction it belongs to, so undo() can reverse either.
12205 delete_token := provenance_times(tx_token, delete_token);
12206
12207 PERFORM set_config('provsql.update_provenance', 'off', false);
12208 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
12209 PERFORM set_config('provsql.update_provenance', 'on', false);
12210
12211 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
12212 old_token := r.provsql;
12213 new_token := provenance_monus(old_token, delete_token);
12214
12215 PERFORM set_config('provsql.update_provenance', 'off', false);
12216 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
12217 USING new_token, old_token;
12218 PERFORM set_config('provsql.update_provenance', 'on', false);
12219 END LOOP;
12220
12221 RETURN NULL;
12222END
12223$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
12224/** @endcond */
12225
12226/**
12227 * @brief Trigger function for INSERT statement provenance tracking
12228 *
12229 * Records the insertion in update_provenance and multiplies provenance
12230 * tokens of inserted rows with the insert token.
12231 */
12232CREATE OR REPLACE FUNCTION insert_statement_trigger()
12233 RETURNS TRIGGER AS
12234$$
12235DECLARE
12236 query_text TEXT;
12237 insert_token UUID;
12238 old_token UUID;
12239 new_token UUID;
12240 r RECORD;
12241 tx_token UUID;
12242 enable_trigger BOOL;
12243BEGIN
12244 enable_trigger := current_setting('provsql.update_provenance', true);
12245 IF enable_trigger = 'f' THEN
12246 RETURN NULL;
12247 END IF;
12248
12249 insert_token := public.uuid_generate_v4();
12250
12251 PERFORM create_gate(insert_token, 'update');
12252
12253 SELECT query
12254 INTO query_text
12255 FROM pg_stat_activity
12256 WHERE pid = pg_backend_pid();
12257
12258 tx_token := transaction_token();
12259
12260 INSERT INTO update_provenance (provsql, query, query_type, username, ts,
12261 valid_time, xid, tx_token)
12262 VALUES (insert_token, query_text, 'INSERT', current_user, CURRENT_TIMESTAMP,
12263 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
12264 pg_current_xact_id(), tx_token);
12265
12266 -- The effect this statement has on a row names both the statement and
12267 -- the transaction it belongs to, so undo() can reverse either.
12268 insert_token := provenance_times(tx_token, insert_token);
12269
12270 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
12271 old_token := r.provsql;
12272 new_token := provenance_times(old_token, insert_token);
12273 PERFORM set_config('provsql.update_provenance', 'off', false);
12274 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
12275 USING new_token, old_token;
12276 PERFORM set_config('provsql.update_provenance', 'on', false);
12277 END LOOP;
12278
12279 RETURN NULL;
12280END
12281$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
12282
12283/**
12284 * @brief Trigger function for UPDATE statement provenance tracking
12285 *
12286 * Records the update in update_provenance. Multiplies new-row tokens
12287 * with the update token and applies monus to old-row tokens.
12288 */
12289CREATE OR REPLACE FUNCTION update_statement_trigger()
12290 RETURNS TRIGGER AS
12291$$
12292DECLARE
12293 query_text TEXT;
12294 update_token UUID;
12295 old_token UUID;
12296 new_token UUID;
12297 r RECORD;
12298 tx_token UUID;
12299 enable_trigger BOOL;
12300BEGIN
12301 enable_trigger := current_setting('provsql.update_provenance', true);
12302 IF enable_trigger = 'f' THEN
12303 RETURN NULL;
12304 END IF;
12305 update_token := public.uuid_generate_v4();
12306
12307 PERFORM create_gate(update_token, 'update');
12308
12309 SELECT query
12310 INTO query_text
12311 FROM pg_stat_activity
12312 WHERE pid = pg_backend_pid();
12313
12314 tx_token := transaction_token();
12315
12316 INSERT INTO update_provenance (provsql, query, query_type, username, ts,
12317 valid_time, xid, tx_token)
12318 VALUES (update_token, query_text, 'UPDATE', current_user, CURRENT_TIMESTAMP,
12319 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
12320 pg_current_xact_id(), tx_token);
12321
12322 -- The effect this statement has on a row names both the statement and
12323 -- the transaction it belongs to, so undo() can reverse either.
12324 update_token := provenance_times(tx_token, update_token);
12325
12326 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
12327 old_token := r.provsql;
12328 new_token := provenance_times(old_token, update_token);
12329
12330 PERFORM set_config('provsql.update_provenance', 'off', false);
12331 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
12332 USING new_token, old_token;
12333 PERFORM set_config('provsql.update_provenance', 'on', false);
12334 END LOOP;
12335
12336 PERFORM set_config('provsql.update_provenance', 'off', false);
12337 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
12338 PERFORM set_config('provsql.update_provenance', 'on', false);
12339
12340 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
12341 old_token := r.provsql;
12342 new_token := provenance_monus(old_token, update_token);
12343
12344 PERFORM set_config('provsql.update_provenance', 'off', false);
12345 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
12346 USING new_token, old_token;
12347 PERFORM set_config('provsql.update_provenance', 'on', false);
12348 END LOOP;
12349
12350 RETURN NULL;
12351END
12352$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
12353
12354
12355/** @} */
12356
12357/** @defgroup temporal_db Temporal DB (PostgreSQL 14+)
12358 * Functions for temporal database support. These use provenance
12359 * evaluation over the multirange semiring to track temporal validity
12360 * of tuples.
12361 * @{
12362 */
12363
12364SET search_path TO provsql;
12365
12366/**
12367 * @brief Evaluate provenance over the temporal (interval-union) m-semiring
12368 *
12369 * Inputs are read as %TSTZMULTIRANGE validity intervals; the additive
12370 * identity is <tt>'{}'::%TSTZMULTIRANGE</tt> (empty), the multiplicative
12371 * identity is <tt>'{(,)}'::%TSTZMULTIRANGE</tt> (universal). Returns the union
12372 * of intervals supporting the result, computed via the compiled circuit
12373 * traversal.
12374 *
12375 * @param token Provenance token to evaluate.
12376 * @param token2value Mapping from input gates to validity multiranges.
12377 */
12378CREATE FUNCTION sr_temporal(token ANYELEMENT, token2value REGCLASS)
12379 RETURNS TSTZMULTIRANGE AS
12380$$
12381BEGIN
12382 RETURN provsql.provenance_evaluate_compiled(
12383 token,
12384 token2value,
12385 'interval_union',
12386 '{(,)}'::TSTZMULTIRANGE
12387 );
12388END
12389$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
12390
12391/**
12392 * @brief Evaluate provenance over the interval-union m-semiring
12393 * with a NUMERIC multirange carrier
12394 *
12395 * Inputs are read as %nummultirange validity ranges over a NUMERIC
12396 * domain (e.g. sensor measurement-validity ranges). Addition is
12397 * multirange union, multiplication is intersection, monus is set
12398 * difference; the additive identity is <tt>'{}'::%nummultirange</tt>
12399 * and the multiplicative identity is <tt>'{(,)}'::%nummultirange</tt>
12400 * (universal range).
12401 *
12402 * @param token Provenance token to evaluate.
12403 * @param token2value Mapping from input gates to NUMERIC multiranges.
12404 */
12405CREATE FUNCTION sr_interval_num(token ANYELEMENT, token2value REGCLASS)
12406 RETURNS nummultirange AS
12407$$
12408BEGIN
12409 RETURN provsql.provenance_evaluate_compiled(
12410 token,
12411 token2value,
12412 'interval_union',
12413 '{(,)}'::nummultirange
12414 );
12415END
12416$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
12417
12418/**
12419 * @brief Evaluate provenance over the interval-union m-semiring
12420 * with an int4 multirange carrier
12421 *
12422 * Inputs are read as %int4multirange validity ranges over the
12423 * integers (e.g. page or line ranges of supporting documents).
12424 * Addition is multirange union, multiplication is intersection,
12425 * monus is set difference; the additive identity is
12426 * <tt>'{}'::%int4multirange</tt> and the multiplicative identity is
12427 * <tt>'{(,)}'::%int4multirange</tt>.
12428 *
12429 * @param token Provenance token to evaluate.
12430 * @param token2value Mapping from input gates to int4 multiranges.
12431 */
12432CREATE FUNCTION sr_interval_int(token ANYELEMENT, token2value REGCLASS)
12433 RETURNS int4multirange AS
12434$$
12435BEGIN
12436 RETURN provsql.provenance_evaluate_compiled(
12437 token,
12438 token2value,
12439 'interval_union',
12440 '{(,)}'::int4multirange
12441 );
12442END
12443$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
12444
12445/**
12446 * @brief Evaluate temporal provenance as a TIMESTAMP multirange
12447 *
12448 * Thin wrapper around :sqlfunc:`sr_temporal` retained for backward
12449 * compatibility; both compute the same union of validity intervals.
12450 *
12451 * @param token provenance token to evaluate
12452 * @param token2value mapping table from tokens to temporal validity ranges
12453 */
12454CREATE OR REPLACE FUNCTION union_tstzintervals(
12455 token UUID,
12456 token2value REGCLASS
12457)
12458RETURNS TSTZMULTIRANGE AS
12459$$
12460 SELECT sr_temporal(token, token2value)
12461$$ LANGUAGE SQL PARALLEL SAFE STABLE;
12462
12463/**
12464 * @brief Query a table as it was at a specific point in time
12465 *
12466 * Returns all rows whose temporal validity includes the given TIMESTAMP.
12467 *
12468 * @param tablename name of the provenance-tracked table
12469 * @param at_time the point in time to query
12470 */
12471CREATE OR REPLACE FUNCTION timetravel(
12472 tablename TEXT,
12473 at_time TIMESTAMPTZ
12474)
12475RETURNS SETOF RECORD
12476LANGUAGE plpgsql
12477AS
12478$$
12479BEGIN
12480 RETURN QUERY EXECUTE format(
12481 '
12482 SELECT
12483 %1$I.*,
12484 sr_temporal(provenance(), %2$L)
12485 FROM
12486 %1$I
12487 WHERE
12488 sr_temporal(provenance(), %2$L) @> %3$L::TIMESTAMPTZ
12489 ',
12490 tablename,
12491 'provsql.time_validity_view',
12492 at_time::TEXT
12493 );
12494END;
12495$$;
12496
12497/**
12498 * @brief Query a table for rows valid during a time interval
12499 *
12500 * Returns all rows whose temporal validity overlaps the given range.
12501 *
12502 * @param tablename name of the provenance-tracked table
12503 * @param from_time start of the time interval
12504 * @param to_time end of the time interval
12505 */
12506CREATE OR REPLACE FUNCTION timeslice(
12507 tablename TEXT,
12508 from_time TIMESTAMPTZ,
12509 to_time TIMESTAMPTZ
12510)
12511RETURNS SETOF RECORD
12512LANGUAGE plpgsql
12513AS
12514$$
12515BEGIN
12516 RETURN QUERY EXECUTE format(
12517 '
12518 SELECT
12519 %1$I.*,
12520 sr_temporal(provenance(), %2$L)
12521 FROM
12522 %1$I
12523 WHERE
12524 sr_temporal(provenance(), %2$L)
12525 && tstzrange(%3$L::TIMESTAMPTZ, %4$L::TIMESTAMPTZ)
12526 ',
12527 tablename,
12528 'provsql.time_validity_view',
12529 from_time::TEXT,
12530 to_time::TEXT
12531 );
12532END;
12533$$;
12534
12535/**
12536 * @brief Query the full temporal history of specific rows
12537 *
12538 * Returns all versions of rows matching the given column values,
12539 * with their temporal validity ranges.
12540 *
12541 * @param tablename name of the provenance-tracked table
12542 * @param col_names array of column names to filter on
12543 * @param col_values array of corresponding values to match
12544 */
12545CREATE OR REPLACE FUNCTION history(
12546 tablename TEXT,
12547 col_names TEXT[],
12548 col_values TEXT[]
12549)
12550RETURNS SETOF RECORD
12551LANGUAGE plpgsql
12552AS
12553$$
12554DECLARE
12555 condition TEXT := '';
12556 i INT;
12557BEGIN
12558 IF array_length(col_names, 1) IS NULL
12559 OR array_length(col_values, 1) IS NULL
12560 OR array_length(col_names, 1) != array_length(col_values, 1)
12561 THEN
12562 RAISE EXCEPTION 'col_names and col_values must have the same (non-null) length';
12563 END IF;
12564
12565 FOR i IN 1..array_length(col_names, 1)
12566 LOOP
12567 IF i > 1 THEN
12568 condition := condition || ' AND ';
12569 END IF;
12570 condition := condition || format('%I = %L', col_names[i], col_values[i]);
12571 END LOOP;
12572
12573 RETURN QUERY EXECUTE format(
12574 '
12575 SELECT
12576 %I.*,
12577 sr_temporal(provenance(), %L)
12578 FROM
12579 %I
12580 WHERE
12581 %s
12582 ',
12583 tablename,
12584 'provsql.time_validity_view',
12585 tablename,
12586 condition
12587 );
12588END;
12589$$;
12590
12591/**
12592 * @brief Get the valid time range for a specific tuple
12593 *
12594 * @param token provenance token of the tuple
12595 * @param tablename name of the table containing the tuple
12596 */
12597CREATE OR REPLACE FUNCTION get_valid_time(
12598 token UUID,
12599 tablename TEXT
12600)
12601RETURNS TSTZMULTIRANGE
12602LANGUAGE plpgsql
12603AS $$
12604DECLARE
12605 result TSTZMULTIRANGE;
12606BEGIN
12607 EXECUTE format(
12608 '
12609 SELECT
12610 sr_temporal(provenance(), %L)
12611 FROM
12612 %I
12613 WHERE
12614 provsql = %L
12615 ',
12616 'provsql.time_validity_view',
12617 tablename,
12618 token
12619 )
12620 INTO result;
12621
12622 RETURN result;
12623END;
12624$$;
12625
12626/**
12627 * @brief Undo a previously recorded update operation
12628 *
12629 * Traverses all provenance-tracked tables and rewrites their circuits
12630 * to apply monus with respect to the given update token, effectively
12631 * undoing the operation.
12632 *
12633 * @param c UUID of the update operation to undo (from update_provenance)
12634 */
12635CREATE OR REPLACE FUNCTION undo(
12636 c UUID
12637)
12638RETURNS UUID
12639LANGUAGE plpgsql
12640AS $$
12641DECLARE
12642 undo_query TEXT;
12643 undone_query TEXT;
12644 undo_token UUID;
12645 schema_rec RECORD;
12646 table_rec RECORD;
12647 row_rec RECORD;
12648 new_x UUID;
12649BEGIN
12650 -- Test for the row, not for its query text: a TRANSACTION row has no
12651 -- query of its own, and undoing a whole transaction is exactly what it
12652 -- is there for.
12653 SELECT query INTO undone_query
12654 FROM update_provenance
12655 WHERE provsql = c
12656 LIMIT 1;
12657
12658 IF NOT FOUND THEN
12659 RAISE NOTICE 'Unable to find % in update_provenance', c;
12660 RETURN c;
12661 END IF;
12662
12663 SELECT query
12664 INTO undo_query
12665 FROM pg_stat_activity
12666 WHERE pid = pg_backend_pid();
12667
12668 undo_token := public.uuid_generate_v4();
12669 PERFORM create_gate(undo_token, 'update');
12670 INSERT INTO update_provenance(provsql, query, query_type, username, ts,
12671 valid_time, xid, tx_token)
12672 VALUES (
12673 undo_token,
12674 undo_query,
12675 'UNDO',
12676 current_user,
12677 CURRENT_TIMESTAMP,
12678 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
12679 pg_current_xact_id(),
12680 transaction_token()
12681 );
12682
12683 PERFORM set_config('provsql.update_provenance', 'off', false);
12684
12685 FOR schema_rec IN
12686 SELECT nspname
12687 FROM pg_namespace
12688 WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1')
12689 LOOP
12690 FOR table_rec IN
12691 EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname)
12692 LOOP
12693 IF EXISTS (
12694 SELECT 1
12695 FROM information_schema.columns
12696 WHERE table_schema = schema_rec.nspname
12697 AND table_name = table_rec.tname
12698 AND table_name <> 'update_provenance'
12699 AND column_name = 'provsql'
12700 ) THEN
12701 FOR row_rec IN
12702 EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname)
12703 LOOP
12704 new_x := replace_the_circuit(row_rec.x, c, undo_token);
12705 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2',
12706 schema_rec.nspname, table_rec.tname)
12707 USING new_x, row_rec.x;
12708 END LOOP;
12709 END IF;
12710 END LOOP;
12711 END LOOP;
12712
12713 PERFORM set_config('provsql.update_provenance', 'on', false);
12714
12715 RETURN undo_token;
12716END;
12717$$;
12718
12719/**
12720 * @brief Recursively rewrite a circuit to undo a specific operation
12721 *
12722 * Helper for undo(). Walks the circuit and replaces occurrences of
12723 * the target update gate with its monus.
12724 *
12725 * @param x provenance token to rewrite
12726 * @param c UUID of the update operation to undo
12727 * @param u UUID of the undo operation
12728 */
12729CREATE OR REPLACE FUNCTION replace_the_circuit(
12730 x UUID,
12731 c UUID,
12732 u UUID
12733)
12734RETURNS UUID
12735LANGUAGE plpgsql
12736AS $$
12737DECLARE
12738 nchildren UUID[];
12739 child UUID;
12740 ntoken UUID;
12741 ntype PROVENANCE_GATE;
12742BEGIN
12743 IF x = c THEN
12744 RETURN provenance_monus(c, u);
12745 -- update and input gates cannot have children
12746 ELSIF get_gate_type(x) = 'update' OR get_gate_type(x) = 'input' THEN
12747 RETURN x;
12748 ELSE
12749 nchildren := '{}';
12750 FOREACH child IN ARRAY get_children(x)
12751 LOOP
12752 nchildren := array_append(nchildren, replace_the_circuit(child, c, u));
12753 END LOOP;
12754
12755 ntoken := public.uuid_generate_v4();
12756 ntype := get_gate_type(x);
12757
12758 PERFORM create_gate(ntoken, ntype, nchildren);
12759 RETURN ntoken;
12760 END IF;
12761END;
12762$$;
12763
12764/**
12765 * @brief Rewrite a circuit, substituting one gate for another
12766 *
12767 * Walks @p x and rebuilds every gate above an occurrence of @p old over
12768 * @p new instead. Leaves that are not @p old come back unchanged, so a
12769 * token that does not mention @p old is returned as it is.
12770 *
12771 * @param x the token to rewrite
12772 * @param old the gate to substitute away
12773 * @param new the gate to put in its place
12774 */
12775CREATE OR REPLACE FUNCTION substitute_gate(
12776 x UUID,
12777 old UUID,
12778 new UUID
12779)
12780RETURNS UUID
12781LANGUAGE plpgsql
12782AS $$
12783DECLARE
12784 nchildren UUID[];
12785 child UUID;
12786 rewritten UUID;
12787 changed BOOLEAN := false;
12788 ntoken UUID;
12789 ntype PROVENANCE_GATE;
12790BEGIN
12791 IF x = old THEN
12792 RETURN new;
12793 END IF;
12794 ntype := get_gate_type(x);
12795 -- Leaves have no children to walk into.
12796 IF ntype IN ('input', 'update', 'rv', 'value', 'zero', 'one') THEN
12797 RETURN x;
12798 END IF;
12799 nchildren := '{}';
12800 FOREACH child IN ARRAY get_children(x)
12801 LOOP
12802 rewritten := substitute_gate(child, old, new);
12803 IF rewritten <> child THEN
12804 changed := true;
12805 END IF;
12806 nchildren := array_append(nchildren, rewritten);
12807 END LOOP;
12808 IF NOT changed THEN
12809 RETURN x;
12810 END IF;
12811 ntoken := public.uuid_generate_v4();
12812 PERFORM create_gate(ntoken, ntype, nchildren);
12813 RETURN ntoken;
12814END;
12815$$;
12816
12817/**
12818 * @brief Give a recorded data modification a different probability
12819 *
12820 * The @c update-gate counterpart of @c provsql.replace_input. A gate's
12821 * probability is written once, so "how likely is it that this
12822 * modification happened" is changed by minting a new @c update gate with
12823 * the new probability, logging it in @c update_provenance beside the one
12824 * it replaces, and rewriting every tracked row whose provenance mentions
12825 * the old gate to mention the new one instead -- the same walk @c undo
12826 * performs. The old gate and its log row are kept: the history of the
12827 * database is not rewritten, it is extended.
12828 *
12829 * @param old the @c update gate to replace, as found in
12830 * @c update_provenance
12831 * @param p the new probability, in [0,1]
12832 * @return the new @c update gate
12833 */
12834CREATE OR REPLACE FUNCTION replace_update(
12835 old UUID,
12836 p double precision
12837)
12838RETURNS UUID
12839LANGUAGE plpgsql
12840AS $$
12841DECLARE
12842 new_token UUID;
12843 old_row RECORD;
12844 schema_rec RECORD;
12845 table_rec RECORD;
12846 row_rec RECORD;
12847 new_x UUID;
12848BEGIN
12849 IF old IS NULL OR p IS NULL THEN
12850 RAISE EXCEPTION 'replace_update: neither argument may be NULL';
12851 END IF;
12852 IF get_gate_type(old) <> 'update' THEN
12853 RAISE EXCEPTION 'replace_update: % is not an update gate', old
12854 USING HINT = 'Use provsql.replace_input() for a tuple''s own input gate.';
12855 END IF;
12856
12857 SELECT * INTO old_row FROM update_provenance WHERE provsql = old LIMIT 1;
12858 IF old_row IS NULL THEN
12859 RAISE EXCEPTION 'replace_update: % is not recorded in update_provenance', old;
12860 END IF;
12861
12862 new_token := public.uuid_generate_v4();
12863 PERFORM create_gate(new_token, 'update');
12864 PERFORM set_prob(new_token, p);
12865
12866 INSERT INTO update_provenance(provsql, query, query_type, username, ts,
12867 valid_time, xid, tx_token)
12868 VALUES (new_token, old_row.query, 'REPLACE', current_user,
12869 CURRENT_TIMESTAMP,
12870 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)),
12871 pg_current_xact_id(), transaction_token());
12872
12873 PERFORM set_config('provsql.update_provenance', 'off', false);
12874
12875 FOR schema_rec IN
12876 SELECT nspname
12877 FROM pg_namespace
12878 WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1')
12879 LOOP
12880 FOR table_rec IN
12881 EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname)
12882 LOOP
12883 IF EXISTS (
12884 SELECT 1
12885 FROM information_schema.columns
12886 WHERE table_schema = schema_rec.nspname
12887 AND table_name = table_rec.tname
12888 AND table_name <> 'update_provenance'
12889 AND column_name = 'provsql'
12890 ) THEN
12891 FOR row_rec IN
12892 EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname)
12893 LOOP
12894 new_x := substitute_gate(row_rec.x, old, new_token);
12895 IF new_x <> row_rec.x THEN
12896 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2',
12897 schema_rec.nspname, table_rec.tname)
12898 USING new_x, row_rec.x;
12899 END IF;
12900 END LOOP;
12901 END IF;
12902 END LOOP;
12903 END LOOP;
12904
12905 PERFORM set_config('provsql.update_provenance', 'on', false);
12906
12907 RETURN new_token;
12908END;
12909$$;
12910
12911-- The base validity mapping is a plain view over the data-modification log:
12912-- update_provenance is append-only and never has its provsql rewritten, so a
12913-- view stays correct (unlike a tracked table's mapping, which must be a
12914-- maintained mapping table -- see create_provenance_mapping(maintained)).
12915CREATE VIEW provsql.time_validity_view AS
12916 SELECT valid_time AS value, provsql AS provenance FROM provsql.update_provenance;
12917
12918/** @} */
12919
12920SET search_path TO public;
12921
12922-- Final constants-cache refresh: same rationale as at the end of
12923-- provsql.common.sql. On PG14+ this file is appended after the common
12924-- script, so this is the last statement of the generated install script;
12925-- the refresh must come after every object has been created for the
12926-- installing session's memoized constants to be complete.
12927SELECT provsql.reset_constants_cache();