ProvSQL SQL API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
provsql.sql
Go to the documentation of this file.
1/**
2 * @file
3 * @brief ProvSQL PL/pgSQL extension code
4 *
5 * This file contains the PL/pgSQL code of the ProvSQL extension. This
6 * extension requires the standard UUID-ossp extension.
7 */
8
9/**
10 * @brief <tt>provsql</tt> schema
11 *
12 * All types and functions introduced by ProvSQL are defined in the
13 * provsql schema, requiring prefixing them by <tt>provsql.</tt> or
14 * using PostgreSQL's <tt>search_path</tt> variable with a command such
15 * as \code{.sql}SET search_path TO public, provsql;\endcode
16 */
17CREATE SCHEMA provsql;
18
19SET search_path TO provsql;
20
21/**
22 * @brief Provenance circuit gate types
23 *
24 * Each gate in the provenance circuit has a type that determines
25 * its semantics during semiring evaluation.
26 */
27CREATE TYPE PROVENANCE_GATE AS
28 ENUM(
29 'input', -- Input (variable) gate of the circuit
30 'plus', -- Semiring plus
31 'times', -- Semiring times
32 'monus', -- M-Semiring monus
33 'project', -- Project gate (for where provenance)
34 'zero', -- Semiring zero
35 'one', -- Semiring one
36 'eq', -- Equijoin gate (for where provenance)
37 'agg', -- Aggregation operator (for aggregate provenance)
38 'semimod', -- Semimodule scalar multiplication (for aggregate provenance)
39 'cmp', -- Comparison of aggregate values (HAVING-clause provenance)
40 'delta', -- δ-semiring operator (see Amsterdamer, Deutch, Tannen, PODS 2011)
41 'value', -- Scalar value (for aggregate provenance)
42 'mulinput',-- Multivalued input (for Boolean provenance)
43 'update', -- Update operation
44 'rv', -- Continuous random-variable leaf
45 'arith', -- n-ary arithmetic gate over scalar-valued children
46 'mixture', -- Probabilistic mixture of two scalar RV roots with a Bernoulli weight
47 'assumed', -- Structural assumption marker over a single child: the
48 -- wrapped sub-circuit was computed under the
49 -- assumption named by the gate's extra label --
50 -- 'BOOLEAN' (e.g. the safe-query rewrite; the
51 -- default when the label is absent) or
52 -- 'absorptive' (cyclic recursion truncated at the
53 -- absorptive value fixpoint). Transparent for
54 -- evaluation semirings satisfying the assumption,
55 -- fatal error for the rest, rendered as an
56 -- explicit element in PROV-XML export.
57 'annotation', -- Transparent single-child wrapper carrying a
58 -- query-level annotation string in @c extra
59 -- (e.g. the inversion-free tractability
60 -- certificate / per-input order key). Identity
61 -- for EVERY evaluator; its UUID folds in @c extra
62 -- so distinct annotations over the same child are
63 -- distinct gates.
64 'conditioned', -- Conditioning marker: two children
65 -- [target, evidence]. Evaluated only in the
66 -- measure interpretation: probability_evaluate
67 -- returns P(target ∧ evidence) / P(evidence); the
68 -- RV / AGG_TOKEN evaluators return the restricted
69 -- distribution. For the UUID carrier it is a
70 -- TERMINAL gate (never a child of a semiring gate);
71 -- nested conditioning folds into a conjunction of
72 -- evidence. Refused by every general sr_* semiring
73 -- (normalization is not a semiring operation).
74 'mobius', -- Signed Möbius combination over child islands: one
75 -- INTEGER coefficient per child in @c extra (the
76 -- gate_arith precedent), probability_evaluate returns
77 -- Σ_i coeff_i · P(child_i). The one new primitive of
78 -- the safe-UCQ Möbius-inversion route, evaluated only
79 -- in the measure interpretation; refused by every
80 -- general sr_* semiring (a signed combination is not a
81 -- semiring operation).
82 'case', -- N-ary guarded selection over scalar (RV) children:
83 -- wires [guard_1, value_1, ..., guard_k, value_k,
84 -- default], first-match semantics (the value of the
85 -- first guard event that holds, else the default).
86 -- Backs a CASE expression over random variables (and
87 -- abs / clamp / ReLU as sugar). RV/measure-carrier;
88 -- refused by every general sr_* semiring.
89 'observe' -- Latent-variable observation (likelihood-weighting
90 -- evidence): one wire -> an observed bare gate_rv
91 -- leaf, the datum in extra. Contributes a
92 -- continuous density factor (the leaf's pdf at the
93 -- datum) rather than a Boolean truth value,
94 -- composing into an evidence circuit by gate_times
95 -- exactly like a conditioning event. Evaluated only
96 -- by the importance-sampling weight walk; refused by
97 -- every Boolean / semiring evaluator.
98 );
99
100/** @defgroup gate_manipulation Circuit gate manipulation
101 * Low-level functions for creating and querying provenance circuit gates.
102 * @{
103 */
104
105/**
106 * @brief Create a new gate in the provenance circuit
107 *
108 * @param token UUID identifying the new gate
109 * @param type gate type (see PROVENANCE_GATE)
110 * @param children optional array of child gate UUIDs
111 */
112CREATE OR REPLACE FUNCTION create_gate(
113 token UUID,
114 type PROVENANCE_GATE,
115 children UUID[] DEFAULT NULL)
116 RETURNS VOID AS
117 'provsql','create_gate' LANGUAGE C PARALLEL SAFE;
118/**
119 * @brief Return the gate type of a provenance token
120 *
121 * Returns @c 'input' for any token not yet materialized in the circuit,
122 * since input is the default semantics of an unmaterialized provenance token.
123 */
124CREATE OR REPLACE FUNCTION get_gate_type(
125 token UUID)
126 RETURNS PROVENANCE_GATE AS
127 'provsql','get_gate_type' LANGUAGE C IMMUTABLE PARALLEL SAFE;
128/** @brief Return the children of a provenance gate */
129CREATE OR REPLACE FUNCTION get_children(
130 token UUID)
131 RETURNS UUID[] AS
132 'provsql','get_children' LANGUAGE C IMMUTABLE PARALLEL SAFE;
133/**
134 * @brief Set the probability of an input gate
135 *
136 * @param token UUID of the input gate
137 * @param p probability value in [0,1]
138 */
139CREATE OR REPLACE FUNCTION set_prob(
140 token UUID, p DOUBLE PRECISION)
141 RETURNS VOID AS
142 'provsql','set_prob' LANGUAGE C PARALLEL SAFE;
143/** @brief Get the probability associated with an input gate */
144CREATE OR REPLACE FUNCTION get_prob(
145 token UUID)
146 RETURNS DOUBLE PRECISION AS
147 'provsql','get_prob' LANGUAGE C STABLE PARALLEL SAFE;
149/**
150 * @brief Set additional INTEGER values on provenance circuit gate
151 *
152 * This function sets two INTEGER values associated to a circuit gate, used in
153 * different ways by different gate types:
154 * - for mulinput, info1 indicates the value of this multivalued variable
155 * - for eq, info1 and info2 indicate the attribute index of the
156 equijoin in, respectively, the first and second columns
157 * - for agg, info1 is the oid of the aggregate function and info2 the
158 oid of the aggregate result type
159 * - for cmp, info1 is the oid of the comparison operator
160 *
161 * @param token UUID of the circuit gate
162 * @param info1 first INTEGER value
163 * @param info2 second INTEGER value
164 */
165CREATE OR REPLACE FUNCTION set_infos(
166 token UUID, info1 INT, info2 INT DEFAULT NULL)
167 RETURNS VOID AS
168 'provsql','set_infos' LANGUAGE C PARALLEL SAFE;
169
170/** @brief Get the INTEGER info values associated with a circuit gate */
171CREATE OR REPLACE FUNCTION get_infos(
172 token UUID, OUT info1 INT, OUT info2 INT)
173 RETURNS RECORD AS
174 'provsql','get_infos' LANGUAGE C STABLE PARALLEL SAFE;
175
176/**
177 * @brief Wrap @p token in a fresh @c gate_assumed carrying @p assumption
178 * as its label, and return the wrapper's UUID.
179 *
180 * Public primitive callable from any rewrite or driver that needs to
181 * flag a sub-circuit as sound only under an evaluation assumption:
183 * - @c 'BOOLEAN' -- the sub-circuit only preserves the Boolean function
184 * of the lineage (e.g. the safe-query rewrite collapses derivation
185 * multiplicities); transparent for semirings admitting a homomorphism
186 * from Boolean functions.
187 * - @c 'absorptive' -- the sub-circuit was truncated at the absorptive
188 * value fixpoint (cyclic recursive query); transparent for absorptive
189 * semirings (probability, BOOLEAN, min-plus over nonnegative
190 * costs...), fatal for the rest (counting, why-provenance).
191 *
192 * Incompatible evaluators raise a @c CircuitException. Always kept as
193 * an explicit node in PROV-XML export.
194 *
195 * The wrapper UUID is content-derived via @c uuid_generate_v5 on the
196 * assumption and the child, so identical children always wrap to the
197 * same outer UUID per assumption. No-op (returns NULL) on a NULL
198 * input.
199 */
200CREATE OR REPLACE FUNCTION provenance_assume(token UUID, assumption TEXT)
201 RETURNS UUID AS
202$$
203DECLARE
204 wrapped UUID;
205BEGIN
206 IF token IS NULL THEN
207 RETURN NULL;
208 END IF;
209 IF assumption NOT IN ('BOOLEAN', 'absorptive') THEN
210 RAISE EXCEPTION 'provenance_assume: unknown assumption %', assumption;
211 END IF;
212 wrapped := public.uuid_generate_v5(uuid_ns_provsql(),
213 concat('assumed', assumption, token));
214 PERFORM create_gate(wrapped, 'assumed', ARRAY[token]);
215 PERFORM set_extra(wrapped, assumption);
216 RETURN wrapped;
217END
218$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
219 SECURITY DEFINER PARALLEL SAFE;
220
221/**
222 * @brief Wrap @p token in a Boolean-assumption marker (compatibility
223 * name; see @c provenance_assume).
224 *
225 * This is the entry point the safe-query (read-once) rewriter calls on
226 * every per-row root it produces, so the wrapper additionally carries
227 * @c PROVSQL_ROUTE_SQ_REWRITE in @c info1: the assumption kind alone does
228 * not identify the route (@c provenance_assume(t, 'BOOLEAN') is public),
229 * and the probability dispatcher reads the tag back to report
230 * @c sq-rewrite rather than the generic @c independent. Build an untagged
231 * Boolean-assumption wrapper with @c provenance_assume directly.
232 */
233CREATE OR REPLACE FUNCTION assume_boolean(token UUID) RETURNS UUID AS
235DECLARE
236 wrapped UUID;
237BEGIN
238 wrapped := provenance_assume(token, 'BOOLEAN');
239 IF wrapped IS NOT NULL THEN
240 -- 1 = PROVSQL_ROUTE_SQ_REWRITE (see provsql_route in src/provsql_utils.h)
241 PERFORM set_infos(wrapped, 1, 0);
242 END IF;
243 RETURN wrapped;
244END
245$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
246 SECURITY DEFINER PARALLEL SAFE;
247
248/**
249 * @brief Wrap @p token in a fresh transparent @c gate_annotation carrying
250 * @p extra, and return the wrapper's UUID.
251 *
252 * Unlike every other gate, the annotation wrapper's UUID folds in @p extra
253 * (not just the child): @c uuid_generate_v5 over @c concat('annotation',
254 * token, extra). This is deliberate -- two annotations over the same child
255 * with different @p extra must be distinct gates (e.g. the same input tuple
256 * carrying different per-occurrence order keys, or two queries attaching
257 * different certificates to a shared root). The wrapper is transparent
258 * (identity) for EVERY evaluator; @p extra is inert metadata read only by the
259 * code that placed it. No-op (returns NULL) on a NULL input.
260 */
261CREATE OR REPLACE FUNCTION annotate(token UUID, extra TEXT) RETURNS UUID AS
262$$
263DECLARE
264 annotated UUID;
265BEGIN
266 IF token IS NULL THEN
267 RETURN NULL;
268 END IF;
269 annotated := public.uuid_generate_v5(uuid_ns_provsql(),
270 concat('annotation', token, extra));
271 PERFORM create_gate(annotated, 'annotation', ARRAY[token]);
272 PERFORM set_extra(annotated, extra);
273 RETURN annotated;
274END
275$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
276 SECURITY DEFINER PARALLEL SAFE;
277
278/**
279 * @brief Peel every transparent @c gate_annotation wrapper off @p token,
280 * returning the first non-annotation gate underneath.
281 *
282 * The dual of @c annotate, for consumers keyed to gate *identity* rather
283 * than gate value: a provenance mapping matches input-gate UUIDs, and the
284 * reachability edge classifier matches token shapes, so both must see the
285 * wrapped gate, not the wrapper (e.g. the inversion-free certificate /
286 * order marker the planner attaches to a certified query's row roots).
287 * Identity on a token with no annotation wrapper; NULL on NULL.
288 */
289CREATE OR REPLACE FUNCTION strip_annotations(token UUID) RETURNS UUID AS
290$$
291WITH RECURSIVE peel(g) AS (
292 SELECT token
293 UNION ALL
294 SELECT (provsql.get_children(p.g))[1] FROM peel p
295 WHERE provsql.get_gate_type(p.g) = 'annotation'
296)
297SELECT g FROM peel WHERE provsql.get_gate_type(g) <> 'annotation' LIMIT 1;
298$$ LANGUAGE sql STABLE PARALLEL SAFE;
299
300/**
301 * @brief Condition a provenance token (a Boolean event) on another.
302 *
303 * Builds the terminal @c gate_conditioned that the measure evaluators read
304 * as @c "P(target ∧ evidence) / P(evidence)". This is the backing function
305 * of the binary @c | operator (@c "target | evidence", value-level
306 * conditioning of the UUID carrier).
307 *
308 * The gate stores three children @c [target, evidence, joint] with
309 * @c joint @c = @c times(target, @c evidence); evaluation is then the plain
310 * ratio @c P(joint)/P(evidence), and content-addressing makes a base tuple
311 * shared by @p target and @p evidence the same input gate in both circuits,
312 * so the conditional is exact and correlation-aware.
313 *
314 * Conventions:
315 * - Conditioning on a certain or absent event is a no-op: @c evidence NULL
316 * or @c gate_one() returns @p target unchanged (@c "P(X|true)=P(X)").
317 * - A @p target with no provenance defaults to the certain event 1, so
318 * @c "1 | c" is the well-defined certain-row posterior.
319 * - Nested conditioning folds (sequential Bayesian update):
320 * @c "(X | A) | B = X | (A ∧ B)" -- the gate never nests, it stays one
321 * level deep with the evidence accumulated by @c times.
322 *
323 * The result is TERMINAL: a conditioned token may not become a child of a
324 * @c plus / @c times / @c monus / @c agg gate (those constructors refuse
325 * it); the only operation it admits is more conditioning.
326 */
327CREATE OR REPLACE FUNCTION cond(target UUID, evidence UUID) RETURNS UUID AS
329DECLARE
330 tgt UUID;
331 ev UUID;
332 jnt UUID;
333 result UUID;
334 ch UUID[];
335BEGIN
336 -- P(X | true) = P(X): conditioning on a certain / absent event is inert.
337 IF evidence IS NULL OR evidence = gate_one() THEN
338 RETURN target;
339 END IF;
340
341 -- A row with no provenance defaults to the certain event 1.
342 tgt := coalesce(target, gate_one());
343
344 IF get_gate_type(tgt) = 'conditioned' THEN
345 -- Sequential update (X | A) | B = X | (A ∧ B): fold B into both the
346 -- evidence and the joint of the inner gate so the result stays a single
347 -- gate_conditioned over the ORIGINAL target.
348 ch := get_children(tgt);
349 tgt := ch[1]; -- original target X
350 ev := provenance_times(ch[2], evidence); -- A ∧ B
351 jnt := provenance_times(ch[3], evidence); -- (X ∧ A) ∧ B
352 ELSE
353 ev := evidence;
354 jnt := provenance_times(tgt, evidence); -- X ∧ C
355 END IF;
356
357 result := public.uuid_generate_v5(uuid_ns_provsql(),
358 concat('conditioned', tgt, ev, jnt));
359 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev, jnt]);
360 RETURN result;
361END
362$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
363 SECURITY DEFINER PARALLEL SAFE;
364
365/**
366 * @brief Binary @c | : value-level conditioning, @c "target | evidence".
367 *
368 * Carrier-parametric in its left operand; the UUID form builds the terminal
369 * @c gate_conditioned via @c cond. Does not collide with core PostgreSQL's
370 * INTEGER bitwise @c | (different argument types).
371 */
372CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=UUID, PROCEDURE=cond);
373
374/**
375 * @brief Placeholder for @c "X | (predicate)" on a UUID event.
376 *
377 * Lets the conditioning event be written as a natural Boolean combination of
378 * random_variable / aggregate comparisons (e.g. @c "event | (sensor > 3)")
379 * instead of a hand-built gate. Never executes: the ProvSQL planner hook
380 * converts the Boolean operand into a condition gate and emits @c cond.
381 */
382CREATE OR REPLACE FUNCTION cond_predicate(target UUID, predicate BOOLEAN)
383 RETURNS UUID AS
384$$
385BEGIN
386 RAISE EXCEPTION 'UUID | (predicate) must be rewritten by the ProvSQL '
387 'planner hook: the right operand must be a Boolean combination of '
388 'random_variable / aggregate comparisons (is provsql.active off?)';
389END
390$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
391
392CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=BOOLEAN, PROCEDURE=cond_predicate);
393
394/**
395 * @brief Placeholder for @c "(predicate) | (predicate)" on two events.
396 *
397 * Conditions one comparison event on another when both operands are written
398 * as comparisons rather than pre-built tokens (e.g.
399 * @c "probability((x >= 2000) | (x >= 1000))"): an @c random_variable /
400 * @c AGG_TOKEN comparison is statically @c BOOLEAN-typed, so neither the
401 * @c "UUID | UUID" (@c cond) nor the @c "UUID | BOOLEAN" (@c cond_predicate)
402 * operator resolves. Never executes: the ProvSQL planner hook lowers each
403 * Boolean operand to its event gate and emits @c cond(target, evidence), so
404 * the result carries the correlation-aware @c Pr(A ∧ B) / Pr(B). Returns
405 * @c UUID, so @c "A | B" is a first-class event token in every position
406 * (a @c probability(UUID) argument, a projected column, a further @c "|").
407 */
408CREATE OR REPLACE FUNCTION predicate_cond_predicate(target BOOLEAN, evidence BOOLEAN)
409 RETURNS UUID AS
410$$
411BEGIN
412 RAISE EXCEPTION '(predicate) | (predicate) must be rewritten by the ProvSQL '
413 'planner hook: both operands must be Boolean combinations of '
414 'random_variable / aggregate comparisons (is provsql.active off?)';
415END
416$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
417
418CREATE OPERATOR | (LEFTARG=BOOLEAN, RIGHTARG=BOOLEAN, PROCEDURE=predicate_cond_predicate);
419
420/**
421 * @brief Deterministic indicator gate for an ordinary (regular) comparison.
422 *
423 * The predicate-provenance of an ordinary comparison (both sides of regular
424 * type, e.g. @c "region = 'north'") is the deterministic indicator
425 * @c "χ(cond)": @c gate_one() when the comparison holds on the row,
426 * @c gate_zero() otherwise (Definition in the HAVING-provenance semantics).
427 * The planner emits this for a regular comparison appearing inside a MIXED
428 * conditioning predicate (one that also has a random_variable / aggregate
429 * comparison); @c cond is evaluated per row, so the indicator is the row's
430 * own truth value, combined by @c ⊗ / @c ⊕ with the probabilistic gates.
431 */
432CREATE OR REPLACE FUNCTION regular_indicator(cond BOOLEAN) RETURNS UUID AS
433$$
434 SELECT CASE WHEN cond THEN provsql.gate_one() ELSE provsql.gate_zero() END;
435$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
437/**
438 * @brief Whole-tuple output conditioning directive: @c "given(evidence)".
439 *
440 * Written as a term in the select list, @c given(c) conditions the OUTPUT
441 * provenance of the current query's rows on @p c:
442 *
443 * @code
444 * SELECT a, b, given((SELECT provenance() FROM tests
445 * WHERE patient_id = s.id AND result = 'positive'))
446 * FROM source s;
447 * -- visible columns: a, b (the given(...) term is stripped)
448 * -- per-row output provenance: provenance() | <that row's evidence>
449 * @endcode
450 *
451 * The query rewriter recognises the marker, STRIPS it from the visible
452 * projection, and wraps each output row's provenance expression in
453 * @c cond(row_provenance, c) -- deriving a new conditioned relation, never
454 * mutating any stored provenance. @p c is evaluated per output row and may
455 * correlate with the row's columns, so each tuple is conditioned on its own
456 * evidence. When the rewriter is inactive the call is a harmless identity
457 * (it returns @p evidence as an ordinary column).
458 *
459 * When @b executed rather than stripped -- i.e. nested inside an expression,
460 * the idiom @c "and_agg(given(Y = d))" that folds one observation per row
461 * into a latent-variable evidence circuit -- a point-equality @c "Y = d" on
462 * a bare random-variable leaf is turned into likelihood-weighting evidence
463 * (@c evidence_as_observation); any other evidence passes through unchanged.
464 */
465CREATE OR REPLACE FUNCTION given(evidence UUID) RETURNS UUID AS
466$$
467BEGIN
468 RETURN provsql.evidence_as_observation(evidence);
469END
470$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE
471 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
472
473/**
474 * @brief Prefix unary @c | : alias for @c given, @c "| evidence".
475 *
476 * Disambiguated from the binary @c | by the absence of a left operand
477 * (@c "a, | c" parses @c "| c" as the prefix form). PostgreSQL keeps
478 * prefix operators on every supported version (postfix operators were
479 * removed in PG14), so @c "| c" is safe across the CI matrix.
480 */
481CREATE OPERATOR | (RIGHTARG=UUID, PROCEDURE=given);
482
483/**
484 * @brief Conditioning-evidence from a predicate: @c "given(predicate)"
485 * (also the prefix @c "| (predicate)").
486 *
487 * Two uses of the same marker:
488 * - whole-tuple output conditioning written as a select-list term, the
489 * natural-predicate spelling of @c given (@c "SELECT a, given(sensor > 3)");
490 * - per-row evidence for a latent-variable posterior, folded with
491 * @c and_agg -- @c "and_agg(given(normal(mu,1) = x))" turns each row's
492 * observation into likelihood-weighting evidence.
493 *
494 * Never executes: the planner converts the Boolean operand into a condition
495 * gate and emits @c given(gate); a point-equality @c "Y = d" on a bare
496 * random-variable leaf then becomes an observation (see @c given(UUID) /
497 * @c evidence_as_observation).
498 */
499CREATE OR REPLACE FUNCTION given(predicate BOOLEAN) RETURNS UUID AS
500$$
501BEGIN
502 RAISE EXCEPTION 'given(predicate) / prefix | (predicate) must be rewritten '
503 'by the ProvSQL planner hook: the operand must be a Boolean combination '
504 'of random_variable / aggregate comparisons (is provsql.active off?)';
505END
506$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
507
508CREATE OPERATOR | (RIGHTARG=BOOLEAN, PROCEDURE=given);
509
510/**
511 * @brief Event negation: @c "! event" / @c "provenance_not(event)".
512 *
513 * The complement of a Boolean provenance event: @c "!x" holds in exactly the
514 * worlds where @p x does not. It is sugar for @c "monus(one, x)" -- an
515 * ordinary m-semiring expression (Boolean @c NOT, probability @c "1 - P(x)"),
516 * NOT a measure-only marker -- so it composes like any @c monus, and a
517 * conditioned / terminal token is refused as its child (so @c "!(x | c)"
518 * errors, as conditioning cannot be buried under further algebra).
519 *
520 * The motivating use is conditioning on the NON-occurrence of an arbitrary
521 * violation query @p W (a denial constraint), where @p W itself is built with
522 * ordinary idioms and needs no hand-rolled gates:
523 *
524 * @code
525 * -- W = "some pair of overlapping same-room bookings is present"
526 * WITH w AS (SELECT provenance() AS tok
527 * FROM bookings a JOIN bookings b
528 * ON a.id < b.id AND a.room = b.room
529 * AND a.lo < b.hi AND b.lo < a.hi
530 * GROUP BY ())
531 * SELECT probability_evaluate((SELECT provenance() FROM bookings WHERE id=1)
532 * | !w.tok) -- P(booking 1 | no overlap)
533 * FROM w;
534 * @endcode
535 *
536 * Named @c provenance_not, after the @c "provenance_times / _plus / _monus"
537 * family; the prefix @c ! operator is the ergonomic form (SQL's reserved
538 * @c NOT keyword cannot serve as a function name).
539 */
540CREATE OR REPLACE FUNCTION provenance_not(event UUID) RETURNS UUID AS
541$$
542 SELECT provsql.provenance_monus(provsql.gate_one(), event);
543$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
544 SET search_path=provsql,pg_temp,public;
545
546/**
547 * @brief Prefix unary @c ! : alias for @c provenance_not, @c "! event".
548 *
549 * Prefix operators are kept on every supported PostgreSQL version (postfix
550 * operators were removed in PG14), and core PG defines no prefix @c ! on
551 * @c UUID, so @c "! event" is safe across the CI matrix.
552 */
553CREATE OPERATOR ! (RIGHTARG=UUID, PROCEDURE=provenance_not);
554
555/**
556 * @brief Build a per-input order-key string for the inversion-free path.
557 *
558 * Emitted by the planner per certified atom: @c K-prefixed, length-prefixed
559 * @c "K<factor> <octet_length(root)>:<root><octet_length(sec)>:<sec>", parsed
560 * back at evaluation by @c safe_cert_key_parse. @p root / @p sec are the
561 * tuple's root- and secondary-class column values (TEXT-cast by the caller);
562 * the byte-length prefixes keep the values unambiguous for @em any column type,
563 * including TEXT containing spaces, colons or digits. @p factor is the atom's
564 * factor id (or -1 for the shared self-join guard). @c IMMUTABLE so the planner
565 * can fold it and the marker dedups by content-addressing.
566 */
567CREATE OR REPLACE FUNCTION inversion_free_key(root TEXT, sec TEXT, factor INT)
568 RETURNS TEXT AS
569$$ SELECT 'K' || factor::TEXT || ' '
570 || octet_length(root) || ':' || root
571 || octet_length(sec) || ':' || sec $$
572 LANGUAGE sql IMMUTABLE PARALLEL SAFE;
573
574/**
575 * @brief Set extra TEXT information on provenance circuit gate
576 *
577 * This function sets TEXT-encoded data associated to a circuit gate, used in
578 * different ways by different gate types:
579 * - for project, it is a TEXT-encoded ARRAY of two-element ARRAYs that
580 * indicate mappings between input attribute (first element) and output
581 * attribute (second element)
582 * - for value and agg, it is the TEXT-encoded (base for value, computed
583 * for agg) scalar value
585 * @param token UUID of the circuit gate
586 * @param data TEXT-encoded information
587 */
588CREATE OR REPLACE FUNCTION set_extra(
589 token UUID, data TEXT)
590 RETURNS VOID AS
591 'provsql','set_extra' LANGUAGE C PARALLEL SAFE STRICT;
592/** @brief Get the TEXT-encoded extra data associated with a circuit gate */
593CREATE OR REPLACE FUNCTION get_extra(token UUID)
594 RETURNS TEXT AS
595 'provsql','get_extra' LANGUAGE C STABLE PARALLEL SAFE RETURNS NULL ON NULL INPUT;
596
597/**
598 * @brief Return the total number of materialized gates in the provenance circuit
599 *
600 * Input gates for provenance-tracked table rows are created lazily on
601 * first reference; rows that have never appeared in a query result are
602 * not counted.
603 */
604CREATE OR REPLACE FUNCTION get_nb_gates() RETURNS BIGINT AS
605 'provsql', 'get_nb_gates' LANGUAGE C PARALLEL SAFE;
606
607/** @} */
608
609/** @defgroup table_management Provenance table management
610 * Functions for enabling, disabling, and configuring provenance
611 * tracking on user tables.
612 * @{
613 */
614
615
616/**
617 * @brief Trigger function for DELETE statement provenance tracking
618 *
619 * Records the deletion and applies monus to provenance tokens of
620 * deleted rows. This is the version for PostgreSQL < 14.
621 */
622CREATE OR REPLACE FUNCTION delete_statement_trigger()
623 RETURNS TRIGGER AS
624$$
625DECLARE
626 query_text TEXT;
627 delete_token UUID;
628 old_token UUID;
629 new_token UUID;
630 r RECORD;
631BEGIN
632 delete_token := public.uuid_generate_v4();
633
634 PERFORM create_gate(delete_token, 'input');
635
636 SELECT query
637 INTO query_text
638 FROM pg_stat_activity
639 WHERE pid = pg_backend_pid();
640
641 INSERT INTO delete_provenance (delete_token, query, deleted_by, deleted_at)
642 VALUES (delete_token, query_text, current_user, CURRENT_TIMESTAMP);
644 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
645
646 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
647 old_token := r.provsql;
648 new_token := provenance_monus(old_token, delete_token);
649
650 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
651 USING new_token, old_token;
652 END LOOP;
653
654 RETURN NULL;
655END
656$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
657
658
659/**
660 * @brief Record per-relation provenance metadata used by the
661 * safe-query optimisation.
662 *
663 * Stores a @c (relid, kind, block_key) RECORD in the persistent
664 * mmap-backed table-info store. @p kind is one of:
665 * - @c 'tid' -- independent input leaves (post-@c add_provenance default)
666 * - @c 'bid' -- block-correlated leaves; rows sharing the same value
667 * of @p block_key are mutually exclusive. An empty
668 * @p block_key means the whole table is one block.
669 * - @c 'opaque' -- arbitrary correlations from a derived source
670 * (CREATE TABLE AS SELECT, INSERT INTO SELECT,
671 * UPDATE under provsql.update_provenance); the
672 * safe-query rewriter must bail on these.
673 *
674 * @param relid pg_class OID of the relation.
675 * @param kind One of @c 'tid' / @c 'bid' / @c 'opaque'.
676 * @param block_key Block-key column numbers (only meaningful for
677 * @c 'bid'; ignored otherwise but conventionally
678 * passed empty).
679 */
680CREATE OR REPLACE FUNCTION set_table_info(
681 relid OID, kind TEXT, block_key INT2[] DEFAULT ARRAY[]::INT2[])
682 RETURNS VOID AS
683 'provsql','set_table_info' LANGUAGE C PARALLEL SAFE;
684
685/** @brief Remove per-relation provenance metadata. No-op when missing. */
686CREATE OR REPLACE FUNCTION remove_table_info(relid OID)
687 RETURNS VOID AS
688 'provsql','remove_table_info' LANGUAGE C PARALLEL SAFE;
689
690/**
691 * @brief Read per-relation provenance metadata.
692 *
693 * Returns NULL if no RECORD exists. @c kind is one of @c 'tid' /
694 * @c 'bid' / @c 'opaque'; @c block_key is the (possibly empty) array
695 * of block-key column numbers, only meaningful when @c kind = @c 'bid'.
696 * Used by the planner-time hierarchy detector to gate the safe-query
697 * rewrite.
698 */
699CREATE OR REPLACE FUNCTION get_table_info(
700 relid OID, OUT kind TEXT, OUT block_key INT2[])
701 RETURNS RECORD AS
702 'provsql','get_table_info' LANGUAGE C STABLE PARALLEL SAFE;
703
704/**
705 * @brief Record the base-relation ancestor set of a tracked relation.
706 *
707 * Base tables created with @c add_provenance / @c repair_key carry
708 * @c {self}; CTAS-derived tables inherit the union of their sources'
709 * ancestor sets. The safe-query rewriter consults the registry to
710 * enforce that joined FROM entries have disjoint base ancestors
711 * before firing the read-once factoring.
712 *
713 * The worker preserves the relation's existing @c kind / @c block_key
714 * half on update; it silently no-ops when no kind RECORD exists for
715 * @p relid (callers should run @c add_provenance / @c repair_key
716 * first). The ancestor list is capped at 64 entries (clear error if
717 * exceeded).
718 *
719 * @param relid pg_class OID of the relation.
720 * @param ancestors Sorted, deduplicated base-relation OIDs.
721 */
722CREATE OR REPLACE FUNCTION set_ancestors(
723 relid OID, ancestors OID[] DEFAULT ARRAY[]::OID[])
724 RETURNS VOID AS
725 'provsql','set_ancestors' LANGUAGE C PARALLEL SAFE;
726
727/** @brief Clear the ancestor half of a per-relation RECORD (keeps kind/block_key).
728 * No-op when missing. */
729CREATE OR REPLACE FUNCTION remove_ancestors(relid OID)
730 RETURNS VOID AS
731 'provsql','remove_ancestors' LANGUAGE C PARALLEL SAFE;
732
733/**
734 * @brief Read the base-relation ancestor set of a tracked relation.
735 *
736 * Returns @c NULL when no ancestor RECORD exists for @p relid (or the
737 * RECORD is empty -- both cases make the safe-query rewriter take
738 * its conservative refuse path, so they collapse here).
739 */
740CREATE OR REPLACE FUNCTION get_ancestors(relid OID)
741 RETURNS OID[] AS
742 'provsql','get_ancestors' LANGUAGE C STABLE PARALLEL SAFE;
743
744/**
745 * @brief BEFORE INSERT OR UPDATE OF provsql row trigger installed by
746 * @c add_provenance.
747 *
748 * Two jobs:
749 *
750 * 1. Fill @c NEW.provsql with a fresh @c uuid_generate_v4 leaf when
751 * the user did not supply one (a column DEFAULT would not do here:
752 * it fires before the trigger sees the row, so we could not tell
753 * "user omitted the column" from "user supplied a value").
754 * 2. When the user does supply a non-NULL @c provsql on @c INSERT,
755 * or changes it on @c UPDATE, flip the table's per-table
756 * metadata to @c OPAQUE. The user is free to write whatever
757 * UUIDs they want (cross-table reuse, compound tokens minted
758 * via @c create_gate, ...); the cost is that the safe-query
759 * rewriter then refuses to fire on this table, because TID
760 * independence can no longer be assumed.
761 */
762CREATE OR REPLACE FUNCTION provenance_guard()
763 RETURNS TRIGGER AS $$
764DECLARE
765 _m RECORD;
766BEGIN
767 IF TG_OP = 'INSERT' THEN
768 IF NEW.provsql IS NULL THEN
769 -- A genuine insert: mint a fresh atomic input variable. This is the
770 -- one place a new input token is born, so it is also where any
771 -- maintained mapping on this table is extended (keyed to that token).
772 -- Data-modification re-insertions (INSERT ... SELECT * FROM OLD_TABLE)
773 -- carry a supplied provsql and take the ELSE branch, so they are
774 -- correctly skipped: the validity stays keyed to the original input,
775 -- which is exactly the child a later monus/update gate wraps.
776 NEW.provsql := public.uuid_generate_v4();
777 FOR _m IN SELECT mapping, attribute
778 FROM provsql.provenance_mapping_registry WHERE source = TG_RELID
779 LOOP
780 EXECUTE format(
781 'INSERT INTO %s(value, provenance) SELECT ($1).%I, $2',
782 _m.mapping::REGCLASS, _m.attribute)
783 USING NEW, NEW.provsql;
784 END LOOP;
785 ELSE
786 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
787 END IF;
788 ELSIF TG_OP = 'UPDATE' THEN
789 IF NEW.provsql IS DISTINCT FROM OLD.provsql THEN
790 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
791 END IF;
792 END IF;
793 RETURN NEW;
794END;
795$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
796 SECURITY DEFINER;
797
798/**
799 * @brief Enable provenance tracking on an existing table
800 *
801 * Adds a <tt>provsql</tt> UUID column to the table, an index for
802 * fast UUID-keyed lookups, and a BEFORE INSERT/UPDATE row trigger
803 * (@c provenance_guard) that mints a fresh @c uuid_generate_v4
804 * leaf when the user omits the column on INSERT, or flips the
805 * table's metadata to @c OPAQUE when the user supplies their own
806 * value. Input gates for existing rows are created lazily when
807 * first referenced by a query.
808 *
809 * @param _tbl the table to add provenance tracking to
810 */
811CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
812 RETURNS VOID AS
813$$
814BEGIN
815 -- Idempotence: a second add_provenance on an already-tracked table is
816 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
817 -- re-run freely.
818 IF EXISTS (
819 SELECT 1 FROM pg_attribute
820 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
821 ) THEN
822 RAISE NOTICE 'table % already has provenance tracking', _tbl;
823 RETURN;
824 END IF;
825 -- No DEFAULT: the guard trigger mints the UUID, so the trigger can
826 -- distinguish "user omitted" (NULL) from "user supplied a value".
827 -- No UNIQUE: we no longer rely on it to keep the table TID -- the
828 -- guard does that semantically -- and a UNIQUE would reject the
829 -- legitimate cross-table UUID copy that just flips the table to
830 -- OPAQUE. We keep a plain index for fast UUID-keyed lookups.
831 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
832 EXECUTE format(
833 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
834 _tbl);
835 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
836 EXECUTE format(
837 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
838 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
839 _tbl);
840 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
841 -- Seed the base-ancestor set to {self}: a base TID table's atoms
842 -- come from itself and no other relation. CTAS-derived tables
843 -- inherit unions of source ancestor sets; that is handled by the
844 -- CTAS hook (a separate slice), not here.
845 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
846END
847$$ LANGUAGE plpgsql SECURITY DEFINER;
848
849/**
850 * @brief Remove provenance tracking from a table
851 *
852 * Drops the <tt>provsql</tt> column and associated triggers.
853 *
854 * @param _tbl the table to remove provenance tracking from
855 */
856CREATE OR REPLACE FUNCTION remove_provenance(_tbl REGCLASS)
857 RETURNS VOID AS
858$$
859DECLARE
860BEGIN
861 PERFORM provsql.remove_table_info(_tbl::oid);
862 -- Idempotence, mirroring add_provenance: removing provenance from a
863 -- table that does not have it is a NOTICE-and-no-op, so setup scripts
864 -- and notebook cells can be re-run freely. The metadata strip above
865 -- still runs, so a table left half-tracked is cleaned up.
866 IF NOT EXISTS (
867 SELECT 1 FROM pg_attribute
868 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
869 ) THEN
870 RAISE NOTICE 'table % does not have provenance tracking', _tbl;
871 RETURN;
872 END IF;
873 -- Drop the BEFORE INSERT/UPDATE guard first: it has a column
874 -- dependency on provsql (via the OF provsql clause), so the
875 -- subsequent DROP COLUMN would otherwise raise.
876 BEGIN
877 EXECUTE format('DROP TRIGGER provenance_guard on %s', _tbl);
878 EXCEPTION WHEN undefined_object THEN
879 END;
880 EXECUTE format('ALTER TABLE %s DROP COLUMN provsql', _tbl);
881 BEGIN
882 EXECUTE format('DROP TRIGGER add_gate on %s', _tbl);
883 EXCEPTION WHEN undefined_object THEN
884 END;
885 BEGIN
886 EXECUTE format('DROP TRIGGER insert_statement on %s', _tbl);
887 EXECUTE format('DROP TRIGGER update_statement on %s', _tbl);
888 EXECUTE format('DROP TRIGGER delete_statement on %s', _tbl);
889 EXCEPTION WHEN undefined_object THEN
890 END;
891END
892$$ LANGUAGE plpgsql;
893
894/**
895 * @brief Set up provenance for a table with duplicate key values
896 *
897 * When a table has duplicate rows for a given key, this function
898 * replaces simple input gates with multivalued input (mulinput) gates
899 * that model a uniform distribution over duplicates.
900 *
901 * @param _tbl the table to repair
902 * @param key_att the key attribute(s) as a comma-separated string, or
903 * empty string if the whole table is one group
904 */
905CREATE OR REPLACE FUNCTION repair_key(_tbl REGCLASS, key_att TEXT)
906 RETURNS VOID AS
907$$
908DECLARE
909 r RECORD;
910 rows_query TEXT;
911 block_key_cols INT2[];
912BEGIN
913 -- Resolve the (possibly comma-separated) key_att TEXT into the
914 -- corresponding pg_attribute.attnum values for the safe-query
915 -- metadata. Names are trimmed; quoting is not supported because
916 -- repair_key has never accepted quoted identifiers in key_att.
917 IF key_att = '' THEN
918 block_key_cols := ARRAY[]::INT2[];
919 ELSE
920 SELECT array_agg(a.attnum ORDER BY t.ord)::INT2[]
921 INTO block_key_cols
922 FROM unnest(string_to[](key_att, ',')) WITH ORDINALITY AS t(name, ord)
923 JOIN pg_attribute a
924 ON a.attrelid = _tbl
925 AND a.attname = trim(t.name)
926 AND a.attnum > 0
927 AND NOT a.attisdropped;
928 IF block_key_cols IS NULL OR array_length(block_key_cols, 1) IS NULL THEN
929 RAISE EXCEPTION 'repair_key: could not resolve key columns from "%"', key_att;
930 END IF;
931 IF array_length(block_key_cols, 1) > 16 THEN
932 RAISE EXCEPTION 'repair_key: block key wider than 16 columns is not supported';
933 END IF;
934 END IF;
935
936 -- Same column shape as add_provenance: no UNIQUE, no DEFAULT past
937 -- the initial backfill (the guard trigger added after the rename
938 -- takes over both jobs once the column has been renamed to its
939 -- final name). The DEFAULT is kept here only so the second pass
940 -- below can read provsql_temp from the user-visible rows
941 -- without a separate UPDATE.
942 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql_temp UUID DEFAULT public.uuid_generate_v4()', _tbl);
943
944 -- Build a per-group mapping (key columns + a fresh key_token + the
945 -- group size) once, then use it for both the create_gate(key_token,
946 -- 'input') first pass and the per-row mulinput second pass. Going
947 -- through a temp table avoids re-running uuid_generate_v4() (which
948 -- would produce different UUIDs the second time). USING (%1$s) on
949 -- the second pass handles the multi-column case uniformly.
950 -- ON COMMIT DROP plus the explicit DROP TABLE at the end of this
951 -- function leave the temp table cleaned up across transactions and
952 -- across repeated calls in the same transaction.
953 IF key_att = '' THEN
954 EXECUTE format(
955 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
956 SELECT public.uuid_generate_v4() AS provsql_key_token,
957 COUNT(*) AS provsql_group_size
958 FROM %s', _tbl);
959 rows_query := format(
960 'SELECT t.provsql_temp,
961 k.provsql_key_token AS key_token,
962 ROW_NUMBER() OVER (ORDER BY t.ctid) AS within_group,
963 k.provsql_group_size AS group_size
964 FROM %s t CROSS JOIN provsql_repair_key_tmp k', _tbl);
965 ELSE
966 EXECUTE format(
967 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
968 SELECT %1$s,
969 public.uuid_generate_v4() AS provsql_key_token,
970 COUNT(*) AS provsql_group_size
971 FROM %2$s
972 GROUP BY %1$s', key_att, _tbl);
973 rows_query := format(
974 'SELECT t.provsql_temp,
975 k.provsql_key_token AS key_token,
976 ROW_NUMBER() OVER (PARTITION BY k.provsql_key_token
977 ORDER BY t.ctid) AS within_group,
978 k.provsql_group_size AS group_size
979 FROM %2$s t
980 JOIN provsql_repair_key_tmp k USING (%1$s)', key_att, _tbl);
981 END IF;
982
983 -- Pass 1: one input gate per group key.
984 FOR r IN SELECT provsql_key_token FROM provsql_repair_key_tmp LOOP
985 PERFORM provsql.create_gate(r.provsql_key_token, 'input');
986 END LOOP;
987
988 -- Pass 2: per row, attach a mulinput gate to its group's key token.
989 FOR r IN EXECUTE rows_query LOOP
990 PERFORM provsql.create_gate(r.provsql_temp, 'mulinput', ARRAY[r.key_token]);
991 PERFORM provsql.set_prob(r.provsql_temp, 1./r.group_size);
992 PERFORM provsql.set_infos(r.provsql_temp, r.within_group::INT);
993 END LOOP;
994
995 DROP TABLE provsql_repair_key_tmp;
996
997 EXECUTE format('ALTER TABLE %s ALTER COLUMN provsql_temp DROP DEFAULT', _tbl);
998 EXECUTE format('ALTER TABLE %s RENAME COLUMN provsql_temp TO provsql', _tbl);
999 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
1000 EXECUTE format(
1001 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
1002 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
1003 _tbl);
1004 PERFORM provsql.set_table_info(_tbl::oid, 'bid', block_key_cols);
1005 -- Base BID tables also have themselves as their sole ancestor. Same
1006 -- rationale as the @c add_provenance branch above.
1007 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
1008END
1009$$ LANGUAGE plpgsql;
1010
1011/**
1012 * @brief Event trigger that purges per-table provenance metadata when
1013 * a tracked relation is dropped outside of remove_provenance().
1014 *
1015 * Plain DROP TABLE bypasses remove_provenance() and would otherwise
1016 * leave a stale entry in the table-info store keyed by a now-recycled
1017 * OID, with confusing consequences for the safe-query rewriter the
1018 * next time the OID is reused. This trigger forwards every dropped
1019 * relation OID to provsql.remove_table_info(), which is a no-op for
1020 * relations that were not tracked.
1021 */
1022CREATE OR REPLACE FUNCTION cleanup_table_info()
1023 RETURNS event_trigger AS
1024$$
1025DECLARE
1026 r RECORD;
1027BEGIN
1028 FOR r IN
1029 SELECT objid FROM pg_event_trigger_dropped_objects()
1030 WHERE object_type IN ('table', 'foreign table', 'materialized view')
1031 LOOP
1032 PERFORM provsql.remove_table_info(r.objid);
1033 -- Forget any maintained mapping whose source or mapping table is gone.
1034 DELETE FROM provsql.provenance_mapping_registry
1035 WHERE source = r.objid OR mapping = r.objid;
1036 END LOOP;
1037END
1038$$ LANGUAGE plpgsql;
1039
1040DROP EVENT TRIGGER IF EXISTS provsql_cleanup_table_info;
1041-- @c EXECUTE @c PROCEDURE (rather than the PG 11+ @c EXECUTE
1042-- @c FUNCTION alias) so the extension installs on PG 10 too.
1043CREATE EVENT TRIGGER provsql_cleanup_table_info ON sql_drop
1044 EXECUTE PROCEDURE provsql.cleanup_table_info();
1045
1046/**
1047 * @brief Registry of maintained provenance mappings
1048 *
1049 * Each row records that mapping table @c mapping is kept current for the
1050 * @c attribute column of the provenance-tracked @c source table: every
1051 * genuine insert into @c source appends @c (value, provenance) to it (see
1052 * @c provenance_guard). Keyed on the mapping table, indexed on the source
1053 * so the guard can look up a table's mappings cheaply. Entries are removed
1054 * when either table is dropped (see @c cleanup_table_info).
1055 */
1056CREATE TABLE IF NOT EXISTS provsql.provenance_mapping_registry(
1057 mapping oid PRIMARY KEY,
1058 source oid NOT NULL,
1059 attribute name NOT NULL
1060);
1061CREATE INDEX IF NOT EXISTS provenance_mapping_registry_source_idx
1062 ON provsql.provenance_mapping_registry(source);
1063
1064/**
1065 * @brief Create a provenance mapping table from an attribute
1066 *
1067 * Creates a new table mapping provenance tokens to values of a given
1068 * attribute, for use with semiring evaluation functions.
1069 * Idempotent: if the mapping table already exists, raises a NOTICE and
1070 * changes nothing (drop it first to rebuild).
1071 *
1072 * @param newtbl name of the mapping table to create
1073 * @param oldtbl source table with provenance tracking
1074 * @param att attribute whose values populate the mapping
1075 * @param preserve_case if true, quote the table name to preserve case
1076 * @param maintained if true, register the mapping so later inserts into
1077 * @c oldtbl keep it current, and it stays correct after data
1078 * modification (deletes/updates rewrite a row's provsql, but the
1079 * validity stays keyed to the original input token). @c att must
1080 * then be a plain column name. When false (the default) the table is
1081 * a one-off snapshot.
1083CREATE OR REPLACE FUNCTION create_provenance_mapping(
1084 newtbl TEXT,
1085 oldtbl REGCLASS,
1086 att TEXT,
1087 preserve_case BOOL DEFAULT 'f',
1088 maintained BOOL DEFAULT false
1089) RETURNS VOID AS
1090$$
1091DECLARE
1092BEGIN
1093 -- Idempotence: when the mapping table already exists, leave it alone
1094 -- with a NOTICE (re-runnable setup scripts / notebook cells). Drop it
1095 -- first to rebuild a stale mapping.
1096 IF (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
1097 ELSE to_regclass(newtbl) END) IS NOT NULL THEN
1098 RAISE NOTICE 'mapping table % already exists', newtbl;
1099 RETURN;
1100 END IF;
1101 -- ON COMMIT DROP only fires at COMMIT: several mapping creations in
1102 -- one transaction (a notebook cell, a setup script run via psql -1)
1103 -- would otherwise collide on the leftover temp table. The to_regclass
1104 -- probe (rather than DROP IF EXISTS) keeps the first call NOTICE-free.
1105 IF to_regclass('pg_temp.tmp_provsql') IS NOT NULL THEN
1106 DROP TABLE tmp_provsql;
1107 END IF;
1108 EXECUTE format('CREATE TEMP TABLE tmp_provsql ON COMMIT DROP AS TABLE %s', oldtbl);
1109 ALTER TABLE tmp_provsql RENAME provsql TO provenance;
1110 -- The mapping is keyed by gate identity (input-token UUIDs), so peel any
1111 -- transparent annotation wrapper (e.g. the inversion-free certificate a
1112 -- certified query attaches to its row roots) off the captured tokens.
1113 UPDATE tmp_provsql SET provenance = provsql.strip_annotations(provenance)
1114 WHERE provsql.get_gate_type(provenance) = 'annotation';
1115 IF preserve_case THEN
1116 EXECUTE format('CREATE TABLE %I AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
1117 EXECUTE format('CREATE INDEX ON %I(provenance)', newtbl);
1118 ELSE
1119 EXECUTE format('CREATE TABLE %s AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
1120 EXECUTE format('CREATE INDEX ON %s(provenance)', newtbl);
1121 END IF;
1122 IF maintained THEN
1123 -- Register so genuine inserts into oldtbl keep the mapping current
1124 -- (see provenance_guard); keyed to the input token, so it survives the
1125 -- provsql rewrites that data modification performs.
1126 INSERT INTO provsql.provenance_mapping_registry(mapping, source, attribute)
1127 VALUES (
1128 (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
1129 ELSE to_regclass(newtbl) END)::oid,
1130 oldtbl::oid, att)
1131 ON CONFLICT (mapping)
1132 DO UPDATE SET source = EXCLUDED.source, attribute = EXCLUDED.attribute;
1133 END IF;
1134END
1135$$ LANGUAGE plpgsql;
1136
1137/** @} */
1139/** @defgroup internal_constants Internal constants
1140 * UUID namespace and identity element functions used for
1141 * deterministic gate generation.
1142 * @{
1143 */
1144
1145/** @brief Return the ProvSQL UUID namespace (used for deterministic gate UUIDs) */
1146CREATE OR REPLACE FUNCTION uuid_ns_provsql() RETURNS UUID AS
1147$$
1148 -- uuid_generate_v5(uuid_ns_url(),'http://pierre.senellart.com/software/provsql/')
1149 SELECT '920d4f02-8718-5319-9532-d4ab83a64489'::UUID
1150$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1151
1152/** @brief Return the UUID of the semiring zero gate */
1153CREATE OR REPLACE FUNCTION gate_zero() RETURNS UUID AS
1154$$
1155 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'zero');
1156$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1157
1158/** @brief Return the UUID of the semiring one gate */
1159CREATE OR REPLACE FUNCTION gate_one() RETURNS UUID AS
1160$$
1161 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'one');
1162$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1163
1164/** @brief Return the epsilon threshold used for probability comparisons */
1165CREATE OR REPLACE FUNCTION epsilon() RETURNS DOUBLE PRECISION AS
1166$$
1167 SELECT CAST(0.001 AS DOUBLE PRECISION)
1168$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1169
1170/** @} */
1171
1172/** @defgroup semiring_operations Semiring operations
1173 * Functions that build provenance circuit gates for semiring operations.
1174 * These are called internally by the query rewriter.
1175 *
1176 * They are declared @c IMMUTABLE: each derives its gate UUID
1177 * deterministically from its arguments (a @c uuid5 content address) and
1178 * the @c create_gate write at that address is idempotent, so the token a
1179 * call returns is a pure function of its inputs. The marking matters for
1180 * parallelism: PL/pgSQL runs a non-volatile function's inner SPI
1181 * read-only, so the per-row builders the rewriter injects into a scan do
1182 * not call @c CommandCounterIncrement -- which would raise "cannot start
1183 * commands during a parallel operation" once the enclosing statement has
1184 * gone parallel. A @c VOLATILE builder both blocks that parallel plan and
1185 * loses the query-wide speed-up.
1186 * @{
1187 */
1188
1189/**
1190 * @brief Create a times (product) gate from multiple provenance tokens
1191 *
1192 * Filters out NULL and one-gates; returns gate_one() if all tokens
1193 * are trivial, or a single token if only one remains.
1194 *
1195 * Before creating an ordinary gate, the *times-canonical* address of
1196 * the surviving multiset -- @c uuid5('times-canonical{sorted tokens}')
1197 * -- is probed: the reachability rewriter pre-creates there, for
1198 * self-join conjunctions of reachability tokens, a certified
1199 * equivalent (the all-members-reachable circuit; see
1200 * @c plant_reach_cover). Ordinary creation never writes under that
1201 * recipe, so a hit is always a deliberate plant; the ordinary
1202 * order-dependent recipe is used otherwise, so ordinary
1203 * times gates (and their formula rendering) are untouched.
1204 */
1205CREATE OR REPLACE FUNCTION provenance_times(VARIADIC tokens UUID[])
1206 RETURNS UUID AS
1207$$
1208DECLARE
1209 times_token UUID;
1210 filtered_tokens UUID[];
1211 canonical UUID;
1212BEGIN
1213 -- A NULL element reads as the ⊗-neutral 1: it is the token slot of an
1214 -- untracked source (a join against an untracked table), which is
1215 -- certain. Contrast provenance_plus / provenance_monus, where NULL
1216 -- reads as the ⊕- / ⊖-right-neutral 0: each combinator maps NULL to
1217 -- its own neutral element. Nothing may therefore hand a NULL to ⊗
1218 -- meaning "false"; a comparison with a NULL operand goes through
1219 -- provenance_cmp, which returns gate_zero for it.
1220 SELECT array_agg(t) FROM unnest(tokens) t WHERE t IS NOT NULL AND t <> gate_one() INTO filtered_tokens;
1221
1222 -- Dispatch on the FILTERED count: a single survivor short-circuits
1223 -- to that token directly (no useless single-child times gate); zero
1224 -- survivors collapse to the identity. Using array_length(tokens, 1)
1225 -- here would miss the [one, cmp] → [cmp] case, leaving the cmp wrapped
1226 -- in a one-child times when its only sibling was gate_one().
1227 CASE coalesce(array_length(filtered_tokens, 1), 0)
1228 WHEN 0 THEN
1229 times_token:=gate_one();
1230 WHEN 1 THEN
1231 times_token:=filtered_tokens[1];
1232 ELSE
1233 -- Computed separately from the filtering aggregate above: an
1234 -- ORDER BY aggregate there would make the planner feed *both*
1235 -- aggregates sorted input, scrambling the stored children order.
1236 SELECT uuid_generate_v5(uuid_ns_provsql(),
1237 concat('times-canonical', array_agg(t ORDER BY t)))
1238 FROM unnest(filtered_tokens) t
1239 INTO canonical;
1240 IF get_gate_type(canonical) = 'times' THEN
1241 -- A deliberate pre-creation at the canonical address: same
1242 -- children, same product.
1243 times_token := canonical;
1244 ELSE
1245 times_token := uuid_generate_v5(uuid_ns_provsql(),concat('times',filtered_tokens));
1246
1247 PERFORM create_gate(times_token, 'times', ARRAY_AGG(t)) FROM UNNEST(filtered_tokens) AS t WHERE t IS NOT NULL;
1248 END IF;
1249 END CASE;
1250
1251 RETURN times_token;
1252END
1253$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1254
1255/**
1256 * @brief Create a monus (difference) gate from two provenance tokens
1257 *
1258 * Implements m-semiring monus. Returns token1 if token2 is NULL
1259 * (used for LEFT OUTER JOIN semantics in the EXCEPT rewriting).
1260 */
1261CREATE OR REPLACE FUNCTION provenance_monus(token1 UUID, token2 UUID)
1262 RETURNS UUID AS
1263$$
1264DECLARE
1265 monus_token UUID;
1266BEGIN
1267 IF token1 IS NULL THEN
1268 RAISE EXCEPTION USING MESSAGE='provenance_monus is called with first argument NULL';
1269 END IF;
1270
1271 IF token2 IS NULL THEN
1272 -- The ⊖-right-neutral 0: a NULL second argument is the no-match case
1273 -- of the difference operator's LEFT OUTER JOIN (nothing to subtract),
1274 -- so X ⊖ NULL = X ⊖ 0 = X. Note this is NOT the NULL ≡ 1 reading of
1275 -- provenance_times; each combinator maps NULL to its own neutral.
1276 RETURN token1;
1277 END IF;
1278
1279 IF token1 = token2 THEN
1280 -- X-X=0
1281 monus_token:=gate_zero();
1282 ELSIF token1 = gate_zero() THEN
1283 -- 0-X=0
1284 monus_token:=gate_zero();
1285 ELSIF token2 = gate_zero() THEN
1286 -- X-0=X
1287 monus_token:=token1;
1288 ELSE
1289 monus_token:=uuid_generate_v5(uuid_ns_provsql(),concat('monus',token1,token2));
1290 PERFORM create_gate(monus_token, 'monus', ARRAY[token1::UUID, token2::UUID]);
1291 END IF;
1292
1293 RETURN monus_token;
1294END
1295$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1296
1297/**
1298 * @brief Create a project gate for where-provenance tracking
1299 *
1300 * Records the mapping between input and output attribute positions.
1301 *
1302 * @param token child provenance token
1303 * @param positions array encoding attribute position mappings
1304 */
1305CREATE OR REPLACE FUNCTION provenance_project(token UUID, VARIADIC positions INT[])
1306 RETURNS UUID AS
1307$$
1308DECLARE
1309 project_token UUID;
1310 rec RECORD;
1311BEGIN
1312 project_token:=uuid_generate_v5(uuid_ns_provsql(),concat('project', token, positions));
1313 PERFORM create_gate(project_token, 'project', ARRAY[token]);
1314 PERFORM set_extra(project_token, ARRAY_AGG(pair)::TEXT)
1315 FROM (
1316 SELECT ARRAY[(CASE WHEN info=0 THEN NULL ELSE info END), idx] AS pair
1317 FROM unnest(positions) WITH ORDINALITY AS a(info, idx)
1318 ORDER BY idx
1319 ) t;
1320
1321 RETURN project_token;
1322END
1323$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1324
1325/**
1326 * @brief Create an equijoin gate for where-provenance tracking
1327 *
1328 * @param token child provenance token
1329 * @param pos1 attribute index in the first relation
1330 * @param pos2 attribute index in the second relation
1331 */
1332CREATE OR REPLACE FUNCTION provenance_eq(token UUID, pos1 INT, pos2 INT)
1333 RETURNS UUID AS
1334$$
1335DECLARE
1336 eq_token UUID;
1337 rec RECORD;
1338BEGIN
1339 eq_token:=uuid_generate_v5(uuid_ns_provsql(),concat('eq',token,pos1,',',pos2));
1340
1341 PERFORM create_gate(eq_token, 'eq', ARRAY[token::UUID]);
1342 PERFORM set_infos(eq_token, pos1, pos2);
1343 RETURN eq_token;
1344END
1345$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1346
1347/**
1348 * @brief Create a plus (sum) gate from an array of provenance tokens
1349 *
1350 * Filters out NULL and zero-gates; returns gate_zero() if all tokens
1351 * are trivial, or a single token if only one remains. Before creating
1352 * a gate, probes the *canonical* address of the multiset -- a dedicated
1353 * v5 recipe namespace over the sorted tokens (plus is commutative), in
1354 * which this function never creates anything, so a gate found there is
1355 * always a deliberate pre-creation computing the same sum. That is the
1356 * bounded-hop reachability route's hook: it plants, at the canonical
1357 * address of a vertex's per-length tokens, a certified gate over its
1358 * native within-bound circuit, keeping the natural hop-discarding query
1359 * on the linear evaluation route. Absent a canonical gate, the
1360 * ordinary order-dependent recipe is used, so ordinary plus
1361 * gates (and their formula rendering) are untouched.
1362 */
1363CREATE OR REPLACE FUNCTION provenance_plus(tokens UUID[])
1364 RETURNS UUID AS
1365$$
1366DECLARE
1367 c INTEGER;
1368 plus_token UUID;
1369 filtered_tokens UUID[];
1370 canonical UUID;
1371BEGIN
1372 -- A NULL element reads as the ⊕-neutral 0: it stands for a row absent
1373 -- from the disjunction (a null-padded antijoin row whose token array
1374 -- slot is NULL), not for an untracked source. Contrast provenance_times,
1375 -- where NULL reads as the ⊗-neutral 1 (untracked source): each
1376 -- combinator maps NULL to its own neutral element.
1377 SELECT array_agg(t) FROM unnest(tokens) t
1378 WHERE t IS NOT NULL AND t <> gate_zero()
1379 INTO filtered_tokens;
1380
1381 c:=array_length(filtered_tokens, 1);
1382
1383 IF c = 0 THEN
1384 plus_token := gate_zero();
1385 ELSIF c = 1 THEN
1386 plus_token := filtered_tokens[1];
1387 ELSE
1388 -- Computed separately from the filtering aggregate above: an ORDER
1389 -- BY aggregate there would make the planner feed *both* aggregates
1390 -- sorted input, scrambling the stored (aggregation-order) children.
1391 SELECT uuid_generate_v5(uuid_ns_provsql(),
1392 concat('plus-canonical', array_agg(t ORDER BY t)))
1393 FROM unnest(filtered_tokens) t
1394 INTO canonical;
1395 IF get_gate_type(canonical) = 'plus' THEN
1396 -- A deliberate pre-creation at the canonical address: same
1397 -- children, same sum.
1398 plus_token := canonical;
1399 ELSE
1400 plus_token := uuid_generate_v5(
1401 uuid_ns_provsql(),
1402 concat('plus', filtered_tokens));
1403
1404 PERFORM create_gate(plus_token, 'plus', filtered_tokens);
1405 END IF;
1406 END IF;
1407
1408 RETURN plus_token;
1409END
1410$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1411
1412/**
1413 * @brief Driver for provenance over recursive queries (WITH RECURSIVE).
1414 *
1415 * Invoked by the planner hook (@c lower_recursive_cte in @c provsql.c) when it
1416 * lowers a recursive CTE whose body touches provenance-tracked relations. The
1417 * hook deparses the CTE body to SQL and calls this function, which runs naive
1418 * bottom-up (fixpoint) evaluation: each round re-evaluates the body
1419 * @c base @c UNION @c recursive over a tracked working table until the
1420 * provenance tokens stop changing. Every round goes through ProvSQL's normal
1421 * rewriting, so the recursive join yields @c times gates, the untracked base
1422 * branch yields @c gate_one, and the @c UNION yields the @c plus merge of
1423 * alternative derivations -- no provenance is plumbed by hand here. The result
1424 * is left in a tracked temp table named @p work_name, which the hook then scans
1425 * in place of the CTE.
1426 *
1427 * The working tables (@p work_name and a scratch @c _new) are created once and
1428 * reused across rounds (TRUNCATE + INSERT), so the round count never
1429 * accumulates relation locks. Because content-addressed gate UUIDs make
1430 * structurally identical sub-circuits share, the fixpoint test is an exact
1431 * relational @c EXCEPT and the circuit stays the shared (polynomial) form.
1432 *
1433 * Scope: UNION (set) recursion. On *acyclic* input the structural fixpoint is
1434 * reached and the resulting circuit is the universal provenance, sound for any
1435 * semiring. On *cyclic* input the circuit never stabilises structurally; when
1436 * the session's provenance class (@c provsql.provenance) is @c 'absorptive' or
1437 * @c 'BOOLEAN' we instead stop at the value-fixpoint bound (number of
1438 * derivable tuples) -- every minimal, tuple-repetition-free derivation is then
1439 * covered, and the longer ones are absorbed in any absorptive semiring (after
1440 * Deutch, Milo, Roy & Tannen, ICDT 2014) -- and wrap the resulting tokens in
1441 * the @c 'absorptive' assumption marker, so that non-absorptive semiring
1442 * evaluations (counting, why-provenance: genuinely infinite on cyclic data)
1443 * refuse them while probability, Boolean, formula-as-circuit and min-plus
1444 * evaluations proceed. Under the general classes, cyclic input trips the
1445 * @p max_iter guard.
1446 *
1447 * This function has no @c SET @c search_path on purpose: @p body_sql is the
1448 * caller's deparsed query and must resolve relation names in the caller's path.
1450 * @param body_sql the recursive CTE body, e.g.
1451 * @c 'SELECT 1 UNION SELECT e.dst FROM edge e JOIN reach r ON e.src=r.node'
1452 * @param work_name the working relation name @p body_sql references (the CTE name)
1453 * @param colnames comma-separated user columns, e.g. @c 'node'
1454 * @param coldef column definitions for the working table, e.g. @c 'node INTEGER'
1455 * @param max_iter safety bound on fixpoint rounds (non-termination guard)
1456 */
1457CREATE OR REPLACE FUNCTION eval_recursive(
1458 body_sql TEXT,
1459 work_name TEXT,
1460 colnames TEXT,
1461 coldef TEXT,
1462 max_iter INT DEFAULT 1000)
1463 RETURNS VOID AS
1464$$
1465DECLARE
1466 changed BOOLEAN; -- circuit changed structurally this round
1467 set_stable BOOLEAN; -- user-column tuple set unchanged this round
1468 iters INT := 0;
1469 new_count INT; -- rows in _new this round (INSERT ROW_COUNT)
1470 -- Under an absorptive semiring the provenance *value* converges on cyclic
1471 -- data even though the circuit keeps growing structurally. A minimal
1472 -- derivation cannot repeat a tuple, so it has depth <= (number of derivable
1473 -- tuples); after that many naive rounds the value equals the least fixpoint,
1474 -- and the surplus (longer, cyclic) derivations are absorbed at evaluation
1475 -- time. We learn that bound from the tuple-set fixpoint, stop there, and
1476 -- mark the resulting tokens with the 'absorptive' assumption so evaluation
1477 -- under a non-absorptive semiring refuses rather than silently returning a
1478 -- truncated value.
1479 absorptive_mode BOOLEAN :=
1480 coalesce(current_setting('provsql.provenance', true), 'semiring')
1481 IN ('absorptive', 'BOOLEAN');
1482 truncated BOOLEAN := false; -- exited at the value fixpoint (cyclic data)
1483 ntuples INT := NULL; -- the bound above, set once the tuple set stabilises
1484BEGIN
1485 EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
1486 DROP TABLE IF EXISTS _new;
1487
1488 -- Tracked working table (carries provsql), initially empty, plus a scratch
1489 -- table of the same shape; both reused across rounds.
1490 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID)', work_name, coldef);
1491 EXECUTE format('CREATE TEMP TABLE _new (LIKE %I)', work_name);
1492
1493 LOOP
1494 iters := iters + 1;
1495 -- Hard safety bound (also catches genuinely unbounded recursion, e.g. an
1496 -- unbounded counter, where even the tuple set never stabilises).
1497 IF iters > max_iter THEN
1498 RAISE EXCEPTION 'eval_recursive: no fixpoint after % rounds (cyclic data?)', max_iter;
1499 END IF;
1501 -- One round of naive evaluation: re-run the CTE body over the current
1502 -- working table. INSERT targets a tracked table, so ProvSQL fills provsql.
1503 -- Take the row count from the INSERT itself (counting _new directly would be
1504 -- an aggregate over a provenance-tracked table -> an AGG_TOKEN).
1505 EXECUTE 'TRUNCATE _new';
1506 EXECUTE format('INSERT INTO _new(%s) %s', colnames, body_sql);
1507 GET DIAGNOSTICS new_count = ROW_COUNT;
1508
1509 -- Exact structural fixpoint test (content-addressed tokens => set equality).
1510 EXECUTE format(
1511 'SELECT EXISTS((TABLE _new EXCEPT TABLE %1$I) UNION ALL (TABLE %1$I EXCEPT TABLE _new))',
1512 work_name) INTO changed;
1513
1514 -- In an absorptive class, learn the round bound from the tuple-set
1515 -- fixpoint (the set always stabilises after finitely many rounds, even on
1516 -- cyclic data).
1517 IF absorptive_mode AND ntuples IS NULL THEN
1518 EXECUTE format(
1519 'SELECT NOT EXISTS('
1520 || '(SELECT %2$s FROM _new EXCEPT SELECT %2$s FROM %1$I) UNION ALL '
1521 || '(SELECT %2$s FROM %1$I EXCEPT SELECT %2$s FROM _new))',
1522 work_name, colnames) INTO set_stable;
1523 IF set_stable THEN
1524 ntuples := new_count;
1525 END IF;
1526 END IF;
1527
1528 -- Copy _new into the working table (tracked -> tracked carries the tokens).
1529 EXECUTE format('TRUNCATE %I', work_name);
1530 EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM _new', work_name, colnames);
1531
1532 -- Structural fixpoint: done (acyclic / fully converged) -- sound for any
1533 -- semiring.
1534 EXIT WHEN NOT changed;
1535
1536 -- Absorptive class on cyclic data: once the value-fixpoint bound is
1537 -- reached (plus one confirming round, so that acyclic circuits whose
1538 -- token depth lags the tuple-set saturation still exit through the
1539 -- structural test above, untagged) we stop, even though the circuit
1540 -- is not structurally stable.
1541 IF absorptive_mode AND ntuples IS NOT NULL AND iters >= ntuples + 1 THEN
1542 truncated := true;
1543 EXIT;
1544 END IF;
1545 END LOOP;
1546
1547 -- Tokens of a truncated (cyclic) fixpoint are sound only under absorptive
1548 -- evaluation: RECORD that in the circuit itself.
1549 IF truncated THEN
1550 EXECUTE format(
1551 'UPDATE %I SET provsql = provsql.provenance_assume(provsql, ''absorptive'')',
1552 work_name);
1553 END IF;
1554END
1555$$ LANGUAGE plpgsql SET client_min_messages = warning;
1556
1557/**
1558 * @brief Create a comparison gate for HAVING clause provenance
1559 *
1560 * @param left_token provenance token for the left operand
1561 * @param comparison_op OID of the comparison operator
1562 * @param right_token provenance token for the right operand
1563 */
1564CREATE OR REPLACE FUNCTION provenance_cmp(
1565 left_token UUID,
1566 comparison_op OID,
1567 right_token UUID
1568)
1569RETURNS UUID AS
1570$$
1571DECLARE
1572 cmp_token UUID;
1573BEGIN
1574 -- A comparison with a NULL operand (a NULL random_variable cell, or an
1575 -- aggregate that is NULL on the instance) is unknown under SQL's 3VL in
1576 -- every possible world: the row is annotated zero. The function must
1577 -- not be STRICT: a NULL result would read as the neutral token
1578 -- (provenance_times drops it), silently turning "unknown" into
1579 -- "certainly true".
1580 IF left_token IS NULL OR right_token IS NULL OR comparison_op IS NULL THEN
1581 RETURN gate_zero();
1582 END IF;
1583 -- deterministic v5 namespace id
1584 cmp_token := public.uuid_generate_v5(
1585 uuid_ns_provsql(),
1586 concat('cmp', left_token::TEXT, comparison_op::TEXT, right_token::TEXT)
1587 );
1588 -- wire it up in the circuit
1589 PERFORM create_gate(cmp_token, 'cmp', ARRAY[left_token, right_token]);
1590 PERFORM set_infos(cmp_token, comparison_op::INTEGER);
1591 RETURN cmp_token;
1592END
1593$$ LANGUAGE plpgsql
1594 SET search_path=provsql,pg_temp,public
1595 SECURITY DEFINER
1596 IMMUTABLE
1597 PARALLEL SAFE;
1598
1600 * @brief The factors of a row annotation an aggregate comparison does not
1601 * subsume.
1602 *
1603 * A lifted comparison entails the existence of the group it ranges over, so it
1604 * supersedes that group's @c gate_delta instead of multiplying with it. This
1605 * reports which factors of @p tokens survive that supersede: a bare δ over the
1606 * compared group disappears, a @c times keeps its other factors, and anything
1607 * else -- an earlier comparison on the same group, an input -- is kept whole.
1608 */
1609CREATE FUNCTION cmp_surviving_factors(tokens UUID[], cmp UUID)
1610 RETURNS UUID[] AS
1611 'provsql', 'cmp_surviving_factors' LANGUAGE C PARALLEL SAFE STABLE;
1612
1613/**
1614 * @brief Combine a lifted aggregate comparison with the row annotation it
1615 * supersedes only part of.
1617 * @param cmp Gate of the lifted comparison.
1618 * @param tokens Row-annotation factors at the level owning the comparison.
1619 * @return @c cmp multiplied with whatever of @p tokens it does not subsume.
1620 */
1621CREATE OR REPLACE FUNCTION provenance_cmp_times(cmp UUID, tokens UUID[])
1622 RETURNS UUID AS
1623$$
1624DECLARE
1625 kept UUID[];
1626BEGIN
1627 kept := provsql.cmp_surviving_factors(tokens, cmp);
1628 IF kept IS NULL OR array_length(kept, 1) IS NULL THEN
1629 RETURN cmp;
1630 END IF;
1631 RETURN provsql.provenance_times(VARIADIC kept || cmp);
1633$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
1634
1635/**
1636 * @brief Create an arithmetic gate over scalar-valued provenance children
1637 *
1638 * Builds a deterministic @c gate_arith from an operator tag and an
1639 * ordered list of children. The tag is one of the @c provsql_arith_op
1640 * ENUM values declared in @c src/provsql_utils.h
1641 * (@c PLUS=0, @c TIMES=1, @c MINUS=2, @c DIV=3, @c NEG=4) and is
1642 * stored in the gate's @c info1 field. Children must be UUIDs of
1643 * scalar-producing gates (@c gate_rv, @c gate_value, or another
1644 * @c gate_arith). The token UUID is derived deterministically from
1645 * @p op and @p children so identical sub-expressions share their gate.
1646 *
1647 * @param op Operator tag (@c provsql_arith_op).
1648 * @param children Ordered list of child gate UUIDs.
1649 * @return UUID of the (possibly pre-existing) @c gate_arith.
1650 */
1651CREATE OR REPLACE FUNCTION provenance_arith(
1652 op INTEGER,
1653 children UUID[]
1654)
1655RETURNS UUID AS
1656$$
1657DECLARE
1658 arith_token UUID;
1659BEGIN
1660 arith_token := public.uuid_generate_v5(
1661 uuid_ns_provsql(),
1662 concat('arith', op::TEXT, children::TEXT)
1663 );
1664 PERFORM create_gate(arith_token, 'arith', children);
1665 PERFORM set_infos(arith_token, op);
1666 RETURN arith_token;
1667END
1668$$ LANGUAGE plpgsql
1669 SET search_path=provsql,pg_temp,public
1670 SECURITY DEFINER
1671 IMMUTABLE
1672 PARALLEL SAFE
1673 STRICT;
1674
1675/**
1676 * @brief Create a guarded-selection gate over scalar (RV) children.
1677 *
1678 * Builds a deterministic @c gate_case from the flattened wire list
1679 * @c [guard_1, value_1, ..., guard_k, value_k, default] (odd length): the
1680 * value of the first guard event that holds, else the default (first-match
1681 * semantics). Each guard is a Boolean event token (a @c gate_cmp or Boolean
1682 * combination); each value and the default are scalar-producing gates
1683 * (@c gate_rv, @c gate_value, @c gate_arith, another @c gate_case, ...). The
1684 * token UUID is derived deterministically from @p children so identical
1685 * @c CASE expressions share their gate.
1686 *
1687 * @param children Flattened guard/value wires ending with the default
1688 * (@c array_length must be odd and @c >= 1).
1689 * @return UUID of the (possibly pre-existing) @c gate_case.
1690 */
1691CREATE OR REPLACE FUNCTION provenance_case(
1692 children UUID[]
1693)
1694RETURNS UUID AS
1695$$
1696DECLARE
1697 case_token UUID;
1698BEGIN
1699 IF array_length(children, 1) IS NULL OR array_length(children, 1) % 2 = 0 THEN
1700 RAISE EXCEPTION 'provenance_case expects an odd number of children '
1701 '(guard/value pairs followed by a default), got %',
1702 coalesce(array_length(children, 1), 0);
1703 END IF;
1704 case_token := public.uuid_generate_v5(
1705 uuid_ns_provsql(),
1706 concat('case', children::TEXT)
1707 );
1708 PERFORM create_gate(case_token, 'case', children);
1709 RETURN case_token;
1710END
1711$$ LANGUAGE plpgsql
1712 SET search_path=provsql,pg_temp,public
1713 SECURITY DEFINER
1714 IMMUTABLE
1715 PARALLEL SAFE
1716 STRICT;
1717
1718/** @} */
1719
1720/** @defgroup semiring_evaluation Semiring evaluation
1721 * Functions for evaluating provenance circuits over semirings,
1722 * both user-defined (via function references) and compiled (built-in).
1723 * @{
1724 */
1725
1726/**
1727 * @brief Evaluate provenance using a compiled (built-in) semiring
1728 *
1729 * This C function handles semiring evaluation entirely in C++ for
1730 * better performance. The semiring is specified by name.
1731 *
1732 * @param token provenance token to evaluate
1733 * @param token2value mapping table from tokens to semiring values
1734 * @param semiring name of the compiled semiring (e.g., "formula", "counting")
1735 * @param element_one identity element of the semiring
1736 */
1737CREATE OR REPLACE FUNCTION provenance_evaluate_compiled(
1738 token UUID,
1739 token2value REGCLASS,
1740 semiring TEXT,
1741 element_one ANYELEMENT)
1742RETURNS ANYELEMENT AS
1743 'provsql', 'provenance_evaluate_compiled' LANGUAGE C PARALLEL SAFE STABLE;
1744
1745
1746/**
1747 * @brief Evaluate provenance over a user-defined semiring (PL/pgSQL version)
1748 *
1749 * Recursively walks the provenance circuit and evaluates each gate
1750 * using the provided semiring operations. This is the generic version
1751 * that accepts semiring operations as function references.
1752 *
1753 * @param token provenance token to evaluate
1754 * @param token2value mapping table from tokens to semiring values
1755 * @param element_one identity element of the semiring
1756 * @param value_type OID of the semiring value type
1757 * @param plus_function semiring addition (aggregate)
1758 * @param times_function semiring multiplication (aggregate)
1759 * @param monus_function semiring monus (binary), or NULL
1760 * @param delta_function δ-semiring operator, or NULL
1761 */
1762CREATE OR REPLACE FUNCTION provenance_evaluate(
1763 token UUID,
1764 token2value REGCLASS,
1765 element_one ANYELEMENT,
1766 value_type REGTYPE,
1767 plus_function REGPROC,
1768 times_function REGPROC,
1769 monus_function REGPROC,
1770 delta_function REGPROC)
1771 RETURNS ANYELEMENT AS
1772$$
1773DECLARE
1774 gate_type PROVENANCE_GATE;
1775 result ALIAS FOR $0;
1776 children UUID[];
1777-- cmp_value ANYELEMENT;
1778-- temp_result ANYELEMENT;
1779 value_text TEXT;
1780BEGIN
1781 SELECT get_gate_type(token) INTO gate_type;
1783 IF gate_type IS NULL THEN
1784 RETURN NULL;
1785
1786 ELSIF gate_type = 'input' THEN
1787 EXECUTE format('SELECT value FROM %s WHERE provenance=%L', token2value, token)
1788 INTO result;
1789 IF result IS NULL THEN
1790 result := element_one;
1791 END IF;
1792 ELSIF gate_type = 'mulinput' THEN
1793 SELECT concat('{',(get_children(token))[1]::TEXT,'=',(get_infos(token)).info1,'}')
1794 INTO result;
1795 ELSIF gate_type='update' THEN
1796 EXECUTE format('SELECT value FROM %s WHERE provenance=%L',token2value,token) INTO result;
1797 IF result IS NULL THEN
1798 result:=element_one;
1799 END IF;
1800 ELSIF gate_type = 'plus' THEN
1801 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
1802 plus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
1803 INTO result;
1805 ELSIF gate_type = 'times' THEN
1806 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
1807 times_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
1808 INTO result;
1810 ELSIF gate_type = 'monus' THEN
1811 IF monus_function IS NULL THEN
1812 RAISE EXCEPTION USING MESSAGE='Provenance with negation evaluated over a semiring without monus function';
1813 ELSE
1814 EXECUTE format('SELECT %s(a1,a2) FROM (SELECT provsql.provenance_evaluate(c[1],%L,%L::%s,%L,%L,%L,%L,%L) AS a1, ' ||
1815 'provsql.provenance_evaluate(c[2],%L,%L::%s,%L,%L,%L,%L,%L) AS a2 FROM get_children(%L) c) tmp',
1816 monus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function,
1817 token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
1818 INTO result;
1819 END IF;
1821 ELSIF gate_type = 'eq' THEN
1822 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1823 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1824 INTO result;
1825
1826/* elsif gate_type = 'cmp' then
1827
1828 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1829 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1830 INTO temp_result;
1832 EXECUTE format('SELECT get_extra((get_children(%L))[2])', token)
1833 INTO cmp_value;
1834
1835 IF temp_result::TEXT = cmp_value::TEXT THEN
1836 SELECT concat('{',temp_result::TEXT,'=',cmp_value::TEXT,'}')
1837 INTO result;
1838 ELSE
1839 RETURN gate_zero()
1840 */
1841
1843
1844 ELSIF gate_type = 'delta' THEN
1845 IF delta_function IS NULL THEN
1846 RAISE EXCEPTION USING MESSAGE='Provenance with aggregation evaluated over a semiring without delta function';
1847 ELSE
1848 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',
1849 delta_function, token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1850 INTO result;
1851 END IF;
1852
1853 ELSIF gate_type = 'zero' THEN
1854 EXECUTE format('SELECT %I(a) FROM (SELECT %L::%I AS a WHERE FALSE) temp', plus_function, element_one, value_type)
1855 INTO result;
1856
1857 ELSIF gate_type = 'one' THEN
1858 EXECUTE format('SELECT %L::%I', element_one, value_type)
1859 INTO result;
1860
1861 ELSIF gate_type = 'project' THEN
1862 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1863 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1864 INTO result;
1865
1866 ELSIF gate_type = 'annotation' THEN
1867 -- Transparent single-child wrapper (carries the inversion-free certificate
1868 -- / per-input order keys in extra, inert for every semiring): evaluate
1869 -- through to the child, like 'project'.
1870 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1871 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1872 INTO result;
1873
1874 ELSE
1875 RAISE EXCEPTION USING MESSAGE='provenance_evaluate cannot be called on formulas using ' || gate_type || ' gates; use compiled semirings instead';
1876 END IF;
1878 RETURN result;
1879END
1880$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
1881
1883/**
1884 * @brief Evaluate provenance over a user-defined semiring (C version)
1885 *
1886 * Optimized C implementation of provenance_evaluate. Infers the
1887 * value type from element_one. Monus and delta functions are optional.
1888 *
1889 * @param token provenance token to evaluate
1890 * @param token2value mapping table from tokens to semiring values
1891 * @param element_one identity element of the semiring
1892 * @param plus_function semiring addition (aggregate)
1893 * @param times_function semiring multiplication (aggregate)
1894 * @param monus_function semiring monus, or NULL if not needed
1895 * @param delta_function δ-semiring operator, or NULL if not needed
1896 */
1897CREATE OR REPLACE FUNCTION provenance_evaluate(
1898 token UUID,
1899 token2value REGCLASS,
1900 element_one ANYELEMENT,
1901 plus_function REGPROC,
1902 times_function REGPROC,
1903 monus_function REGPROC = NULL,
1904 delta_function REGPROC = NULL)
1905 RETURNS ANYELEMENT AS
1906 'provsql','provenance_evaluate' LANGUAGE C STABLE;
1907
1908/** @} */
1910/** @defgroup circuit_introspection Circuit introspection
1911 * Functions for examining the structure of provenance circuits,
1912 * used by visualization and where-provenance features.
1913 * @{
1914 */
1915
1916/** @brief Row type for sub_circuit_with_desc results */
1917CREATE TYPE GATE_WITH_DESC AS (f UUID, t UUID, gate_type PROVENANCE_GATE, desc_str CHARACTER VARYING, infos INTEGER[], extra TEXT);
1918
1919/**
1920 * @brief Return the sub-circuit reachable from a token, with descriptions
1922 * Recursively traverses the provenance circuit from the given token and
1923 * returns all edges together with input gate descriptions from the
1924 * mapping table.
1925 *
1926 * @param token root provenance token
1927 * @param token2desc mapping table providing descriptions for input gates
1928 */
1929CREATE OR REPLACE FUNCTION sub_circuit_with_desc(
1930 token UUID,
1931 token2desc REGCLASS) RETURNS SETOF GATE_WITH_DESC AS
1933BEGIN
1934 RETURN QUERY EXECUTE
1935 'WITH RECURSIVE transitive_closure(f,t,gate_type) AS (
1936 SELECT $1,t,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) AS t
1937 UNION ALL
1938 SELECT p1.t,u,provsql.get_gate_type(p1.t) FROM transitive_closure p1, unnest(provsql.get_children(p1.t)) AS u)
1939 SELECT *, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
1940 SELECT f::UUID,t::UUID,gate_type,NULL FROM transitive_closure
1941 UNION ALL
1942 SELECT p2.provenance::UUID as f, NULL::UUID, ''input'', CAST (p2.value AS varchar) FROM transitive_closure p1 JOIN ' || token2desc || ' AS p2
1943 ON p2.provenance=t
1944 UNION ALL
1945 SELECT provenance::UUID as f, NULL::UUID, ''input'', CAST (value AS varchar) FROM ' || token2desc || ' WHERE provenance=$1
1946 ) t'
1947 USING token LOOP;
1948 RETURN;
1949END
1950$$ LANGUAGE plpgsql PARALLEL SAFE;
1951
1952/**
1953 * @brief Identify which table and how many columns a provenance token belongs to
1954 *
1955 * Searches all provenance-tracked tables for a row matching the given
1956 * token and returns the table name and column count.
1957 *
1958 * @param token provenance token to look up
1959 * @param table_name (OUT) the table containing this token
1960 * @param nb_columns (OUT) number of non-provenance columns in that table
1961 */
1962CREATE OR REPLACE FUNCTION identify_token(
1963 token UUID, OUT table_name REGCLASS, OUT nb_columns INTEGER) AS
1964$$
1965DECLARE
1966 t RECORD;
1967 result RECORD;
1968BEGIN
1969 table_name:=NULL;
1970 nb_columns:=-1;
1971 FOR t IN
1972 SELECT relname,
1973 (SELECT count(*) FROM pg_attribute a2 WHERE a2.attrelid=a1.attrelid AND attnum>0 AND atttypid<>0)-1 c
1974 FROM pg_attribute a1 JOIN pg_type ON atttypid=pg_type.oid
1975 JOIN pg_class ON attrelid=pg_class.oid
1976 JOIN pg_namespace ON relnamespace=pg_namespace.oid
1977 WHERE typname='UUID' AND relkind='r'
1978 AND nspname<>'provsql'
1979 AND attname='provsql'
1980 LOOP
1981 EXECUTE format('SELECT * FROM %I WHERE provsql=%L',t.relname,token) INTO result;
1982 -- Test result.provsql rather than the whole RECORD: "RECORD IS NOT NULL"
1983 -- is true only when every field is non-null, so a matched row that has any
1984 -- NULL data column would be wrongly skipped. The provsql column is the
1985 -- (non-null) token we matched on, so it is set iff a row was found.
1986 IF result.provsql IS NOT NULL THEN
1987 table_name:=t.relname;
1988 nb_columns:=t.c;
1989 EXIT;
1990 END IF;
1991 END LOOP;
1992END
1993$$ LANGUAGE plpgsql STRICT;
1994
1995/**
1996 * @brief Return the sub-circuit for where-provenance computation
1997 *
1998 * Similar to sub_circuit_with_desc but resolves input gates to their
1999 * source table and column count for where-provenance evaluation.
2000 */
2001CREATE OR REPLACE FUNCTION sub_circuit_for_where(token UUID)
2002 RETURNS TABLE(f UUID, t UUID, gate_type PROVENANCE_GATE, table_name REGCLASS, nb_columns INTEGER, infos INTEGER[], extra TEXT) AS
2004 WITH RECURSIVE transitive_closure(f,t,idx,gate_type) AS (
2005 SELECT $1,t,id,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) WITH ORDINALITY AS a(t,id)
2006 UNION ALL
2007 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)
2008 ) SELECT f, t, gate_type, table_name, nb_columns, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
2009 -- One row per distinct (parent, child, child-position) edge. The
2010 -- recursive closure (UNION ALL) re-emits a gate's outgoing edges once per
2011 -- path that reaches it, so a *shared* non-input gate would otherwise be
2012 -- reported with duplicate edges; DISTINCT on the (f,t,idx) triple
2013 -- collapses those while keeping genuine repeated children (same f,t,
2014 -- different idx, e.g. a self-product). Without this, a shared
2015 -- single-child gate (notably an inversion-free order-marker annotation)
2016 -- gets its child wired k times in the where-circuit -> the locator sets
2017 -- are duplicated k-fold.
2018 SELECT DISTINCT f, t::UUID, idx, gate_type, NULL::REGCLASS AS table_name, NULL::INTEGER AS nb_columns FROM transitive_closure
2019 UNION ALL
2020 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
2021 UNION ALL
2022 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
2023 ) t
2024 -- order each parent's edges by child position so the where-circuit's TIMES
2025 -- concatenation reproduces the column order (input rows have idx NULL).
2026 ORDER BY f, idx
2027$$
2028LANGUAGE sql;
2029
2030/**
2031 * @brief BFS expansion of a provenance circuit, capped at @p max_depth
2032 *
2033 * Returns one row per (parent, child) edge in the BFS-bounded subgraph
2034 * rooted at @p root, plus one row for the root with <tt>parent</tt> and
2035 * <tt>child_pos</tt> NULL. Provenance circuits are DAGs, so a child gate
2036 * may have several parents within the bound; each such edge is reported
2037 * as a separate row, so callers must deduplicate on <tt>node</tt> if they
2038 * need a one-row-per-node view.
2039 *
2040 * <tt>depth</tt> is the node's longest-path distance from @p root
2041 * within the depth bound (the standard circuit-depth notion), so for
2042 * an edge (parent, child) it is the case that
2043 * <tt>child.depth &gt;= parent.depth + 1</tt>, except at the
2044 * <tt>max_depth</tt> truncation frontier. A node at
2045 * <tt>depth = max_depth</tt> is not
2046 * expanded; callers can detect a partial expansion by comparing
2047 * <tt>provsql.get_children</tt> length against the number of outgoing
2048 * edges reported.
2049 *
2050 * <tt>info1</tt> and <tt>info2</tt> are the INTEGER values stored on
2051 * the gate by <tt>provsql.set_infos</tt>, formatted as TEXT; their
2052 * meaning is gate-type-specific (see <tt>provsql.set_infos</tt>).
2053 *
2054 * @param root root provenance token
2055 * @param max_depth maximum BFS depth (default 8)
2056 */
2057CREATE OR REPLACE FUNCTION circuit_subgraph(root UUID, max_depth INT DEFAULT 8)
2058 RETURNS TABLE(node UUID, parent UUID, child_pos INT, gate_type TEXT, info1 TEXT, info2 TEXT, depth INT) AS
2059$$
2060 WITH RECURSIVE bfs(node, parent, child_pos, depth) AS (
2061 SELECT root, NULL::UUID, NULL::INT, 0
2062 UNION ALL
2063 SELECT c.t, b.node, c.idx::INT, b.depth + 1
2064 FROM bfs b
2065 CROSS JOIN LATERAL unnest(provsql.get_children(b.node))
2066 WITH ORDINALITY AS c(t, idx)
2067 WHERE b.depth < max_depth
2068 ),
2069 -- Each node's canonical depth is its longest-path distance from the
2070 -- root (the standard circuit-depth notion: the longest chain of
2071 -- gates separating the node from the output). The recursive CTE
2072 -- enumerates paths up to @c max_depth, so MAX over those is the
2073 -- longest path of length at most @c max_depth.
2074 node_depth AS (
2075 SELECT node, MAX(depth) AS depth FROM bfs GROUP BY node
2077 -- All distinct (parent, child, child_pos) triples seen during the BFS.
2078 -- A child reached from k parents within the bound contributes k rows.
2079 -- Self-joins (times(x, x)) contribute one row per child position.
2080 edges AS (
2081 SELECT DISTINCT parent, node AS child, child_pos
2082 FROM bfs WHERE parent IS NOT NULL
2083 )
2084 SELECT
2085 d.node,
2086 e.parent,
2087 e.child_pos,
2088 provsql.get_gate_type(d.node)::TEXT,
2089 i.info1::TEXT,
2090 i.info2::TEXT,
2091 d.depth
2092 FROM node_depth d
2093 LEFT JOIN edges e ON e.child = d.node
2094 LEFT JOIN LATERAL provsql.get_infos(d.node) i ON TRUE
2095 ORDER BY d.depth, d.node, e.parent;
2096$$ LANGUAGE sql STABLE PARALLEL SAFE;
2097
2098/**
2099 * @brief BFS subgraph of the IN-MEMORY simplified circuit rooted at @p root.
2100 *
2101 * Same row shape as @ref circuit_subgraph plus an inline @c extra
2102 * column, but built from the @c GenericCircuit returned by
2103 * @c getGenericCircuit -- i.e. AFTER @c provsql.simplify_on_load
2104 * passes (RangeCheck, ...) have rewritten any decidable @c gate_cmp
2105 * into Bernoulli @c gate_input / @c gate_zero / @c gate_one leaves.
2106 * Lets a renderer show the user what the evaluator actually sees,
2107 * without mutating the persisted DAG.
2108 *
2109 * Returns @c jsonb (an array of objects) rather than @c SETOF RECORD
2110 * to keep the C++ implementation free of SRF / @c FuncCallContext
2111 * boilerplate; callers either consume the array directly or expand
2112 * it via @c jsonb_array_elements.
2113 *
2114 * @param root Root provenance token.
2115 * @param max_depth Maximum BFS depth (default 8).
2116 */
2117CREATE OR REPLACE FUNCTION simplified_circuit_subgraph(
2118 root UUID, max_depth INT DEFAULT 8) RETURNS jsonb
2119 AS 'provsql','simplified_circuit_subgraph'
2120 LANGUAGE C STABLE PARALLEL SAFE;
2122/**
2123 * @brief Empirical histogram of a scalar sub-circuit
2124 *
2125 * Returns a jsonb array of @c {bin_lo, bin_hi, count} objects covering
2126 * the observed @c [min, max] range of @p bins equal-width samples from
2127 * the sub-circuit rooted at @p token. Sample count is taken from
2128 * @c provsql.rv_mc_samples; pinning @c provsql.monte_carlo_seed makes
2129 * the result reproducible.
2130 *
2131 * Accepted root gate types are the scalar ones: @c gate_value (Dirac
2132 * at the constant, single bin), @c gate_rv (sampled from the leaf's
2133 * distribution), and @c gate_arith (sampled by recursing through the
2134 * arithmetic DAG, with shared @c gate_rv leaves correctly correlated
2135 * within an iteration). Any other gate type raises.
2136 *
2137 * @param token Root provenance token of a scalar sub-circuit.
2138 * @param bins Number of equal-width histogram bins (default 30).
2139 * @param prov Conditioning event (defaults to @c gate_one() = no
2140 * conditioning). When non-trivial, the histogram is
2141 * over the conditional distribution recovered by
2142 * rejection sampling on the joint circuit with @p token.
2144CREATE OR REPLACE FUNCTION rv_histogram(
2145 token UUID, bins INT DEFAULT 30, prov UUID DEFAULT gate_one())
2146 RETURNS jsonb
2147 AS 'provsql','rv_histogram'
2148 LANGUAGE C VOLATILE PARALLEL SAFE;
2149
2150/**
2151 * @brief Sample the closed-form PDF and CDF of a (possibly truncated)
2152 * scalar distribution.
2154 * Returns @c {"pdf": [{x, p}, ...], "cdf": [{x, p}, ...]} with @p samples
2155 * evenly-spaced points spanning the distribution's natural display
2156 * range (intersected with the conditioning event's interval when
2157 * @c prov is non-trivial). Used by ProvSQL Studio's Distribution
2158 * profile panel to overlay the analytical curve on the empirical
2159 * histogram from :sqlfunc:`rv_histogram` -- the simplifier's
2160 * analytical wins (e.g. @c c·Exp(λ) folding to @c Exp(λ/c)) become
2161 * visible as a smooth curve riding over the MC-sampled bars.
2162 *
2163 * Returns @c NULL when the root sub-circuit is not a closed-form
2164 * shape (V1: only bare @c gate_rv of Normal / Uniform / Exponential
2165 * / INTEGER-Erlang). The frontend reads @c NULL as "skip overlay"
2166 * without erroring, so the caller can dispatch this in parallel with
2167 * @c rv_histogram regardless of the underlying shape.
2168 *
2169 * @param token Scalar gate token (random_variable's UUID).
2170 * @param samples Number of (x, p) points; must be >= 2.
2171 * @param prov Conditioning event (defaults to @c gate_one() = no
2172 * conditioning). When non-trivial, the curves are
2173 * over the truncated distribution.
2174 */
2175CREATE OR REPLACE FUNCTION rv_analytical_curves(
2176 token UUID, samples INT DEFAULT 100, prov UUID DEFAULT gate_one())
2177 RETURNS jsonb
2178 AS 'provsql','rv_analytical_curves'
2179 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2180
2181/**
2182 * @brief Draw conditional Monte Carlo samples from a scalar gate.
2183 *
2184 * Returns up to @c n samples of the scalar value at @c token; when
2185 * @c prov is not the trivial @c gate_one() event, draws are accepted
2186 * only on iterations where @c prov evaluates true (rejection
2187 * sampling). Shared @c gate_rv leaves between @c token and @c prov
2188 * are loaded into a single joint circuit so the indicator's draw
2189 * and the value's draw share their per-iteration state.
2190 *
2191 * @param token Scalar sub-circuit root.
2192 * @param n Number of accepted samples to attempt.
2193 * @param prov Conditioning event (defaults to @c gate_one() = no
2194 * conditioning).
2195 *
2196 * Emits a @c NOTICE when the conditional acceptance rate yields fewer
2197 * than @c n samples within the @c provsql.rv_mc_samples budget so the
2198 * caller can choose to widen the budget.
2199 */
2200CREATE OR REPLACE FUNCTION rv_sample(
2201 token UUID, n INTEGER, prov UUID DEFAULT gate_one())
2202 RETURNS SETOF float8
2203 AS 'provsql','rv_sample'
2204 LANGUAGE C VOLATILE PARALLEL SAFE;
2205
2206/**
2207 * @brief Resolve an input gate UUID back to its source row
2208 *
2209 * Searches every provenance-tracked relation for a row whose
2210 * <tt>provsql</tt> column equals @p UUID and returns the relation's
2211 * REGCLASS together with the row encoded as JSONB. Returns zero
2212 * rows when @p UUID is not the provenance token of any tracked row,
2213 * including when it identifies an internal gate (<tt>plus</tt>,
2214 * <tt>times</tt>, ...) rather than an input.
2215 *
2216 * Ordinarily exactly one row is returned, but if the same UUID
2217 * happens to appear as a <tt>provsql</tt> value in several tracked
2218 * tables, all matches are returned.
2219 *
2220 * @param UUID token to resolve
2221 */
2222CREATE OR REPLACE FUNCTION resolve_input(UUID UUID)
2223 RETURNS TABLE(relation REGCLASS, row_data JSONB) AS
2224$$
2225DECLARE
2226 t RECORD;
2227 rel REGCLASS;
2228 rd JSONB;
2229 -- ProvSQL's rewriter unconditionally appends a provsql column to the
2230 -- targetlist of any SELECT reading from a tracked relation; capture and
2231 -- discard it here rather than disabling the rewriter for the whole call.
2232 ign UUID;
2233BEGIN
2234 FOR t IN
2235 SELECT c.oid::REGCLASS AS regc
2236 FROM pg_attribute a
2237 JOIN pg_class c ON a.attrelid = c.oid
2238 JOIN pg_namespace ns ON c.relnamespace = ns.oid
2239 JOIN pg_type ty ON a.atttypid = ty.oid
2240 WHERE a.attname = 'provsql'
2241 AND ty.typname = 'UUID'
2242 AND c.relkind = 'r'
2243 AND ns.nspname <> 'provsql'
2244 AND a.attnum > 0
2245 LOOP
2246 FOR rel, rd, ign IN
2247 EXECUTE format(
2248 'SELECT %L::REGCLASS, to_jsonb(t) - ''provsql'', t.provsql FROM %s AS t WHERE provsql = $1',
2249 t.regc, t.regc)
2250 USING UUID
2251 LOOP
2252 relation := rel;
2253 row_data := rd;
2254 RETURN NEXT;
2255 END LOOP;
2256 END LOOP;
2257END
2258$$ LANGUAGE plpgsql STABLE;
2259
2260/** @} */
2261
2262/** @defgroup agg_token_type Type for the result of aggregate queries
2263 *
2264 * Custom type <tt>AGG_TOKEN</tt> for a provenance semimodule value, to
2265 * be used in attributes that are computed as a result of aggregation.
2266 * As for provenance tokens, this is simply a UUID, but this UUID is
2267 * displayed in a specific way (as the result of the aggregation
2268 * followed by a "(*)") to help with readability.
2270 * The TEXT output is controlled by the
2271 * <tt>provsql.aggtoken_text_as_uuid</tt> GUC. By default it is off and
2272 * the cell renders as <tt>"value (*)"</tt>. When set to on (typical
2273 * for UI layers such as ProvSQL Studio), the cell renders as the
2274 * underlying UUID instead, so the caller can click through to the
2275 * provenance circuit; the value side is then recovered via
2276 * <tt>provsql.agg_token_value_text(UUID)</tt>.
2277 *
2278 * @{
2279 */
2280
2281CREATE TYPE AGG_TOKEN;
2282
2283/** @brief Input function for the AGG_TOKEN type (parses TEXT representation) */
2284CREATE OR REPLACE FUNCTION agg_token_in(CSTRING)
2285 RETURNS AGG_TOKEN
2286 AS 'provsql','agg_token_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2287
2288/**
2289 * @brief Output function for the AGG_TOKEN type
2290 *
2291 * Default: produces the human-friendly @c "value (*)" form, where
2292 * @c value is the running aggregate state.
2293 *
2294 * When the @c provsql.aggtoken_text_as_uuid GUC is on, returns the
2295 * underlying provenance UUID instead. UI layers (notably ProvSQL
2296 * Studio) flip this on per session so aggregate cells expose the
2297 * circuit root UUID for click-through; the @c "value (*)" display
2298 * string is recovered via @c provsql.agg_token_value_text(UUID).
2299 *
2300 * Marked STABLE rather than IMMUTABLE because the chosen output
2301 * shape now depends on a GUC that the same session can flip at
2302 * runtime.
2303 */
2304CREATE OR REPLACE FUNCTION agg_token_out(AGG_TOKEN)
2305 RETURNS CSTRING
2306 AS 'provsql','agg_token_out' LANGUAGE C STABLE STRICT PARALLEL SAFE;
2307
2308/** @brief Cast an AGG_TOKEN to its TEXT representation */
2309CREATE OR REPLACE FUNCTION agg_token_cast(AGG_TOKEN)
2310 RETURNS TEXT
2311 AS 'provsql','agg_token_cast' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2312
2313CREATE TYPE AGG_TOKEN (
2314 internallength = 117,
2315 input = agg_token_in,
2316 output = agg_token_out,
2317 alignment = char
2318);
2319
2320/** @brief Extract the UUID from an AGG_TOKEN (implicit cast to UUID) */
2321CREATE OR REPLACE FUNCTION agg_token_uuid(aggtok AGG_TOKEN)
2322 RETURNS UUID AS
2323$$
2324BEGIN
2325 RETURN agg_token_cast(aggtok)::UUID;
2326END
2327$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE PARALLEL SAFE;
2328
2329/** @brief Implicit PostgreSQL cast from AGG_TOKEN to UUID (delegates to agg_token_uuid()) */
2330CREATE CAST (AGG_TOKEN AS UUID) WITH FUNCTION agg_token_uuid(AGG_TOKEN) AS IMPLICIT;
2331
2332/**
2333 * @brief Deterministic truth of a Boolean guard sub-circuit over aggregate
2334 * comparisons, evaluated in the actual world (all input tuples present).
2335 *
2336 * Guards are the shapes @c having_Expr_to_provenance_cmp mints: @c cmp gates
2337 * over aggregate-valued children (comparison-operator OID in @c info1),
2338 * @c times / @c plus combinations (AND / OR, with negation pushed into the
2339 * comparison operators), and the @c one / @c zero indicators of regular
2340 * (aggregate-free) conditions. Uses Kleene three-valued logic: returns
2341 * @c NULL on any other gate shape, or when an operand's deterministic value
2342 * cannot be resolved.
2343 */
2344CREATE OR REPLACE FUNCTION agg_guard_holds(token UUID)
2345 RETURNS BOOLEAN AS
2346$$
2347DECLARE
2348 gt PROVENANCE_GATE := get_gate_type(token);
2349 ch UUID[];
2350 opname TEXT;
2351 l NUMERIC;
2352 r NUMERIC;
2353 all_true BOOLEAN;
2354 any_true BOOLEAN;
2355 any_null BOOLEAN;
2356BEGIN
2357 IF gt = 'one' THEN
2358 RETURN true;
2359 ELSIF gt = 'zero' THEN
2360 RETURN false;
2361 ELSIF gt IN ('times', 'plus') THEN
2362 SELECT bool_and(h), bool_or(h), bool_or(h IS NULL)
2363 INTO all_true, any_true, any_null
2364 FROM (SELECT provsql.agg_guard_holds(c) AS h
2365 FROM unnest(get_children(token)) AS c) AS s;
2366 IF gt = 'times' THEN
2367 -- AND: false dominates unknown (bool_and skips NULL inputs, so it is
2368 -- false exactly when some child is false).
2369 RETURN CASE WHEN NOT all_true THEN false
2370 WHEN any_null THEN NULL
2371 ELSE true END;
2372 ELSE
2373 -- OR: true dominates unknown.
2374 RETURN CASE WHEN any_true THEN true
2375 WHEN any_null THEN NULL
2376 ELSE false END;
2377 END IF;
2378 ELSIF gt = 'cmp' THEN
2379 ch := get_children(token);
2380 l := agg_gate_value(ch[1]);
2381 r := agg_gate_value(ch[2]);
2382 IF l IS NULL OR r IS NULL THEN
2383 RETURN NULL;
2384 END IF;
2385 SELECT oprname INTO opname
2386 FROM pg_catalog.pg_operator WHERE oid = (get_infos(token)).info1;
2387 RETURN CASE opname
2388 WHEN '<' THEN l < r
2389 WHEN '<=' THEN l <= r
2390 WHEN '=' THEN l = r
2391 WHEN '<>' THEN l <> r
2392 WHEN '>=' THEN l >= r
2393 WHEN '>' THEN l > r
2394 END;
2395 END IF;
2396 RETURN NULL;
2397END
2398$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2399 SET search_path=provsql,pg_temp,public;
2400
2401/**
2402 * @brief Deterministic (actual-world) scalar value of an aggregate-carrying
2403 * gate.
2404 *
2405 * Resolves the value an aggregate expression takes on the actual data -- the
2406 * value an @c AGG_TOKEN display cell carries: @c agg / @c arith gates RECORD
2407 * it in @c extra (set by aggregate evaluation and @c agg_arith_make), a
2408 * @c value gate carries its constant, a @c semimod wraps a value gate, a
2409 * @c conditioned gate has its target's value, and a @c case gate selects the
2410 * first branch whose guard holds in the actual world (per
2411 * @c agg_guard_holds), else the default. Returns @c NULL when the gate is
2412 * not aggregate-carrying or the value cannot be resolved (e.g. a
2413 * non-NUMERIC aggregate).
2414 */
2415CREATE OR REPLACE FUNCTION agg_gate_value(token UUID)
2416 RETURNS NUMERIC AS
2417$$
2418DECLARE
2419 gt PROVENANCE_GATE := get_gate_type(token);
2420 ch UUID[];
2421 n INTEGER;
2422 holds BOOLEAN;
2423BEGIN
2424 IF gt IN ('agg', 'arith', 'value') THEN
2425 BEGIN
2426 RETURN get_extra(token)::NUMERIC;
2427 EXCEPTION WHEN others THEN
2428 RETURN NULL; -- non-NUMERIC aggregate (e.g. min over TEXT)
2429 END;
2430 ELSIF gt = 'semimod' THEN
2431 RETURN agg_gate_value((get_children(token))[2]);
2432 ELSIF gt = 'conditioned' THEN
2433 RETURN agg_gate_value((get_children(token))[1]);
2434 ELSIF gt = 'case' THEN
2435 ch := get_children(token);
2436 n := array_length(ch, 1);
2437 FOR i IN 1 .. (n - 1) / 2 LOOP
2438 holds := agg_guard_holds(ch[2 * i - 1]);
2439 IF holds IS NULL THEN
2440 RETURN NULL;
2441 ELSIF holds THEN
2442 RETURN agg_gate_value(ch[2 * i]);
2443 END IF;
2444 END LOOP;
2445 RETURN agg_gate_value(ch[n]);
2446 END IF;
2447 RETURN NULL;
2448END
2449$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
2450 SET search_path=provsql,pg_temp,public;
2451
2452/**
2453 * @brief Recover the @c "value (*)" display string for an aggregation gate
2454 *
2455 * Companion helper to the @c provsql.aggtoken_text_as_uuid GUC. With
2456 * the GUC on, an @c AGG_TOKEN cell prints as the underlying provenance
2457 * UUID, which is convenient for tooling that wants to click through to
2458 * the circuit but loses the human-readable aggregate value. This
2459 * function takes such a UUID and returns the original @c "value (*)"
2460 * string by reading the gate's @c extra (set by aggregate evaluation
2461 * for @c agg gates, and by @c agg_arith_make for the @c arith gates
2462 * that AGG_TOKEN arithmetic mints); for the other aggregate-carrying
2463 * gates (@c case, @c conditioned, @c semimod, @c value) the value is
2464 * resolved through the circuit by @c agg_gate_value. Returns @c NULL
2465 * if @p token does not resolve to an aggregate-carrying gate.
2466 *
2467 * @param token UUID of an @c agg gate (typically obtained from an
2468 * @c AGG_TOKEN cell when @c aggtoken_text_as_uuid is on,
2469 * or via a manual UUID cast otherwise).
2470 */
2471CREATE OR REPLACE FUNCTION agg_token_value_text(token UUID)
2472 RETURNS TEXT AS
2473$$
2474 SELECT CASE
2475 -- agg gates: extra is set by aggregate evaluation; arith gates
2476 -- (AGG_TOKEN arithmetic): extra is recorded by agg_arith_make.
2477 WHEN provsql.get_gate_type(token) IN ('agg', 'arith')
2478 THEN provsql.get_extra(token) || ' (*)'
2479 -- other aggregate-carrying gates: resolve the actual-world value
2480 -- through the circuit.
2481 WHEN provsql.get_gate_type(token) IN ('case', 'conditioned', 'semimod', 'value')
2482 THEN provsql.agg_gate_value(token)::TEXT || ' (*)'
2483 ELSE NULL
2484 END;
2485$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE;
2486
2487/** @brief Cast an AGG_TOKEN to NUMERIC (extracts the aggregate value, loses provenance) */
2488CREATE OR REPLACE FUNCTION agg_token_to_numeric(AGG_TOKEN)
2489 RETURNS NUMERIC
2490 AS 'provsql','agg_token_to_numeric' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2491
2492/** @brief Cast an AGG_TOKEN to double precision (extracts the aggregate value, loses provenance) */
2493CREATE OR REPLACE FUNCTION agg_token_to_float8(AGG_TOKEN)
2494 RETURNS double precision
2495 AS 'provsql','agg_token_to_float8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2496
2497/** @brief Cast an AGG_TOKEN to INTEGER (extracts the aggregate value, loses provenance) */
2498CREATE OR REPLACE FUNCTION agg_token_to_int4(AGG_TOKEN)
2499 RETURNS INTEGER
2500 AS 'provsql','agg_token_to_int4' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2501
2502/** @brief Cast an AGG_TOKEN to bigint (extracts the aggregate value, loses provenance) */
2503CREATE OR REPLACE FUNCTION agg_token_to_int8(AGG_TOKEN)
2504 RETURNS bigint
2505 AS 'provsql','agg_token_to_int8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2506
2507/** @brief Cast an AGG_TOKEN to TEXT (extracts the aggregate value, loses provenance) */
2508CREATE OR REPLACE FUNCTION agg_token_to_text(AGG_TOKEN)
2509 RETURNS TEXT
2510 AS 'provsql','agg_token_to_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2511
2512/** @brief Assignment cast from AGG_TOKEN to NUMERIC (extracts the scalar
2513 * value, dropping provenance). ASSIGNMENT, not IMPLICIT: provenance-
2514 * preserving arithmetic on aggregates is provided by the native
2515 * AGG_TOKEN operators below, so an implicit NUMERIC coercion would only
2516 * silently steal `s + 1` away from them (and reroute it differently
2517 * depending on whether provsql is in search_path). Write `s::NUMERIC`
2518 * to opt into the lossy scalar. */
2519CREATE CAST (AGG_TOKEN AS NUMERIC) WITH FUNCTION agg_token_to_numeric(AGG_TOKEN) AS ASSIGNMENT;
2520
2521-- ---------------------------------------------------------------------
2522-- Arithmetic on aggregates (AGG_TOKEN)
2523--
2524-- Mirrors the random_variable arithmetic surface: the operators build a
2525-- `gate_arith` over the operand provenance UUIDs (via provenance_arith,
2526-- info1 = PROVSQL_ARITH_*), so the arithmetic is recorded symbolically
2527-- in the circuit and can be resolved when a comparison (gate_cmp) over
2528-- the result is evaluated. Unlike random_variable (a bare UUID), an
2529-- AGG_TOKEN also carries a running scalar value, so each operator
2530-- additionally computes the resulting value and bundles it back with the
2531-- new gate.
2532-- ---------------------------------------------------------------------
2533
2534/** @brief Running value of an AGG_TOKEN as NUMERIC, without the
2535 * provenance-loss warning the public cast emits (internal use). */
2536CREATE OR REPLACE FUNCTION agg_token_value(AGG_TOKEN)
2537 RETURNS NUMERIC
2538 AS 'provsql','agg_token_value' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2539
2540/** @brief Bundle a provenance gate UUID with a running value into an
2541 * AGG_TOKEN (inverse of the agg_token_uuid / agg_token_value
2542 * accessors). */
2543CREATE OR REPLACE FUNCTION agg_token_make(tok UUID, val NUMERIC)
2544 RETURNS AGG_TOKEN AS
2545$$
2546 SELECT format('( %s , %s )', tok::TEXT, val::TEXT)::provsql.AGG_TOKEN;
2547$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
2548 SET search_path=provsql,pg_temp,public;
2549
2550/** @brief Lift a scalar NUMERIC constant into a gate_value leaf and
2551 * return its UUID, so it can be a child of a gate_arith (the agg-side
2552 * analogue of as_random for random_variable). */
2553CREATE OR REPLACE FUNCTION agg_value_gate(v NUMERIC)
2554 RETURNS UUID AS
2555$$
2556DECLARE
2557 token UUID := public.uuid_generate_v5(
2558 provsql.uuid_ns_provsql(), concat('value', v::TEXT));
2559BEGIN
2560 PERFORM provsql.create_gate(token, 'value');
2561 PERFORM provsql.set_extra(token, v::TEXT);
2562 RETURN token;
2563END
2564$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE
2565 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
2566
2567/** @brief Mint (or reuse) the gate_arith for an AGG_TOKEN arithmetic
2568 * result and return the AGG_TOKEN carrying it.
2569 *
2570 * Also records the computed scalar in the gate's @c extra -- exactly
2571 * what aggregate evaluation does for @c agg gates -- so
2572 * @c agg_token_value_text can recover the @c "value (*)" display from
2573 * the bare UUID (as ProvSQL Studio does for result cells under
2574 * @c provsql.aggtoken_text_as_uuid). The gate UUID is deterministic in
2575 * (op, children), so re-recording the (identical) value is idempotent. */
2576CREATE OR REPLACE FUNCTION agg_arith_make(op INT, children UUID[], val NUMERIC)
2577 RETURNS AGG_TOKEN AS
2578$$
2579DECLARE
2580 token UUID := provsql.provenance_arith(op, children);
2581BEGIN
2582 PERFORM provsql.set_extra(token, val::TEXT);
2583 RETURN provsql.agg_token_make(token, val);
2584END
2585$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
2586 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
2587
2588-- AGG_TOKEN <op> AGG_TOKEN --------------------------------------------
2589/** @brief AGG_TOKEN + AGG_TOKEN (gate_arith PLUS). */
2590CREATE OR REPLACE FUNCTION agg_token_plus(a AGG_TOKEN, b AGG_TOKEN)
2591 RETURNS AGG_TOKEN AS
2592$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, (b)::UUID],
2593 provsql.agg_token_value(a) + provsql.agg_token_value(b)); $$
2594 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2595
2596/** @brief AGG_TOKEN - AGG_TOKEN (gate_arith MINUS). */
2597CREATE OR REPLACE FUNCTION agg_token_minus(a AGG_TOKEN, b AGG_TOKEN)
2598 RETURNS AGG_TOKEN AS
2599$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, (b)::UUID],
2600 provsql.agg_token_value(a) - provsql.agg_token_value(b)); $$
2601 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2602
2603/** @brief AGG_TOKEN * AGG_TOKEN (gate_arith TIMES). */
2604CREATE OR REPLACE FUNCTION agg_token_times(a AGG_TOKEN, b AGG_TOKEN)
2605 RETURNS AGG_TOKEN AS
2606$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, (b)::UUID],
2607 provsql.agg_token_value(a) * provsql.agg_token_value(b)); $$
2608 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2609
2610/** @brief AGG_TOKEN / AGG_TOKEN (gate_arith DIV). */
2611CREATE OR REPLACE FUNCTION agg_token_div(a AGG_TOKEN, b AGG_TOKEN)
2612 RETURNS AGG_TOKEN AS
2613$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, (b)::UUID],
2614 provsql.agg_token_value(a) / provsql.agg_token_value(b)); $$
2615 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2616
2617/** @brief Unary -AGG_TOKEN (gate_arith NEG). */
2618CREATE OR REPLACE FUNCTION agg_token_neg(a AGG_TOKEN)
2619 RETURNS AGG_TOKEN AS
2620$$ SELECT provsql.agg_arith_make(4, ARRAY[(a)::UUID],
2621 - provsql.agg_token_value(a)); $$
2622 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2623
2624-- AGG_TOKEN <op> NUMERIC ----------------------------------------------
2625/** @brief AGG_TOKEN + NUMERIC (gate_arith PLUS, constant lifted to a value gate). */
2626CREATE OR REPLACE FUNCTION agg_token_plus_numeric(a AGG_TOKEN, b NUMERIC)
2627 RETURNS AGG_TOKEN AS
2628$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2629 provsql.agg_token_value(a) + b); $$
2630 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2631
2632/** @brief AGG_TOKEN - NUMERIC. */
2633CREATE OR REPLACE FUNCTION agg_token_minus_numeric(a AGG_TOKEN, b NUMERIC)
2634 RETURNS AGG_TOKEN AS
2635$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2636 provsql.agg_token_value(a) - b); $$
2637 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2638
2639/** @brief AGG_TOKEN * NUMERIC. */
2640CREATE OR REPLACE FUNCTION agg_token_times_numeric(a AGG_TOKEN, b NUMERIC)
2641 RETURNS AGG_TOKEN AS
2642$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2643 provsql.agg_token_value(a) * b); $$
2644 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2645
2646/** @brief AGG_TOKEN / NUMERIC. */
2647CREATE OR REPLACE FUNCTION agg_token_div_numeric(a AGG_TOKEN, b NUMERIC)
2648 RETURNS AGG_TOKEN AS
2649$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2650 provsql.agg_token_value(a) / b); $$
2651 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2652
2653-- NUMERIC <op> AGG_TOKEN ----------------------------------------------
2654/** @brief NUMERIC + AGG_TOKEN. */
2655CREATE OR REPLACE FUNCTION numeric_plus_agg_token(a NUMERIC, b AGG_TOKEN)
2656 RETURNS AGG_TOKEN AS
2657$$ SELECT provsql.agg_arith_make(0, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2658 a + provsql.agg_token_value(b)); $$
2659 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2660
2661/** @brief NUMERIC - AGG_TOKEN. */
2662CREATE OR REPLACE FUNCTION numeric_minus_agg_token(a NUMERIC, b AGG_TOKEN)
2663 RETURNS AGG_TOKEN AS
2664$$ SELECT provsql.agg_arith_make(2, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2665 a - provsql.agg_token_value(b)); $$
2666 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2667
2668/** @brief NUMERIC * AGG_TOKEN. */
2669CREATE OR REPLACE FUNCTION numeric_times_agg_token(a NUMERIC, b AGG_TOKEN)
2670 RETURNS AGG_TOKEN AS
2671$$ SELECT provsql.agg_arith_make(1, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2672 a * provsql.agg_token_value(b)); $$
2673 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2674
2675/** @brief NUMERIC / AGG_TOKEN. */
2676CREATE OR REPLACE FUNCTION numeric_div_agg_token(a NUMERIC, b AGG_TOKEN)
2677 RETURNS AGG_TOKEN AS
2678$$ SELECT provsql.agg_arith_make(3, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2679 a / provsql.agg_token_value(b)); $$
2680 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2681
2682-- Operator declarations -----------------------------------------------
2683CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_plus, COMMUTATOR = +);
2684CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_minus);
2685CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_times, COMMUTATOR = *);
2686CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_div);
2687CREATE OPERATOR - (RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_neg);
2688
2689CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_plus_numeric, COMMUTATOR = +);
2690CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_minus_numeric);
2691CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_times_numeric, COMMUTATOR = *);
2692CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_div_numeric);
2693
2694CREATE OPERATOR + (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_plus_agg_token, COMMUTATOR = +);
2695CREATE OPERATOR - (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_minus_agg_token);
2696CREATE OPERATOR * (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_times_agg_token, COMMUTATOR = *);
2697CREATE OPERATOR / (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_div_agg_token);
2698
2699/** @brief Assignment cast from AGG_TOKEN to double precision */
2700CREATE CAST (AGG_TOKEN AS double precision) WITH FUNCTION agg_token_to_float8(AGG_TOKEN) AS ASSIGNMENT;
2701/** @brief Assignment cast from AGG_TOKEN to INTEGER */
2702CREATE CAST (AGG_TOKEN AS INTEGER) WITH FUNCTION agg_token_to_int4(AGG_TOKEN) AS ASSIGNMENT;
2703/** @brief Assignment cast from AGG_TOKEN to bigint */
2704CREATE CAST (AGG_TOKEN AS bigint) WITH FUNCTION agg_token_to_int8(AGG_TOKEN) AS ASSIGNMENT;
2705/** @brief Assignment cast from AGG_TOKEN to TEXT (extracts value, not UUID) */
2706CREATE CAST (AGG_TOKEN AS TEXT) WITH FUNCTION agg_token_to_text(AGG_TOKEN) AS ASSIGNMENT;
2707
2708/**
2709 * @brief Condition a discrete aggregate's distribution on an event:
2710 * @c "SUM(x) | C".
2711 *
2712 * Mirrors @c random_variable_cond for the @c AGG_TOKEN carrier: returns a
2713 * conditioned @c AGG_TOKEN that flows onward, its provenance token wrapped in
2714 * the composable two-child @c gate_conditioned @c [agg_target, condition]
2715 * while its running value is preserved. The moment / support dispatchers
2716 * unpack it (@c agg_conditioned_target + @c rv_conditioned_prov) and route
2717 * through the existing @c agg_raw_moment with the condition conjoined into the
2718 * @c prov argument, so @c expected(SUM(x)|C) / @c variance(SUM(x)|C) report
2719 * the conditional aggregate distribution. Nested conditioning folds.
2720 */
2721CREATE OR REPLACE FUNCTION agg_token_cond(a AGG_TOKEN, cond UUID)
2722 RETURNS AGG_TOKEN AS
2723$$
2724DECLARE
2725 tok UUID;
2726 ev UUID;
2727 result UUID;
2728 ch UUID[];
2729BEGIN
2730 IF cond IS NULL OR cond = gate_one() THEN
2731 RETURN a;
2732 END IF;
2733
2734 tok := (a)::UUID;
2735 IF get_gate_type(tok) = 'conditioned'
2736 AND array_length(get_children(tok), 1) = 2 THEN
2737 ch := get_children(tok);
2738 tok := ch[1];
2739 ev := provenance_times(ch[2], cond);
2740 ELSE
2741 ev := cond;
2742 END IF;
2743
2744 result := public.uuid_generate_v5(uuid_ns_provsql(),
2745 concat('conditioned', tok, ev));
2746 PERFORM create_gate(result, 'conditioned', ARRAY[tok, ev]);
2747 RETURN agg_token_make(result, agg_token_value(a));
2748END
2749$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
2750 SECURITY DEFINER PARALLEL SAFE;
2751
2752CREATE OPERATOR | (
2753 LEFTARG = AGG_TOKEN,
2754 RIGHTARG = UUID,
2755 PROCEDURE = agg_token_cond
2756);
2757
2758/**
2759 * @brief Placeholder for @c "SUM(x) | (predicate)" on an AGG_TOKEN.
2760 *
2761 * Lets the conditioning event be a natural Boolean predicate (e.g.
2762 * @c "SUM(x) | (SUM(x) > 5)") instead of a hand-built gate. Never executes:
2763 * the planner converts the Boolean operand into a condition gate and emits
2764 * @c agg_token_cond.
2765 */
2766CREATE OR REPLACE FUNCTION agg_token_cond_predicate(
2767 a AGG_TOKEN, predicate BOOLEAN) RETURNS AGG_TOKEN AS
2768$$
2769BEGIN
2770 RAISE EXCEPTION 'AGG_TOKEN | (predicate) must be rewritten by the ProvSQL '
2771 'planner hook: the right operand must be a Boolean combination of '
2772 'aggregate / random_variable comparisons (is provsql.active off?)';
2773END
2774$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
2775
2776CREATE OPERATOR | (
2777 LEFTARG = AGG_TOKEN,
2778 RIGHTARG = BOOLEAN,
2779 PROCEDURE = agg_token_cond_predicate
2780);
2781
2782/**
2783 * @brief Unpack the target of a conditioned @c AGG_TOKEN.
2784 *
2785 * For a @c "SUM(x) | C" whose provenance token is the two-child
2786 * @c gate_conditioned @c [agg_target, condition] returns the AGG_TOKEN over
2787 * @c agg_target (same running value); for any other AGG_TOKEN returns it
2788 * unchanged. The conditioning event itself is recovered separately via
2789 * @c rv_conditioned_prov on the token's UUID.
2790 */
2791CREATE OR REPLACE FUNCTION agg_conditioned_target(a AGG_TOKEN)
2792 RETURNS AGG_TOKEN AS
2793$$
2794 SELECT CASE
2795 WHEN provsql.get_gate_type((a)::UUID) = 'conditioned'
2796 AND array_length(provsql.get_children((a)::UUID), 1) = 2
2797 THEN provsql.agg_token_make(
2798 (provsql.get_children((a)::UUID))[1], provsql.agg_token_value(a))
2799 ELSE a
2800 END;
2801$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2802
2803/**
2804 * @brief Placeholder comparison of AGG_TOKEN with NUMERIC
2805 *
2806 * This function is never actually called; it exists so the SQL parser
2807 * accepts comparison operators between AGG_TOKEN and NUMERIC values.
2808 * The ProvSQL query rewriter replaces these comparisons at plan time.
2809 */
2810CREATE OR REPLACE FUNCTION agg_token_comp_numeric(a AGG_TOKEN, b NUMERIC)
2811RETURNS BOOLEAN
2812LANGUAGE plpgsql
2813IMMUTABLE STRICT PARALLEL SAFE
2814AS $$
2815BEGIN
2816 RAISE EXCEPTION 'Comparison AGG_TOKEN-NUMERIC not implemented, should be replaced by ProvSQL behavior';
2817END;
2818$$;
2819
2820/**
2821 * @brief Placeholder comparison of NUMERIC with AGG_TOKEN
2822 *
2823 * Symmetric to agg_token_comp_numeric; never actually called.
2824 * The ProvSQL query rewriter replaces these comparisons at plan time.
2825 */
2826CREATE OR REPLACE FUNCTION numeric_comp_agg_token(a NUMERIC, b AGG_TOKEN)
2827RETURNS BOOLEAN
2828LANGUAGE plpgsql
2829IMMUTABLE STRICT PARALLEL SAFE
2830AS $$
2831BEGIN
2832 RAISE EXCEPTION 'Comparison NUMERIC-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
2833END;
2834$$;
2835
2836/** @brief SQL operator AGG_TOKEN < NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2837CREATE OPERATOR < (
2838 LEFTARG = AGG_TOKEN,
2839 RIGHTARG = NUMERIC,
2840 PROCEDURE = agg_token_comp_numeric,
2841 COMMUTATOR = >,
2842 NEGATOR = >=
2843);
2844/** @brief SQL operator NUMERIC < AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2845CREATE OPERATOR < (
2846 LEFTARG = NUMERIC,
2847 RIGHTARG = AGG_TOKEN,
2848 PROCEDURE = numeric_comp_agg_token,
2849 COMMUTATOR = >,
2850 NEGATOR = >=
2851);
2852
2853/** @brief SQL operator AGG_TOKEN <= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2854CREATE OPERATOR <= (
2855 LEFTARG = AGG_TOKEN,
2856 RIGHTARG = NUMERIC,
2857 PROCEDURE = agg_token_comp_numeric,
2858 COMMUTATOR = >=,
2859 NEGATOR = >
2860);
2861/** @brief SQL operator NUMERIC <= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2862CREATE OPERATOR <= (
2863 LEFTARG = NUMERIC,
2864 RIGHTARG = AGG_TOKEN,
2865 PROCEDURE = numeric_comp_agg_token,
2866 COMMUTATOR = >=,
2867 NEGATOR = >
2868);
2869
2870/** @brief SQL operator AGG_TOKEN = NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2871CREATE OPERATOR = (
2872 LEFTARG = AGG_TOKEN,
2873 RIGHTARG = NUMERIC,
2874 PROCEDURE = agg_token_comp_numeric,
2875 COMMUTATOR = =,
2876 NEGATOR = <>
2877);
2878/** @brief SQL operator NUMERIC = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2879CREATE OPERATOR = (
2880 LEFTARG = NUMERIC,
2881 RIGHTARG = AGG_TOKEN,
2882 PROCEDURE = numeric_comp_agg_token,
2883 COMMUTATOR = =,
2884 NEGATOR = <>
2885);
2886
2887/** @brief SQL operator AGG_TOKEN <> NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2888CREATE OPERATOR <> (
2889 LEFTARG = AGG_TOKEN,
2890 RIGHTARG = NUMERIC,
2891 PROCEDURE = agg_token_comp_numeric,
2892 COMMUTATOR = <>,
2893 NEGATOR = =
2894);
2895/** @brief SQL operator NUMERIC <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2896CREATE OPERATOR <> (
2897 LEFTARG = NUMERIC,
2898 RIGHTARG = AGG_TOKEN,
2899 PROCEDURE = numeric_comp_agg_token,
2900 COMMUTATOR = <>,
2901 NEGATOR = =
2902);
2903
2904/** @brief SQL operator AGG_TOKEN >= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2905CREATE OPERATOR >= (
2906 LEFTARG = AGG_TOKEN,
2907 RIGHTARG = NUMERIC,
2908 PROCEDURE = agg_token_comp_numeric,
2909 COMMUTATOR = <=,
2910 NEGATOR = <
2911);
2912/** @brief SQL operator NUMERIC >= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2913CREATE OPERATOR >= (
2914 LEFTARG = NUMERIC,
2915 RIGHTARG = AGG_TOKEN,
2916 PROCEDURE = numeric_comp_agg_token,
2917 COMMUTATOR = <=,
2918 NEGATOR = <
2919);
2920
2921/** @brief SQL operator AGG_TOKEN > NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2922CREATE OPERATOR > (
2923 LEFTARG = AGG_TOKEN,
2924 RIGHTARG = NUMERIC,
2925 PROCEDURE = agg_token_comp_numeric,
2926 COMMUTATOR = <,
2927 NEGATOR = <=
2928);
2929/** @brief SQL operator NUMERIC > AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2930CREATE OPERATOR > (
2931 LEFTARG = NUMERIC,
2932 RIGHTARG = AGG_TOKEN,
2933 PROCEDURE = numeric_comp_agg_token,
2934 COMMUTATOR = <,
2935 NEGATOR = <=
2936);
2937
2938/**
2939 * @brief Placeholder comparison of two AGG_TOKEN values (the diagonal)
2940 *
2941 * Never actually called; lets the parser accept AGG_TOKEN \<op\> AGG_TOKEN
2942 * (e.g. sum(x) > sum(y) on materialised tokens), which the ProvSQL
2943 * rewriter lowers to a gate_cmp at plan time. Declaring this diagonal
2944 * also disambiguates `s = s2` (otherwise "operator is not unique",
2945 * because both AGG_TOKEN -> UUID and AGG_TOKEN -> NUMERIC casts apply).
2946 */
2947CREATE OR REPLACE FUNCTION agg_token_comp_agg_token(a AGG_TOKEN, b AGG_TOKEN)
2948RETURNS BOOLEAN
2949LANGUAGE plpgsql
2950IMMUTABLE STRICT PARALLEL SAFE
2951AS $$
2952BEGIN
2953 RAISE EXCEPTION 'Comparison AGG_TOKEN-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
2954END;
2955$$;
2956
2957/** @brief SQL operator AGG_TOKEN < AGG_TOKEN (placeholder rewritten at plan time) */
2958CREATE OPERATOR < (
2959 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2960 COMMUTATOR = >, NEGATOR = >=
2961);
2962/** @brief SQL operator AGG_TOKEN <= AGG_TOKEN (placeholder rewritten at plan time) */
2963CREATE OPERATOR <= (
2964 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2965 COMMUTATOR = >=, NEGATOR = >
2966);
2967/** @brief SQL operator AGG_TOKEN > AGG_TOKEN (placeholder rewritten at plan time) */
2968CREATE OPERATOR > (
2969 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2970 COMMUTATOR = <, NEGATOR = <=
2971);
2972/** @brief SQL operator AGG_TOKEN >= AGG_TOKEN (placeholder rewritten at plan time) */
2973CREATE OPERATOR >= (
2974 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2975 COMMUTATOR = <=, NEGATOR = <
2976);
2977/** @brief SQL operator AGG_TOKEN = AGG_TOKEN (placeholder rewritten at plan time) */
2978CREATE OPERATOR = (
2979 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2980 COMMUTATOR = =, NEGATOR = <>
2981);
2982/** @brief SQL operator AGG_TOKEN <> AGG_TOKEN (placeholder rewritten at plan time) */
2983CREATE OPERATOR <> (
2984 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2985 COMMUTATOR = <>, NEGATOR = =
2986);
2987
2988/**
2989 * @brief Placeholder comparison of AGG_TOKEN with TEXT
2990 *
2991 * This function is never actually called; it exists so the SQL parser
2992 * accepts comparison operators between AGG_TOKEN and TEXT values.
2993 * The ProvSQL query rewriter replaces these comparisons at plan time.
2994 */
2995CREATE OR REPLACE FUNCTION agg_token_comp_text(a AGG_TOKEN, b TEXT)
2996RETURNS BOOLEAN
2997LANGUAGE plpgsql
2998IMMUTABLE STRICT PARALLEL SAFE
2999AS $$
3000BEGIN
3001 RAISE EXCEPTION 'Comparison AGG_TOKEN-TEXT not implemented, should be replaced by ProvSQL behavior';
3002END;
3003$$;
3004
3005/**
3006 * @brief Placeholder comparison of TEXT with AGG_TOKEN
3007 *
3008 * Symmetric to agg_token_comp_text; never actually called.
3009 * The ProvSQL query rewriter replaces these comparisons at plan time.
3010 */
3011CREATE OR REPLACE FUNCTION text_comp_agg_token(a TEXT, b AGG_TOKEN)
3012RETURNS BOOLEAN
3013LANGUAGE plpgsql
3014IMMUTABLE STRICT PARALLEL SAFE
3015AS $$
3016BEGIN
3017 RAISE EXCEPTION 'Comparison TEXT-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
3018END;
3019$$;
3020
3021/** @brief SQL operator AGG_TOKEN = TEXT (placeholder rewritten by ProvSQL at plan time) */
3022CREATE OPERATOR = (
3023 LEFTARG = AGG_TOKEN,
3024 RIGHTARG = TEXT,
3025 PROCEDURE = agg_token_comp_text,
3026 COMMUTATOR = =,
3027 NEGATOR = <>
3028);
3029/** @brief SQL operator TEXT = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3030CREATE OPERATOR = (
3031 LEFTARG = TEXT,
3032 RIGHTARG = AGG_TOKEN,
3033 PROCEDURE = text_comp_agg_token,
3034 COMMUTATOR = =,
3035 NEGATOR = <>
3036);
3037
3038/** @brief SQL operator AGG_TOKEN <> TEXT (placeholder rewritten by ProvSQL at plan time) */
3039CREATE OPERATOR <> (
3040 LEFTARG = AGG_TOKEN,
3041 RIGHTARG = TEXT,
3042 PROCEDURE = agg_token_comp_text,
3043 COMMUTATOR = <>,
3044 NEGATOR = =
3045);
3046/** @brief SQL operator TEXT <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
3047CREATE OPERATOR <> (
3048 LEFTARG = TEXT,
3049 RIGHTARG = AGG_TOKEN,
3050 PROCEDURE = text_comp_agg_token,
3051 COMMUTATOR = <>,
3052 NEGATOR = =
3053);
3054
3055/** @} */
3057/** @defgroup random_variable_type Type for continuous random variables
3058 *
3059 * Custom type <tt>random_variable</tt>: a thin wrapper around a
3060 * provenance gate UUID, used to expose continuous probabilistic
3061 * c-tables in SQL. The UUID indexes either a <tt>gate_rv</tt>
3062 * (an actual distribution) or a <tt>gate_value</tt> (a
3063 * zero-variance constant produced by <tt>provsql.as_random</tt>).
3064 * Binary-coercible with <tt>UUID</tt> (same 16-byte layout), so an
3065 * <tt>rv</tt>-typed expression flows directly into any function
3066 * expecting a UUID at zero runtime cost.
3067 *
3068 * Constructors live in this group: <tt>provsql.normal(μ, σ)</tt>,
3069 * <tt>provsql.uniform(a, b)</tt>, <tt>provsql.exponential(λ)</tt>,
3070 * <tt>provsql.erlang(k, λ)</tt>, <tt>provsql.gamma(k, λ)</tt>,
3071 * <tt>provsql.chi_squared(k)</tt>, <tt>provsql.lognormal(μ, σ)</tt>,
3072 * <tt>provsql.weibull(k, λ)</tt>, <tt>provsql.pareto(xₘ, α)</tt>,
3073 * <tt>provsql.beta(α, β)</tt>,
3074 * the discrete count constructors (<tt>provsql.poisson(λ)</tt>,
3075 * <tt>provsql.binomial(n, p)</tt>, <tt>provsql.geometric(p)</tt>,
3076 * <tt>provsql.hypergeometric(N, K, n)</tt>,
3077 * <tt>provsql.negative_binomial(r, p)</tt>, all lowering to
3078 * @c categorical via @c categorical_from_log_pmf),
3079 * and <tt>provsql.as_random(c)</tt>.
3080 * Operator overloads
3081 * (<tt>+ - * /</tt> and the six comparators) are defined further
3082 * below, alongside direct <tt>rv_cmp_*</tt> UUID constructors for
3083 * callers that want a <tt>gate_cmp</tt> token without going through
3084 * the planner hook.
3085 * @{
3086 */
3087
3088CREATE TYPE random_variable;
3089
3090/** @brief Input function for the random_variable type */
3091CREATE OR REPLACE FUNCTION random_variable_in(CSTRING)
3092 RETURNS random_variable
3093 AS 'provsql','random_variable_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3094
3095/** @brief Output function for the random_variable type */
3096CREATE OR REPLACE FUNCTION random_variable_out(random_variable)
3097 RETURNS CSTRING
3098 AS 'provsql','random_variable_out' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3099
3100CREATE TYPE random_variable (
3101 internallength = 16,
3102 input = random_variable_in,
3103 output = random_variable_out,
3104 alignment = char
3105);
3107/** @brief Build a random_variable from a UUID (internal). */
3108CREATE OR REPLACE FUNCTION random_variable_make(tok UUID)
3109 RETURNS random_variable
3110 AS 'provsql','random_variable_make' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
3111
3112/** @brief Binary-coercible cast random_variable -> UUID.
3113 * A random_variable is byte-for-byte a pg_uuid_t (alignment char,
3114 * length 16), so WITHOUT FUNCTION lets PostgreSQL reinterpret the
3115 * bytes at zero runtime cost. The cast is ASSIGNMENT (not IMPLICIT):
3116 * an implicit cross-domain cast would silently reroute a comparison
3117 * such as `v < w` to `UUID < UUID` (raw byte comparison) whenever
3118 * `provsql` is not in search_path, since operators are resolved
3119 * through search_path but casts are not. Demoting to ASSIGNMENT
3120 * turns that silent wrong result into a clean parse error. Passing a
3121 * random_variable to a UUID-taking function now needs an explicit
3122 * `v::UUID` (function resolution never applies assignment casts). */
3123CREATE CAST (random_variable AS UUID) WITHOUT FUNCTION AS ASSIGNMENT;
3124CREATE CAST (UUID AS random_variable) WITHOUT FUNCTION;
3125
3126/**
3127 * @brief Coerce an @c AGG_TOKEN to a @c random_variable (its circuit token).
3128 *
3129 * An aggregate over probabilistic tuples IS a random variable: its
3130 * @c AGG_TOKEN carries the provenance circuit of the aggregate distribution.
3131 * Exposing that as a @c random_variable lets a comparison / conditioning
3132 * predicate mix the two -- e.g. conditioning a latent leaf on a count,
3133 * @c "R | (poisson(lambda) = C)" with @c C a @c count(*) AGG_TOKEN -- resolve
3134 * to the ordinary @c random_variable comparison operators (which the planner
3135 * hook rewrites into a @c gate_cmp). IMPLICIT so the mixed comparison
3136 * type-checks without an explicit cast; the polymorphic dispatchers keep
3137 * their exact @c AGG_TOKEN overloads (an exact match beats the cast).
3138 */
3139CREATE OR REPLACE FUNCTION agg_token_to_random_variable(a AGG_TOKEN)
3140 RETURNS random_variable AS
3141$$ SELECT provsql.random_variable_make(provsql.agg_token_uuid($1)); $$
3142 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3143CREATE CAST (AGG_TOKEN AS random_variable)
3144 WITH FUNCTION agg_token_to_random_variable(AGG_TOKEN) AS IMPLICIT;
3145
3146/**
3147 * @brief Internal: true iff @p x is a finite (non-NaN, non-±∞) float8.
3148 *
3149 * PostgreSQL's <tt>isnan</tt> is defined for <tt>NUMERIC</tt> only,
3150 * not for <tt>double precision</tt>; we use the inequality form,
3151 * which works because PG defines <tt>NaN = NaN</tt> as <tt>TRUE</tt>
3152 * for floats (so <tt>NaN <> 'NaN'::float8</tt> is <tt>FALSE</tt>).
3153 */
3154CREATE OR REPLACE FUNCTION is_finite_float8(x double precision)
3155 RETURNS BOOL AS
3157 SELECT $1 <> 'NaN'::float8 AND $1 <> 'Infinity'::float8 AND $1 <> '-Infinity'::float8;
3158$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3159
3161 * Latent (token-valued) distribution parameters.
3162 *
3163 * A distribution parameter may be a scalar provenance token -- another
3164 * random_variable (or an AGG_TOKEN cast to UUID) -- rather than a
3165 * concrete double. The parameter is then a random variable itself,
3166 * making the leaf a compound (hierarchical) distribution: e.g.
3167 * normal(M, 1) with M ~ normal(0, 10). The token constructors below
3168 * wire such parameters as children of the gate_rv, encoding each wired
3169 * slot as "$i" in the extra TEXT (a literal slot keeps its decimal TEXT,
3170 * so an all-literal call is byte-identical to the plain NUMERIC
3171 * constructor). Only the Monte Carlo sampler resolves the wires (per
3172 * iteration); every analytic path recognises the wired form and falls
3173 * through to MC.
3174 */
3175
3177 * @brief Internal: build a two-parameter latent @c gate_rv.
3178 *
3179 * Each parameter is supplied as EITHER a token (@p pN_tok, a scalar
3180 * gate @c UUID) OR a literal (@p pN_lit); exactly one is non-NULL per
3181 * parameter. Token parameters are appended to the gate's wire vector
3182 * in order and referenced as @c "$i" in the @c extra TEXT; literal
3183 * parameters keep their decimal TEXT. Not @c STRICT: the NULLs are the
3184 * literal-vs-token sentinels.
3185 */
3186CREATE OR REPLACE FUNCTION rv_parametric2(
3187 family TEXT,
3188 p1_tok UUID, p1_lit double precision,
3189 p2_tok UUID, p2_lit double precision)
3190 RETURNS random_variable AS
3191$$
3192DECLARE
3193 token UUID;
3194 wires UUID[] := ARRAY[]::UUID[];
3195 s1 TEXT;
3196 s2 TEXT;
3197BEGIN
3198 IF p1_tok IS NOT NULL THEN
3199 wires := wires || p1_tok;
3200 s1 := '$' || (array_length(wires, 1) - 1);
3201 ELSE
3202 IF NOT provsql.is_finite_float8(p1_lit) THEN
3203 RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
3204 family, p1_lit;
3205 END IF;
3206 s1 := p1_lit::TEXT;
3207 END IF;
3208 IF p2_tok IS NOT NULL THEN
3209 wires := wires || p2_tok;
3210 s2 := '$' || (array_length(wires, 1) - 1);
3211 ELSE
3212 IF NOT provsql.is_finite_float8(p2_lit) THEN
3213 RAISE EXCEPTION 'provsql.%: literal parameter must be finite (got %)',
3214 family, p2_lit;
3215 END IF;
3216 s2 := p2_lit::TEXT;
3217 END IF;
3218 token := public.uuid_generate_v4();
3219 PERFORM provsql.create_gate(token, 'rv', wires);
3220 PERFORM provsql.set_extra(token, family || ':' || s1 || ',' || s2);
3221 RETURN provsql.random_variable_make(token);
3222END
3223$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE;
3224
3225/**
3226 * @brief Internal: build a one-parameter latent @c gate_rv (rate/scale).
3227 */
3228CREATE OR REPLACE FUNCTION rv_parametric1(family TEXT, p_tok UUID)
3229 RETURNS random_variable AS
3230$$
3231DECLARE
3232 token UUID;
3233BEGIN
3234 token := public.uuid_generate_v4();
3235 PERFORM provsql.create_gate(token, 'rv', ARRAY[p_tok]);
3236 PERFORM provsql.set_extra(token, family || ':$0');
3237 RETURN provsql.random_variable_make(token);
3238END
3239$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3240
3241/*
3242 * Token-accepting constructor overloads. For each NUMERIC family the
3243 * three mixed-arity forms (token, literal), (literal, token), (token,
3244 * token) let any parameter be a random_variable; an AGG_TOKEN parameter
3245 * is passed as @c (agg)::UUID::random_variable. The all-literal call
3246 * still resolves to the plain NUMERIC constructor (an exact match beats
3247 * the implicit NUMERIC->random_variable cast), so the literal fast path
3248 * is unchanged. STRICT: a NULL parameter yields a NULL random_variable.
3250
3251-- normal(mu, sigma)
3252CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma double precision)
3253 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::UUID, NULL, NULL, $2); $$
3254 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3255CREATE OR REPLACE FUNCTION normal(mu double precision, sigma random_variable)
3256 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', NULL, $1, ($2)::UUID, NULL); $$
3257 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3258CREATE OR REPLACE FUNCTION normal(mu random_variable, sigma random_variable)
3259 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('normal', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3260 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3261
3262-- logistic(mu, s)
3263CREATE OR REPLACE FUNCTION logistic(mu random_variable, s double precision)
3264 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::UUID, NULL, NULL, $2); $$
3265 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3266CREATE OR REPLACE FUNCTION logistic(mu double precision, s random_variable)
3267 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', NULL, $1, ($2)::UUID, NULL); $$
3268 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3269CREATE OR REPLACE FUNCTION logistic(mu random_variable, s random_variable)
3270 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('logistic', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3271 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3272
3273-- uniform(a, b)
3274CREATE OR REPLACE FUNCTION uniform(a random_variable, b double precision)
3275 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::UUID, NULL, NULL, $2); $$
3276 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3277CREATE OR REPLACE FUNCTION uniform(a double precision, b random_variable)
3278 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', NULL, $1, ($2)::UUID, NULL); $$
3279 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3280CREATE OR REPLACE FUNCTION uniform(a random_variable, b random_variable)
3281 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('uniform', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3282 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3283
3284-- exponential(lambda)
3285CREATE OR REPLACE FUNCTION exponential(lambda random_variable)
3286 RETURNS random_variable AS $$ SELECT provsql.rv_parametric1('exponential', ($1)::UUID); $$
3287 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3288
3289-- gamma(k, lambda)
3290CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda double precision)
3291 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::UUID, NULL, NULL, $2); $$
3292 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3293CREATE OR REPLACE FUNCTION gamma(k double precision, lambda random_variable)
3294 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', NULL, $1, ($2)::UUID, NULL); $$
3295 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3296CREATE OR REPLACE FUNCTION gamma(k random_variable, lambda random_variable)
3297 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('gamma', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3298 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3299
3300-- lognormal(mu, sigma)
3301CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma double precision)
3302 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::UUID, NULL, NULL, $2); $$
3303 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3304CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma random_variable)
3305 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', NULL, $1, ($2)::UUID, NULL); $$
3306 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3307CREATE OR REPLACE FUNCTION lognormal(mu random_variable, sigma random_variable)
3308 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('lognormal', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3309 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3310
3311-- weibull(k, lambda)
3312CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda double precision)
3313 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::UUID, NULL, NULL, $2); $$
3314 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3315CREATE OR REPLACE FUNCTION weibull(k double precision, lambda random_variable)
3316 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', NULL, $1, ($2)::UUID, NULL); $$
3317 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3318CREATE OR REPLACE FUNCTION weibull(k random_variable, lambda random_variable)
3319 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('weibull', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3320 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3321
3322-- pareto(xm, alpha)
3323CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha double precision)
3324 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::UUID, NULL, NULL, $2); $$
3325 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3326CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha random_variable)
3327 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', NULL, $1, ($2)::UUID, NULL); $$
3328 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3329CREATE OR REPLACE FUNCTION pareto(xm random_variable, alpha random_variable)
3330 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('pareto', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3331 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3333-- beta(alpha, beta)
3334CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta double precision)
3335 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::UUID, NULL, NULL, $2); $$
3336 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3337CREATE OR REPLACE FUNCTION beta(alpha double precision, beta random_variable)
3338 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', NULL, $1, ($2)::UUID, NULL); $$
3339 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3340CREATE OR REPLACE FUNCTION beta(alpha random_variable, beta random_variable)
3341 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('beta', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3342 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3343
3344-- inverse_gamma(alpha, beta)
3345CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta double precision)
3346 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::UUID, NULL, NULL, $2); $$
3347 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3348CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta random_variable)
3349 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', NULL, $1, ($2)::UUID, NULL); $$
3350 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3351CREATE OR REPLACE FUNCTION inverse_gamma(alpha random_variable, beta random_variable)
3352 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gamma', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3353 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3354
3355-- inverse_gaussian(mu, lambda)
3356CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda double precision)
3357 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::UUID, NULL, NULL, $2); $$
3358 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3359CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda random_variable)
3360 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', NULL, $1, ($2)::UUID, NULL); $$
3361 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3362CREATE OR REPLACE FUNCTION inverse_gaussian(mu random_variable, lambda random_variable)
3363 RETURNS random_variable AS $$ SELECT provsql.rv_parametric2('inverse_gaussian', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
3364 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3365
3366/**
3367 * @brief Construct a normal-distribution random variable
3368 *
3369 * Creates a fresh <tt>gate_rv</tt> with @c "normal:μ,σ" stored in
3370 * the gate's @c extra field, and returns a <tt>random_variable</tt>
3371 * pointing at it.
3372 *
3373 * Validation:
3374 * - @p mu and @p sigma must be finite (no @c NaN, no @c ±Infinity).
3375 * - @p sigma must be non-negative.
3376 * - When @p sigma is zero the distribution degenerates to the Dirac
3377 * at @p mu; the call is silently routed through @c as_random(mu),
3378 * producing a @c gate_value rather than a zero-variance @c gate_rv.
3379 * This keeps the sampler / moment / boundcheck paths free of σ=0
3380 * special cases and lets <tt>normal(x, 0)</tt> share its gate with
3381 * <tt>as_random(x)</tt>.
3382 *
3383 * @warning The <tt>VOLATILE</tt> marking is load-bearing and must
3384 * not be weakened. Each call mints a fresh <tt>uuid_generate_v4</tt>
3385 * token because two calls to <tt>normal(0, 1)</tt> are *independent*
3386 * random variables; if PostgreSQL were allowed to fold the function
3387 * (which it would under <tt>STABLE</tt> / <tt>IMMUTABLE</tt>), two
3388 * calls in the same query would share a UUID and collapse into a
3389 * single dependent RV, silently breaking the c-table semantics.
3390 * Same warning applies to @c uniform and @c exponential below.
3391 *
3392 * @sa <a href="https://en.wikipedia.org/wiki/Normal_distribution">Wikipedia: Normal distribution</a>
3393 */
3394CREATE OR REPLACE FUNCTION normal(mu double precision, sigma double precision)
3395 RETURNS random_variable AS
3396$$
3397DECLARE
3398 token UUID;
3399BEGIN
3400 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
3401 RAISE EXCEPTION 'provsql.normal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
3402 END IF;
3403 IF sigma < 0 THEN
3404 RAISE EXCEPTION 'provsql.normal: sigma must be non-negative (got %)', sigma;
3405 END IF;
3406 IF sigma = 0 THEN
3407 RETURN provsql.as_random(mu);
3408 END IF;
3409 token := public.uuid_generate_v4();
3410 PERFORM provsql.create_gate(token, 'rv');
3411 PERFORM provsql.set_extra(token, 'normal:' || mu || ',' || sigma);
3412 RETURN provsql.random_variable_make(token);
3413END
3414$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3415
3416/**
3417 * @brief Construct a logistic-distribution random variable Logistic(μ, s)
3418 *
3419 * The location-scale family whose CDF is the logistic sigmoid; a threshold
3420 * event over a Logistic(0, 1) noise realises the logit link exactly
3421 * (@c P(eps < score) = 1/(1 + exp(-score))), the natural link for a
3422 * log-odds / latent-utility selection model.
3423 *
3424 * Validation:
3425 * - @p mu and @p s must be finite.
3426 * - @p s (the scale) must be non-negative; <tt>s = 0</tt> is the Dirac at
3427 * @p mu, routed through @c as_random(mu) as with @c normal's sigma = 0.
3428 *
3429 * @param mu location (the mean and median).
3430 * @param s scale (> 0); the variance is @f$\pi^2 s^2 / 3@f$.
3431 * @return a @c random_variable token for Logistic(μ, s).
3432 *
3433 * @sa <a href="https://en.wikipedia.org/wiki/Logistic_distribution">Wikipedia: Logistic distribution</a>
3434 */
3435CREATE OR REPLACE FUNCTION logistic(mu double precision, s double precision)
3436 RETURNS random_variable AS
3437$$
3438DECLARE
3439 token UUID;
3440BEGIN
3441 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(s) THEN
3442 RAISE EXCEPTION 'provsql.logistic: parameters must be finite (got mu=%, s=%)', mu, s;
3443 END IF;
3444 IF s < 0 THEN
3445 RAISE EXCEPTION 'provsql.logistic: scale s must be non-negative (got %)', s;
3446 END IF;
3447 IF s = 0 THEN
3448 RETURN provsql.as_random(mu);
3449 END IF;
3450 token := public.uuid_generate_v4();
3451 PERFORM provsql.create_gate(token, 'rv');
3452 PERFORM provsql.set_extra(token, 'logistic:' || mu || ',' || s);
3453 RETURN provsql.random_variable_make(token);
3454END
3455$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3456
3457/**
3458 * @brief Construct a uniform-distribution random variable on [a, b]
3459 *
3460 * Validation:
3461 * - @p a and @p b must be finite.
3462 * - @p a must be ≤ @p b (reversed bounds are rejected).
3463 * - When <tt>a = b</tt> the distribution is the Dirac at @p a; the
3464 * call is silently routed through @c as_random(a) for the same
3465 * reason as @c normal with @p sigma = 0.
3466 *
3467 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3468 * @ref normal.
3469 *
3470 * @sa <a href="https://en.wikipedia.org/wiki/Continuous_uniform_distribution">Wikipedia: Continuous uniform distribution</a>
3471 */
3472CREATE OR REPLACE FUNCTION uniform(a double precision, b double precision)
3473 RETURNS random_variable AS
3474$$
3475DECLARE
3476 token UUID;
3477BEGIN
3478 IF NOT provsql.is_finite_float8(a) OR NOT provsql.is_finite_float8(b) THEN
3479 RAISE EXCEPTION 'provsql.uniform: bounds must be finite (got a=%, b=%)', a, b;
3480 END IF;
3481 IF a > b THEN
3482 RAISE EXCEPTION 'provsql.uniform: a must be <= b (got a=%, b=%)', a, b;
3483 END IF;
3484 IF a = b THEN
3485 RETURN provsql.as_random(a);
3486 END IF;
3487 token := public.uuid_generate_v4();
3488 PERFORM provsql.create_gate(token, 'rv');
3489 PERFORM provsql.set_extra(token, 'uniform:' || a || ',' || b);
3490 RETURN provsql.random_variable_make(token);
3491END
3492$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3493
3494/**
3495 * @brief Construct an exponential-distribution random variable with rate λ
3496 *
3497 * Validation:
3498 * - @p lambda must be finite and strictly positive. No degenerate
3499 * form exists for the exponential distribution, so there is no
3500 * silent route through @c as_random.
3501 *
3502 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3503 * @ref normal.
3504 *
3505 * @sa <a href="https://en.wikipedia.org/wiki/Exponential_distribution">Wikipedia: Exponential distribution</a>
3506 */
3507CREATE OR REPLACE FUNCTION exponential(lambda double precision)
3508 RETURNS random_variable AS
3509$$
3510DECLARE
3511 token UUID;
3512BEGIN
3513 IF NOT provsql.is_finite_float8(lambda) THEN
3514 RAISE EXCEPTION 'provsql.exponential: lambda must be finite (got %)', lambda;
3515 END IF;
3516 IF lambda <= 0 THEN
3517 RAISE EXCEPTION 'provsql.exponential: lambda must be strictly positive (got %)', lambda;
3518 END IF;
3519 token := public.uuid_generate_v4();
3520 PERFORM provsql.create_gate(token, 'rv');
3521 PERFORM provsql.set_extra(token, 'exponential:' || lambda);
3522 RETURN provsql.random_variable_make(token);
3523END
3524$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3525
3526/**
3527 * @brief Construct an Erlang-distribution random variable, sum of
3528 * @p k i.i.d. exponentials with shared rate @p lambda
3529 *
3530 * The Erlang distribution is the sum of @p k independent
3531 * <tt>Exp(λ)</tt> random variables (equivalently the gamma with
3532 * INTEGER shape). It is the natural closure of i.i.d.
3533 * exponentials under addition, and is materialised here as a single
3534 * <tt>gate_rv</tt> so the analytic CDF and closed-form moments fire
3535 * directly (rather than the sampler having to draw and sum @p k
3536 * exponential leaves per Monte-Carlo iteration).
3538 * Validation:
3539 * - @p k must be ≥ 1. The degenerate @c k=1 case is silently routed
3540 * through @c exponential so <tt>erlang(1, λ)</tt> shares its gate
3541 * with <tt>exponential(λ)</tt>.
3542 * - @p lambda must be finite and strictly positive.
3543 *
3544 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3545 * @ref normal.
3546 *
3547 * @sa <a href="https://en.wikipedia.org/wiki/Erlang_distribution">Wikipedia: Erlang distribution</a>
3548 */
3549CREATE OR REPLACE FUNCTION erlang(k INTEGER, lambda double precision)
3550 RETURNS random_variable AS
3551$$
3552DECLARE
3553 token UUID;
3554BEGIN
3555 IF k < 1 THEN
3556 RAISE EXCEPTION 'provsql.erlang: k must be >= 1 (got %)', k;
3557 END IF;
3558 IF NOT provsql.is_finite_float8(lambda) THEN
3559 RAISE EXCEPTION 'provsql.erlang: lambda must be finite (got %)', lambda;
3560 END IF;
3561 IF lambda <= 0 THEN
3562 RAISE EXCEPTION 'provsql.erlang: lambda must be strictly positive (got %)', lambda;
3563 END IF;
3564 IF k = 1 THEN
3565 RETURN provsql.exponential(lambda);
3566 END IF;
3567 token := public.uuid_generate_v4();
3568 PERFORM provsql.create_gate(token, 'rv');
3569 PERFORM provsql.set_extra(token, 'erlang:' || k || ',' || lambda);
3570 RETURN provsql.random_variable_make(token);
3571END
3572$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3573
3574/**
3575 * @brief Construct a gamma-distribution random variable with shape @p k
3576 * (any positive real) and rate @p lambda
3577 *
3578 * The gamma distribution generalises Erlang to non-INTEGER shape; its
3579 * CDF is the regularised lower incomplete gamma, evaluated in closed
3580 * form by the analytic passes. Sums of independent gammas with the
3581 * same rate fold to a single gamma in the simplifier.
3582 *
3583 * Validation:
3584 * - @p k must be finite and strictly positive. An INTEGER @p k (in
3585 * @c INTEGER range) is silently routed through @c erlang -- the gamma
3586 * with INTEGER shape *is* Erlang -- so <tt>gamma(2, λ)</tt> shares
3587 * its gate encoding and closure interplay with <tt>erlang(2, λ)</tt>.
3588 * - @p lambda must be finite and strictly positive.
3589 *
3590 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3591 * @ref normal.
3592 *
3593 * @sa <a href="https://en.wikipedia.org/wiki/Gamma_distribution">Wikipedia: Gamma distribution</a>
3594 */
3595CREATE OR REPLACE FUNCTION gamma(k double precision, lambda double precision)
3596 RETURNS random_variable AS
3597$$
3598DECLARE
3599 token UUID;
3600BEGIN
3601 IF NOT provsql.is_finite_float8(k) THEN
3602 RAISE EXCEPTION 'provsql.gamma: k must be finite (got %)', k;
3603 END IF;
3604 IF k <= 0 THEN
3605 RAISE EXCEPTION 'provsql.gamma: k must be strictly positive (got %)', k;
3606 END IF;
3607 IF NOT provsql.is_finite_float8(lambda) THEN
3608 RAISE EXCEPTION 'provsql.gamma: lambda must be finite (got %)', lambda;
3609 END IF;
3610 IF lambda <= 0 THEN
3611 RAISE EXCEPTION 'provsql.gamma: lambda must be strictly positive (got %)', lambda;
3612 END IF;
3613 IF k = floor(k) AND k <= 2147483647 THEN
3614 RETURN provsql.erlang(k::INTEGER, lambda);
3615 END IF;
3616 token := public.uuid_generate_v4();
3617 PERFORM provsql.create_gate(token, 'rv');
3618 PERFORM provsql.set_extra(token, 'gamma:' || k || ',' || lambda);
3619 RETURN provsql.random_variable_make(token);
3620END
3621$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3622
3623/**
3624 * @brief Construct a chi-squared random variable with @p k degrees of
3625 * freedom: syntactic sugar for <tt>gamma(k/2, 1/2)</tt>
3626 *
3627 * @p k is accepted as @c double @c precision so fractional degrees of
3628 * freedom work; it must be finite and strictly positive. Even degrees
3629 * of freedom route through @c erlang via @c gamma's INTEGER-shape rule.
3630 *
3631 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3632 * @ref normal.
3633 *
3634 * @sa <a href="https://en.wikipedia.org/wiki/Chi-squared_distribution">Wikipedia: Chi-squared distribution</a>
3635 */
3636CREATE OR REPLACE FUNCTION chi_squared(k double precision)
3637 RETURNS random_variable AS
3638$$
3639BEGIN
3640 IF NOT provsql.is_finite_float8(k) THEN
3641 RAISE EXCEPTION 'provsql.chi_squared: k must be finite (got %)', k;
3642 END IF;
3643 IF k <= 0 THEN
3644 RAISE EXCEPTION 'provsql.chi_squared: k must be strictly positive (got %)', k;
3645 END IF;
3646 RETURN provsql.gamma(k / 2, 0.5);
3647END
3648$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3649
3650/**
3651 * @brief Construct a log-normal random variable: @c exp of a
3652 * Normal(@p mu, @p sigma), parameterised by the underlying
3653 * normal (so its median is <tt>exp(mu)</tt> and its mean
3654 * <tt>exp(mu + sigma^2/2)</tt>)
3655 *
3656 * The multiplicative counterpart of @c normal: products of independent
3657 * lognormals fold to a lognormal in the simplifier, and the
3658 * <tt>exp(normal(...))</tt> / <tt>ln(lognormal(...))</tt> bridges fold
3659 * in both directions, so log-scale models stay closed-form.
3660 *
3661 * Validation mirrors @c normal: both parameters must be finite,
3662 * @p sigma non-negative; the degenerate @c sigma = 0 case is silently
3663 * routed through @c as_random (a Dirac at <tt>exp(mu)</tt>).
3664 *
3665 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3666 * @ref normal.
3667 *
3668 * @sa <a href="https://en.wikipedia.org/wiki/Log-normal_distribution">Wikipedia: Log-normal distribution</a>
3669 */
3670CREATE OR REPLACE FUNCTION lognormal(mu double precision, sigma double precision)
3671 RETURNS random_variable AS
3672$$
3673DECLARE
3674 token UUID;
3675BEGIN
3676 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
3677 RAISE EXCEPTION 'provsql.lognormal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
3678 END IF;
3679 IF sigma < 0 THEN
3680 RAISE EXCEPTION 'provsql.lognormal: sigma must be non-negative (got %)', sigma;
3681 END IF;
3682 IF sigma = 0 THEN
3683 RETURN provsql.as_random(exp(mu));
3684 END IF;
3685 token := public.uuid_generate_v4();
3686 PERFORM provsql.create_gate(token, 'rv');
3687 PERFORM provsql.set_extra(token, 'lognormal:' || mu || ',' || sigma);
3688 RETURN provsql.random_variable_make(token);
3689END
3690$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3691
3692/**
3693 * @brief Construct a Weibull random variable with shape @p k and
3694 * scale @p lambda
3696 * @p lambda is the SCALE (the 63.2% quantile), not a rate: @c k = 1 is
3697 * the exponential with rate <tt>1/lambda</tt>, and that case is
3698 * silently routed through @c exponential to share its gate. The shape
3699 * tunes the hazard: @c k < 1 infant mortality, @c k > 1 wear-out.
3700 * Quantiles are exact, truncated moments are closed-form (via the
3701 * regularised incomplete gamma), and the min of i.i.d. Weibulls has a
3702 * closed-form mean (min-stability).
3703 *
3704 * Validation: both parameters must be finite and strictly positive.
3705 *
3706 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3707 * @ref normal.
3708 *
3709 * @sa <a href="https://en.wikipedia.org/wiki/Weibull_distribution">Wikipedia: Weibull distribution</a>
3710 */
3711CREATE OR REPLACE FUNCTION weibull(k double precision, lambda double precision)
3712 RETURNS random_variable AS
3713$$
3714DECLARE
3715 token UUID;
3716BEGIN
3717 IF NOT provsql.is_finite_float8(k) OR NOT provsql.is_finite_float8(lambda) THEN
3718 RAISE EXCEPTION 'provsql.weibull: parameters must be finite (got k=%, lambda=%)', k, lambda;
3719 END IF;
3720 IF k <= 0 OR lambda <= 0 THEN
3721 RAISE EXCEPTION 'provsql.weibull: parameters must be strictly positive (got k=%, lambda=%)', k, lambda;
3722 END IF;
3723 IF k = 1 THEN
3724 RETURN provsql.exponential(1 / lambda);
3725 END IF;
3726 token := public.uuid_generate_v4();
3727 PERFORM provsql.create_gate(token, 'rv');
3728 PERFORM provsql.set_extra(token, 'weibull:' || k || ',' || lambda);
3729 RETURN provsql.random_variable_make(token);
3730END
3731$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3732
3733/**
3734 * @brief Construct a Pareto random variable with scale (minimum)
3735 * @p xm and shape @p alpha
3736 *
3737 * The canonical heavy-tailed power law. Raw moments are @b infinite
3738 * for <tt>alpha <= k</tt> and reported as <tt>Infinity</tt> (the mean
3739 * for <tt>alpha <= 1</tt>, the variance for <tt>alpha <= 2</tt>)
3740 * rather than estimated; quantiles, truncated moments, conditional
3741 * sampling (self-similarity: <tt>X | X > a</tt> is Pareto(a, alpha)),
3742 * and Pareto-vs-Pareto comparisons are all exact.
3743 *
3744 * Validation: both parameters must be finite and strictly positive.
3745 *
3746 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3747 * @ref normal.
3748 *
3749 * @sa <a href="https://en.wikipedia.org/wiki/Pareto_distribution">Wikipedia: Pareto distribution</a>
3750 */
3751CREATE OR REPLACE FUNCTION pareto(xm double precision, alpha double precision)
3752 RETURNS random_variable AS
3753$$
3754DECLARE
3755 token UUID;
3756BEGIN
3757 IF NOT provsql.is_finite_float8(xm) OR NOT provsql.is_finite_float8(alpha) THEN
3758 RAISE EXCEPTION 'provsql.pareto: parameters must be finite (got xm=%, alpha=%)', xm, alpha;
3759 END IF;
3760 IF xm <= 0 OR alpha <= 0 THEN
3761 RAISE EXCEPTION 'provsql.pareto: parameters must be strictly positive (got xm=%, alpha=%)', xm, alpha;
3762 END IF;
3763 token := public.uuid_generate_v4();
3764 PERFORM provsql.create_gate(token, 'rv');
3765 PERFORM provsql.set_extra(token, 'pareto:' || xm || ',' || alpha);
3766 RETURN provsql.random_variable_make(token);
3768$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3769
3770/**
3771 * @brief Construct an inverse-gamma random variable with shape
3772 * @p alpha and scale @p beta
3773 *
3774 * The distribution of <tt>1/Y</tt> for <tt>Y ~ gamma(alpha, beta)</tt>
3775 * (the conjugate prior for a Gaussian variance). Its CDF is the
3776 * regularised upper incomplete gamma, evaluated in closed form by the
3777 * analytic passes; raw moments are @b infinite for <tt>alpha <= k</tt>
3778 * and reported as <tt>Infinity</tt> (the mean for <tt>alpha <= 1</tt>,
3779 * the variance for <tt>alpha <= 2</tt>) rather than estimated. Positive
3780 * scalings rescale @p beta in the simplifier.
3781 *
3782 * Validation: both parameters must be finite and strictly positive.
3783 *
3784 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3785 * @ref normal.
3786 *
3787 * @sa <a href="https://en.wikipedia.org/wiki/Inverse-gamma_distribution">Wikipedia: Inverse-gamma distribution</a>
3788 */
3789CREATE OR REPLACE FUNCTION inverse_gamma(alpha double precision, beta double precision)
3790 RETURNS random_variable AS
3791$$
3792DECLARE
3793 token UUID;
3794BEGIN
3795 IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
3796 RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
3797 END IF;
3798 IF alpha <= 0 OR beta <= 0 THEN
3799 RAISE EXCEPTION 'provsql.inverse_gamma: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
3800 END IF;
3801 token := public.uuid_generate_v4();
3802 PERFORM provsql.create_gate(token, 'rv');
3803 PERFORM provsql.set_extra(token, 'inverse_gamma:' || alpha || ',' || beta);
3804 RETURN provsql.random_variable_make(token);
3805END
3806$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3807
3808/**
3809 * @brief Construct an inverse-Gaussian (Wald) random variable with mean
3810 * @p mu and shape @p lambda
3811 *
3812 * The first-passage time of Brownian motion with drift: a positive,
3813 * right-skewed family. Its CDF has a closed form in the standard normal
3814 * @c Phi, so comparisons and quantiles are analytic; all raw moments are
3815 * finite. Positive scalings map <tt>c·IG(mu, lambda)</tt> to
3816 * <tt>IG(c·mu, c·lambda)</tt>, and a sum of independent inverse
3817 * Gaussians sharing the ratio <tt>lambda/mu²</tt> folds to a single
3818 * inverse Gaussian in the simplifier. @ref wald is an alias.
3819 *
3820 * Validation: both parameters must be finite and strictly positive.
3821 *
3822 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3823 * @ref normal.
3824 *
3825 * @sa <a href="https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution">Wikipedia: Inverse Gaussian distribution</a>
3826 */
3827CREATE OR REPLACE FUNCTION inverse_gaussian(mu double precision, lambda double precision)
3828 RETURNS random_variable AS
3829$$
3830DECLARE
3831 token UUID;
3832BEGIN
3833 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(lambda) THEN
3834 RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be finite (got mu=%, lambda=%)', mu, lambda;
3835 END IF;
3836 IF mu <= 0 OR lambda <= 0 THEN
3837 RAISE EXCEPTION 'provsql.inverse_gaussian: parameters must be strictly positive (got mu=%, lambda=%)', mu, lambda;
3838 END IF;
3839 token := public.uuid_generate_v4();
3840 PERFORM provsql.create_gate(token, 'rv');
3841 PERFORM provsql.set_extra(token, 'inverse_gaussian:' || mu || ',' || lambda);
3842 RETURN provsql.random_variable_make(token);
3843END
3844$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3846/**
3847 * @brief Wald distribution: alias for @ref inverse_gaussian.
3848 *
3849 * @sa <a href="https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution">Wikipedia: Inverse Gaussian distribution</a>
3850 */
3851CREATE OR REPLACE FUNCTION wald(mu double precision, lambda double precision)
3852 RETURNS random_variable AS
3853$$
3854 SELECT provsql.inverse_gaussian(mu, lambda);
3855$$ LANGUAGE sql VOLATILE PARALLEL SAFE;
3856
3857/**
3858 * @brief Build a discrete (categorical) random variable from outcomes
3859 * and UNNORMALISED log-masses
3860 *
3861 * The shared back end of the discrete count constructors (@c poisson,
3862 * @c binomial, @c geometric, @c hypergeometric,
3863 * @c negative_binomial), and directly usable for any custom discrete
3864 * pmf: the log-masses are shifted by their maximum (so only relative
3865 * magnitudes matter and no @c exp underflows), outcomes whose relative
3866 * mass is below <tt>1e-15</tt> are dropped, and the rest is
3867 * renormalised before being handed to @c categorical. Working in log
3868 * space keeps arbitrarily large parameters stable (e.g. a
3869 * <tt>Poisson(1000)</tt> pmf whose linear-space recurrence would
3870 * underflow at @c exp(-1000)).
3871 *
3872 * @param outcomes outcome values, same length as @p log_pmf
3873 * @param log_pmf natural logs of the (unnormalised) masses
3875CREATE OR REPLACE FUNCTION categorical_from_log_pmf(
3876 outcomes double precision[], log_pmf double precision[])
3877 RETURNS random_variable AS
3878$$
3879DECLARE
3880 n INT := array_length(outcomes, 1);
3881 max_lp double precision := '-Infinity';
3882 kept_o double precision[] := '{}';
3883 kept_p double precision[] := '{}';
3884 total double precision := 0;
3885 v double precision;
3886 i INT;
3887BEGIN
3888 IF n IS NULL OR n = 0 OR n <> coalesce(array_length(log_pmf, 1), 0) THEN
3889 RAISE EXCEPTION 'provsql.categorical_from_log_pmf: outcomes and log_pmf must be non-empty arrays of the same length';
3890 END IF;
3891 FOR i IN 1..n LOOP
3892 IF log_pmf[i] > max_lp THEN max_lp := log_pmf[i]; END IF;
3893 END LOOP;
3894 IF max_lp = '-Infinity' THEN
3895 RAISE EXCEPTION 'provsql.categorical_from_log_pmf: all masses are zero';
3896 END IF;
3897 FOR i IN 1..n LOOP
3898 v := exp(log_pmf[i] - max_lp);
3899 IF v >= 1e-15 THEN
3900 kept_o := array_append(kept_o, outcomes[i]);
3901 kept_p := array_append(kept_p, v);
3902 total := total + v;
3903 END IF;
3904 END LOOP;
3905 FOR i IN 1..array_length(kept_p, 1) LOOP
3906 kept_p[i] := kept_p[i] / total;
3907 END LOOP;
3908 RETURN provsql.categorical(kept_p, kept_o);
3909END
3910$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3911
3912/**
3913 * @brief Construct a Poisson random variable with mean @p lambda, as a
3914 * truncated categorical
3915 *
3916 * The pmf is enumerated over <tt>[max(0, λ-12√λ), λ+12√λ+30]</tt> (the
3917 * omitted tails carry ~1e-30 of mass) by the log-space recurrence
3918 * <tt>ln p(k+1) = ln p(k) + ln λ - ln(k+1)</tt> and handed to
3919 * @c categorical_from_log_pmf, so moments, quantiles, and (in)equality
3920 * comparisons are exact over the enumerated support. @c lambda = 0 is
3921 * a Dirac at @c 0 (routed through @c as_random); supports up to 10000
3922 * outcomes (λ up to ~170000), beyond which it raises -- approximate
3923 * huge means by @c normal(λ, √λ) instead.
3924 *
3925 * @sa <a href="https://en.wikipedia.org/wiki/Poisson_distribution">Wikipedia: Poisson distribution</a>
3926 */
3927CREATE OR REPLACE FUNCTION poisson(lambda double precision)
3928 RETURNS random_variable AS
3929$$
3930DECLARE
3931 lo INT;
3932 hi INT;
3933 outcomes double precision[] := '{}';
3934 lps double precision[] := '{}';
3935 lp double precision := 0;
3936 k INT;
3937BEGIN
3938 IF NOT provsql.is_finite_float8(lambda) OR lambda < 0 THEN
3939 RAISE EXCEPTION 'provsql.poisson: lambda must be finite and non-negative (got %)', lambda;
3940 END IF;
3941 IF lambda = 0 THEN
3942 RETURN provsql.as_random(0);
3943 END IF;
3944 lo := greatest(0, floor(lambda - 12 * sqrt(lambda)))::INT;
3945 hi := ceil(lambda + 12 * sqrt(lambda))::INT + 30;
3946 IF hi - lo + 1 > 10000 THEN
3947 RAISE EXCEPTION 'provsql.poisson: support window of % outcomes exceeds 10000; approximate with normal(%, sqrt(%))', hi - lo + 1, lambda, lambda;
3948 END IF;
3949 -- ln p(0) = -λ; walk the recurrence, keeping only the window.
3950 lp := -lambda;
3951 FOR k IN 1..hi LOOP
3952 lp := lp + ln(lambda) - ln(k::double precision);
3953 IF k >= lo THEN
3954 outcomes := array_append(outcomes, k::double precision);
3955 lps := array_append(lps, lp);
3956 END IF;
3957 END LOOP;
3958 IF lo = 0 THEN
3959 outcomes := array_prepend(0::double precision, outcomes);
3960 lps := array_prepend(-lambda, lps);
3961 END IF;
3962 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
3964$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3965
3966/**
3967 * @brief Poisson with a LATENT rate: @c poisson(random_variable).
3968 *
3969 * A latent (token-valued) rate cannot be enumerated into a categorical at
3970 * construction, so this builds a parametric @c gate_rv leaf (family
3971 * @c "poisson") wiring the rate, exactly like the continuous latent
3972 * constructors. Only the Monte Carlo sampler resolves the rate (per draw,
3973 * then draws a Poisson); @c observe weights by the Poisson pmf; the mean is
3974 * exact (E[Poisson(Λ)] = E[Λ], affine). Unblocks discrete-likelihood
3975 * posteriors such as @c "R | (poisson(120*R) = observed_count)".
3976 */
3977CREATE OR REPLACE FUNCTION poisson(lambda random_variable)
3978 RETURNS random_variable AS
3979$$ SELECT provsql.rv_parametric1('poisson', ($1)::UUID); $$
3980 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
3981
3982/**
3983 * @brief Construct a Beta(α, β) random variable on the unit interval
3984 *
3985 * The conjugate prior of Bernoulli / binomial success probabilities:
3986 * closed-form moments, CDF through the regularised incomplete beta,
3987 * quantiles through the generic CDF bisection over the finite
3988 * @c [0, 1] support, and closed-form truncated moments (interval
3989 * conditioning). <tt>Beta(1, 1)</tt> IS <tt>Uniform(0, 1)</tt> and is
3990 * silently routed through @c uniform to share its richer closed forms.
3992 * Validation: both shapes must be finite and strictly positive.
3993 *
3994 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
3995 * @ref normal.
3996 *
3997 * @sa <a href="https://en.wikipedia.org/wiki/Beta_distribution">Wikipedia: Beta distribution</a>
3998 */
3999CREATE OR REPLACE FUNCTION beta(alpha double precision, beta double precision)
4000 RETURNS random_variable AS
4001$$
4002DECLARE
4003 token UUID;
4004BEGIN
4005 IF NOT provsql.is_finite_float8(alpha) OR NOT provsql.is_finite_float8(beta) THEN
4006 RAISE EXCEPTION 'provsql.beta: parameters must be finite (got alpha=%, beta=%)', alpha, beta;
4007 END IF;
4008 IF alpha <= 0 OR beta <= 0 THEN
4009 RAISE EXCEPTION 'provsql.beta: parameters must be strictly positive (got alpha=%, beta=%)', alpha, beta;
4010 END IF;
4011 IF alpha = 1 AND beta = 1 THEN
4012 RETURN provsql.uniform(0, 1);
4013 END IF;
4014 token := public.uuid_generate_v4();
4015 PERFORM provsql.create_gate(token, 'rv');
4016 PERFORM provsql.set_extra(token, 'beta:' || alpha || ',' || beta);
4017 RETURN provsql.random_variable_make(token);
4018END
4019$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4020
4021/**
4022 * @brief Construct a Binomial(n, p) random variable (number of
4023 * successes in @p n independent trials), as a categorical
4024 *
4025 * Enumerated over <tt>{0..n}</tt> by the log-space recurrence
4026 * <tt>ln p(k+1) = ln p(k) + ln((n-k)/(k+1)) + ln(p/(1-p))</tt>
4027 * (outcomes below 1e-15 relative mass are dropped). @c p = 0 /
4028 * @c p = 1 are Diracs at @c 0 / @c n; @c n is capped at 10000.
4030 * @sa <a href="https://en.wikipedia.org/wiki/Binomial_distribution">Wikipedia: Binomial distribution</a>
4031 */
4032CREATE OR REPLACE FUNCTION binomial(n INTEGER, p double precision)
4033 RETURNS random_variable AS
4035DECLARE
4036 outcomes double precision[] := '{}';
4037 lps double precision[] := '{}';
4038 lp double precision;
4039 k INT;
4040BEGIN
4041 IF n IS NULL OR n < 0 THEN
4042 RAISE EXCEPTION 'provsql.binomial: n must be non-negative (got %)', n;
4043 END IF;
4044 IF NOT provsql.is_finite_float8(p) OR p < 0 OR p > 1 THEN
4045 RAISE EXCEPTION 'provsql.binomial: p must be in [0, 1] (got %)', p;
4046 END IF;
4047 IF n > 10000 THEN
4048 RAISE EXCEPTION 'provsql.binomial: n = % exceeds 10000; approximate with normal(n*p, sqrt(n*p*(1-p)))', n;
4049 END IF;
4050 IF n = 0 OR p = 0 THEN
4051 RETURN provsql.as_random(0);
4052 END IF;
4053 IF p = 1 THEN
4054 RETURN provsql.as_random(n);
4055 END IF;
4056 lp := n * ln(1 - p); -- ln p(0)
4057 outcomes := array_append(outcomes, 0::double precision);
4058 lps := array_append(lps, lp);
4059 FOR k IN 0..(n - 1) LOOP
4060 lp := lp + ln((n - k)::double precision / (k + 1)) + ln(p / (1 - p));
4061 outcomes := array_append(outcomes, (k + 1)::double precision);
4062 lps := array_append(lps, lp);
4063 END LOOP;
4064 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4065END
4066$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4067
4068/**
4069 * @brief Binomial with a fixed trial count and a LATENT success
4070 * probability: @c binomial(INTEGER, random_variable).
4071 *
4072 * @p n is a literal trial count; @p p is a latent (token-valued) success
4073 * probability (e.g. @c "40.0 / N" for a latent population size @c N).
4074 * Builds a parametric @c gate_rv leaf (family @c "binomial", @c extra
4075 * @c "binomial:n,$0") the Monte Carlo sampler resolves per draw; @c observe
4076 * weights by the Binomial pmf. Unblocks capture-recapture-style posteriors
4077 * such as @c "N | (binomial(50, 40.0/N) = recaptured_count)".
4078 */
4079CREATE OR REPLACE FUNCTION binomial(n INTEGER, p random_variable)
4080 RETURNS random_variable AS
4081$$ SELECT provsql.rv_parametric2('binomial', NULL, $1::double precision,
4082 ($2)::UUID, NULL); $$
4083 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4085/**
4086 * @brief Construct a Geometric(p) random variable -- the number of
4087 * TRIALS up to and including the first success (support
4088 * starting at 1; subtract 1 for the failures convention)
4089 *
4090 * <tt>P(X = k) = (1-p)^{k-1} p</tt>, enumerated up to the 1e-15
4091 * relative-mass tail and renormalised. @c p = 1 is a Dirac at @c 1.
4093 * @sa <a href="https://en.wikipedia.org/wiki/Geometric_distribution">Wikipedia: Geometric distribution</a>
4094 */
4095CREATE OR REPLACE FUNCTION geometric(p double precision)
4096 RETURNS random_variable AS
4097$$
4098DECLARE
4099 k_max INT;
4100 outcomes double precision[] := '{}';
4101 lps double precision[] := '{}';
4102 k INT;
4103BEGIN
4104 IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
4105 RAISE EXCEPTION 'provsql.geometric: p must be in (0, 1] (got %)', p;
4106 END IF;
4107 IF p = 1 THEN
4108 RETURN provsql.as_random(1);
4109 END IF;
4110 k_max := 1 + ceil(ln(1e-15) / ln(1 - p))::INT;
4111 IF k_max > 10000 THEN
4112 RAISE EXCEPTION 'provsql.geometric: support window of % outcomes exceeds 10000 (p = % is too small); approximate with exponential(%)', k_max, p, p;
4113 END IF;
4114 FOR k IN 1..k_max LOOP
4115 outcomes := array_append(outcomes, k::double precision);
4116 lps := array_append(lps, (k - 1) * ln(1 - p) + ln(p));
4117 END LOOP;
4118 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4119END
4120$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4121
4122/**
4123 * @brief Geometric with a LATENT success probability: @c geometric(random_variable).
4124 *
4125 * A latent (token-valued) @p p cannot be enumerated at construction, so this
4126 * builds a parametric @c gate_rv leaf (family @c "geometric") wiring the
4127 * probability, resolved per draw by the sampler. @c observe weights by the
4128 * geometric pmf; unblocks a Beta-Geometric conjugate posterior.
4129 */
4130CREATE OR REPLACE FUNCTION geometric(p random_variable)
4131 RETURNS random_variable AS
4132$$ SELECT provsql.rv_parametric1('geometric', ($1)::UUID); $$
4133 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4134
4135/**
4136 * @brief Construct a Hypergeometric(N, K, n) random variable: the
4137 * number of marked items among @p n draws WITHOUT replacement
4138 * from a population of @p pop_n items of which @p k_marked are
4139 * marked
4140 *
4141 * The exact finite support <tt>[max(0, n-(N-K)), min(n, K)]</tt> is
4142 * enumerated by the pmf ratio recurrence (in log space, so large
4143 * populations cannot overflow) and normalised -- exact "sampling
4144 * without replacement" probabilities with no combinatorial functions
4145 * needed.
4147 * @sa <a href="https://en.wikipedia.org/wiki/Hypergeometric_distribution">Wikipedia: Hypergeometric distribution</a>
4148 */
4149CREATE OR REPLACE FUNCTION hypergeometric(pop_n INTEGER, k_marked INTEGER, n INTEGER)
4150 RETURNS random_variable AS
4151$$
4152DECLARE
4153 lo INT;
4154 hi INT;
4155 outcomes double precision[] := '{}';
4156 lps double precision[] := '{}';
4157 lp double precision := 0; -- relative log-mass; normalised later
4158 k INT;
4159BEGIN
4160 IF pop_n IS NULL OR k_marked IS NULL OR n IS NULL
4161 OR pop_n < 0 OR k_marked < 0 OR n < 0
4162 OR k_marked > pop_n OR n > pop_n THEN
4163 RAISE EXCEPTION 'provsql.hypergeometric: need 0 <= k_marked, n <= pop_n (got pop_n=%, k_marked=%, n=%)', pop_n, k_marked, n;
4164 END IF;
4165 lo := greatest(0, n - (pop_n - k_marked));
4166 hi := least(n, k_marked);
4167 IF hi - lo + 1 > 10000 THEN
4168 RAISE EXCEPTION 'provsql.hypergeometric: support window of % outcomes exceeds 10000', hi - lo + 1;
4169 END IF;
4170 outcomes := array_append(outcomes, lo::double precision);
4171 lps := array_append(lps, lp);
4172 FOR k IN lo..(hi - 1) LOOP
4173 -- pmf(k+1)/pmf(k) = (K-k)(n-k) / ((k+1)(N-K-n+k+1))
4174 lp := lp + ln((k_marked - k)::double precision * (n - k))
4175 - ln((k + 1)::double precision * (pop_n - k_marked - n + k + 1));
4176 outcomes := array_append(outcomes, (k + 1)::double precision);
4177 lps := array_append(lps, lp);
4178 END LOOP;
4179 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4180END
4181$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4182
4183/**
4184 * @brief Construct a negative-binomial random variable: the number of
4185 * FAILURES before the @p r-th success (support starting at 0),
4186 * with real @p r > 0 allowed (the Polya / overdispersed-count
4187 * parameterisation, the Poisson-Gamma mixture)
4188 *
4189 * <tt>P(X = k) = C(k+r-1, k) p^r (1-p)^k</tt>, enumerated by the
4190 * log-space recurrence
4191 * <tt>ln p(k+1) = ln p(k) + ln((k+r)/(k+1)) + ln(1-p)</tt> up to the
4192 * 1e-15 relative-mass tail. @c p = 1 is a Dirac at @c 0.
4193 *
4194 * @sa <a href="https://en.wikipedia.org/wiki/Negative_binomial_distribution">Wikipedia: Negative binomial distribution</a>
4195 */
4196CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p double precision)
4197 RETURNS random_variable AS
4198$$
4199DECLARE
4200 outcomes double precision[] := '{}';
4201 lps double precision[] := '{}';
4202 lp double precision;
4203 max_lp double precision;
4204 mean double precision;
4205 k INT := 0;
4206BEGIN
4207 IF NOT provsql.is_finite_float8(r) OR r <= 0 THEN
4208 RAISE EXCEPTION 'provsql.negative_binomial: r must be finite and strictly positive (got %)', r;
4209 END IF;
4210 IF NOT provsql.is_finite_float8(p) OR p <= 0 OR p > 1 THEN
4211 RAISE EXCEPTION 'provsql.negative_binomial: p must be in (0, 1] (got %)', p;
4212 END IF;
4213 IF p = 1 THEN
4214 RETURN provsql.as_random(0);
4215 END IF;
4216 mean := r * (1 - p) / p;
4217 lp := r * ln(p); -- ln p(0)
4218 max_lp := lp;
4219 outcomes := array_append(outcomes, 0::double precision);
4220 lps := array_append(lps, lp);
4221 LOOP
4222 lp := lp + ln((k + r) / (k + 1)) + ln(1 - p);
4223 k := k + 1;
4224 IF lp > max_lp THEN max_lp := lp; END IF;
4225 outcomes := array_append(outcomes, k::double precision);
4226 lps := array_append(lps, lp);
4227 EXIT WHEN k > mean AND lp < max_lp + ln(1e-15);
4228 IF k >= 10000 THEN
4229 RAISE EXCEPTION 'provsql.negative_binomial: support window exceeds 10000 outcomes (r=%, p=%)', r, p;
4230 END IF;
4231 END LOOP;
4232 RETURN provsql.categorical_from_log_pmf(outcomes, lps);
4233END
4234$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4235
4236/*
4237 * NegativeBinomial with a LATENT parameter: the count r (number of successes)
4238 * stays a plain number while the success probability p is a random_variable,
4239 * built as a parametric gate_rv leaf (family "negative_binomial") -- the
4240 * Beta-NegativeBinomial conjugate shape. The all-random and latent-r forms
4241 * are provided for uniformity with the continuous constructors.
4242 */
4243CREATE OR REPLACE FUNCTION negative_binomial(r double precision, p random_variable)
4244 RETURNS random_variable AS
4245$$ SELECT provsql.rv_parametric2('negative_binomial', NULL, $1, ($2)::UUID, NULL); $$
4246 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4247CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p double precision)
4248 RETURNS random_variable AS
4249$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::UUID, NULL, NULL, $2); $$
4250 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4251CREATE OR REPLACE FUNCTION negative_binomial(r random_variable, p random_variable)
4252 RETURNS random_variable AS
4253$$ SELECT provsql.rv_parametric2('negative_binomial', ($1)::UUID, NULL, ($2)::UUID, NULL); $$
4254 LANGUAGE sql STRICT VOLATILE PARALLEL SAFE;
4255
4256/**
4257 * @brief Catalog of the registered continuous-distribution families.
4258 *
4259 * One row per @c gate_rv family known to this build of the extension:
4260 * @c name is the on-disk token (the part before the colon in the gate's
4261 * @c extra encoding), @c nparams the parameter count, @c param_names the
4262 * conventional parameter symbols in @c extra order (e.g.
4263 * <tt>{μ, σ}</tt>), and @c label a short display glyph (e.g. @c "N",
4264 * @c "Γ"). UI clients (ProvSQL Studio's circuit inspector) read this to
4265 * render families they were not hard-coded for, so a newly added family
4266 * shows up without a client release.
4267 */
4268CREATE OR REPLACE FUNCTION rv_families()
4269 RETURNS TABLE(name TEXT, nparams INT, param_names TEXT[], label TEXT) AS
4270 'provsql','rv_families' LANGUAGE C STABLE PARALLEL SAFE;
4271
4272/**
4273 * @brief Construct a probabilistic-mixture random variable.
4274 *
4275 * Returns a @c random_variable whose distribution is a Bernoulli
4276 * mixture of two scalar RV roots: with probability <tt>P(p = true)</tt>
4277 * the mixture samples @p x, with the complementary probability it
4278 * samples @p y. The mixing token @p p is a @c gate_input Bernoulli
4279 * whose probability has been pinned with @c set_prob, and the same
4280 * @p p can be shared with other branches of the circuit -- the
4281 * Monte-Carlo sampler's per-iteration cache couples every reference
4282 * to the same draw, so users can build joint conditional structures
4283 * (e.g. <tt>mixture(p, X1, Y1) + mixture(p, X2, Y2)</tt> samples
4284 * X1 + X2 with prob π and Y1 + Y2 with prob 1-π).
4285 *
4286 * @p x and @p y may be any scalar RV root: a base @c gate_rv
4287 * (@c normal / @c uniform / @c exponential / @c erlang), a
4288 * @c gate_value Dirac (@c as_random), a @c gate_arith expression, or
4289 * another @c mixture. N-ary mixtures are built by composition --
4290 * <tt>mixture(p1, A, mixture(p2, B, C))</tt> realises a 3-component
4291 * mixture with effective weights <tt>π1, (1-π1)·π2, (1-π1)·(1-π2)</tt>.
4293 * Validation:
4294 * - @p p must point to a Boolean gate (@c input, @c mulinput,
4295 * @c update, @c plus, @c times, @c monus, @c project, @c eq,
4296 * @c cmp, @c zero, @c one). Compound Boolean gates derive their
4297 * probability from their atoms via the active probability-evaluation
4298 * method; a bare @c gate_input's probability is whatever @c set_prob
4299 * pinned (@c set_prob is responsible for keeping it in [0, 1]).
4300 * - @p x and @p y must be scalar RV roots; aggregate / Boolean roots
4301 * are rejected at construction.
4302 *
4303 * Two calls to @c mixture with the same @c (p, x, y) operands collapse
4304 * to the same @c gate_mixture node by v5-hash, exactly like
4305 * @c arith(PLUS, X, Y). Draw independence is controlled by @p p:
4306 * sharing @p p couples branch selection across consumers via the
4307 * sampler's @c bool_cache_; minting independent Bernoullis (e.g. via
4308 * the @c mixture(p_value, …) overload) decouples them.
4309 *
4310 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
4311 */
4312CREATE OR REPLACE FUNCTION mixture(
4313 p UUID, x random_variable, y random_variable)
4314 RETURNS random_variable AS
4315$$
4316DECLARE
4317 token UUID;
4318 p_kind provsql.PROVENANCE_GATE;
4319 x_uuid UUID;
4320 y_uuid UUID;
4321 x_kind provsql.PROVENANCE_GATE;
4322 y_kind provsql.PROVENANCE_GATE;
4323BEGIN
4324 p_kind := provsql.get_gate_type(p);
4325 IF p_kind NOT IN ('input','mulinput','update',
4326 'plus','times','monus',
4327 'project','eq','cmp',
4328 'zero','one') THEN
4329 RAISE EXCEPTION 'provsql.mixture: p must be a Boolean gate '
4330 '(input/mulinput/update/plus/times/monus/project/eq/cmp/zero/one), got %', p_kind;
4331 END IF;
4332
4333 x_uuid := (x)::UUID;
4334 y_uuid := (y)::UUID;
4335 x_kind := provsql.get_gate_type(x_uuid);
4336 y_kind := provsql.get_gate_type(y_uuid);
4337 IF x_kind NOT IN ('rv','value','arith','mixture') THEN
4338 RAISE EXCEPTION 'provsql.mixture: x must be a scalar RV root (rv / value / arith / mixture), got %', x_kind;
4339 END IF;
4340 IF y_kind NOT IN ('rv','value','arith','mixture') THEN
4341 RAISE EXCEPTION 'provsql.mixture: y must be a scalar RV root (rv / value / arith / mixture), got %', y_kind;
4342 END IF;
4343
4344 token := public.uuid_generate_v5(
4345 provsql.uuid_ns_provsql(),
4346 concat('mixture', p, x_uuid, y_uuid));
4347 PERFORM provsql.create_gate(token, 'mixture', ARRAY[p, x_uuid, y_uuid]);
4348 RETURN provsql.random_variable_make(token);
4349END
4350$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
4351
4352/**
4353 * @brief Ad-hoc mixture constructor that mints a fresh anonymous
4354 * @c gate_input Bernoulli with probability @p p_value.
4355 *
4356 * Sugar over the @c mixture(UUID, x, y) form: when the caller doesn't
4357 * care about reusing the Bernoulli token elsewhere in the circuit
4358 * (which is the common case &ndash; "give me a 0.3 / 0.7 weighted GMM,
4359 * I don't need to share the coin"), this overload creates the
4360 * underlying @c gate_input on the fly with a fresh
4361 * @c uuid_generate_v4() token, pins @p p_value via @c set_prob, and
4362 * threads everything into the UUID-keyed constructor.
4363 *
4364 * Each call mints a NEW Bernoulli, so two calls to
4365 * <tt>mixture(0.5, X, Y)</tt> are *independent* mixtures whose branch
4366 * selections are uncorrelated. When coupling is desired (e.g. two
4367 * mixtures sharing a coin), use the @c mixture(UUID, x, y) form with a
4368 * user-managed @c gate_input token.
4369 *
4370 * @warning <tt>VOLATILE</tt> is load-bearing for the same reason as
4371 * @ref normal and the other RV constructors -- folding under
4372 * @c STABLE / @c IMMUTABLE would collapse two independent draws into
4373 * one shared gate.
4374 *
4375 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
4376 */
4377CREATE OR REPLACE FUNCTION mixture(
4378 p_value double precision,
4379 x random_variable,
4380 y random_variable)
4381 RETURNS random_variable AS
4382$$
4383DECLARE
4384 p_token UUID;
4385BEGIN
4386 IF p_value IS NULL OR p_value <> p_value OR p_value < 0 OR p_value > 1 THEN
4387 RAISE EXCEPTION 'provsql.mixture: probability must be in [0,1] (got %)', p_value;
4388 END IF;
4389 p_token := public.uuid_generate_v4();
4390 PERFORM provsql.create_gate(p_token, 'input');
4391 PERFORM provsql.set_prob(p_token, p_value);
4392 RETURN provsql.mixture(p_token, x, y);
4394$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4395
4396/**
4397 * @brief Categorical-RV constructor over explicit (probabilities,
4398 * values) arrays.
4399 *
4400 * Builds a categorical-form @c gate_mixture directly: a fresh
4401 * @c gate_input "key" anchor and one @c gate_mulinput per outcome with
4402 * positive mass, all sharing the key. The wires
4403 * <tt>[key, mul_1, ..., mul_n]</tt> are what downstream evaluators
4404 * (@c Expectation, @c MonteCarloSampler, @c AnalyticEvaluator,
4405 * @c RangeCheck) recognise via @c isCategoricalMixture and treat as a
4406 * scalar RV with the categorical distribution @p probs over
4407 * @p outcomes.
4408 *
4409 * Validation:
4410 * - @p probs and @p outcomes must be non-null, same length, length &ge; 1.
4411 * - Each @c probs[i] must be finite, in <tt>[0, 1]</tt>, and the array
4412 * must sum to 1 within @c 1e-9.
4413 * - Each @c outcomes[i] must be finite.
4414 *
4415 * Each call mints a fresh key gate and a fresh set of mulinputs, so
4416 * two calls to @c categorical with the same arrays are *independent*
4417 * categorical RVs. The marking is @c VOLATILE accordingly.
4418 *
4419 * Degenerate case: a categorical with exactly one positive-mass
4420 * outcome reduces to @c as_random(v) at construction (the block would
4421 * just be a single mulinput, which is operationally a Dirac point
4422 * mass). Two such calls share the @c gate_value UUID via the v5
4423 * convention @c as_random already uses.
4424 *
4425 * @sa @c mixture for the Bernoulli-weighted choice constructor.
4426 * @sa <a href="https://en.wikipedia.org/wiki/Categorical_distribution">Wikipedia: Categorical distribution</a>
4428CREATE OR REPLACE FUNCTION categorical(
4429 probs double precision[],
4430 outcomes double precision[])
4431 RETURNS random_variable AS
4432$$
4433DECLARE
4434 n INTEGER;
4435 p_sum double precision := 0.0;
4436 i INTEGER;
4437 key_token UUID;
4438 mix_token UUID;
4439 mul_token UUID;
4440 mul_tokens UUID[] := ARRAY[]::UUID[];
4441 mix_wires UUID[];
4442 pi_i double precision;
4443 vi_i double precision;
4444BEGIN
4445 IF probs IS NULL OR outcomes IS NULL THEN
4446 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must be non-null';
4447 END IF;
4448 n := array_length(probs, 1);
4449 IF n IS NULL OR n < 1 THEN
4450 RAISE EXCEPTION 'provsql.categorical: probs must be non-empty';
4451 END IF;
4452 IF array_length(outcomes, 1) <> n THEN
4453 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must have the same length (got % and %)',
4454 n, array_length(outcomes, 1);
4455 END IF;
4456
4457 FOR i IN 1..n LOOP
4458 pi_i := probs[i];
4459 vi_i := outcomes[i];
4460 -- PostgreSQL diverges from IEEE 754: NaN = NaN is TRUE there, so
4461 -- the canonical x <> x NaN test doesn't fire. Compare against the
4462 -- literal 'NaN'::float8 instead, and reject ±Infinity for outcomes
4463 -- explicitly.
4464 IF pi_i IS NULL OR pi_i = 'NaN'::float8 OR pi_i < 0 OR pi_i > 1 THEN
4465 RAISE EXCEPTION 'provsql.categorical: probs[%] must be in [0,1] (got %)', i, pi_i;
4466 END IF;
4467 IF vi_i IS NULL OR vi_i = 'NaN'::float8
4468 OR vi_i = 'Infinity'::float8 OR vi_i = '-Infinity'::float8 THEN
4469 RAISE EXCEPTION 'provsql.categorical: outcomes[%] must be finite (got %)', i, vi_i;
4470 END IF;
4471 p_sum := p_sum + pi_i;
4472 END LOOP;
4473 IF abs(p_sum - 1.0) > 1e-9 THEN
4474 RAISE EXCEPTION 'provsql.categorical: probs must sum to 1 within 1e-9 (got %)', p_sum;
4475 END IF;
4476
4477 -- Degenerate case: exactly one positive-mass outcome (the rest are
4478 -- zero). The "categorical" is then a Dirac point mass; skip the
4479 -- block-allocation entirely and return @c as_random(v), which yields
4480 -- a shared, v5-keyed gate_value -- exactly what downstream
4481 -- evaluators (rv_moment, AnalyticEvaluator, rv_support) treat
4482 -- specially. Saves a key gate and a mulinput per call, and lets
4483 -- two calls to @c categorical({1.0}, {v}) collide on the same
4484 -- gate_value UUID instead of producing distinct anonymous blocks.
4485 DECLARE
4486 nb_positive INTEGER := 0;
4487 only_idx INTEGER := 0;
4488 BEGIN
4489 FOR i IN 1..n LOOP
4490 IF probs[i] > 0.0 THEN
4491 nb_positive := nb_positive + 1;
4492 only_idx := i;
4493 END IF;
4494 END LOOP;
4495 IF nb_positive = 1 THEN
4496 RETURN provsql.as_random(outcomes[only_idx]);
4497 END IF;
4498 END;
4499
4500 -- Mint the block's key anchor. Probability 1.0 matches the
4501 -- joint-table convention: the categorical mass lives on the
4502 -- mulinputs, the key just identifies the block.
4503 key_token := public.uuid_generate_v4();
4504 PERFORM provsql.create_gate(key_token, 'input');
4505 PERFORM provsql.set_prob(key_token, 1.0);
4506
4507 -- One mulinput per positive-probability outcome. Zero-probability
4508 -- entries contribute no mass and are skipped: the gate_mixture's
4509 -- wire vector is otherwise polluted with no-op leaves.
4510 FOR i IN 1..n LOOP
4511 pi_i := probs[i];
4512 IF pi_i <= 0.0 THEN CONTINUE; END IF;
4513 mul_token := public.uuid_generate_v4();
4514 PERFORM provsql.create_gate(mul_token, 'mulinput', ARRAY[key_token]);
4515 PERFORM provsql.set_prob(mul_token, pi_i);
4516 PERFORM provsql.set_infos(mul_token, (i - 1));
4517 PERFORM provsql.set_extra(mul_token, outcomes[i]::TEXT);
4518 mul_tokens := mul_tokens || mul_token;
4519 END LOOP;
4520
4521 mix_wires := ARRAY[key_token] || mul_tokens;
4522 mix_token := public.uuid_generate_v4();
4523 PERFORM provsql.create_gate(mix_token, 'mixture', mix_wires);
4524 RETURN provsql.random_variable_make(mix_token);
4525END
4526$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4527
4528/**
4529 * @brief Gaussian-mixture-model (GMM) constructor.
4530 *
4531 * Packages the common fitted-density pattern -- a categorical choice
4532 * among Normal components -- into one call:
4533 *
4534 * @code
4535 * provsql.gmm(weights => ARRAY[0.3, 0.5, 0.2],
4536 * means => ARRAY[120.0, 380.0, 1200.0],
4537 * stddevs => ARRAY[40.0, 90.0, 250.0])
4538 * @endcode
4539 *
4540 * No new gate: the mixture decomposes into a stick-breaking cascade of
4541 * Bernoulli @c gate_mixture nodes over @c gate_rv Normal leaves
4542 * (component @c i is selected with conditional probability
4543 * @c w_i / (w_i + ... + w_n), so the joint selection probabilities are
4544 * exactly @p weights), which every evaluator already handles: moments
4545 * are closed-form through the mixture recursion, sampling is exact,
4546 * and comparisons ride the existing mixture machinery. Zero-weight
4547 * components are skipped; a single positive-weight component returns
4548 * its Normal directly (no mixture node).
4549 *
4550 * Validation mirrors @c categorical: same-length non-empty arrays,
4551 * weights finite in <tt>[0, 1]</tt> summing to 1 within @c 1e-9; the
4552 * component parameters are validated by @c provsql.normal (finite
4553 * @c mu, non-negative @c sigma; @c sigma @c = @c 0 degenerates to a
4554 * Dirac component).
4555 *
4556 * @sa @c mixture, @c categorical, @c normal
4557 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_model">Wikipedia: Mixture model</a>
4558 */
4559CREATE OR REPLACE FUNCTION gmm(
4560 weights double precision[],
4561 means double precision[],
4562 stddevs double precision[])
4563 RETURNS random_variable AS
4564$$
4565DECLARE
4566 n INTEGER;
4567 w_sum double precision := 0.0;
4568 i INTEGER;
4569 acc random_variable := NULL;
4570 remaining double precision := 0.0;
4571BEGIN
4572 IF weights IS NULL OR means IS NULL OR stddevs IS NULL THEN
4573 RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must be non-null';
4574 END IF;
4575 n := array_length(weights, 1);
4576 IF n IS NULL OR n < 1 THEN
4577 RAISE EXCEPTION 'provsql.gmm: weights must be non-empty';
4578 END IF;
4579 IF array_length(means, 1) <> n OR array_length(stddevs, 1) <> n THEN
4580 RAISE EXCEPTION 'provsql.gmm: weights, means, and stddevs must have the same length (got %, %, %)',
4581 n, array_length(means, 1), array_length(stddevs, 1);
4582 END IF;
4583 FOR i IN 1..n LOOP
4584 IF weights[i] IS NULL OR weights[i] = 'NaN'::float8
4585 OR weights[i] < 0 OR weights[i] > 1 THEN
4586 RAISE EXCEPTION 'provsql.gmm: weights[%] must be in [0,1] (got %)',
4587 i, weights[i];
4588 END IF;
4589 w_sum := w_sum + weights[i];
4590 END LOOP;
4591 IF abs(w_sum - 1.0) > 1e-9 THEN
4592 RAISE EXCEPTION 'provsql.gmm: weights must sum to 1 within 1e-9 (got %)', w_sum;
4593 END IF;
4594
4595 -- Stick-breaking, built back to front: acc holds the mixture of
4596 -- components i+1..n, and prepending component i selects it with
4597 -- conditional probability w_i / (w_i + ... + w_n).
4598 FOR i IN REVERSE n..1 LOOP
4599 IF weights[i] <= 0.0 THEN
4600 CONTINUE;
4601 END IF;
4602 IF acc IS NULL THEN
4603 acc := provsql.normal(means[i], stddevs[i]);
4604 remaining := weights[i];
4605 ELSE
4606 remaining := remaining + weights[i];
4607 acc := provsql.mixture(least(1.0, weights[i] / remaining),
4608 provsql.normal(means[i], stddevs[i]), acc);
4609 END IF;
4610 END LOOP;
4611 RETURN acc;
4612END
4613$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4614
4615/**
4616 * @brief Empirical-samples constructor: the ecdf of a sample bundle as
4617 * a @c random_variable.
4618 *
4619 * Loads a Monte Carlo / MCMC / bootstrap sample array as the discrete
4620 * distribution putting mass @c 1/n on each draw (duplicates merge, so a
4621 * value drawn @c k times carries @c k/n) -- the standard empirical
4622 * distribution. Reduces entirely to @ref categorical, so the whole
4623 * exact discrete surface applies: moments are the sample moments,
4624 * comparisons against constants are decided analytically ("fraction of
4625 * samples below c"), and quantiles are the exact empirical quantiles.
4626 *
4627 * @code
4628 * -- Bulk load via array_agg over a sample table
4629 * INSERT INTO model_posteriors
4630 * SELECT param, provsql.empirical_samples(array_agg(value))
4631 * FROM mcmc_chain GROUP BY param;
4632 * @endcode
4633 *
4634 * At most 10000 distinct values (the categorical block cap): thin the
4635 * chain or bin the samples (e.g. with @c width_bucket) beyond that.
4636 *
4637 * @sa @ref categorical, @ref empirical_cdf
4638 * @sa <a href="https://en.wikipedia.org/wiki/Empirical_distribution_function">Wikipedia: Empirical distribution function</a>
4639 */
4640CREATE OR REPLACE FUNCTION empirical_samples(samples double precision[])
4641 RETURNS random_variable AS
4642$$
4643DECLARE
4644 n INTEGER;
4645 sorted double precision[];
4646 outcomes double precision[] := '{}';
4647 probs double precision[] := '{}';
4648 v double precision;
4649 prev double precision;
4650 run INTEGER := 0;
4651 started BOOLEAN := false;
4652BEGIN
4653 n := array_length(samples, 1);
4654 IF n IS NULL OR n < 1 THEN
4655 RAISE EXCEPTION 'provsql.empirical_samples: samples must be non-empty';
4656 END IF;
4657 sorted := ARRAY(SELECT s FROM unnest(samples) AS s ORDER BY 1);
4658 FOREACH v IN ARRAY sorted LOOP
4659 IF v IS NULL OR v = 'NaN'::float8
4660 OR v = 'Infinity'::float8 OR v = '-Infinity'::float8 THEN
4661 RAISE EXCEPTION
4662 'provsql.empirical_samples: samples must be finite (got %)', v;
4663 END IF;
4664 IF started AND v = prev THEN
4665 run := run + 1;
4666 ELSE
4667 IF started THEN
4668 outcomes := outcomes || prev;
4669 probs := probs || (run::double precision / n);
4670 END IF;
4671 prev := v;
4672 run := 1;
4673 started := true;
4674 END IF;
4675 END LOOP;
4676 outcomes := outcomes || prev;
4677 probs := probs || (run::double precision / n);
4678 IF array_length(outcomes, 1) > 10000 THEN
4679 RAISE EXCEPTION
4680 'provsql.empirical_samples: at most 10000 distinct values are '
4681 'supported (got %); thin the chain or bin the samples (e.g. with '
4682 'width_bucket)', array_length(outcomes, 1);
4683 END IF;
4684 RETURN provsql.categorical(probs, outcomes);
4685END
4686$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4687
4688/**
4689 * @brief Empirical-CDF constructor: a piecewise-linear CDF table as a
4690 * @c random_variable.
4691 *
4692 * Loads a tabulated CDF -- simulation output percentile tables, risk
4693 * models, expert-elicited forecasts -- as the distribution whose CDF is
4694 * @c cdf[i] at @c grid[i], linear in between: mass
4695 * @c cdf[i+1] @c - @c cdf[i] spread uniformly over
4696 * <tt>(grid[i], grid[i+1])</tt>, plus (when @c cdf[1] @c > @c 0) an
4697 * atom of mass @c cdf[1] at @c grid[1] for the probability at or below
4698 * the grid start. Packaged, like @ref gmm, as a stick-breaking cascade
4699 * of Bernoulli @ref mixture nodes over @ref uniform components (and the
4700 * optional @ref as_random atom), so moments and sampling are exact
4701 * through the existing mixture machinery; comparisons ride Monte Carlo.
4702 *
4703 * @code
4704 * provsql.empirical_cdf(
4705 * grid => ARRAY[0.0, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0],
4706 * cdf => ARRAY[0.32, 0.51, 0.67, 0.82, 0.94, 0.99, 1.0])
4707 * @endcode
4708 *
4709 * Validation: same-length arrays of at least two entries, @p grid
4710 * strictly increasing and finite, @p cdf non-decreasing within
4711 * <tt>[0, 1]</tt> and ending at @c 1 within @c 1e-9.
4712 *
4713 * @sa @ref gmm, @ref empirical_samples
4714 * @sa <a href="https://en.wikipedia.org/wiki/Cumulative_distribution_function">Wikipedia: Cumulative distribution function</a>
4715 */
4716CREATE OR REPLACE FUNCTION empirical_cdf(grid double precision[],
4717 cdf double precision[])
4718 RETURNS random_variable AS
4719$$
4720DECLARE
4721 n INTEGER;
4722 i INTEGER;
4723 acc random_variable := NULL;
4724 remaining double precision := 0.0;
4725 w double precision;
4726 comp random_variable;
4727BEGIN
4728 n := array_length(grid, 1);
4729 IF n IS NULL OR n < 2 THEN
4730 RAISE EXCEPTION 'provsql.empirical_cdf: grid must have at least two entries';
4731 END IF;
4732 IF array_length(cdf, 1) <> n THEN
4733 RAISE EXCEPTION 'provsql.empirical_cdf: grid and cdf must have the same length (got % and %)',
4734 n, array_length(cdf, 1);
4735 END IF;
4736 IF n > 10000 THEN
4737 RAISE EXCEPTION 'provsql.empirical_cdf: at most 10000 grid points are supported (got %)', n;
4738 END IF;
4739 FOR i IN 1..n LOOP
4740 IF grid[i] IS NULL OR grid[i] = 'NaN'::float8
4741 OR grid[i] = 'Infinity'::float8 OR grid[i] = '-Infinity'::float8 THEN
4742 RAISE EXCEPTION 'provsql.empirical_cdf: grid[%] must be finite (got %)', i, grid[i];
4743 END IF;
4744 IF i > 1 AND NOT grid[i] > grid[i-1] THEN
4745 RAISE EXCEPTION 'provsql.empirical_cdf: grid must be strictly increasing (grid[%] = %, grid[%] = %)',
4746 i-1, grid[i-1], i, grid[i];
4747 END IF;
4748 IF cdf[i] IS NULL OR cdf[i] = 'NaN'::float8 OR cdf[i] < 0 OR cdf[i] > 1 THEN
4749 RAISE EXCEPTION 'provsql.empirical_cdf: cdf[%] must be in [0,1] (got %)', i, cdf[i];
4750 END IF;
4751 IF i > 1 AND cdf[i] < cdf[i-1] THEN
4752 RAISE EXCEPTION 'provsql.empirical_cdf: cdf must be non-decreasing (cdf[%] = %, cdf[%] = %)',
4753 i-1, cdf[i-1], i, cdf[i];
4754 END IF;
4755 END LOOP;
4756 IF abs(cdf[n] - 1.0) > 1e-9 THEN
4757 RAISE EXCEPTION 'provsql.empirical_cdf: cdf must end at 1 within 1e-9 (got %)', cdf[n];
4758 END IF;
4759
4760 -- Stick-breaking cascade, back to front: component i = 1 is the atom
4761 -- at the grid start (mass cdf[1]); component i >= 2 is
4762 -- uniform(grid[i-1], grid[i]) with mass cdf[i] - cdf[i-1].
4763 FOR i IN REVERSE n..1 LOOP
4764 w := CASE WHEN i = 1 THEN cdf[1] ELSE cdf[i] - cdf[i-1] END;
4765 IF w <= 0.0 THEN
4766 CONTINUE;
4767 END IF;
4768 comp := CASE WHEN i = 1 THEN provsql.as_random(grid[1])
4769 ELSE provsql.uniform(grid[i-1], grid[i]) END;
4770 IF acc IS NULL THEN
4771 acc := comp;
4772 remaining := w;
4773 ELSE
4774 remaining := remaining + w;
4775 acc := provsql.mixture(least(1.0, w / remaining), comp, acc);
4776 END IF;
4777 END LOOP;
4778 RETURN acc;
4779END
4780$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
4781
4782/**
4783 * @brief Lift a deterministic constant into a random_variable
4784 *
4785 * Creates a <tt>gate_value</tt> carrying the constant's TEXT form so
4786 * that comparisons against a <tt>random_variable</tt> column produce
4787 * the same circuit shape regardless of whether the operand is an
4788 * actual RV or a literal constant.
4789 *
4790 * Marked <tt>IMMUTABLE</tt>: the gate UUID is derived deterministically
4791 * from the constant via the same v5 convention as <tt>provenance_semimod</tt>'s
4792 * inline value gate (<tt>concat('value', CAST(c AS VARCHAR))</tt>), so
4793 * <tt>as_random(2)</tt> always resolves to the same gate, and any other
4794 * code path that already creates a value gate for the same constant
4795 * (e.g. <tt>provenance_semimod</tt>) shares the UUID.
4796 * <tt>create_gate</tt> is idempotent on already-mapped tokens, so
4797 * repeat invocations are harmless.
4798 *
4799 * @sa <a href="https://en.wikipedia.org/wiki/Degenerate_distribution">Wikipedia: Degenerate distribution (Dirac point mass)</a>
4800 */
4801CREATE OR REPLACE FUNCTION as_random(c double precision)
4802 RETURNS random_variable AS
4803$$
4804DECLARE
4805 -- Canonicalise -0.0 to +0.0: IEEE 754 defines x + 0.0 = +0.0 for
4806 -- both signed zeros, and is identity for finite, NaN, and ±Infinity.
4807 -- Without this, as_random(-0.0) and as_random(+0.0) would produce
4808 -- different gate UUIDs (their CAST AS VARCHAR TEXT representations
4809 -- differ: '-0' vs '0') even though they denote the same constant.
4810 c_canon double precision := c + 0.0;
4811 c_text varchar := CAST(c_canon AS VARCHAR);
4812 token UUID := public.uuid_generate_v5(
4813 provsql.uuid_ns_provsql(), concat('value', c_text));
4814BEGIN
4815 PERFORM provsql.create_gate(token, 'value');
4816 PERFORM provsql.set_extra(token, c_text);
4817 RETURN provsql.random_variable_make(token);
4818END
4819$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
4820
4821/**
4822 * @brief Implicit cast double precision -> random_variable (lifts a
4823 * scalar literal to a constant RV).
4824 *
4825 * Lets users write <tt>WHERE reading > 2.5::float8</tt> instead of
4826 * <tt>WHERE reading > provsql.as_random(2.5)</tt>; the planner-hook
4827 * rewriter then sees a uniform <tt>random_variable</tt> on both sides.
4828 * Sibling casts below cover @c INTEGER and @c NUMERIC literals so
4829 * plain <tt>WHERE reading > 2</tt> and <tt>WHERE reading > 2.5</tt>
4830 * also work; PostgreSQL's operator resolution does not chain casts
4831 * across more than one step, so each NUMERIC-source type needs its
4832 * own direct cast.
4833 */
4834CREATE CAST (double precision AS random_variable)
4835 WITH FUNCTION as_random(double precision) AS IMPLICIT;
4836
4837/** @brief @c as_random for @c INTEGER (delegates to the @c float8 form). */
4838CREATE OR REPLACE FUNCTION as_random(c INTEGER)
4839 RETURNS random_variable AS
4840$$ SELECT provsql.as_random(c::double precision); $$
4841LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
4842
4843/** @brief @c as_random for @c NUMERIC (delegates to the @c float8 form). */
4844CREATE OR REPLACE FUNCTION as_random(c NUMERIC)
4845 RETURNS random_variable AS
4846$$ SELECT provsql.as_random(c::double precision); $$
4847LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
4848
4849/** @brief Implicit cast INTEGER -> random_variable. */
4850CREATE CAST (INTEGER AS random_variable)
4851 WITH FUNCTION as_random(INTEGER) AS IMPLICIT;
4852
4853/** @brief Implicit cast NUMERIC -> random_variable. */
4854CREATE CAST (NUMERIC AS random_variable)
4855 WITH FUNCTION as_random(NUMERIC) AS IMPLICIT;
4856
4857/**
4858 * @name Arithmetic and comparison on random_variable
4859 *
4860 * Each binary operator below is declared on @c (random_variable,
4861 * random_variable) only; mixed shapes such as <tt>rv + 2</tt> or
4862 * <tt>2.5 > rv</tt> resolve through the implicit casts from
4863 * @c INTEGER / @c NUMERIC / @c double @c precision to
4864 * @c random_variable declared above. This avoids the resolution
4865 * ambiguity that would arise if both <tt>(rv, NUMERIC)</tt> and
4866 * <tt>(rv, rv)</tt> overloads were declared while implicit casts also
4867 * existed.
4868 *
4869 * Arithmetic operators build a @c gate_arith via @c provenance_arith
4870 * and return a new @c random_variable wrapping its UUID.
4871 *
4872 * Comparison operators are placeholders that return @c BOOLEAN and
4873 * raise if executed -- the @c BOOLEAN return type is required so that
4874 * PostgreSQL accepts <tt>WHERE rv > 2</tt> at parse-analyze. The
4875 * planner hook intercepts every such @c OpExpr (matched by
4876 * @c opfuncid against @c constants_t::OID_FUNCTION_RV_CMP) and rewrites
4877 * it into a @c provenance_cmp call whose UUID is conjoined into the
4878 * tuple's @c provsql column via @c provenance_times. Code that needs
4879 * a @c gate_cmp UUID directly (without going through the planner hook)
4880 * uses the @c rv_cmp_* family below, which call @c provenance_cmp
4881 * with the matching float8-comparator OID.
4882 *
4883 * @{
4884 */
4885
4886/** @brief @c random_variable + @c random_variable (gate_arith PLUS). */
4887CREATE OR REPLACE FUNCTION random_variable_plus(
4888 a random_variable, b random_variable)
4889 RETURNS random_variable AS
4890$$
4891 SELECT provsql.random_variable_make(
4892 provsql.provenance_arith(
4893 0, -- PROVSQL_ARITH_PLUS
4894 ARRAY[(a)::UUID,
4895 (b)::UUID]));
4896$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4897
4898/** @brief @c random_variable - @c random_variable (gate_arith MINUS). */
4899CREATE OR REPLACE FUNCTION random_variable_minus(
4900 a random_variable, b random_variable)
4901 RETURNS random_variable AS
4902$$
4903 SELECT provsql.random_variable_make(
4904 provsql.provenance_arith(
4905 2, -- PROVSQL_ARITH_MINUS
4906 ARRAY[(a)::UUID,
4907 (b)::UUID]));
4908$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4909
4910/** @brief @c random_variable * @c random_variable (gate_arith TIMES). */
4911CREATE OR REPLACE FUNCTION random_variable_times(
4912 a random_variable, b random_variable)
4913 RETURNS random_variable AS
4914$$
4915 SELECT provsql.random_variable_make(
4916 provsql.provenance_arith(
4917 1, -- PROVSQL_ARITH_TIMES
4918 ARRAY[(a)::UUID,
4919 (b)::UUID]));
4920$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4921
4922/** @brief @c random_variable / @c random_variable (gate_arith DIV). */
4923CREATE OR REPLACE FUNCTION random_variable_div(
4924 a random_variable, b random_variable)
4925 RETURNS random_variable AS
4926$$
4927 SELECT provsql.random_variable_make(
4928 provsql.provenance_arith(
4929 3, -- PROVSQL_ARITH_DIV
4930 ARRAY[(a)::UUID,
4931 (b)::UUID]));
4932$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4933
4934/** @brief Unary @c -random_variable (gate_arith NEG). */
4935CREATE OR REPLACE FUNCTION random_variable_neg(a random_variable)
4936 RETURNS random_variable AS
4937$$
4938 SELECT provsql.random_variable_make(
4939 provsql.provenance_arith(
4940 4, -- PROVSQL_ARITH_NEG
4941 ARRAY[(a)::UUID]));
4942$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4943
4945 * @brief @c random_variable ^ @c random_variable (gate_arith POW).
4946 *
4947 * Real-valued branch only: evaluation raises if a negative base is
4948 * drawn together with a non-INTEGER exponent (write
4949 * <tt>pow(greatest(x, 0), p)</tt> for the non-negative branch).
4950 */
4951CREATE OR REPLACE FUNCTION random_variable_pow(
4952 a random_variable, b random_variable)
4953 RETURNS random_variable AS
4954$$
4955 SELECT provsql.random_variable_make(
4956 provsql.provenance_arith(
4957 7, -- PROVSQL_ARITH_POW
4958 ARRAY[(a)::UUID,
4959 (b)::UUID]));
4960$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4961
4962/**
4963 * @brief Natural logarithm of a @c random_variable (gate_arith LN).
4964 *
4965 * Defined on @c [0, +Infinity): evaluation raises if a negative value
4966 * is drawn (restrict the argument's support); a draw of exactly @c 0
4967 * yields @c -Infinity.
4968 */
4969CREATE OR REPLACE FUNCTION ln(a random_variable)
4970 RETURNS random_variable AS
4972 SELECT provsql.random_variable_make(
4973 provsql.provenance_arith(
4974 8, -- PROVSQL_ARITH_LN
4975 ARRAY[(a)::UUID]));
4976$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4977
4978/** @brief @c e^x for a @c random_variable (gate_arith EXP). Total. */
4979CREATE OR REPLACE FUNCTION exp(a random_variable)
4980 RETURNS random_variable AS
4981$$
4982 SELECT provsql.random_variable_make(
4983 provsql.provenance_arith(
4984 9, -- PROVSQL_ARITH_EXP
4985 ARRAY[(a)::UUID]));
4986$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4987
4988/**
4989 * @brief @c pow / @c power spellings of the @c ^ operator, mirroring
4990 * PostgreSQL's NUMERIC surface. Scalar exponents resolve
4991 * through the implicit NUMERIC-to-rv casts:
4992 * <tt>pow(x, 0.5)</tt> is <tt>x ^ 0.5</tt>.
4994CREATE OR REPLACE FUNCTION pow(a random_variable, b random_variable)
4995 RETURNS random_variable AS
4996$$
4997 SELECT provsql.random_variable_pow(a, b);
4998$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
4999
5000CREATE OR REPLACE FUNCTION power(a random_variable, b random_variable)
5001 RETURNS random_variable AS
5002$$
5003 SELECT provsql.random_variable_pow(a, b);
5004$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5005
5006/**
5007 * @brief Square root of a @c random_variable: sugar for
5008 * <tt>x ^ 0.5</tt> (no gate or opcode of its own). Evaluation
5009 * raises on a negative draw, like any non-INTEGER exponent.
5010 */
5011CREATE OR REPLACE FUNCTION sqrt(a random_variable)
5012 RETURNS random_variable AS
5013$$
5014 SELECT provsql.random_variable_pow(a, provsql.as_random(0.5));
5015$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5016
5017/**
5018 * @brief Internal helper: float8-comparator OID for a given symbol.
5019 *
5020 * Wraps the @c '&lt;sym&gt;(double precision,double precision)'::regoperator
5021 * lookup so the per-comparator functions read uniformly. Marked
5022 * @c IMMUTABLE because the resolved OID is fixed at catalog level
5023 * (the float8 comparators are core PG and never re-installed).
5024 */
5025CREATE OR REPLACE FUNCTION random_variable_cmp_oid(sym TEXT)
5026 RETURNS oid AS
5027$$
5028 SELECT (sym || '(double precision,double precision)')::regoperator::oid;
5029$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5030
5031/* The six @c random_variable_{lt,le,eq,ne,ge,gt} functions below are
5032 * BOOLEAN placeholders -- they exist only so the @c (rv, rv) operators
5033 * can be declared at all (PostgreSQL needs a procedure to bind to the
5034 * operator definition, and a procedure returning anything but @c BOOLEAN
5035 * would be rejected by parse-analyze in a WHERE position). They MUST
5036 * NOT be invoked directly: the planner hook in @c src/provsql.c
5037 * intercepts every @c OpExpr whose @c opfuncid matches one of these and
5038 * rewrites it into a @c provenance_cmp() call against the row's
5039 * provenance. If the executor ever reaches one of these, it means the
5040 * planner hook was bypassed (e.g. @c provsql.active was off), in which
5041 * case raising is the right behaviour. */
5042
5043/** @brief Placeholder body shared by every <tt>random_variable_*</tt>
5044 * comparison procedure. Raises with a uniform message. */
5045CREATE OR REPLACE FUNCTION random_variable_cmp_placeholder(
5046 a random_variable, b random_variable)
5047 RETURNS BOOLEAN AS
5048$$
5049BEGIN
5050 RAISE EXCEPTION 'random_variable comparison must be rewritten by the '
5051 'ProvSQL planner hook (is provsql.active off?)';
5052END
5053$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
5054
5055CREATE OR REPLACE FUNCTION random_variable_lt(
5056 a random_variable, b random_variable) RETURNS BOOLEAN AS
5057$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5058LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5059
5060CREATE OR REPLACE FUNCTION random_variable_le(
5061 a random_variable, b random_variable) RETURNS BOOLEAN AS
5062$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5063LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5064
5065CREATE OR REPLACE FUNCTION random_variable_eq(
5066 a random_variable, b random_variable) RETURNS BOOLEAN AS
5067$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5068LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5069
5070CREATE OR REPLACE FUNCTION random_variable_ne(
5071 a random_variable, b random_variable) RETURNS BOOLEAN AS
5072$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5073LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5074
5075CREATE OR REPLACE FUNCTION random_variable_ge(
5076 a random_variable, b random_variable) RETURNS BOOLEAN AS
5077$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5078LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5079
5080CREATE OR REPLACE FUNCTION random_variable_gt(
5081 a random_variable, b random_variable) RETURNS BOOLEAN AS
5082$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
5083LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5084
5085/* Direct UUID constructors -- used by tests and any caller that wants
5086 * a @c gate_cmp without going through the planner hook (e.g. building
5087 * a circuit fragment in a SELECT list). Each delegates to
5088 * @c provenance_cmp with the matching float8-comparator OID. */
5089
5090/** @brief Build a @c gate_cmp for <tt>a &lt; b</tt> and return its UUID. */
5091CREATE OR REPLACE FUNCTION rv_cmp_lt(
5092 a random_variable, b random_variable) RETURNS UUID AS
5093$$
5094 SELECT provsql.provenance_cmp(
5095 (a)::UUID,
5096 provsql.random_variable_cmp_oid('<'),
5097 (b)::UUID);
5098$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5099
5100/** @brief Build a @c gate_cmp for <tt>a &le; b</tt> and return its UUID. */
5101CREATE OR REPLACE FUNCTION rv_cmp_le(
5102 a random_variable, b random_variable) RETURNS UUID AS
5103$$
5104 SELECT provsql.provenance_cmp(
5105 (a)::UUID,
5106 provsql.random_variable_cmp_oid('<='),
5107 (b)::UUID);
5108$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5109
5110/** @brief Build a @c gate_cmp for <tt>a = b</tt> and return its UUID. */
5111CREATE OR REPLACE FUNCTION rv_cmp_eq(
5112 a random_variable, b random_variable) RETURNS UUID AS
5113$$
5114 SELECT provsql.provenance_cmp(
5115 (a)::UUID,
5116 provsql.random_variable_cmp_oid('='),
5117 (b)::UUID);
5118$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5119
5120/** @brief Build a @c gate_cmp for <tt>a &lt;&gt; b</tt> and return its UUID. */
5121CREATE OR REPLACE FUNCTION rv_cmp_ne(
5122 a random_variable, b random_variable) RETURNS UUID AS
5123$$
5124 SELECT provsql.provenance_cmp(
5125 (a)::UUID,
5126 provsql.random_variable_cmp_oid('<>'),
5127 (b)::UUID);
5128$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5129
5130/** @brief Build a @c gate_cmp for <tt>a &ge; b</tt> and return its UUID. */
5131CREATE OR REPLACE FUNCTION rv_cmp_ge(
5132 a random_variable, b random_variable) RETURNS UUID AS
5133$$
5134 SELECT provsql.provenance_cmp(
5135 (a)::UUID,
5136 provsql.random_variable_cmp_oid('>='),
5137 (b)::UUID);
5138$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5139
5140/** @brief Build a @c gate_cmp for <tt>a &gt; b</tt> and return its UUID. */
5141CREATE OR REPLACE FUNCTION rv_cmp_gt(
5142 a random_variable, b random_variable) RETURNS UUID AS
5143$$
5144 SELECT provsql.provenance_cmp(
5145 (a)::UUID,
5146 provsql.random_variable_cmp_oid('>'),
5147 (b)::UUID);
5148$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5149
5150CREATE OPERATOR + (
5151 LEFTARG = random_variable,
5152 RIGHTARG = random_variable,
5153 PROCEDURE = random_variable_plus,
5154 COMMUTATOR = +
5155);
5156
5157CREATE OPERATOR - (
5158 LEFTARG = random_variable,
5159 RIGHTARG = random_variable,
5160 PROCEDURE = random_variable_minus
5161);
5162
5163CREATE OPERATOR * (
5164 LEFTARG = random_variable,
5165 RIGHTARG = random_variable,
5166 PROCEDURE = random_variable_times,
5167 COMMUTATOR = *
5168);
5169
5170CREATE OPERATOR / (
5171 LEFTARG = random_variable,
5172 RIGHTARG = random_variable,
5173 PROCEDURE = random_variable_div
5174);
5175
5176/** @brief Prefix unary minus on @c random_variable. */
5177CREATE OPERATOR - (
5178 RIGHTARG = random_variable,
5179 PROCEDURE = random_variable_neg
5180);
5181
5182CREATE OPERATOR ^ (
5183 LEFTARG = random_variable,
5184 RIGHTARG = random_variable,
5185 PROCEDURE = random_variable_pow
5186);
5187
5188CREATE OPERATOR < (
5189 LEFTARG = random_variable,
5190 RIGHTARG = random_variable,
5191 PROCEDURE = random_variable_lt,
5192 COMMUTATOR = >,
5193 NEGATOR = >=
5194);
5195
5196CREATE OPERATOR <= (
5197 LEFTARG = random_variable,
5198 RIGHTARG = random_variable,
5199 PROCEDURE = random_variable_le,
5200 COMMUTATOR = >=,
5201 NEGATOR = >
5202);
5203
5204CREATE OPERATOR = (
5205 LEFTARG = random_variable,
5206 RIGHTARG = random_variable,
5207 PROCEDURE = random_variable_eq,
5208 COMMUTATOR = =,
5209 NEGATOR = <>
5210);
5211
5212CREATE OPERATOR <> (
5213 LEFTARG = random_variable,
5214 RIGHTARG = random_variable,
5215 PROCEDURE = random_variable_ne,
5216 COMMUTATOR = <>,
5217 NEGATOR = =
5218);
5219
5220CREATE OPERATOR >= (
5221 LEFTARG = random_variable,
5222 RIGHTARG = random_variable,
5223 PROCEDURE = random_variable_ge,
5224 COMMUTATOR = <=,
5225 NEGATOR = <
5226);
5227
5228CREATE OPERATOR > (
5229 LEFTARG = random_variable,
5230 RIGHTARG = random_variable,
5231 PROCEDURE = random_variable_gt,
5232 COMMUTATOR = <,
5233 NEGATOR = <=
5234);
5235
5236/**
5237 * @brief btree comparison support for @c random_variable -- always an error.
5238 *
5239 * A @c random_variable is a distribution, not a scalar, so it has no total
5240 * order: sorting (@c ORDER @c BY), de-duplicating (@c DISTINCT), grouping, and
5241 * the built-in @c GREATEST / @c LEAST all reduce to this btree comparison
5242 * proc, which raises a clear diagnostic rather than a placeholder message.
5243 *
5244 * The proc exists only so a DEFAULT btree operator class can be declared for
5245 * @c random_variable -- which is what lets PostgreSQL's @c GREATEST / @c LEAST
5246 * grammar parse over random variables so the planner hook can lift it into a
5247 * @c gate_arith @c MAX / @c MIN order statistic. When the hook is active the
5248 * @c GREATEST / @c LEAST node is rewritten before it ever calls this proc.
5249 */
5250CREATE OR REPLACE FUNCTION random_variable_btree_cmp(
5251 a random_variable, b random_variable) RETURNS INTEGER AS
5252$$
5253BEGIN
5254 RAISE EXCEPTION 'comparison or ordering of random_variable values is '
5255 'meaningless: a random_variable is a distribution, not a scalar'
5256 USING HINT =
5257 'Compare them as a probabilistic event -- in a WHERE / JOIN clause or '
5258 'with probability(x > y); take order statistics with provsql.greatest / '
5259 'provsql.least (or the min / max aggregates); summarise numerically with '
5260 'expected / variance / support.';
5261END
5262$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
5264-- DEFAULT btree operator class over the (planner-hook-lifted) comparison
5265-- operators. Its only purpose is to make GREATEST / LEAST over random_variable
5266-- parse; every actual comparison it would drive (ORDER BY, DISTINCT, an
5267-- un-rewritten GREATEST) funnels through random_variable_btree_cmp above and
5268-- raises the "meaningless" diagnostic.
5269CREATE OPERATOR CLASS random_variable_ops
5270 DEFAULT FOR TYPE random_variable USING btree AS
5271 OPERATOR 1 <,
5272 OPERATOR 2 <=,
5273 OPERATOR 3 =,
5274 OPERATOR 4 >=,
5275 OPERATOR 5 >,
5276 FUNCTION 1 random_variable_btree_cmp(random_variable, random_variable);
5277
5278/**
5279 * @brief Condition a random variable on an event: @c "X | C".
5280 *
5281 * Returns a conditioned distribution that flows onward like any other
5282 * @c random_variable: it can be stored, re-conditioned, and queried with
5283 * @c expected / @c variance / @c moment / @c support, which then report the
5284 * conditional distribution. @p cond is a Boolean-event provenance token,
5285 * typically a comparison over the variable itself (@c "X | rv_cmp_gt(X,
5286 * as_random(3))" -- a truncation) or any external event.
5287 *
5288 * Unlike the UUID carrier's terminal @c cond, the random-variable form is a
5289 * composable two-child @c gate_conditioned @c [target, condition]: the moment
5290 * / support dispatchers unpack it and route through the existing conditional
5291 * evaluator (@c rv_moment over the joint of the target and the condition).
5292 * Nested conditioning folds: @c "(X|A)|B = X|(A∧B)".
5293 */
5294CREATE OR REPLACE FUNCTION random_variable_cond(rv random_variable, cond UUID)
5295 RETURNS random_variable AS
5296$$
5297DECLARE
5298 tgt UUID;
5299 ev UUID;
5300 result UUID;
5301 ch UUID[];
5302BEGIN
5303 IF cond IS NULL OR cond = gate_one() THEN
5304 RETURN rv;
5305 END IF;
5306
5307 -- A point-equality "Y = c" on a bare random-variable leaf is an
5308 -- OBSERVATION, not a truncation: rewrite it to the internal likelihood-
5309 -- weighting evidence (its density / mass at c). This is what lets
5310 -- "X | (normal(mu,1) = 8)" (a continuous point event, measure-zero as a
5311 -- Boolean selection) condition as the disintegration rather than fold to
5312 -- an infeasible event.
5313 cond := provsql.evidence_as_observation(cond);
5314
5315 tgt := (rv)::UUID;
5316 IF get_gate_type(tgt) = 'conditioned'
5317 AND array_length(get_children(tgt), 1) = 2 THEN
5318 -- Fold (X|A)|B = X|(A∧B): the rv-carrier conditioned gate is the
5319 -- two-child [target, condition] shape; accumulate the new event.
5320 ch := get_children(tgt);
5321 tgt := ch[1];
5322 ev := provenance_times(ch[2], cond);
5323 ELSE
5324 ev := cond;
5325 END IF;
5326
5327 result := public.uuid_generate_v5(uuid_ns_provsql(),
5328 concat('conditioned', tgt, ev));
5329 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev]);
5330 RETURN (result)::random_variable;
5331END
5332$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
5333 SECURITY DEFINER PARALLEL SAFE;
5334
5335CREATE OPERATOR | (
5336 LEFTARG = random_variable,
5337 RIGHTARG = UUID,
5338 PROCEDURE = random_variable_cond
5339);
5340
5341/**
5342 * @brief Placeholder for @c "X | (predicate)" -- conditioning a random
5343 * variable on a Boolean comparison written naturally.
5344 *
5345 * Lets one write @c "X | (X > 3)" instead of
5346 * @c "X | rv_cmp_gt(X, as_random(3))". Never executes: the ProvSQL planner
5347 * hook rewrites the Boolean operand (a combination of random_variable
5348 * comparisons) into the corresponding condition gate and emits
5349 * @c random_variable_cond. Reaching it at runtime means the rewriter was
5350 * inactive or the predicate was not a random_variable comparison.
5351 */
5352CREATE OR REPLACE FUNCTION random_variable_cond_predicate(
5353 rv random_variable, predicate BOOLEAN) RETURNS random_variable AS
5354$$
5355BEGIN
5356 RAISE EXCEPTION 'random_variable | (predicate) must be rewritten by the '
5357 'ProvSQL planner hook: the right operand must be a Boolean combination '
5358 'of random_variable comparisons (is provsql.active off?)';
5359END
5360$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
5361
5362CREATE OPERATOR | (
5363 LEFTARG = random_variable,
5364 RIGHTARG = BOOLEAN,
5365 PROCEDURE = random_variable_cond_predicate
5366);
5367
5368/**
5369 * @brief Unpack the target of a random-variable conditioning gate.
5370 *
5371 * For a two-child @c gate_conditioned @c [target, condition] (the @c "X | C"
5372 * shape) returns @p target; for any other token returns it unchanged. Used
5373 * by the moment / support dispatchers to route a conditioned distribution
5374 * through the existing conditional evaluator.
5375 */
5376CREATE OR REPLACE FUNCTION rv_conditioned_target(token UUID) RETURNS UUID AS
5377$$
5378 SELECT CASE
5379 WHEN provsql.get_gate_type(token) = 'conditioned'
5380 AND array_length(provsql.get_children(token), 1) = 2
5381 THEN (provsql.get_children(token))[1]
5382 ELSE token
5383 END;
5384$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
5385
5386/**
5387 * @brief Combine a conditioning gate's event with an explicit @p prov.
5388 *
5389 * For a two-child @c gate_conditioned @c [target, condition] returns
5390 * @c "condition ∧ prov"; otherwise returns @p prov unchanged. Lets a stored
5391 * @c "X | C" be queried as @c expected(X|C) (prov defaulting to one) or have
5392 * an extra condition conjoined as @c expected(X|C, extra_prov).
5393 */
5394CREATE OR REPLACE FUNCTION rv_conditioned_prov(token UUID, prov UUID)
5395 RETURNS UUID AS
5396$$
5397 SELECT CASE
5398 WHEN provsql.get_gate_type(token) = 'conditioned'
5399 AND array_length(provsql.get_children(token), 1) = 2
5400 THEN provsql.provenance_times((provsql.get_children(token))[2], prov)
5401 ELSE prov
5402 END;
5403$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
5404
5405/*
5406 * Latent-variable posterior inference.
5407 *
5408 * Likelihood weighting (self-normalised importance sampling): bind an
5409 * observed datum to a latent-dependent random-variable leaf with observe,
5410 * conjoin the per-observation evidence with and_agg into a single evidence
5411 * token, and pass it as the prov conditioning argument of any moment /
5412 * quantile / sample readout. Latents are drawn from the prior (the
5413 * existing forward recursion) and each draw is weighted by the observed
5414 * leaves' densities at the data; the readouts then report the posterior.
5415 * It is the continuous generalisation of the rejection-based conditioning:
5416 * a Boolean event in the evidence contributes a 0/1 weight, an observe
5417 * contributes a pdf weight -- same evidence conjunction, same
5418 * "P(query AND evidence)/P(evidence)" normaliser, now weighted.
5419 */
5420
5422 * @brief Internal: rewrite a point-equality conditioning event into an
5423 * observation. If @p ev is a @c gate_cmp with the @c "=" operator,
5424 * one side a bare @c gate_rv leaf and the other a constant, return
5425 * @c observe(leaf, const); otherwise return @p ev unchanged.
5426 *
5427 * This is the bridge that makes the natural equality form the surface for
5428 * likelihood-weighting conditioning: @c "X | (Y = c)" and @c "given(Y = c)"
5429 * both produce a @c gate_cmp, which this turns into density evidence. A
5430 * point event on a bare leaf is only meaningful as an observation (a
5431 * continuous @c "Y = c" is measure-zero as a Boolean selection), so the
5432 * rewrite is unambiguous. Non-equality / non-leaf events pass through as
5433 * ordinary Boolean conditioning.
5434 */
5435CREATE OR REPLACE FUNCTION evidence_as_observation(ev UUID) RETURNS UUID AS
5437DECLARE
5438 ch UUID[];
5439 i1 INTEGER;
5440 leaf UUID;
5441 datum_gate UUID;
5442BEGIN
5443 IF ev IS NULL OR provsql.get_gate_type(ev) <> 'cmp' THEN
5444 RETURN ev;
5445 END IF;
5446 ch := provsql.get_children(ev);
5447 IF array_length(ch, 1) <> 2 THEN
5448 RETURN ev;
5449 END IF;
5450 -- The cmp stores the comparison OPERATOR's OID in info1; match on its name
5451 -- '=' the same way the C-side cmpOpFromOid does (get_opname), rather than a
5452 -- fixed operator OID (which varies per install / carrier type).
5453 SELECT info1 INTO i1 FROM provsql.get_infos(ev);
5454 IF (SELECT oprname FROM pg_catalog.pg_operator WHERE oid = i1) IS DISTINCT FROM '=' THEN
5455 RETURN ev; -- not an equality
5456 END IF;
5457 IF provsql.get_gate_type(ch[1]) = 'rv'
5458 AND provsql.get_gate_type(ch[2]) = 'value' THEN
5459 leaf := ch[1]; datum_gate := ch[2];
5460 ELSIF provsql.get_gate_type(ch[2]) = 'rv'
5461 AND provsql.get_gate_type(ch[1]) = 'value' THEN
5462 leaf := ch[2]; datum_gate := ch[1];
5463 ELSE
5464 RETURN ev; -- not a bare-leaf-vs-constant point event
5465 END IF;
5466 RETURN provsql.observe((leaf)::random_variable,
5467 provsql.get_extra(datum_gate)::double precision);
5468END
5469$$ LANGUAGE plpgsql VOLATILE
5470 SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
5471
5473 * @brief Internal: bind an observed datum to a random-variable leaf --
5474 * the likelihood-weighting evidence behind @c "X | (Y = d)".
5475 *
5476 * @p x MUST be a bare @c gate_rv leaf (typically a latent-parameterised
5477 * one, e.g. @c normal(mu, 1) sharing a latent @c mu across rows).
5478 * Returns an @b evidence UUID -- a @c gate_observe wrapping the leaf with
5479 * the datum in @c extra -- that composes with other evidence through
5480 * @c and_agg (a @c gate_times conjunction) and is consumed by the
5481 * importance-sampling weight walk, contributing the factor @c f_X(d).
5482 *
5483 * Internal: the user-facing surface is the equality form @c "X | (Y = d)"
5484 * (single conditioning) and @c "given(Y = d)" (per-row evidence for
5485 * @c and_agg), both of which route here through @c evidence_as_observation.
5486 *
5487 * A fresh gate is minted per call (each observation is a distinct
5488 * evidence atom, so a repeated @c (leaf, datum) contributes its density
5489 * factor once per row -- and each is a separate Shapley atom). Observing
5490 * a derived quantity (@c observe(X+Y, d)) is out of scope: it needs a
5491 * change-of-variables density; a non-leaf argument is refused.
5492 */
5493CREATE OR REPLACE FUNCTION observe(x random_variable, datum double precision)
5494 RETURNS UUID AS
5495$$
5496DECLARE
5497 leaf UUID := (x)::UUID;
5498 result UUID;
5499BEGIN
5500 IF provsql.get_gate_type(leaf) <> 'rv' THEN
5501 RAISE EXCEPTION 'provsql.observe: the argument must be a bare '
5502 'random-variable leaf (a gate_rv), got a % gate', provsql.get_gate_type(leaf)
5503 USING HINT = 'observe binds a datum to a single distribution leaf; '
5504 'observing a derived quantity (a sum, product, or comparison) needs '
5505 'a change-of-variables density and is out of scope.';
5506 END IF;
5507 IF NOT provsql.is_finite_float8(datum) THEN
5508 RAISE EXCEPTION 'provsql.observe: datum must be finite (got %)', datum;
5509 END IF;
5510 result := public.uuid_generate_v4();
5511 PERFORM provsql.create_gate(result, 'observe', ARRAY[leaf]);
5512 PERFORM provsql.set_extra(result, datum::TEXT);
5513 RETURN result;
5514END
5515$$ LANGUAGE plpgsql VOLATILE
5516 SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
5517
5518/**
5519 * @brief Conjunction state function for @c and_agg (evidence @c gate_times).
5520 *
5521 * Not @c STRICT: @c provenance_times maps a @c NULL operand to the times
5522 * neutral, so an empty group leaves the state @c NULL (no evidence) and a
5523 * first row seeds it with that row's evidence.
5524 */
5525CREATE OR REPLACE FUNCTION and_agg_sfunc(state UUID, ev UUID)
5526 RETURNS UUID AS
5527$$
5528 SELECT provsql.provenance_times(state, ev);
5529$$ LANGUAGE sql PARALLEL SAFE;
5530
5531/**
5532 * @brief Conjoin per-row evidence tokens into one evidence circuit.
5533 *
5534 * The evidence-conjunction counterpart used to fold one @c observe (or any
5535 * Boolean conditioning event) per row into a single @c gate_times root, to
5536 * be passed as the @c prov argument of the moment / quantile / sample
5537 * readouts. An empty group yields @c NULL (no evidence).
5538 */
5539CREATE AGGREGATE and_agg(UUID) (
5540 SFUNC = and_agg_sfunc,
5541 STYPE = UUID
5542);
5544/**
5545 * @brief Marginal likelihood @c P(data) of an evidence circuit.
5546 *
5547 * The mean raw importance weight over @c provsql.rv_mc_samples prior draws
5548 * -- the same quantity rejection conditioning computes as @c P(C), now the
5549 * product of the observations' densities. @p evidence is an @c and_agg
5550 * conjunction of @c observe tokens (and/or Boolean events).
5551 */
5552CREATE OR REPLACE FUNCTION evidence(evidence UUID)
5553 RETURNS double precision
5554 AS 'provsql','rv_evidence' LANGUAGE C STRICT PARALLEL SAFE;
5555
5556/**
5557 * @brief The @c observe atoms of an evidence circuit.
5558 *
5559 * Collects every @c gate_observe leaf reachable through the @c gate_times
5560 * conjunction spine (the shape @c and_agg builds -- a possibly left-nested
5561 * tree, since @c provenance_times does not flatten). Used by
5562 * @c shapley_observe to recover the flat observation set regardless of the
5563 * conjunction's nesting.
5564 */
5565CREATE OR REPLACE FUNCTION observe_atoms(evidence UUID)
5566 RETURNS UUID[] AS
5567$$
5568 WITH RECURSIVE walk(tok) AS (
5569 SELECT evidence
5570 UNION
5571 SELECT c
5572 FROM walk, LATERAL unnest(provsql.get_children(walk.tok)) AS c
5573 WHERE provsql.get_gate_type(walk.tok) = 'times'
5574 )
5575 SELECT array_agg(tok ORDER BY tok)
5576 FROM walk
5577 WHERE provsql.get_gate_type(tok) = 'observe';
5578$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
5579
5580/**
5581 * @brief Shapley attribution of each observation to a posterior moment.
5582 *
5583 * "Which observation most shifted my posterior?" Because the importance
5584 * weight is a product of per-observation density factors, dropping an
5585 * observation is dropping one factor: the classical Shapley value of each
5586 * @c gate_observe atom over the coalitional value function
5587 * @c "v(S) = payoff(target | observations in S)" is the attribution, a
5588 * byproduct of the same likelihood-weighting machinery (see the
5589 * explainable-inference angle in the continuous-distributions notes).
5590 *
5591 * @p target is the latent (its @c UUID); @p evidence is the @c and_agg
5592 * conjunction of @c observe atoms; @p payoff is @c 'expected' or
5593 * @c 'variance'. Returns each observation atom with its Shapley value; the
5594 * values sum to @c "payoff(target | all data) - payoff(target)" (Shapley
5595 * efficiency: the total shift from prior to posterior).
5596 *
5597 * Exact enumeration over the @c 2^n observation subsets, so it is capped at
5598 * @c n = 12 observations (sampling-based attribution for larger sets is
5599 * future work); pin @c provsql.monte_carlo_seed so the coalitional value
5600 * functions share common random numbers (lower-variance differences).
5601 */
5602CREATE OR REPLACE FUNCTION shapley_observe(
5603 target UUID, evidence UUID, payoff TEXT DEFAULT 'expected')
5604 RETURNS TABLE(observation UUID, value double precision) AS
5605$$
5606DECLARE
5607 atoms UUID[];
5608 n INT;
5609 nmasks INT;
5610 pv double precision[];
5611 popc INT[];
5612 fact double precision[];
5613 mask INT;
5614 i INT;
5615 j INT;
5616 cnt INT;
5617 subset UUID[];
5618 ev_s UUID;
5619 sh double precision;
5620 bit INT;
5621 s_size INT;
5622BEGIN
5623 IF payoff NOT IN ('expected', 'variance') THEN
5624 RAISE EXCEPTION 'provsql.shapley_observe: payoff must be ''expected'' or '
5625 '''variance'' (got %)', payoff;
5626 END IF;
5627 atoms := provsql.observe_atoms(evidence);
5628 n := coalesce(array_length(atoms, 1), 0);
5629 IF n = 0 THEN
5630 RAISE EXCEPTION 'provsql.shapley_observe: evidence contains no observe() '
5631 'atoms (got a % gate)', provsql.get_gate_type(evidence);
5632 END IF;
5633 IF n > 12 THEN
5634 RAISE EXCEPTION 'provsql.shapley_observe: exact attribution over % '
5635 'observations is exponential; capped at 12 (sampling-based '
5636 'attribution is future work)', n;
5637 END IF;
5638
5639 -- factorials 0!..n! (fact[k+1] = k!)
5640 fact := ARRAY[1::double precision];
5641 FOR i IN 1..n LOOP fact := fact || (fact[i] * i); END LOOP;
5642
5643 nmasks := (1 << n);
5644 pv := array_fill(NULL::double precision, ARRAY[nmasks]);
5645 popc := array_fill(0, ARRAY[nmasks]);
5646
5647 -- Payoff value function for every subset of observations.
5648 FOR mask IN 0 .. nmasks - 1 LOOP
5649 subset := ARRAY[]::UUID[];
5650 cnt := 0;
5651 FOR i IN 0 .. n - 1 LOOP
5652 IF (mask >> i) & 1 = 1 THEN
5653 subset := subset || atoms[i + 1];
5654 cnt := cnt + 1;
5655 END IF;
5656 END LOOP;
5657 popc[mask + 1] := cnt;
5658 IF cnt = 0 THEN
5659 ev_s := provsql.gate_one(); -- prior (no evidence)
5660 ELSE
5661 ev_s := provsql.provenance_times(VARIADIC subset);
5662 END IF;
5663 IF payoff = 'expected' THEN
5664 pv[mask + 1] := provsql.rv_moment(target, 1, false, ev_s);
5665 ELSE
5666 pv[mask + 1] := provsql.rv_moment(target, 2, true, ev_s);
5667 END IF;
5668 END LOOP;
5669
5670 -- Shapley value of each observation atom.
5671 FOR i IN 0 .. n - 1 LOOP
5672 sh := 0;
5673 bit := (1 << i);
5674 FOR mask IN 0 .. nmasks - 1 LOOP
5675 IF (mask >> i) & 1 = 0 THEN -- subsets S not containing i
5676 s_size := popc[mask + 1];
5677 -- weight |S|! (n-|S|-1)! / n!
5678 sh := sh + (fact[s_size + 1] * fact[n - s_size] / fact[n + 1])
5679 * (pv[(mask | bit) + 1] - pv[mask + 1]);
5680 END IF;
5681 END LOOP;
5682 observation := atoms[i + 1];
5683 value := sh;
5684 RETURN NEXT;
5685 END LOOP;
5686END
5687$$ LANGUAGE plpgsql VOLATILE
5688 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
5689
5691 * @name Order statistics over random_variable
5692 *
5693 * Same-row @c greatest / @c least over @c random_variable arguments: the
5694 * order-statistic counterpart of the element-wise @c "+ - * /" operators.
5695 * They lower to a single @c gate_arith with the @c MAX / @c MIN opcode over
5696 * the argument circuits, the same n-ary shape the @c max / @c min aggregates
5697 * build. Evaluation is Monte-Carlo-correct out of the box (@c std::max /
5698 * @c std::min over the jointly-sampled children, so shared base RVs stay
5699 * coupled); closed forms for i.i.d. families come from the analytic
5700 * order-statistic pass.
5701 *
5702 * PostgreSQL's built-in @c GREATEST / @c LEAST are dedicated syntax (a
5703 * @c MinMaxExpr requiring a btree comparison), not overloadable functions, so
5704 * the surface is the schema-qualified @c provsql.greatest(...) /
5705 * @c provsql.least(...). @c NULL arguments are ignored, matching the built-in
5706 * (an all-@c NULL / empty call returns @c NULL).
5707 * @{
5708 */
5709
5710-- "greatest" / "least" are col_name keywords, so the CREATE FUNCTION name
5711-- must be quoted; callers reach them qualified as provsql.greatest(...).
5712-- Idempotence: max / min ignore repeats, so identical children (same gate)
5713-- are de-duplicated -- greatest(x, x, y) == greatest(x, y) -- and a single
5714-- surviving child collapses to itself -- greatest(x) == x. DISTINCT also sorts
5715-- the children, so the argument order does not matter for gate sharing. (Two
5716-- independent draws of the same distribution are distinct gates and are NOT
5717-- de-duplicated.)
5718CREATE OR REPLACE FUNCTION "greatest"(VARIADIC args random_variable[])
5719 RETURNS random_variable AS
5720$$
5721DECLARE
5722 children UUID[];
5723BEGIN
5724 IF args IS NULL THEN
5725 RETURN NULL;
5726 END IF;
5727 SELECT array_agg(DISTINCT (a)::UUID) INTO children
5728 FROM unnest(args) a WHERE a IS NOT NULL;
5729 IF children IS NULL OR array_length(children, 1) IS NULL THEN
5730 RETURN NULL;
5731 END IF;
5732 IF array_length(children, 1) = 1 THEN
5733 RETURN provsql.random_variable_make(children[1]);
5734 END IF;
5735 RETURN provsql.random_variable_make(
5736 provsql.provenance_arith(5, children)); -- 5 = PROVSQL_ARITH_MAX
5737END
5738$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
5739
5740CREATE OR REPLACE FUNCTION "least"(VARIADIC args random_variable[])
5741 RETURNS random_variable AS
5742$$
5743DECLARE
5744 children UUID[];
5745BEGIN
5746 IF args IS NULL THEN
5747 RETURN NULL;
5748 END IF;
5749 SELECT array_agg(DISTINCT (a)::UUID) INTO children
5750 FROM unnest(args) a WHERE a IS NOT NULL;
5751 IF children IS NULL OR array_length(children, 1) IS NULL THEN
5752 RETURN NULL;
5753 END IF;
5754 IF array_length(children, 1) = 1 THEN
5755 RETURN provsql.random_variable_make(children[1]);
5756 END IF;
5757 RETURN provsql.random_variable_make(
5758 provsql.provenance_arith(6, children)); -- 6 = PROVSQL_ARITH_MIN
5760$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
5761
5762/**
5763 * @brief Build a @c random_variable from a guarded-selection @c gate_case.
5764 *
5765 * Thin @c random_variable wrapper over @c provenance_case (defined with the
5766 * other gate builders, since it is UUID-only), the target of the planner-hook
5767 * @c CASE-over-RV rewrite: the hook flattens the branches into
5768 * @c [guard_1, value_1, ..., default] and emits this call so an RV-typed
5769 * @c CASE surfaces as a first-class @c random_variable.
5770 */
5771CREATE OR REPLACE FUNCTION rv_case(
5772 children UUID[]
5773)
5774RETURNS random_variable AS
5776 SELECT provsql.random_variable_make(provsql.provenance_case(children));
5777$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5778
5779/**
5780 * @brief Build an @c AGG_TOKEN from a guarded-selection @c gate_case.
5781 *
5782 * The aggregate-carrier analogue of @c rv_case: a thin @c AGG_TOKEN wrapper
5783 * over the carrier-agnostic @c provenance_case, the target of the planner-hook
5784 * lowering of a searched @c CASE whose guards are aggregate comparisons and
5785 * whose branches are aggregates. The branches (and default) are already
5786 * flattened into @c [guard_1, value_1, ..., default] UUIDs. The display cell
5787 * carries the actual-world CASE value -- the branch selected on the actual
5788 * data, resolved by @c agg_gate_value, exactly as a bare aggregate's cell
5789 * carries its actual-world value. The probabilistic result is produced by
5790 * the measure evaluators (``expected`` / ``probability`` / possible-worlds /
5791 * Monte Carlo) from the gate, not the token's cell.
5792 */
5793CREATE OR REPLACE FUNCTION agg_case(
5794 children UUID[]
5796RETURNS AGG_TOKEN AS
5797$$
5798 SELECT provsql.agg_token_make(t, coalesce(provsql.agg_gate_value(t), 0))
5799 FROM (SELECT provsql.provenance_case(children) AS t) AS s;
5800$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5801
5802/** @} */
5804/**
5805 * @name Aggregates over random_variable
5806 *
5807 * An overload of the standard
5808 * @c sum aggregate that takes a @c random_variable per row and returns
5809 * the @c random_variable representing the (provenance-weighted) sum.
5810 * Lives in the @c provsql schema so a @c sum(random_variable) call
5811 * resolves to it without colliding with the built-in NUMERIC @c sum
5812 * overloads in @c pg_catalog.
5813 *
5814 * Direct calls outside a provenance-tracked query treat each row's
5815 * contribution unconditionally (no per-row Boolean selector). When
5816 * the planner hook sees a @c provsql.sum @c Aggref over a
5817 * provenance-tracked query, it wraps the per-row argument @c x in
5818 * <tt>provsql.mixture(prov_token, x, provsql.as_random(0))</tt> so the
5819 * aggregate's effective semantics become
5820 * @f$\mathrm{SUM}(x) = \sum_i \mathbf{1}\{\varphi_i\} \cdot X_i@f$,
5821 * the natural extension of semimodule-provenance to RV-valued M.
5822 *
5823 * The internal state is the array of UUIDs of the per-row mixtures.
5824 * The final function builds a single @c gate_arith @c PLUS over them
5825 * (or returns @c as_random(0) for an empty group, the additive
5826 * identity). Sharing on @c provenance_arith's v5 hash means two
5827 * @c sum invocations over the same set of rows collide on the same
5828 * gate.
5829 *
5830 * @{
5831 */
5832
5834 * @brief Per-row helper: wrap an RV in @c mixture(prov, rv, as_random(0)).
5835 *
5836 * Internal helper used by the planner-hook rewriter to lift a
5837 * @c sum(random_variable) argument into its provenance-aware form.
5838 * Encodes one row's contribution to the SUM as a Bernoulli mixture
5839 * over the row's provenance: with probability @c P(prov) the mixture
5840 * samples @c rv, otherwise it samples the additive identity
5841 * @c as_random(0). Exposed as a regular SQL function so the planner
5842 * can construct a @c FuncExpr by name without needing to disambiguate
5843 * @c mixture / @c as_random overloads at OID-lookup time.
5844 */
5845CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
5846 prov UUID, rv random_variable)
5847 RETURNS random_variable AS
5849 SELECT provsql.mixture(prov, rv, provsql.as_random(0::double precision));
5850$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5851
5852/**
5853 * @brief Identity-parameterised per-row wrap for an RV-returning aggregate.
5854 *
5855 * Generalises the two-argument @ref rv_aggregate_semimod. The else-branch
5856 * (a row's contribution when its provenance is false) is
5857 * @c as_random(@p identity) instead of the additive @c as_random(0). The
5858 * planner-hook rewrite bakes each aggregate's own identity element into the
5859 * wrap -- @c 1 for @c product, @f$-\infty@f$ / @f$+\infty@f$ for @c max /
5860 * @c min -- so the aggregate's final function is a plain fold over the
5861 * per-row mixtures with no gate inspection. @c sum keeps the two-argument
5862 * form (@c identity @c = @c 0).
5863 */
5864CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
5865 prov UUID, rv random_variable, identity double precision)
5866 RETURNS random_variable AS
5867$$
5868 SELECT provsql.mixture(prov, rv, provsql.as_random(identity));
5869$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5870
5871/**
5872 * @brief Per-row denominator wrap for @c avg(random_variable): the
5873 * provenance indicator @f$\mathbf{1}\{\varphi\}@f$.
5874 *
5875 * The row contributes @c 1 to the running count when present and @c 0 when
5876 * absent, so @c sum over these wraps is the provenance-weighted count
5877 * @f$\sum_i \mathbf{1}\{\varphi_i\}@f$. The planner-hook rewrites
5878 * @c avg(x) into @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
5879 * @c sum(rv_aggregate_indicator(prov)) -- the "@c AVG @c = @c SUM @c /
5880 * @c COUNT" identity lifted into the @c random_variable algebra -- so
5881 * @c avg rides entirely on @c sum's fold and never inspects a gate.
5882 */
5883CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov UUID)
5884 RETURNS random_variable AS
5885$$
5886 SELECT provsql.rv_aggregate_semimod(prov, provsql.as_random(1::double precision));
5887$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
5888
5889/**
5890 * @brief Value-aware presence indicator: NULL when the row's aggregated
5891 * value is NULL.
5892 *
5893 * SQL aggregates skip NULL inputs, so a NULL @c random_variable cell must
5894 * not count in @c avg's denominator: the wrap yields NULL (which the
5895 * @c sum fold skips) exactly when the value is NULL, and the plain
5896 * one-argument indicator otherwise. The planner-hook @c avg rewrite
5897 * emits this form; the one-argument indicator remains for the internal
5898 * public-form defaults.
5899 */
5900CREATE OR REPLACE FUNCTION rv_aggregate_indicator(prov UUID, rv random_variable)
5901 RETURNS random_variable AS
5902$$
5903 SELECT CASE WHEN rv IS NULL THEN NULL
5904 ELSE provsql.rv_aggregate_indicator(prov) END;
5905$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
5906
5907/**
5908 * @brief State-transition function for @c sum(random_variable).
5909 *
5910 * Appends the input RV's UUID to the running array. NULL inputs are
5911 * skipped (matching standard SUM semantics). The aggregate's INITCOND
5912 * is @c '{}' so the FINALFUNC always runs and can tell an empty group
5913 * (state @c '{}') apart from a group whose every input was NULL -- both
5914 * of which SQL reports as @c NULL.
5915 */
5916CREATE OR REPLACE FUNCTION sum_rv_sfunc(
5917 state UUID[], rv random_variable)
5918 RETURNS UUID[] AS
5919$$
5920 SELECT CASE
5921 WHEN rv IS NULL THEN state
5922 ELSE array_append(state, (rv)::UUID)
5923 END;
5924$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
5925
5926/**
5927 * @brief Final function for @c sum(random_variable): build a
5928 * @c gate_arith PLUS root.
5930 * Empty group (@c state = @c '{}'): return @c NULL, as SQL's @c sum
5931 * does over zero rows -- the same answer the @c AGG_TOKEN path gives
5932 * for @c sum over an empty aggregation.
5933 *
5934 * Singleton group: return the single child directly without minting a
5935 * useless single-child @c gate_arith.
5936 *
5937 * Otherwise: build @c gate_arith(PLUS, state) via @c provenance_arith.
5938 */
5939CREATE OR REPLACE FUNCTION sum_rv_ffunc(state UUID[])
5940 RETURNS random_variable AS
5941$$
5942DECLARE
5943 arith_token UUID;
5944BEGIN
5945 IF state IS NULL OR array_length(state, 1) IS NULL THEN
5946 RETURN NULL;
5947 END IF;
5948 IF array_length(state, 1) = 1 THEN
5949 RETURN provsql.random_variable_make(state[1]);
5950 END IF;
5951 arith_token := provsql.provenance_arith(0, state); -- 0 = PROVSQL_ARITH_PLUS
5952 RETURN provsql.random_variable_make(arith_token);
5954$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
5955
5956CREATE AGGREGATE sum(random_variable) (
5957 SFUNC = sum_rv_sfunc,
5958 STYPE = UUID[],
5959 INITCOND = '{}',
5960 FINALFUNC = sum_rv_ffunc
5961);
5962
5963/**
5964 * @brief Numerator final function for the @c avg rewrite: @c sum,
5965 * @c NULL on an empty group.
5966 *
5967 * Behaviourally identical to @ref sum_rv_ffunc; kept as a separate
5968 * catalog entry because the @c avg rewrite names it explicitly. The
5969 * planner-hook @c avg rewrite emits
5970 * @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
5971 * @c sum(rv_aggregate_indicator(prov)); @c random_variable_div is
5972 * @c STRICT, so an empty group propagates the numerator's @c NULL and
5973 * @c avg is @c NULL -- the standard SQL @c AVG convention -- while a
5974 * non-empty group behaves exactly like @c sum.
5975 */
5976CREATE OR REPLACE FUNCTION rv_sum_or_null_ffunc(state UUID[])
5977 RETURNS random_variable AS
5978$$
5979BEGIN
5980 IF state IS NULL OR array_length(state, 1) IS NULL THEN
5981 RETURN NULL;
5982 END IF;
5983 IF array_length(state, 1) = 1 THEN
5984 RETURN provsql.random_variable_make(state[1]);
5985 END IF;
5986 RETURN provsql.random_variable_make(
5987 provsql.provenance_arith(0, state)); -- 0 = PROVSQL_ARITH_PLUS
5988END
5989$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
5990
5991CREATE AGGREGATE rv_sum_or_null(random_variable) (
5992 SFUNC = sum_rv_sfunc,
5993 STYPE = UUID[],
5994 INITCOND = '{}',
5995 FINALFUNC = rv_sum_or_null_ffunc
5997
5998/**
5999 * @brief Final function for @c avg(random_variable).
6000 *
6001 * @c avg lifts the "@c AVG @c = @c SUM @c / @c COUNT" identity into the
6002 * @c random_variable algebra:
6003 * @f[
6004 * \mathrm{AVG}(x) \;=\; \frac{\sum_i \mathbf{1}\{\varphi_i\} \cdot X_i}
6005 * {\sum_i \mathbf{1}\{\varphi_i\}}.
6006 * @f]
6007 * In a provenance-tracked query the planner-hook rewrites @c avg(x) into
6008 * @c rv_sum_or_null(rv_aggregate_semimod(prov, x)) @c /
6009 * @c sum(rv_aggregate_indicator(prov)) (see
6010 * @c make_rv_aggregate_expression), so both the numerator and the
6011 * provenance-weighted count denominator are built by @c sum's fold and no
6012 * gate is inspected. This FFUNC is therefore reached only on an
6013 * @em untracked call, where every row is unconditionally present: the
6014 * numerator is @c sum over the raw per-row RVs and the denominator is the
6015 * plain row count @c n (each row contributing @c as_random(1)).
6016 *
6017 * Empty group: returns @c NULL, matching standard SQL @c AVG (and unlike
6018 * @c sum, whose empty group is the additive identity @c as_random(0)):
6019 * the caller cannot otherwise disambiguate "0 rows" from "rows summing
6020 * to 0".
6021 */
6022CREATE OR REPLACE FUNCTION avg_rv_ffunc(state UUID[])
6023 RETURNS random_variable AS
6024$$
6025DECLARE
6026 n INTEGER;
6027 i INTEGER;
6028 num_token UUID;
6029 denom_token UUID;
6030 denom_state UUID[] := '{}';
6031 one_uuid UUID;
6032BEGIN
6033 IF state IS NULL THEN
6034 RETURN NULL;
6035 END IF;
6036 n := array_length(state, 1);
6037 IF n IS NULL THEN
6038 RETURN NULL;
6039 END IF;
6040
6041 one_uuid := (provsql.as_random(1::double precision))::UUID;
6042 FOR i IN 1..n LOOP
6043 denom_state := array_append(denom_state, one_uuid);
6044 END LOOP;
6046 IF n = 1 THEN
6047 num_token := state[1];
6048 denom_token := denom_state[1];
6049 ELSE
6050 num_token := provsql.provenance_arith(0, state); -- 0 = PLUS
6051 denom_token := provsql.provenance_arith(0, denom_state); -- 0 = PLUS
6052 END IF;
6053
6054 RETURN provsql.random_variable_make(
6055 provsql.provenance_arith(
6056 3, -- 3 = PROVSQL_ARITH_DIV
6057 ARRAY[num_token, denom_token]));
6058END
6059$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6060
6061CREATE AGGREGATE avg(random_variable) (
6062 SFUNC = sum_rv_sfunc,
6063 STYPE = UUID[],
6064 INITCOND = '{}',
6065 FINALFUNC = avg_rv_ffunc
6066);
6067
6068/**
6069 * @brief Final function for @c product(random_variable): fold a
6070 * @c gate_arith TIMES root over the per-row contributions.
6072 * Multiplicative analogue of @c sum(random_variable):
6073 * @f[
6074 * \mathrm{PRODUCT}(x) \;=\; \prod_i \big(\mathbf{1}\{\varphi_i\} \cdot X_i
6075 * + \mathbf{1}\{\neg\varphi_i\} \cdot 1\big)
6076 * \;=\; \prod_{i : \varphi_i} X_i.
6077 * @f]
6078 * Each per-row contribution already carries the multiplicative identity
6079 * as its absent-row value: a provenance-tracked query wraps the argument
6080 * as @c mixture(prov_i, X_i, as_random(1)) (identity baked in by the
6081 * three-argument @ref rv_aggregate_semimod), and an untracked call passes
6082 * the raw RV through. So the FFUNC is a plain fold with no gate
6083 * inspection: @c gate_arith(TIMES, state).
6084 *
6085 * Reuses @c sum_rv_sfunc as the state-transition function. Empty group:
6086 * @c NULL, by symmetry with @c sum / @c avg / @c min / @c max, which take
6087 * it from their standard-SQL counterparts. The multiplicative identity
6088 * @c as_random(1) stays the absent-row value inside the fold, where it
6089 * belongs; a group containing no row has no product to report.
6090 * Singleton group: the single child directly, without a one-child TIMES
6091 * root.
6092 */
6093CREATE OR REPLACE FUNCTION product_rv_ffunc(state UUID[])
6094 RETURNS random_variable AS
6095$$
6096BEGIN
6097 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6098 RETURN NULL;
6099 END IF;
6100 IF array_length(state, 1) = 1 THEN
6101 RETURN provsql.random_variable_make(state[1]);
6102 END IF;
6103 RETURN provsql.random_variable_make(
6104 provsql.provenance_arith(1, state)); -- 1 = PROVSQL_ARITH_TIMES
6105END
6106$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6107
6108CREATE AGGREGATE product(random_variable) (
6109 SFUNC = sum_rv_sfunc,
6110 STYPE = UUID[],
6111 INITCOND = '{}',
6112 FINALFUNC = product_rv_ffunc
6114
6115/**
6116 * @brief Final function for @c max(random_variable) / @c min(random_variable):
6117 * fold a @c gate_arith MAX / MIN root over the per-row contributions.
6118 *
6119 * The order-statistic analogues of @c sum / @c product:
6120 * @f[
6121 * \mathrm{MAX}(x) = \max_{i : \varphi_i} X_i, \qquad
6122 * \mathrm{MIN}(x) = \min_{i : \varphi_i} X_i.
6123 * @f]
6124 * A row absent in a world (its provenance @f$\varphi_i@f$ false) must not
6125 * perturb the extremum, so it contributes the order-statistic identity
6126 * @f$\mp\infty@f$. That identity is baked into each per-row contribution
6127 * upstream: a provenance-tracked query wraps the argument as
6128 * @c mixture(prov_i, X_i, as_random(∓∞)) (via the three-argument
6129 * @ref rv_aggregate_semimod), and an untracked call passes the raw RV
6130 * through. So the FFUNC is a plain fold with no gate inspection:
6131 * @c gate_arith(@p op, state).
6132 *
6133 * Empty group: @c NULL, as SQL's @c min / @c max report over zero rows.
6134 * @p identity belongs to the catalog signature and describes the per-row
6135 * absent contribution baked in upstream; the empty group does not consult
6136 * it, since @f$\mp\infty@f$ is an artefact of the fold rather than a value
6137 * the group actually contains.
6138 * Singleton group: the single child directly.
6139 */
6140CREATE OR REPLACE FUNCTION extremum_rv_ffunc(
6141 state UUID[], op INTEGER, identity double precision)
6142 RETURNS random_variable AS
6143$$
6144BEGIN
6145 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6146 RETURN NULL;
6147 END IF;
6148 IF array_length(state, 1) = 1 THEN
6149 RETURN provsql.random_variable_make(state[1]);
6150 END IF;
6151 RETURN provsql.random_variable_make(
6152 provsql.provenance_arith(op, state));
6154$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
6155
6156CREATE OR REPLACE FUNCTION max_rv_ffunc(state UUID[])
6157 RETURNS random_variable AS
6158$$
6159 -- 5 = PROVSQL_ARITH_MAX; empty-group / row-absent identity -inf.
6160 SELECT provsql.extremum_rv_ffunc(state, 5, '-Infinity'::double precision);
6161$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6162
6163CREATE OR REPLACE FUNCTION min_rv_ffunc(state UUID[])
6164 RETURNS random_variable AS
6165$$
6166 -- 6 = PROVSQL_ARITH_MIN; empty-group / row-absent identity +inf.
6167 SELECT provsql.extremum_rv_ffunc(state, 6, 'Infinity'::double precision);
6168$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6169
6170CREATE AGGREGATE max(random_variable) (
6171 SFUNC = sum_rv_sfunc,
6172 STYPE = UUID[],
6173 INITCOND = '{}',
6174 FINALFUNC = max_rv_ffunc
6175);
6176
6177CREATE AGGREGATE min(random_variable) (
6178 SFUNC = sum_rv_sfunc,
6179 STYPE = UUID[],
6180 INITCOND = '{}',
6181 FINALFUNC = min_rv_ffunc
6183
6184-- ---------------------------------------------------------------------
6185-- SQL-standard statistic aggregates over random_variable rows:
6186-- covar_pop / covar_samp / corr (two-argument), stddev_pop / stddev_samp
6187-- (one-argument), and the ordered-set percentile_cont.
6188--
6189-- Row presence is carried by a per-row 0/1 indicator RV: the public
6190-- aggregates use the certain indicator as_random(1) (every row present),
6191-- and a provenance-tracked query is rewritten by the planner hook
6192-- (make_rv_aggregate_expression) to the rv_*_impl aggregates whose extra
6193-- leading argument is rv_aggregate_indicator(prov), so a row absent in a
6194-- world drops out of every sum, the count, and the percentile member set.
6195-- The moment statistics are built from indicator-weighted power sums with
6196-- existing gate_arith opcodes (e.g. covar_pop = SXY/N - (SX/N)(SY/N)); a
6197-- world where the statistic is undefined (N = 0, or N = 1 for the sample
6198-- forms) evaluates to NaN, the established undefined-world convention the
6199-- moment estimators skip. percentile_cont is the one gate the arithmetic
6200-- cannot express: it mints the PROVSQL_ARITH_PERCENTILE gate_arith
6201-- (interleaved [ind_1, x_1, ...] wires, fraction in extra) that the Monte
6202-- Carlo sampler evaluates by sorting each draw's present values and
6203-- interpolating.
6204-- ---------------------------------------------------------------------
6205
6206/** @brief State transition for the one-argument RV statistic aggregates
6207 * (@c stddev_pop / @c stddev_samp): append the certain indicator and the
6208 * row's RV as a pair. NULL rows are skipped (standard SQL). */
6209CREATE OR REPLACE FUNCTION rv_stat1_sfunc(state UUID[], x random_variable)
6210 RETURNS UUID[] AS
6211$$
6212 SELECT CASE
6213 WHEN x IS NULL THEN state
6214 ELSE state || ARRAY[(provsql.as_random(1::double precision))::UUID,
6215 (x)::UUID]
6216 END;
6217$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6218
6219/** @brief State transition for the two-argument RV statistic aggregates
6220 * (@c covar_pop / @c covar_samp / @c corr): append the certain indicator
6221 * and the row's RV pair as a triple. Rows with either side NULL are
6222 * skipped (standard SQL covariance semantics). */
6223CREATE OR REPLACE FUNCTION rv_stat2_sfunc(
6224 state UUID[], x random_variable, y random_variable)
6225 RETURNS UUID[] AS
6226$$
6227 SELECT CASE
6228 WHEN x IS NULL OR y IS NULL THEN state
6229 ELSE state || ARRAY[(provsql.as_random(1::double precision))::UUID,
6230 (x)::UUID, (y)::UUID]
6231 END;
6232$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6233
6234/** @brief Indicator-carrying state transition for the one-argument
6235 * @c rv_*_impl statistic aggregates: the planner-hook rewrite passes the
6236 * row's provenance indicator @c rv_aggregate_indicator(prov) as @p ind. */
6237CREATE OR REPLACE FUNCTION rv_stat1_impl_sfunc(
6238 state UUID[], ind random_variable, x random_variable)
6239 RETURNS UUID[] AS
6240$$
6241 SELECT CASE
6242 WHEN x IS NULL THEN state
6243 ELSE state || ARRAY[coalesce((ind)::UUID,
6244 (provsql.as_random(1::double precision))::UUID),
6245 (x)::UUID]
6246 END;
6247$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6248
6249/** @brief Indicator-carrying state transition for the two-argument
6250 * @c rv_*_impl statistic aggregates. */
6251CREATE OR REPLACE FUNCTION rv_stat2_impl_sfunc(
6252 state UUID[], ind random_variable, x random_variable, y random_variable)
6253 RETURNS UUID[] AS
6255 SELECT CASE
6256 WHEN x IS NULL OR y IS NULL THEN state
6257 ELSE state || ARRAY[coalesce((ind)::UUID,
6258 (provsql.as_random(1::double precision))::UUID),
6259 (x)::UUID, (y)::UUID]
6260 END;
6261$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6263/**
6264 * @brief Mint the indicator-weighted power-sum gates shared by the
6265 * covariance / stddev final functions.
6266 *
6267 * @p state is the flat interleaved aggregate state -- pairs
6268 * @c [ind, x, ...] (@p stride 2) or triples @c [ind, x, y, ...]
6269 * (@p stride 3). Emits @c gate_arith tokens for
6270 * @f$N = \sum_i \mathbf{1}_i@f$, @f$SX = \sum_i \mathbf{1}_i x_i@f$,
6271 * @f$SXX = \sum_i \mathbf{1}_i x_i^2@f$ and, at stride 3, @f$SY@f$,
6272 * @f$SXY@f$, @f$SYY@f$. The per-row indicator gate is shared between
6273 * @f$N@f$ and every product it weighs, so the Monte Carlo per-iteration
6274 * cache keeps the row's presence coupled across all the sums (and a
6275 * repeated child @c [ind, x, x] reuses the same draw of @c x, giving
6276 * @f$x^2@f$, not two independent draws).
6277 */
6278CREATE OR REPLACE FUNCTION rv_stat_sum_tokens(
6279 state UUID[], stride INTEGER,
6280 OUT n_tok UUID, OUT sx_tok UUID, OUT sxx_tok UUID,
6281 OUT sy_tok UUID, OUT sxy_tok UUID, OUT syy_tok UUID)
6282AS
6283$$
6284DECLARE
6285 nrows INTEGER := coalesce(array_length(state, 1), 0) / stride;
6286 inds UUID[] := '{}';
6287 xs UUID[] := '{}';
6288 xxs UUID[] := '{}';
6289 ys UUID[] := '{}';
6290 xys UUID[] := '{}';
6291 yys UUID[] := '{}';
6292 ind UUID;
6293 x UUID;
6294 y UUID;
6295BEGIN
6296 FOR i IN 1..nrows LOOP
6297 ind := state[(i-1) * stride + 1];
6298 x := state[(i-1) * stride + 2];
6299 inds := array_append(inds, ind);
6300 xs := array_append(xs, provenance_arith(1, ARRAY[ind, x]));
6301 xxs := array_append(xxs, provenance_arith(1, ARRAY[ind, x, x]));
6302 IF stride = 3 THEN
6303 y := state[(i-1) * stride + 3];
6304 ys := array_append(ys, provenance_arith(1, ARRAY[ind, y]));
6305 xys := array_append(xys, provenance_arith(1, ARRAY[ind, x, y]));
6306 yys := array_append(yys, provenance_arith(1, ARRAY[ind, y, y]));
6307 END IF;
6308 END LOOP;
6309 n_tok := provenance_arith(0, inds);
6310 sx_tok := provenance_arith(0, xs);
6311 sxx_tok := provenance_arith(0, xxs);
6312 IF stride = 3 THEN
6313 sy_tok := provenance_arith(0, ys);
6314 sxy_tok := provenance_arith(0, xys);
6315 syy_tok := provenance_arith(0, yys);
6316 END IF;
6317END
6318$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6319 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6320
6321/** @brief Population-variance gate @f$SXX/N - (SX/N)^2@f$ from the
6322 * power-sum tokens. */
6323CREATE OR REPLACE FUNCTION rv_stat_var_pop_token(
6324 n_tok UUID, s_tok UUID, ss_tok UUID)
6325 RETURNS UUID AS
6326$$
6327 SELECT provsql.provenance_arith(2, ARRAY[
6328 provsql.provenance_arith(3, ARRAY[ss_tok, n_tok]),
6329 provsql.provenance_arith(1, ARRAY[
6330 provsql.provenance_arith(3, ARRAY[s_tok, n_tok]),
6331 provsql.provenance_arith(3, ARRAY[s_tok, n_tok])])]);
6332$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6333
6334/** @brief Sample-variance gate @f$(SXX - SX^2/N) / (N - 1)@f$ from the
6335 * power-sum tokens (NaN in a world with @f$N \le 1@f$, the undefined-world
6336 * convention). */
6337CREATE OR REPLACE FUNCTION rv_stat_var_samp_token(
6338 n_tok UUID, s_tok UUID, ss_tok UUID)
6339 RETURNS UUID AS
6340$$
6341 SELECT provsql.provenance_arith(3, ARRAY[
6342 provsql.provenance_arith(2, ARRAY[
6343 ss_tok,
6344 provsql.provenance_arith(3, ARRAY[
6345 provsql.provenance_arith(1, ARRAY[s_tok, s_tok]), n_tok])]),
6346 provsql.provenance_arith(2, ARRAY[
6347 n_tok, (provsql.as_random(1::double precision))::UUID])]);
6348$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6349
6350/** @brief @f$\sqrt{\max(v, 0)}@f$ gate over a variance token: the max-clamp
6351 * removes the tiny negative values float error can produce (variance is
6352 * mathematically non-negative), so the POW domain guard never fires. */
6353CREATE OR REPLACE FUNCTION rv_stat_sqrt_token(v_tok UUID)
6354 RETURNS UUID AS
6355$$
6356 SELECT provsql.provenance_arith(7, ARRAY[
6357 provsql.provenance_arith(5, ARRAY[
6358 v_tok, (provsql.as_random(0::double precision))::UUID]),
6359 (provsql.as_random(0.5::double precision))::UUID]);
6360$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6361
6362/** @brief Population-covariance gate @f$SXY/N - (SX/N)(SY/N)@f$ from the
6363 * power-sum tokens. */
6364CREATE OR REPLACE FUNCTION rv_stat_covar_pop_token(
6365 n_tok UUID, sx_tok UUID, sy_tok UUID, sxy_tok UUID)
6366 RETURNS UUID AS
6367$$
6368 SELECT provsql.provenance_arith(2, ARRAY[
6369 provsql.provenance_arith(3, ARRAY[sxy_tok, n_tok]),
6370 provsql.provenance_arith(1, ARRAY[
6371 provsql.provenance_arith(3, ARRAY[sx_tok, n_tok]),
6372 provsql.provenance_arith(3, ARRAY[sy_tok, n_tok])])]);
6373$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
6374
6375/** @brief Final function for @c covar_pop(random_variable, random_variable). */
6376CREATE OR REPLACE FUNCTION covar_pop_rv_ffunc(state UUID[])
6377 RETURNS random_variable AS
6378$$
6379DECLARE
6380 t RECORD;
6381BEGIN
6382 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6383 RETURN NULL;
6384 END IF;
6385 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
6386 RETURN random_variable_make(
6387 rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok));
6388END
6389$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6390 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6391
6392/** @brief Final function for @c covar_samp(random_variable, random_variable):
6393 * @f$(SXY - SX\,SY/N) / (N-1)@f$. */
6394CREATE OR REPLACE FUNCTION covar_samp_rv_ffunc(state UUID[])
6395 RETURNS random_variable AS
6396$$
6397DECLARE
6398 t RECORD;
6399BEGIN
6400 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6401 RETURN NULL;
6402 END IF;
6403 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
6404 RETURN random_variable_make(
6405 provenance_arith(3, ARRAY[
6406 provenance_arith(2, ARRAY[
6407 t.sxy_tok,
6408 provenance_arith(3, ARRAY[
6409 provenance_arith(1, ARRAY[t.sx_tok, t.sy_tok]), t.n_tok])]),
6410 provenance_arith(2, ARRAY[
6411 t.n_tok, (as_random(1::double precision))::UUID])]));
6412END
6413$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6414 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6415
6416/** @brief Final function for @c corr(random_variable, random_variable):
6417 * @f$\mathrm{covar\_pop} / \sqrt{\max(v_x v_y, 0)}@f$ (a zero-variance
6418 * world divides to @f$\pm\infty@f$ / NaN, the undefined-world convention,
6419 * matching SQL's NULL for a zero-stddev input). */
6420CREATE OR REPLACE FUNCTION corr_rv_ffunc(state UUID[])
6421 RETURNS random_variable AS
6422$$
6423DECLARE
6424 t RECORD;
6425 vx UUID;
6426 vy UUID;
6427BEGIN
6428 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6429 RETURN NULL;
6430 END IF;
6431 SELECT * INTO t FROM rv_stat_sum_tokens(state, 3);
6432 vx := rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok);
6433 vy := rv_stat_var_pop_token(t.n_tok, t.sy_tok, t.syy_tok);
6434 RETURN random_variable_make(
6435 provenance_arith(3, ARRAY[
6436 rv_stat_covar_pop_token(t.n_tok, t.sx_tok, t.sy_tok, t.sxy_tok),
6437 rv_stat_sqrt_token(provenance_arith(1, ARRAY[vx, vy]))]));
6438END
6439$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6440 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6441
6442/** @brief Final function for @c stddev_pop(random_variable). */
6443CREATE OR REPLACE FUNCTION stddev_pop_rv_ffunc(state UUID[])
6444 RETURNS random_variable AS
6445$$
6446DECLARE
6447 t RECORD;
6448BEGIN
6449 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6450 RETURN NULL;
6451 END IF;
6452 SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
6453 RETURN random_variable_make(
6454 rv_stat_sqrt_token(
6455 rv_stat_var_pop_token(t.n_tok, t.sx_tok, t.sxx_tok)));
6456END
6457$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6458 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6459
6460/** @brief Final function for @c stddev_samp(random_variable). */
6461CREATE OR REPLACE FUNCTION stddev_samp_rv_ffunc(state UUID[])
6462 RETURNS random_variable AS
6463$$
6464DECLARE
6465 t RECORD;
6466BEGIN
6467 IF state IS NULL OR array_length(state, 1) IS NULL THEN
6468 RETURN NULL;
6469 END IF;
6470 SELECT * INTO t FROM rv_stat_sum_tokens(state, 2);
6471 RETURN random_variable_make(
6472 rv_stat_sqrt_token(
6473 rv_stat_var_samp_token(t.n_tok, t.sx_tok, t.sxx_tok)));
6474END
6475$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6476 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6477
6478CREATE AGGREGATE covar_pop(random_variable, random_variable) (
6479 SFUNC = rv_stat2_sfunc,
6480 STYPE = UUID[],
6481 INITCOND = '{}',
6482 FINALFUNC = covar_pop_rv_ffunc
6483);
6484
6485CREATE AGGREGATE covar_samp(random_variable, random_variable) (
6486 SFUNC = rv_stat2_sfunc,
6487 STYPE = UUID[],
6488 INITCOND = '{}',
6489 FINALFUNC = covar_samp_rv_ffunc
6490);
6491
6492CREATE AGGREGATE corr(random_variable, random_variable) (
6493 SFUNC = rv_stat2_sfunc,
6494 STYPE = UUID[],
6495 INITCOND = '{}',
6496 FINALFUNC = corr_rv_ffunc
6497);
6498
6499CREATE AGGREGATE stddev_pop(random_variable) (
6500 SFUNC = rv_stat1_sfunc,
6501 STYPE = UUID[],
6502 INITCOND = '{}',
6503 FINALFUNC = stddev_pop_rv_ffunc
6504);
6505
6506CREATE AGGREGATE stddev_samp(random_variable) (
6507 SFUNC = rv_stat1_sfunc,
6508 STYPE = UUID[],
6509 INITCOND = '{}',
6510 FINALFUNC = stddev_samp_rv_ffunc
6511);
6512
6513-- The indicator-carrying rewrite targets (planner hook only; never called
6514-- directly by users).
6515
6516CREATE AGGREGATE rv_covar_pop_impl(
6517 random_variable, random_variable, random_variable) (
6518 SFUNC = rv_stat2_impl_sfunc,
6519 STYPE = UUID[],
6520 INITCOND = '{}',
6521 FINALFUNC = covar_pop_rv_ffunc
6522);
6523
6524CREATE AGGREGATE rv_covar_samp_impl(
6525 random_variable, random_variable, random_variable) (
6526 SFUNC = rv_stat2_impl_sfunc,
6527 STYPE = UUID[],
6528 INITCOND = '{}',
6529 FINALFUNC = covar_samp_rv_ffunc
6530);
6531
6532CREATE AGGREGATE rv_corr_impl(
6533 random_variable, random_variable, random_variable) (
6534 SFUNC = rv_stat2_impl_sfunc,
6535 STYPE = UUID[],
6536 INITCOND = '{}',
6537 FINALFUNC = corr_rv_ffunc
6538);
6539
6540CREATE AGGREGATE rv_stddev_pop_impl(random_variable, random_variable) (
6541 SFUNC = rv_stat1_impl_sfunc,
6542 STYPE = UUID[],
6543 INITCOND = '{}',
6544 FINALFUNC = stddev_pop_rv_ffunc
6545);
6546
6547CREATE AGGREGATE rv_stddev_samp_impl(random_variable, random_variable) (
6548 SFUNC = rv_stat1_impl_sfunc,
6549 STYPE = UUID[],
6550 INITCOND = '{}',
6551 FINALFUNC = stddev_samp_rv_ffunc
6552);
6553
6554/**
6555 * @brief Mint the @c PROVSQL_ARITH_PERCENTILE gate: the continuous
6556 * percentile (SQL @c percentile_cont) over a group of RV rows.
6557 *
6558 * @p pairs is the interleaved wire list @c [ind_1, x_1, ..., ind_n, x_n]
6559 * (each @p ind_i a 0/1 presence-indicator RV). The @p fraction is
6560 * TEXT-encoded in the gate's @c extra and participates in the token UUID
6561 * (two percentiles of the same group at different fractions are distinct
6562 * gates). Per Monte Carlo draw, the sampler collects the values whose
6563 * indicator draws 1, sorts them, and linearly interpolates at the
6564 * fraction; a draw with no present row is NaN (undefined world).
6565 */
6566CREATE OR REPLACE FUNCTION rv_percentile_make(fraction double precision,
6567 pairs UUID[])
6568 RETURNS random_variable AS
6569$$
6570DECLARE
6571 token UUID;
6572BEGIN
6573 IF fraction IS NULL THEN
6574 RETURN NULL;
6575 END IF;
6576 IF fraction < 0 OR fraction > 1 THEN
6577 RAISE EXCEPTION
6578 'percentile_cont: fraction must be between 0 and 1 (got %)', fraction;
6579 END IF;
6580 token := public.uuid_generate_v5(
6581 uuid_ns_provsql(),
6582 concat('arith', '10', pairs::TEXT, fraction::TEXT));
6583 PERFORM create_gate(token, 'arith', pairs);
6584 PERFORM set_infos(token, 10); -- 10 = PROVSQL_ARITH_PERCENTILE
6585 PERFORM set_extra(token, fraction::TEXT);
6586 RETURN random_variable_make(token);
6587END
6588$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE
6589 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
6590
6591/** @brief State transition for the public ordered-set
6592 * @c percentile_cont(float8) WITHIN GROUP (ORDER BY random_variable):
6593 * append the certain indicator and the row's RV. Only reachable on
6594 * untracked input (a provenance-tracked query is rewritten to
6595 * @c rv_percentile_impl before planning), where the sort over
6596 * @c random_variable raises the ordering-is-meaningless diagnostic
6597 * first -- so in practice this runs only for empty input. */
6598CREATE OR REPLACE FUNCTION percentile_cont_rv_sfunc(
6599 state UUID[], x random_variable)
6600 RETURNS UUID[] AS
6601$$
6602 SELECT provsql.rv_stat1_sfunc(state, x);
6603$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6604
6605/** @brief Final function for the public ordered-set @c percentile_cont:
6606 * receives the direct @p fraction argument after the state. */
6607CREATE OR REPLACE FUNCTION percentile_cont_rv_ffunc(
6608 state UUID[], fraction double precision)
6609 RETURNS random_variable AS
6610$$
6611 SELECT CASE
6612 WHEN state IS NULL OR array_length(state, 1) IS NULL THEN NULL
6613 ELSE provsql.rv_percentile_make(fraction, state)
6614 END;
6615$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6616
6617CREATE AGGREGATE percentile_cont(double precision ORDER BY random_variable) (
6618 SFUNC = percentile_cont_rv_sfunc,
6619 STYPE = UUID[],
6620 INITCOND = '{}',
6621 FINALFUNC = percentile_cont_rv_ffunc
6622);
6623
6624/** @brief Transition state for @c rv_percentile_impl: the fraction (from
6625 * the first row) plus the interleaved indicator/value token pairs. */
6626CREATE TYPE rv_percentile_state AS (
6627 fraction double precision,
6628 tokens UUID[]
6629);
6630
6631/** @brief State transition for @c rv_percentile_impl, the planner-hook
6632 * rewrite target of a provenance-tracked @c percentile_cont: stashes the
6633 * (group-constant) fraction and appends the indicator/value pair. */
6634CREATE OR REPLACE FUNCTION rv_percentile_impl_sfunc(
6635 state rv_percentile_state, fraction double precision,
6636 ind random_variable, x random_variable)
6637 RETURNS rv_percentile_state AS
6638$$
6639 SELECT ROW(
6640 coalesce((state).fraction, fraction),
6641 CASE
6642 WHEN x IS NULL THEN (state).tokens
6643 ELSE (state).tokens ||
6644 ARRAY[coalesce((ind)::UUID,
6645 (provsql.as_random(1::double precision))::UUID),
6646 (x)::UUID]
6647 END)::provsql.rv_percentile_state;
6648$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6649
6650/** @brief Final function for @c rv_percentile_impl. */
6651CREATE OR REPLACE FUNCTION rv_percentile_impl_ffunc(state rv_percentile_state)
6652 RETURNS random_variable AS
6653$$
6654 SELECT CASE
6655 WHEN state IS NULL OR array_length((state).tokens, 1) IS NULL THEN NULL
6656 ELSE provsql.rv_percentile_make((state).fraction, (state).tokens)
6657 END;
6658$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6659
6660CREATE AGGREGATE rv_percentile_impl(
6661 double precision, random_variable, random_variable) (
6662 SFUNC = rv_percentile_impl_sfunc,
6663 STYPE = rv_percentile_state,
6664 INITCOND = '(,"{}")',
6665 FINALFUNC = rv_percentile_impl_ffunc
6666);
6667
6668/** @} */
6669
6670/** @} */
6671
6672/** @} */
6673
6674/** @defgroup aggregate_provenance Aggregate provenance
6675 * Functions for building and evaluating aggregate (GROUP BY) provenance,
6676 * including the δ-semiring operator and semimodule multiplication.
6677 * @{
6678 */
6679
6680/**
6681 * @brief Create a δ-semiring gate wrapping a provenance token
6682 *
6683 * Used internally for aggregate provenance. Returns the token unchanged
6684 * if it is gate_zero() or gate_one(), and gate_one() if the token is NULL.
6685 */
6686CREATE OR REPLACE FUNCTION provenance_delta
6687 (token UUID)
6688 RETURNS UUID AS
6689$$
6690DECLARE
6691 delta_token UUID;
6692BEGIN
6693 -- NULL token ≡ 1 (untracked source), and δ(1) = 1. Tested first: the
6694 -- equality comparisons below are not NULL-safe.
6695 IF token IS NULL THEN
6696 return gate_one();
6697 END IF;
6698
6699 IF token = gate_zero() OR token = gate_one() THEN
6700 return token;
6701 END IF;
6702
6703 delta_token:=uuid_generate_v5(uuid_ns_provsql(),concat('delta',token));
6704
6705 PERFORM create_gate(delta_token,'delta',ARRAY[token::UUID]);
6706
6707 RETURN delta_token;
6708END
6709$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
6710
6711/**
6712 * @brief Build an aggregate provenance gate from grouped tokens
6713 *
6714 * Called internally by the query rewriter for GROUP BY queries.
6715 * Creates an agg gate linking all contributing tokens and records
6716 * the aggregate function OID and the computed scalar value.
6717 *
6718 * @param aggfnoid OID of the SQL aggregate function
6719 * @param aggtype OID of the aggregate result type
6720 * @param val computed aggregate value
6721 * @param tokens array of provenance tokens being aggregated
6722 * @param is_scalar true for a scalar (no GROUP BY) aggregation, whose
6723 * output row exists even when no tuple is present; stored in the
6724 * high bit of info2
6725 */
6726CREATE OR REPLACE FUNCTION provenance_aggregate(
6727 aggfnoid INTEGER,
6728 aggtype INTEGER,
6729 val ANYELEMENT,
6730 tokens UUID[],
6731 is_scalar BOOLEAN DEFAULT false)
6732 RETURNS AGG_TOKEN AS
6733$$
6734DECLARE
6735 c INTEGER;
6736 agg_tok UUID;
6737 agg_val varchar;
6738BEGIN
6739 -- Drop the NULL placeholders array_agg keeps for rows that did not produce a
6740 -- semimod gate (provenance_semimod returns NULL for a NULL aggregated value),
6741 -- so a NULL input never participates in the aggregate.
6742 tokens := array_remove(tokens, NULL);
6743 c:=COALESCE(array_length(tokens, 1), 0);
6744
6745 agg_val = CAST(val as VARCHAR);
6746
6747 IF c = 0 THEN
6748 agg_tok := gate_zero();
6749 ELSE
6750 -- aggfnoid must be part of the UUID: SUM(id) and AVG(id) over the
6751 -- same children would otherwise collapse to a single gate, and
6752 -- their concurrent set_infos calls would overwrite each other's
6753 -- aggregation operator (resulting in the wrong agg_kind being
6754 -- read by provsql_having under cross-backend contention). The
6755 -- scalar-aggregation flag must likewise be hashed: a scalar and a
6756 -- grouped aggregate over identical children carry different info2 and
6757 -- must stay distinct gates, else the concurrent set_infos calls would
6758 -- clobber the flag. The flag is stored in the high bit of info2 (the
6759 -- low 31 bits keep the result-type OID); aggtype itself is passed clean
6760 -- so the AGG_TOKEN->scalar cast still finds a valid type.
6761 agg_tok := uuid_generate_v5(
6762 uuid_ns_provsql(),
6763 concat('agg',aggfnoid,tokens,CASE WHEN is_scalar THEN 'S' ELSE '' END));
6764 PERFORM create_gate(agg_tok, 'agg', tokens);
6765 PERFORM set_infos(agg_tok, aggfnoid,
6766 CASE WHEN is_scalar THEN aggtype | (-2147483648) ELSE aggtype END);
6767 PERFORM set_extra(agg_tok, agg_val);
6768 END IF;
6769
6770 RETURN '( '||agg_tok||' , '||agg_val||' )';
6771END
6772$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE;
6773
6774/**
6775 * @brief Create a semimodule scalar multiplication gate
6776 *
6777 * Pairs a scalar value with a provenance token, used internally by
6778 * the query rewriter for aggregate provenance.
6779 *
6780 * @param val the scalar value
6781 * @param token the provenance token to multiply
6782 */
6783CREATE OR REPLACE FUNCTION provenance_semimod(val ANYELEMENT, token UUID)
6784 RETURNS UUID AS
6785$$
6786DECLARE
6787 semimod_token UUID;
6788 value_token UUID;
6789BEGIN
6790 -- A NULL value means this row does not participate in the aggregate (SQL
6791 -- aggregates ignore NULL inputs; only count(*) counts rows unconditionally,
6792 -- and it passes a constant 1 here). Produce no semimod gate so the row is
6793 -- skipped when provenance_aggregate builds the agg gate.
6794 IF val IS NULL THEN
6795 RETURN NULL;
6796 END IF;
6797
6798 SELECT uuid_generate_v5(uuid_ns_provsql(),concat('value',CAST(val AS VARCHAR)))
6799 INTO value_token;
6800 SELECT uuid_generate_v5(uuid_ns_provsql(),concat('semimod',value_token,token))
6801 INTO semimod_token;
6802
6803 --create value gates
6804 PERFORM create_gate(value_token,'value');
6805 PERFORM set_extra(value_token, CAST(val AS VARCHAR));
6806
6807 --create semimod gate
6808 PERFORM create_gate(semimod_token,'semimod',ARRAY[token::UUID,value_token]);
6809
6810 RETURN semimod_token;
6811END
6812$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE;
6813
6814/** @} */
6815
6816/** @defgroup probability Probability and Shapley values
6817 * Functions for computing probabilities, expected values, and
6818 * game-theoretic contribution measures (Shapley/Banzhaf values)
6819 * from provenance circuits.
6820 * @{
6821 */
6822
6823/**
6824 * @brief Compute the probability of a provenance token
6825 *
6826 * Compiles the provenance circuit to d-DNNF and evaluates the
6827 * probability. The compilation method can be selected explicitly.
6828 *
6829 * @ref probability() "probability" is a shorter alias bound to the same C symbol, so
6830 * @c probability(token) is exactly @c probability_evaluate(token); it is
6831 * usually preferable, and additionally carries a @c (BOOLEAN) predicate
6832 * overload (e.g. @c probability(x @c > @c y)).
6833 *
6834 * @param token provenance token to evaluate
6835 * @param method knowledge compilation method (NULL for default)
6836 * @param arguments additional arguments for the method
6837 */
6838CREATE OR REPLACE FUNCTION probability_evaluate(
6839 token UUID,
6840 method TEXT = NULL,
6841 arguments TEXT = NULL)
6842 RETURNS DOUBLE PRECISION AS
6843 'provsql','probability_evaluate' LANGUAGE C STABLE;
6844
6845/**
6846 * @brief Short alias of @ref probability_evaluate.
6847 *
6848 * Bound to the same C symbol as @ref probability_evaluate, so
6849 * @c probability(token) is exactly @c probability_evaluate(token).
6850 * Provided to match the concise polymorphic surface of @ref expected,
6851 * @ref variance, and @ref support "support": callers are not forced to
6852 * spell out @c probability_evaluate.
6853 *
6854 * @param token provenance token to evaluate
6855 * @param method knowledge compilation method (NULL for default)
6856 * @param arguments additional arguments for the method
6857 */
6858CREATE OR REPLACE FUNCTION probability(
6859 token UUID,
6860 method TEXT = NULL,
6861 arguments TEXT = NULL)
6862 RETURNS DOUBLE PRECISION AS
6863 'provsql','probability_evaluate' LANGUAGE C STABLE;
6864
6865/**
6866 * @brief Probability of a Boolean event over random variables.
6867 *
6868 * The @c (BOOLEAN) overload of @c probability lets a query ask for the
6869 * probability of an event with the natural infix grammar, e.g.
6870 * @c probability(x @c > @c y @c AND @c x @c < @c z). When the argument
6871 * carries a probabilistic (random_variable / aggregate) comparison, the
6872 * planner hook intercepts the call and rewrites it into
6873 * @c probability_evaluate over the argument's event token (a @c gate_cmp /
6874 * Boolean combination); the body below is then never reached.
6875 *
6876 * When the argument is a purely deterministic Boolean (no probabilistic
6877 * comparison) the hook leaves the call alone and the body runs, so the
6878 * probability of a definite event is simply @c 1 when it holds and @c 0 when
6879 * it does not (@c NULL propagates). This makes @c probability total over
6880 * Booleans -- @c probability(1 @c > @c 0) is @c 1, @c probability(region @c =
6881 * @c 'north') is a per-row @c 0/1 -- and it works even with
6882 * @c provsql.active off. @c NOT strict so a default-NULL @c method does not
6883 * short-circuit the cast.
6884 *
6885 * The predicate surface deliberately lives only on the short @c probability
6886 * name, not on @c probability_evaluate: a Boolean overload of the latter
6887 * would make @c probability_evaluate('<UUID-as-TEXT>') ambiguous (an unknown
6888 * literal matches both the @c UUID and the @c BOOLEAN overload), breaking
6889 * existing string-literal callers. @c probability is new, so it carries the
6890 * predicate overload without that hazard.
6891 */
6892CREATE OR REPLACE FUNCTION probability(
6893 predicate BOOLEAN,
6894 method TEXT = NULL,
6895 arguments TEXT = NULL)
6896 RETURNS DOUBLE PRECISION AS
6897$$
6898 SELECT predicate::INTEGER::double precision;
6899$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
6900
6901/**
6902 * @brief Cheap certified probability interval of a DNF-shaped circuit.
6903 *
6904 * Returns @c [lower,upper] with @c lower <= probability_evaluate(token) <=
6905 * @c upper, computed without compiling the circuit (the Olteanu-Huang d-tree
6906 * leaf bound). Errors when @p token is not a monotone DNF over input leaves.
6907 */
6908CREATE OR REPLACE FUNCTION probability_bounds(
6909 token UUID,
6910 OUT lower DOUBLE PRECISION,
6911 OUT upper DOUBLE PRECISION) AS
6912 'provsql','probability_bounds' LANGUAGE C STABLE;
6913
6914/**
6915 * @brief Compute the expected value of a probabilistic scalar
6916 *
6917 * Computes E[input | prov] for either an @c AGG_TOKEN (discrete
6918 * SUM/MIN/MAX aggregation over Boolean-input gate_agg circuits, with
6919 * @c prov as the Boolean conditioning event) or a @c random_variable
6920 * (continuous distribution, traversed by the analytical / MC
6921 * evaluator from @c Expectation.cpp).
6922 *
6923 * Implementation: thin wrapper over @c moment(input, 1, prov, method,
6924 * arguments). Both branches converge on the same machinery; the
6925 * AGG_TOKEN side computes E[X] as the @f$k=1@f$ instance of the
6926 * @f$n^k@f$-tuple enumeration in @c agg_raw_moment, the
6927 * random_variable side calls @c compute_expectation through
6928 * @c rv_moment.
6929 *
6930 * @param input aggregate expression or random variable to compute E[·] of
6931 * @param prov provenance condition (defaults to gate_one(), i.e., unconditional)
6932 * @param method knowledge compilation method (AGG_TOKEN path only)
6933 * @param arguments additional arguments for the method (AGG_TOKEN path only)
6934 */
6935CREATE OR REPLACE FUNCTION expected(
6936 input ANYELEMENT,
6937 prov UUID = gate_one(),
6938 method TEXT = NULL,
6939 arguments TEXT = NULL)
6940 RETURNS DOUBLE PRECISION AS $$
6941 SELECT moment(input, 1, prov, method, arguments);
6942$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
6943
6944/**
6945 * @brief Internal: shared C entry point for variance / moment / central_moment.
6946 *
6947 * The @c expected() SQL function reaches the Expectation evaluator
6948 * through @c provenance_evaluate_compiled(..., 'expectation', ...).
6949 * The variance / raw-moment / central-moment SQL functions need an
6950 * extra @p k INTEGER argument that does not fit that dispatcher's
6951 * signature, so they go through this dedicated entry point. Returns
6952 * E[X^k] when @p central is FALSE, or E[(X - E[X])^k] when TRUE.
6953 */
6954CREATE OR REPLACE FUNCTION rv_moment(
6955 token UUID, k INTEGER, central BOOLEAN,
6956 prov UUID DEFAULT gate_one())
6957 RETURNS double precision
6958 AS 'provsql','rv_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
6959
6960/** @brief Exact E[AVG^k | COUNT >= 1] over independent rows (the joint
6961 * (sum, count) fold); NULL when the shape is out of scope (shared
6962 * leaves, compound contributors), signalling @c agg_raw_moment's avg
6963 * arm to fall back to the Monte-Carlo scalar path. */
6964CREATE OR REPLACE FUNCTION agg_avg_moment_exact(token UUID, k INTEGER)
6965 RETURNS double precision
6966 AS 'provsql','agg_avg_moment_exact' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
6967
6968/** @brief Collapsed (Rao-Blackwellised) raw moment E[C^k] of a correlated
6969 * COUNT / SUM whose per-row selection events are coupled through a single
6970 * shared continuous latent: 1-D quadrature over the latent, closed-form
6971 * per-row CDF given it (O(G·n), exact up to the grid). NULL when the
6972 * circuit does not match the shared-latent pattern (caller falls back to
6973 * the exact n^k enumeration). k in {1, 2}. */
6974CREATE OR REPLACE FUNCTION agg_collapsed_moment(token UUID, k INTEGER)
6975 RETURNS double precision
6976 AS 'provsql','agg_collapsed_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
6977
6978/** @brief Both collapsed raw moments {E[C], E[C^2]} of a correlated COUNT / SUM
6979 * from a single circuit load and plan build; NULL when the shared-latent
6980 * pattern does not match. @c variance() uses this so a mean+variance readout
6981 * traverses the circuit once rather than calling @c agg_collapsed_moment twice
6982 * (the load and O(n) plan build dominate once the grid loop is arithmetic). */
6983CREATE OR REPLACE FUNCTION agg_collapsed_moments(token UUID)
6984 RETURNS double precision[]
6985 AS 'provsql','agg_collapsed_moments' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
6986
6987/**
6988 * @brief Boolean event "this aggregate-carrying gate's value is defined
6989 * (non-NULL) in the world".
6990 *
6991 * Backs the conditional-on-defined convention of the aggregate moment
6992 * readouts: @c sum / @c count (and constants) have a value in every
6993 * world -- the empty group is the real value @c 0 -- so their defined
6994 * event is @c gate_one(); @c min / @c max / @c avg (and any other
6995 * aggregate) are @c NULL on an empty group, so their defined event is
6996 * "some contributing row is present", the OR of the semimod children's
6997 * row tokens; a @c case gate's value is defined iff its first-match
6998 * selected branch's value is (the same region walk as the moment
6999 * evaluator, conjoined per branch). Anything else (an @c arith
7000 * composite, whose AGG_TOKEN running value is total) counts as always
7001 * defined.
7002 */
7003CREATE OR REPLACE FUNCTION agg_defined_event(token UUID)
7004 RETURNS UUID AS $$
7005DECLARE
7006 gt PROVENANCE_GATE := get_gate_type(token);
7007 fname varchar;
7008 toks UUID[];
7009 wires UUID[];
7010 nw INTEGER;
7011 m INTEGER;
7012 i INTEGER;
7013 running_neg UUID := gate_one();
7014 parts UUID[] := '{}';
7015BEGIN
7016 IF gt = 'agg' THEN
7017 SELECT proname INTO fname
7018 FROM pg_proc WHERE oid = (get_infos(token)).info1;
7019 IF fname IN ('sum', 'count') THEN
7020 RETURN gate_one();
7021 END IF;
7022 SELECT array_agg((get_children(c))[1]) INTO toks
7023 FROM unnest(get_children(token)) AS c;
7024 IF toks IS NULL THEN
7025 RETURN gate_zero(); -- structurally empty aggregate: never defined
7026 END IF;
7027 RETURN provenance_plus(toks);
7028 ELSIF gt = 'case' THEN
7029 wires := get_children(token);
7030 nw := array_length(wires, 1);
7031 m := (nw - 1) / 2;
7032 FOR i IN 1..m LOOP
7033 parts := parts || provenance_times(
7034 running_neg, wires[2 * i - 1],
7035 agg_defined_event(wires[2 * i]));
7036 running_neg := provenance_times(running_neg,
7037 provenance_not(wires[2 * i - 1]));
7038 END LOOP;
7039 parts := parts || provenance_times(running_neg,
7040 agg_defined_event(wires[nw]));
7041 RETURN provenance_plus(parts);
7042 END IF;
7043 -- value / arith / anything else: a value exists in every world.
7044 RETURN gate_one();
7045END
7046$$ LANGUAGE plpgsql STABLE STRICT PARALLEL SAFE
7047 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
7048
7049/**
7050 * @brief Compute the raw moment E[X^k | prov] of an AGG_TOKEN aggregate
7051 *
7052 * Sister of @c expected() for the AGG_TOKEN side of the polymorphic
7053 * @c moment / @c variance / @c central_moment dispatch. Supports the
7054 * same aggregation functions as @c expected: SUM (which COUNT
7055 * normalises to at the gate level via @c Aggregation.cpp:322), MIN,
7056 * MAX, and AVG (exact over independent / laminar rows via the joint
7057 * (sum, count) distribution, Monte-Carlo scalar fallback otherwise).
7058 * MIN / MAX / AVG are NULL on an empty group, so their moments are
7059 * CONDITIONAL on the aggregate being defined -- NULL only when it never
7060 * is; SUM / COUNT treat the empty world as the real value 0.
7061 *
7062 * Strategy:
7063 * - <b>SUM</b>: with X = Σᵢ Iᵢ·vᵢ (Iᵢ the per-row inclusion indicator,
7064 * vᵢ the row's value), expanding X^k and taking expectation gives
7065 * @f$E[X^k] = \sum_{(i_1,\ldots,i_k) \in \{1..n\}^k} v_{i_1}\cdots v_{i_k}
7066 * \cdot P(\bigwedge_{i \in \TEXT{distinct}(i_1..i_k)} I_i)@f$.
7067 * We enumerate the @f$n^k@f$ tuples, conjoin the distinct inclusion
7068 * tokens (and @p prov when conditioning), and evaluate the
7069 * probability via @c probability_evaluate.
7070 * - <b>MIN / MAX</b>: replace @c v with @c v^k in the rank-based
7071 * enumeration that @c expected already uses; @c MAX is handled by
7072 * sign-flipping per the existing trick (negate vs. rerank), with
7073 * the outer multiplier becoming @f$(-1)^k@f$ instead of just @f$-1@f$.
7074 *
7075 * Cost: SUM is @f$O(n^k)@f$ probability evaluations -- tractable for
7076 * small @p k or small @p n; for larger sizes, prefer reaching for the
7077 * sampler. MIN / MAX stay linear in @p n.
7078 */
7079CREATE OR REPLACE FUNCTION agg_raw_moment(
7080 token AGG_TOKEN,
7081 k INTEGER,
7082 prov UUID = gate_one(),
7083 method TEXT = NULL,
7084 arguments TEXT = NULL)
7085 RETURNS DOUBLE PRECISION AS $$
7086DECLARE
7087 aggregation_function VARCHAR;
7088 child_pairs UUID[];
7089 pair_children UUID[];
7090 n INTEGER;
7091 i INTEGER;
7092 j INTEGER;
7093 vals float8[];
7094 toks UUID[];
7095 total float8;
7096 total_probability float8;
7097 tup INTEGER[];
7098 d INTEGER;
7099 prod_v float8;
7100 distinct_tok UUID[];
7101 conj_token UUID;
7102 prob float8;
7103 sign_max float8;
7104BEGIN
7105 IF token IS NULL OR k IS NULL THEN
7106 RETURN NULL;
7107 END IF;
7108 IF k < 0 THEN
7109 RAISE EXCEPTION 'agg_raw_moment(): k must be non-negative (got %)', k;
7110 END IF;
7111
7112 -- Aggregate-carrier CASE (a gate_case over aggregate branches): a first-match
7113 -- guarded selection. The moment is CONDITIONAL on the CASE's value being
7114 -- defined (NULL only when it never is, mirroring the MIN/MAX convention):
7115 -- E[pick^k | defined ∧ prov]
7116 -- = Σ_i P(region_i ∧ def_i) · E[value_i^k | region_i ∧ def_i]
7117 -- / Σ_i P(region_i ∧ def_i),
7118 -- where region_i = (¬g_1 ∧ … ∧ ¬g_{i-1}) ∧ g_i ∧ prov is the world set that
7119 -- selects branch i (the default's region is "all guards false") and def_i is
7120 -- the branch's defined event (agg_defined_event: gate_one for sum / count /
7121 -- constants, "some row present" for min / max / avg, recursive for a nested
7122 -- CASE). Both factors are exact: probability() over the region ∧ def event,
7123 -- and the conditional aggregate moment (a recursive agg_raw_moment on the
7124 -- branch aggregate, which conditions on its own definedness within the
7125 -- region, so the two factors weigh the same worlds). The regions are
7126 -- mutually exclusive, so the terms sum with no inclusion-exclusion, and
7127 -- correlation between a guard and its branch (shared input tuples) is
7128 -- carried by the conditioning, exactly as HAVING carries it. When every
7129 -- branch is defined everywhere, the defined mass equals P(prov) and the
7130 -- formula reduces to the plain region-weighted sum.
7131 IF get_gate_type(token) = 'case' THEN
7132 IF k = 0 THEN
7133 RETURN 1;
7134 END IF;
7135 DECLARE
7136 wires UUID[] := get_children(token);
7137 nw INTEGER := array_length(get_children(token), 1);
7138 m INTEGER := (array_length(get_children(token), 1) - 1) / 2;
7139 running_neg UUID := gate_one();
7140 region_full UUID;
7141 prov_p float8;
7142 p float8;
7143 total float8 := 0;
7144 def_mass float8 := 0;
7145 ci INTEGER;
7146 vuid UUID;
7147 bm float8;
7148 BEGIN
7149 prov_p := probability(prov);
7150 IF prov_p IS NULL OR prov_p <= 0 THEN
7151 RETURN NULL; -- impossible conditioning event
7152 END IF;
7153 -- Branches 1..m are the guarded WHENs; branch m+1 is the ELSE default,
7154 -- whose region is "all guards false".
7155 FOR ci IN 1 .. m + 1 LOOP
7156 IF ci <= m THEN
7157 region_full := provenance_times(running_neg, wires[2 * ci - 1], prov);
7158 vuid := wires[2 * ci];
7159 running_neg :=
7160 provenance_times(running_neg, provenance_not(wires[2 * ci - 1]));
7161 ELSE
7162 region_full := provenance_times(running_neg, prov);
7163 vuid := wires[nw];
7164 END IF;
7165 p := probability(provenance_times(region_full,
7166 agg_defined_event(vuid)));
7167 IF p > 0 THEN
7168 -- E[value_i^k | region_i ∧ def_i]: a constant branch is a Dirac
7169 -- (c^k, exact); a single aggregate or nested CASE is exact via
7170 -- agg_raw_moment (whose MIN/MAX/CASE arms condition on their own
7171 -- definedness within the region); an arithmetic / composite branch
7172 -- takes the Monte-Carlo scalar path (which composes with the
7173 -- aggregate leaves).
7174 IF get_gate_type(vuid) = 'value' THEN
7175 bm := power(CAST(get_extra(vuid) AS float8), k);
7176 ELSIF get_gate_type(vuid) IN ('agg', 'case') THEN
7177 bm := agg_raw_moment(agg_token_make(vuid, 0), k, region_full,
7178 method, arguments);
7179 ELSE
7180 bm := rv_moment(vuid, k, false, region_full);
7181 END IF;
7182 total := total + p * bm;
7183 def_mass := def_mass + p;
7184 END IF;
7185 END LOOP;
7186 IF def_mass <= epsilon() THEN
7187 RETURN NULL; -- the CASE's value is never defined under prov
7188 END IF;
7189 RETURN total / def_mass;
7190 END;
7191 END IF;
7192
7193 IF get_gate_type(token) <> 'agg' THEN
7194 IF get_gate_type(token) IN ('arith', 'conditioned') THEN
7195 RAISE EXCEPTION 'expected / variance / moment over an arithmetic '
7196 'combination of aggregates (e.g. SUM(x) + SUM(y) or SUM(x) + 5), or a '
7197 'conditioning of one, is not yet supported: a moment can be taken only '
7198 'over a single aggregate (SUM / COUNT / MIN / MAX), optionally '
7199 'conditioned (SUM(x) | C)'
7200 USING HINT = 'Take the moment of each aggregate separately, or condition '
7201 'the bare aggregate.';
7202 ELSE
7203 RAISE EXCEPTION USING MESSAGE='Wrong gate type for agg_raw_moment computation';
7204 END IF;
7205 END IF;
7206 IF k = 0 THEN
7207 RETURN 1;
7208 END IF;
7209
7210 SELECT pp.proname::varchar FROM pg_proc pp
7211 WHERE oid=(get_infos(token)).info1
7212 INTO aggregation_function;
7213
7214 child_pairs := get_children(token);
7215 n := COALESCE(array_length(child_pairs, 1), 0);
7216
7217 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
7218 -- count(col) keeps the COUNT identity at the gate level but its value is a
7219 -- SUM of per-row 0/1 indicators, so its moments are computed exactly like
7220 -- SUM (and its empty group is the real value 0, like SUM). count(*)
7221 -- arrives here as 'sum' (it normalises to F_SUM_INT4); count(col) as 'count'.
7222 -- Trivial empty aggregation: SUM = 0, so SUM^k = 0 for k >= 1.
7223 -- Note: AGG_TOKEN semantics treat the "no row included" world as
7224 -- SUM = 0, so this stays consistent with k = 1 (= expected()).
7225 IF n = 0 THEN
7226 RETURN 0;
7227 END IF;
7228
7229 -- Collapsed fast path: a correlated COUNT / SUM whose per-row selection
7230 -- events share a single continuous latent has an O(G·n) 1-D quadrature,
7231 -- vastly cheaper than the O(n^k) tuple enumeration below (which is the
7232 -- O(n^2) pair-probability bottleneck for the variance). Only fires
7233 -- unconditionally (prov = one) and for k in {1, 2}; agg_collapsed_moment
7234 -- returns NULL when the shared-latent pattern does not match, and we
7235 -- fall through to the exact enumeration.
7236 IF prov = gate_one() AND k <= 2 THEN
7237 total := agg_collapsed_moment((token)::UUID, k);
7238 IF total IS NOT NULL THEN
7239 RETURN total;
7240 END IF;
7241 END IF;
7242
7243 -- Extract per-child token + value arrays.
7244 vals := ARRAY[]::float8[];
7245 toks := ARRAY[]::UUID[];
7246 FOR i IN 1..n LOOP
7247 pair_children := get_children(child_pairs[i]);
7248 toks := toks || pair_children[1];
7249 vals := vals || CAST(get_extra(pair_children[2]) AS float8);
7250 END LOOP;
7251
7252 -- Enumerate all k-tuples (i_1, ..., i_k) in {1..n}^k. tup is the
7253 -- current tuple; we step through them in lexicographic order.
7254 total := 0;
7255 tup := array_fill(1, ARRAY[k]);
7256 LOOP
7257 prod_v := 1;
7258 FOR j IN 1..k LOOP
7259 prod_v := prod_v * vals[tup[j]];
7260 END LOOP;
7261
7262 SELECT array_agg(DISTINCT toks[idx]) INTO distinct_tok
7263 FROM unnest(tup) AS idx;
7264
7265 IF prov <> gate_one() THEN
7266 distinct_tok := distinct_tok || prov;
7267 END IF;
7268 conj_token := provenance_times(VARIADIC distinct_tok);
7269 prob := probability_evaluate(conj_token, method, arguments);
7270
7271 total := total + prod_v * prob;
7272
7273 d := k;
7274 WHILE d >= 1 AND tup[d] = n LOOP
7275 tup[d] := 1;
7276 d := d - 1;
7277 END LOOP;
7278 EXIT WHEN d = 0;
7279 tup[d] := tup[d] + 1;
7280 END LOOP;
7281 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
7282 -- Rank enumeration: per distinct value v, P(MIN = v) is the
7283 -- probability that some t_i with v_i=v is true and all t_j with
7284 -- smaller v are false. For MAX we negate values so the same
7285 -- "smaller-than" rank logic computes MIN-of-negated, then flip.
7286 -- The outer multiplier picks up the right sign for the k-th moment
7287 -- of MAX: E[MAX^k] = (-1)^k * E[MIN(-v)^k], so sign_max = (-1)^k.
7288 sign_max := CASE
7289 WHEN aggregation_function = 'max'
7290 THEN power(-1::float8, k)
7291 ELSE 1
7292 END;
7293
7294 -- MIN/MAX over the empty input world are NULL (no elements), not ±Infinity:
7295 -- SQL returns one row with a NULL value. The moment is therefore CONDITIONAL
7296 -- on the aggregate being defined (non-empty) -- the empty world is excluded
7297 -- and the result renormalised by P(prov AND non-empty). (count, whose empty
7298 -- value 0 is a real value, keeps the empty world; sum keeps it too, as 0.)
7299 IF n = 0 THEN
7300 RETURN NULL; -- structurally empty: MIN/MAX undefined
7301 END IF;
7302
7303 -- Numerator E[MIN^k . 1{prov AND non-empty}] (the rank sum naturally omits
7304 -- the empty world, since every term requires a present token).
7305 WITH tok_value AS (
7306 SELECT (get_children(c))[1] AS tok,
7307 (CASE WHEN aggregation_function='max' THEN -1 ELSE 1 END)
7308 * CAST(get_extra((get_children(c))[2]) AS DOUBLE PRECISION) AS v
7309 FROM UNNEST(child_pairs) AS c
7310 ) SELECT sign_max * COALESCE(SUM(p * power(v, k)), 0) FROM (
7311 SELECT t1.v AS v,
7312 probability_evaluate(
7313 CASE WHEN prov = gate_one()
7314 THEN provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
7315 provenance_plus(ARRAY_AGG(t2.tok)))
7316 ELSE provenance_times(prov,
7317 provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
7318 provenance_plus(ARRAY_AGG(t2.tok)))) END,
7319 method, arguments) AS p
7320 FROM tok_value t1 LEFT OUTER JOIN tok_value t2 ON t1.v > t2.v
7321 GROUP BY t1.v) tmp
7322 INTO total;
7323
7324 -- Denominator P(prov AND non-empty) = P(prov (x) (+) tokens).
7325 SELECT probability_evaluate(
7326 CASE WHEN prov = gate_one()
7327 THEN provenance_plus(ARRAY_AGG(tok))
7328 ELSE provenance_times(prov, provenance_plus(ARRAY_AGG(tok))) END,
7329 method, arguments)
7330 FROM (SELECT (get_children(c))[1] AS tok FROM UNNEST(child_pairs) AS c) s
7331 INTO total_probability;
7332
7333 IF total_probability <= epsilon() THEN
7334 RETURN NULL; -- never defined under prov: MIN/MAX undefined
7335 END IF;
7336 RETURN total / total_probability; -- already conditional; skip generic norm
7337 ELSIF aggregation_function = 'avg' THEN
7338 -- AVG = SUM/COUNT is a ratio of two correlated world-dependent
7339 -- quantities, so the k-tuple expansion above does not apply. Like
7340 -- MIN/MAX, AVG over the empty world is NULL, so its moment conditions
7341 -- on the aggregate being defined (COUNT >= 1), NULL when it never is.
7342 -- Two routes:
7343 -- * EXACT (independent rows, unconditional): the joint (sum, count)
7344 -- PMF folded in C by agg_avg_moment_exact --
7345 -- E[AVG^k | COUNT>=1] = Σ_{(s,c), c>=1} (s/c)^k pmf(s,c) / P(c>=1).
7346 -- * Monte-Carlo scalar fallback otherwise (an outer conditioning
7347 -- event, shared leaves, compound contributors): rv_moment samples
7348 -- the agg gate per world; its NaN-skip on empty draws implements
7349 -- the same conditional-on-defined convention, at the
7350 -- provsql.rv_mc_samples budget (0 raises, per convention).
7351 IF n = 0 THEN
7352 RETURN NULL; -- structurally empty: AVG undefined
7353 END IF;
7354 IF prov = gate_one() THEN
7355 total := agg_avg_moment_exact((token)::UUID, k);
7356 IF total IS NOT NULL THEN
7357 RETURN total;
7358 END IF;
7359 END IF;
7360 RETURN rv_moment((token)::UUID, k, false, prov);
7361 ELSE
7362 RAISE EXCEPTION USING MESSAGE=
7363 'Cannot compute moment for aggregation function ' || aggregation_function;
7364 END IF;
7365
7366 -- Conditional normalisation: E[X^k · 1_A] / P(A) = E[X^k | A].
7367 IF prov <> gate_one()
7368 AND total <> 0
7369 AND total <> 'Infinity'::float8
7370 AND total <> '-Infinity'::float8 THEN
7371 total := total / probability_evaluate(prov, method, arguments);
7372 END IF;
7373
7374 RETURN total;
7375END
7376$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7377
7378/**
7379 * @brief Compute the variance Var[X | prov] of a probabilistic scalar
7380 *
7381 * Polymorphic dispatcher that mirrors @c expected: @c random_variable
7382 * inputs go through the analytical / MC evaluator
7383 * (@c rv_moment(UUID, 2, true)); @c AGG_TOKEN inputs go through the
7384 * @c agg_raw_moment helper, computing
7385 * @f$\mathrm{Var}[X|A] = E[X^2|A] - E[X|A]^2@f$. Conditioning on
7386 * @c prov is supported for @c AGG_TOKEN (matching @c expected) but
7387 * not yet for @c random_variable.
7388 */
7389CREATE OR REPLACE FUNCTION variance(
7390 input ANYELEMENT,
7391 prov UUID = gate_one(),
7392 method TEXT = NULL,
7393 arguments TEXT = NULL)
7394 RETURNS DOUBLE PRECISION AS $$
7395DECLARE
7396 m1 float8;
7397 m2 float8;
7398BEGIN
7399 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7400 IF input IS NULL THEN
7401 RETURN NULL;
7402 END IF;
7403 -- Conditioning on prov is handled inside rv_moment: when prov
7404 -- resolves to gate_one() (the default, or load-time
7405 -- simplification of any always-true sub-circuit) the
7406 -- unconditional analytical path runs unchanged; otherwise the
7407 -- joint-circuit loader unifies shared gate_rv leaves between
7408 -- input and prov, and the conditional path runs either
7409 -- truncated-distribution closed form or MC rejection.
7410 RETURN provsql.rv_moment(
7411 rv_conditioned_target((input::random_variable)::UUID), 2, true,
7412 rv_conditioned_prov((input::random_variable)::UUID, prov));
7413 END IF;
7414
7415 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
7416 IF input IS NULL THEN
7417 RETURN NULL;
7418 END IF;
7419 -- Collapsed fast path: E[C] and E[C^2] from a single circuit load and plan
7420 -- build, instead of two agg_raw_moment() calls that each reload. Mirrors
7421 -- the guard in agg_raw_moment (unconditional only, prov = one); on any
7422 -- mismatch agg_collapsed_moments returns NULL and we fall through to the
7423 -- generic per-order path (which handles conditioning, SUM enumeration, ...).
7424 IF rv_conditioned_prov(input::UUID, prov) = gate_one() THEN
7425 DECLARE ms float8[];
7426 BEGIN
7427 ms := agg_collapsed_moments(
7428 (agg_conditioned_target(input::AGG_TOKEN))::UUID);
7429 IF ms IS NOT NULL THEN
7430 RETURN ms[2] - ms[1] * ms[1];
7431 END IF;
7432 END;
7433 END IF;
7434 m1 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
7435 rv_conditioned_prov(input::UUID, prov), method, arguments);
7436 m2 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 2,
7437 rv_conditioned_prov(input::UUID, prov), method, arguments);
7438 IF m1 IS NULL OR m2 IS NULL THEN
7439 RETURN NULL;
7440 END IF;
7441 RETURN m2 - m1 * m1;
7442 END IF;
7443
7444 -- Bernoulli event token (see moment()): Var[X] = p(1 - p).
7445 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
7446 IF input IS NULL THEN
7447 RETURN NULL;
7448 END IF;
7449 m1 := provsql.probability_evaluate(provsql.cond(input::UUID, prov),
7450 method, arguments);
7451 RETURN m1 * (1 - m1);
7452 END IF;
7453
7454 RAISE EXCEPTION 'variance() is not yet supported for input type %', pg_typeof(input);
7455END
7456$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7457
7458/**
7459 * @brief Compute the raw moment E[X^k | prov] of a probabilistic scalar
7460 *
7461 * @c k must be a non-negative INTEGER. @c k = 0 returns 1; @c k = 1
7462 * is equivalent to @c expected(input). Polymorphic dispatcher: routes
7463 * @c random_variable through @c rv_moment (analytical / MC) and
7464 * @c AGG_TOKEN through @c agg_raw_moment (SUM via tuple enumeration,
7465 * MIN / MAX via rank enumeration, AVG via the joint (sum, count)
7466 * distribution over independent / laminar rows with a Monte-Carlo
7467 * fallback).
7468 */
7469CREATE OR REPLACE FUNCTION moment(
7470 input ANYELEMENT,
7471 k INTEGER,
7472 prov UUID = gate_one(),
7473 method TEXT = NULL,
7474 arguments TEXT = NULL)
7475 RETURNS DOUBLE PRECISION AS $$
7476BEGIN
7477 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7478 IF input IS NULL OR k IS NULL THEN
7479 RETURN NULL;
7480 END IF;
7481 -- See variance() above: rv_moment handles the conditional/unconditional
7482 -- dispatch internally based on the resolved prov gate type.
7483 RETURN provsql.rv_moment(
7484 rv_conditioned_target((input::random_variable)::UUID), k, false,
7485 rv_conditioned_prov((input::random_variable)::UUID, prov));
7486 END IF;
7487
7488 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
7489 RETURN agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), k,
7490 rv_conditioned_prov(input::UUID, prov), method, arguments);
7491 END IF;
7492
7493 -- A bare provenance event token (a gate_cmp lifted from an RV comparison,
7494 -- e.g. expected(x <= c)) is a Bernoulli indicator: X in {0,1}, so every raw
7495 -- moment E[X^k] with k >= 1 equals P(event), and E[X^0] = 1. cond() applies
7496 -- the optional conditioning prov (a no-op for the default gate_one()).
7497 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
7498 IF input IS NULL OR k IS NULL THEN
7499 RETURN NULL;
7500 END IF;
7501 IF k = 0 THEN
7502 RETURN 1;
7503 END IF;
7504 RETURN provsql.probability_evaluate(provsql.cond(input::UUID, prov),
7505 method, arguments);
7506 END IF;
7507
7508 RAISE EXCEPTION 'moment() is not yet supported for input type %', pg_typeof(input);
7509END
7510$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7511
7512/**
7513 * @brief Internal: rv-side quantile computation.
7514 *
7515 * C entry point behind the polymorphic @c quantile dispatcher.
7516 * Closed-form inverse CDF where the family has one (Normal via
7517 * Beasley-Springer-Moro polished by Newton steps, Uniform and
7518 * Exponential by algebraic inversion), generic monotone-CDF bisection
7519 * otherwise (Erlang, Gamma), exact generalised inverse for categorical
7520 * mixtures, and the empirical Monte Carlo quantile for compound scalar
7521 * circuits. A non-trivial @p prov conditions (truncates) the
7522 * distribution first, in closed form when the event reduces to an
7523 * interval on a bare @c gate_rv.
7524 */
7525CREATE OR REPLACE FUNCTION rv_quantile(
7526 token UUID, p double precision,
7527 prov UUID DEFAULT gate_one())
7528 RETURNS double precision
7529 AS 'provsql','rv_quantile' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7530
7531/**
7532 * @brief Compute the p-quantile (inverse CDF) of a probabilistic scalar
7533 *
7534 * @f$F^{-1}(p) = \min\{x : P(X \le x) \ge p\}@f$ for @f$p \in [0,1]@f$:
7535 * medians (@c p = 0.5), percentiles, Value-at-Risk, and credible
7536 * intervals. @c p = 0 / @c p = 1 return the (possibly infinite)
7537 * support edges. Polymorphic dispatcher mirroring @c expected /
7538 * @c moment: @c random_variable routes through @c rv_quantile
7539 * (analytical inverse CDF / MC), plain numerics are their own quantile
7540 * (a Dirac's inverse CDF is constant), and the optional @p prov
7541 * argument conditions on a provenance event, e.g.
7542 * <tt>quantile(x | (x > 0), 0.5)</tt> for the median of a truncated
7543 * distribution.
7544 */
7545CREATE OR REPLACE FUNCTION quantile(
7546 input ANYELEMENT,
7547 p double precision,
7548 prov UUID = gate_one(),
7549 method TEXT = NULL,
7550 arguments TEXT = NULL)
7551 RETURNS DOUBLE PRECISION AS $$
7552BEGIN
7553 IF p IS NULL THEN
7554 RETURN NULL;
7555 END IF;
7556 IF p <> p OR p < 0 OR p > 1 THEN
7557 RAISE EXCEPTION 'quantile: p must be in [0, 1] (got %)', p;
7558 END IF;
7559
7560 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7561 IF input IS NULL THEN
7562 RETURN NULL;
7563 END IF;
7564 -- See variance(): rv_quantile handles the conditional/unconditional
7565 -- dispatch internally based on the resolved prov gate type.
7566 RETURN provsql.rv_quantile(
7567 rv_conditioned_target((input::random_variable)::UUID), p,
7568 rv_conditioned_prov((input::random_variable)::UUID, prov));
7569 END IF;
7570
7571 IF pg_typeof(input) IN ('smallint'::REGTYPE, 'INTEGER'::REGTYPE,
7572 'bigint'::REGTYPE, 'NUMERIC'::REGTYPE,
7573 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
7574 -- A deterministic scalar is a Dirac: every quantile is the value.
7575 RETURN input::double precision;
7576 END IF;
7577
7578 RAISE EXCEPTION 'quantile() is not yet supported for input type %', pg_typeof(input);
7579END
7580$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7581
7582/**
7583 * @brief Internal: rv-side support computation
7584 *
7585 * Lifts @c provsql.compute_support out of @c RangeCheck.cpp -- the
7586 * same interval-arithmetic propagation @c runRangeCheck uses to
7587 * decide @c gate_cmps. Returns @c [-Infinity, +Infinity] when the
7588 * tightest bound is the conservative all-real interval (e.g. for a
7589 * normal RV, or any sub-circuit that mixes a normal in).
7590 */
7591CREATE OR REPLACE FUNCTION rv_support(
7592 token UUID, prov UUID DEFAULT gate_one(),
7593 OUT lo float8, OUT hi float8)
7594 AS 'provsql','rv_support' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7595
7596/**
7597 * @brief Compute the support interval @c [lo, hi] of a probabilistic
7598 * (or deterministic) scalar
7599 *
7600 * Polymorphic dispatcher mirroring @c expected / @c variance /
7601 * @c moment / @c central_moment, with two extra "free" branches:
7602 *
7603 * - <b>Plain NUMERIC</b> (@c smallint / @c INTEGER / @c bigint /
7604 * @c NUMERIC / @c real / @c double @c precision): degenerate
7605 * point support @f$[c, c]@f$. Lets callers ask for the support
7606 * of a literal without round-tripping through @c as_random.
7607 * - <b>@c random_variable / bare @c UUID</b> (any provenance gate
7608 * token; the @c random_variable branch reinterprets the value via
7609 * the binary-coercible @c random_variable @c -> @c UUID cast):
7610 * routes to @c rv_support, which propagates distribution
7611 * supports (uniform exact, exponential @c [0,+∞), normal
7612 * @c (-∞,+∞)) through @c gate_arith via interval arithmetic.
7613 * @c gate_value gives the same @f$[c, c]@f$ point support as the
7614 * NUMERIC branch; any non-scalar gate (Boolean gates, aggregates,
7615 * ...) safely falls back to the conservative all-real interval
7616 * without raising. Conditioning on @c prov is not yet supported.
7617 *
7618 * - @c AGG_TOKEN: closed-form per aggregation function:
7619 * - @c SUM : @f$[\sum_i \min(0,v_i), \sum_i \max(0,v_i)]@f$
7620 * (every row is independently in or out of the included set; the
7621 * extreme SUMs are reached by including only positive or only
7622 * negative-valued rows).
7623 * - @c MIN : @f$[\min_i v_i, \max_i v_i]@f$ in the non-empty
7624 * subsets, plus @c +Infinity if the empty subset has positive
7625 * probability under @c prov.
7626 * - @c MAX : symmetric -- @c -Infinity if empty has positive
7627 * probability under @c prov, otherwise @c min_i v_i; @c hi is
7628 * always @c max_i v_i.
7629 *
7630 * Other aggregation functions raise.
7631 *
7632 * Returns the composite RECORD @c (lo, hi) via the function's
7633 * @c OUT parameters, with @c -Infinity / @c +Infinity marking
7634 * unbounded ends.
7635 */
7636CREATE OR REPLACE FUNCTION support(
7637 input ANYELEMENT,
7638 prov UUID = gate_one(),
7639 method TEXT = NULL,
7640 arguments TEXT = NULL,
7641 OUT lo float8,
7642 OUT hi float8)
7643 AS $$
7644DECLARE
7645 aggregation_function VARCHAR;
7646 child_pairs UUID[];
7647 values_arr float8[];
7648 total_probability float8;
7649BEGIN
7650 IF input IS NULL THEN
7651 lo := NULL; hi := NULL; RETURN;
7652 END IF;
7653
7654 -- Plain NUMERIC: degenerate point support. Lets `support(2.5)` /
7655 -- `support(42)` / etc. return (2.5, 2.5) without making the user
7656 -- wrap in `as_random`.
7657 IF pg_typeof(input) IN (
7658 'smallint'::REGTYPE, 'INTEGER'::REGTYPE, 'bigint'::REGTYPE,
7659 'NUMERIC'::REGTYPE, 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
7660 lo := input::double precision;
7661 hi := input::double precision;
7662 RETURN;
7663 END IF;
7664
7665 -- random_variable is binary-coercible to UUID (explicit cast
7666 -- below), so a single rv_support call covers both shapes.
7667 -- rv_support handles
7668 -- gate_value (point), gate_rv (distribution), gate_arith
7669 -- (propagated), and falls back to the conservative all-real
7670 -- interval for any other gate kind. Conditioning on prov is not
7671 -- supported (would require restricting the underlying joint
7672 -- distribution by the indicator of prov, which has no closed form
7673 -- for the basic distributions we ship).
7674 IF pg_typeof(input) IN ('random_variable'::REGTYPE, 'UUID'::REGTYPE) THEN
7675 -- Conditional support: rv_support folds the AND-conjunct interval
7676 -- constraints from prov into the unconditional support. When
7677 -- prov is gate_one() the unconditional support is returned
7678 -- unchanged.
7679 SELECT r.lo, r.hi INTO lo, hi
7680 FROM provsql.rv_support(
7681 rv_conditioned_target(input::UUID),
7682 rv_conditioned_prov(input::UUID, prov)) r;
7683 RETURN;
7684 END IF;
7685
7686 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
7687 -- A conditioned aggregate SUM(x)|C: the value-range support is that of
7688 -- the target aggregate (conditioning can only tighten it; the
7689 -- conservative range stays valid), so unpack to the target gate.
7690 DECLARE
7691 atok AGG_TOKEN := agg_conditioned_target(input::AGG_TOKEN);
7692 BEGIN
7693 IF get_gate_type(atok) <> 'agg' THEN
7694 RAISE EXCEPTION USING MESSAGE='Wrong gate type for support computation';
7695 END IF;
7696 SELECT pp.proname::varchar FROM pg_proc pp
7697 WHERE oid=(get_infos(atok)).info1
7698 INTO aggregation_function;
7699 child_pairs := get_children(atok);
7700
7701 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
7702 -- count(col) is a SUM of per-row 0/1 indicators (empty group = 0), so its
7703 -- support is computed like SUM; count(*) arrives as 'sum'.
7704 -- Empty AGG_TOKEN: SUM is identically 0.
7705 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
7706 lo := 0; hi := 0; RETURN;
7707 END IF;
7708 SELECT sum(LEAST(v, 0::float8)), sum(GREATEST(v, 0::float8))
7709 INTO lo, hi
7710 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
7711 FROM unnest(child_pairs) AS c) sub;
7712 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
7713 -- MIN/MAX over the empty input world are NULL, not ±Infinity (matching the
7714 -- moment surface): the empty world carries no value, so the support is just
7715 -- the range of the per-row values [min(v), max(v)]. A structurally empty
7716 -- aggregate has no defined value at all -> NULL support.
7717 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
7718 lo := NULL; hi := NULL; RETURN;
7719 END IF;
7720
7721 SELECT min(v), max(v)
7722 INTO lo, hi
7723 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
7724 FROM UNNEST(child_pairs) AS c) sub;
7725 ELSE
7726 RAISE EXCEPTION USING MESSAGE=
7727 'Cannot compute support for aggregation function ' || aggregation_function;
7728 END IF;
7729 RETURN;
7730 END;
7731 END IF;
7732
7733 RAISE EXCEPTION 'support() is not yet supported for input type %', pg_typeof(input);
7734END
7735$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7736
7737/**
7738 * @brief Compute the central moment E[(X - E[X|prov])^k | prov]
7739 *
7740 * @c k = 0 returns 1; @c k = 1 returns 0; @c k = 2 is equivalent to
7741 * @c variance(input, prov, ...). Polymorphic dispatcher: routes
7742 * @c random_variable through @c rv_moment, and @c AGG_TOKEN through
7743 * the binomial expansion
7744 * @f$E[(X-\mu)^k|A] = \sum_{i=0}^{k} \binom{k}{i} (-\mu)^{k-i} E[X^i|A]@f$
7745 * with @f$\mu = E[X|A]@f$, where each @f$E[X^i|A]@f$ comes from
7746 * @c agg_raw_moment.
7747 */
7748CREATE OR REPLACE FUNCTION central_moment(
7749 input ANYELEMENT,
7750 k INTEGER,
7751 prov UUID = gate_one(),
7752 method TEXT = NULL,
7753 arguments TEXT = NULL)
7754 RETURNS DOUBLE PRECISION AS $$
7755DECLARE
7756 mu float8;
7757 total float8;
7758 i INTEGER;
7759 raw_i float8;
7760 binom float8;
7761 -- iterative binomial coefficient C(k, i)
7762 k_double float8;
7763BEGIN
7764 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
7765 IF input IS NULL OR k IS NULL THEN
7766 RETURN NULL;
7767 END IF;
7768 -- See variance() above: rv_moment handles the conditional/unconditional
7769 -- dispatch internally based on the resolved prov gate type.
7770 RETURN provsql.rv_moment(
7771 rv_conditioned_target((input::random_variable)::UUID), k, true,
7772 rv_conditioned_prov((input::random_variable)::UUID, prov));
7773 END IF;
7774
7775 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
7776 IF input IS NULL OR k IS NULL THEN
7777 RETURN NULL;
7778 END IF;
7779 IF k < 0 THEN
7780 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
7781 END IF;
7782 IF k = 0 THEN RETURN 1; END IF;
7783 IF k = 1 THEN RETURN 0; END IF;
7784
7785 mu := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
7786 rv_conditioned_prov(input::UUID, prov), method, arguments);
7787 IF mu IS NULL THEN RETURN NULL; END IF;
7788 -- mu may be ±Infinity for empty MIN / MAX with positive empty
7789 -- probability; central_moment is undefined in that case.
7790 IF mu = 'Infinity'::float8 OR mu = '-Infinity'::float8 THEN
7791 RETURN mu;
7792 END IF;
7793
7794 total := 0;
7795 binom := 1; -- C(k, 0)
7796 k_double := k;
7797 FOR i IN 0..k LOOP
7798 raw_i := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), i,
7799 rv_conditioned_prov(input::UUID, prov), method, arguments);
7800 IF raw_i IS NULL THEN RETURN NULL; END IF;
7801 total := total + binom * power(-mu, k - i) * raw_i;
7802 -- C(k, i+1) = C(k, i) * (k - i) / (i + 1)
7803 IF i < k THEN
7804 binom := binom * (k_double - i) / (i + 1);
7805 END IF;
7806 END LOOP;
7807 RETURN total;
7808 END IF;
7809
7810 -- Bernoulli event token (see moment()): with p = P(event),
7811 -- E[(X-p)^k] = (1-p)(-p)^k + p(1-p)^k; k = 0 -> 1, k = 1 -> 0.
7812 IF pg_typeof(input) = 'UUID'::REGTYPE THEN
7813 IF input IS NULL OR k IS NULL THEN
7814 RETURN NULL;
7815 END IF;
7816 IF k < 0 THEN
7817 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
7818 END IF;
7819 IF k = 0 THEN RETURN 1; END IF;
7820 IF k = 1 THEN RETURN 0; END IF;
7821 mu := provsql.probability_evaluate(provsql.cond(input::UUID, prov),
7822 method, arguments);
7823 RETURN (1 - mu) * power(-mu, k) + mu * power(1 - mu, k);
7824 END IF;
7825
7826 RAISE EXCEPTION 'central_moment() is not yet supported for input type %', pg_typeof(input);
7827END
7828$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
7829
7830/** @brief C entry point behind @ref covariance (UUID-level binding). */
7831CREATE OR REPLACE FUNCTION rv_covariance(x UUID, y UUID, prov UUID)
7832 RETURNS double precision
7833 AS 'provsql','rv_covariance' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7834
7835/**
7836 * @brief Covariance Cov(X, Y) = E[XY] − E[X]·E[Y] of two random variables.
7837 *
7838 * The bivariate readout complementing the univariate moment surface
7839 * (@ref expected / @ref variance / @ref moment / @ref central_moment).
7840 * Exact tiers: an exact @c 0 when the two arguments' stochastic-leaf
7841 * footprints are structurally independent (given @p prov), a variance
7842 * readout when the two arguments coincide, and the closed-form
7843 * @c E[XY] − E[X]·E[Y] whenever every factor decomposes analytically.
7844 * When some factor has no closed form, a SINGLE coupled Monte-Carlo pass
7845 * over the joint circuit draws @c (x, y) pairs (shared leaves produce one
7846 * draw both observe) and returns the sample covariance -- the estimator's
7847 * noise then scales with the covariance signal itself, not with the
7848 * product of the means as the naive three-run E[XY] − E[X]·E[Y]
7849 * subtraction would.
7850 *
7851 * @param x the first random variable.
7852 * @param y the second random variable.
7853 * @param prov optional conditioning event (a provenance @c UUID); the
7854 * default @c gate_one() is the unconditional covariance. Conditioning
7855 * is applied jointly: the Monte-Carlo pass rejection-samples the pair on
7856 * @p prov, giving @c Cov(X, Y | prov).
7857 */
7858CREATE OR REPLACE FUNCTION covariance(
7859 x random_variable, y random_variable, prov UUID DEFAULT gate_one())
7860 RETURNS double precision AS $$
7861 SELECT provsql.rv_covariance((x)::UUID, (y)::UUID, prov);
7862$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7863
7864/**
7865 * @brief Standard deviation σ(X) = √Var(X) of a random variable.
7866 *
7867 * A thin NUMERIC readout over @ref variance. The square root is taken on
7868 * the scalar @c double result, so no RV-level @c sqrt is involved and this
7869 * carries no dependency on RV function application (@c pow / @c sqrt).
7870 * @c NULL propagates from a @c NULL input; the order-2 central moment is
7871 * non-negative by construction, so the root is always real.
7872 *
7873 * @param x the random variable.
7874 * @param prov optional conditioning event; default @c gate_one()
7875 * (unconditional).
7876 */
7877CREATE OR REPLACE FUNCTION stddev(
7878 x random_variable, prov UUID DEFAULT gate_one())
7879 RETURNS double precision AS $$
7880 SELECT sqrt(provsql.variance(x, prov));
7881$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7882
7883/** @brief C entry point behind @ref correlation (UUID-level binding). */
7884CREATE OR REPLACE FUNCTION rv_correlation(x UUID, y UUID, prov UUID)
7885 RETURNS double precision
7886 AS 'provsql','rv_correlation' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7887
7888/**
7889 * @brief Pearson correlation ρ(X, Y) = Cov(X, Y) / (σ(X)·σ(Y)).
7890 *
7891 * Same exact tiers as @ref covariance; on the Monte-Carlo path the
7892 * covariance and BOTH standard deviations are read off the same coupled
7893 * pass, instead of stacking five independent estimates (three for the
7894 * covariance, one per standard deviation). Returns @c NULL when either
7895 * standard deviation is @c 0 (a degenerate / constant variable, for which
7896 * correlation is undefined) rather than raising a division-by-zero.
7897 *
7898 * @param x the first random variable.
7899 * @param y the second random variable.
7900 * @param prov optional conditioning event; default @c gate_one()
7901 * (unconditional).
7902 */
7903CREATE OR REPLACE FUNCTION correlation(
7904 x random_variable, y random_variable, prov UUID DEFAULT gate_one())
7905 RETURNS double precision AS $$
7906 SELECT provsql.rv_correlation((x)::UUID, (y)::UUID, prov);
7907$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7908
7909/** @brief C entry point behind @ref entropy (UUID-level binding). */
7910CREATE OR REPLACE FUNCTION rv_entropy(token UUID, prov UUID)
7911 RETURNS double precision
7912 AS 'provsql','rv_entropy' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7913
7914/**
7915 * @brief Entropy H(X) of a random variable, in nats.
7916 *
7917 * Shannon entropy for a discrete distribution (a categorical / discrete
7918 * count / constant -- a point mass has entropy @c 0), differential
7919 * entropy for a continuous one (quadrature of @c -f ln f over the
7920 * family's integration range; also exact through independent-arm
7921 * Bernoulli mixture trees such as @ref gmm's). Shapes with no
7922 * closed density (arithmetic composites) and the conditional form fall
7923 * back to a Monte Carlo histogram plug-in estimate at the
7924 * @c provsql.rv_mc_samples budget.
7925 *
7926 * @param x the random variable.
7927 * @param prov optional conditioning event; default @c gate_one()
7928 * (unconditional).
7929 */
7930CREATE OR REPLACE FUNCTION entropy(
7931 x random_variable, prov UUID DEFAULT gate_one())
7932 RETURNS double precision AS $$
7933 SELECT provsql.rv_entropy((x)::UUID, prov);
7934$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7935
7936/** @brief C entry point behind @ref kl (UUID-level binding). */
7937CREATE OR REPLACE FUNCTION rv_kl(p UUID, q UUID)
7938 RETURNS double precision
7939 AS 'provsql','rv_kl' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7940
7941/**
7942 * @brief Kullback-Leibler divergence KL(P || Q), in nats.
7943 *
7944 * Exact: the defining sum for two discrete distributions (matching
7945 * outcomes by value) and the defining integral (quadrature over P's
7946 * integration window) for two continuous ones, including
7947 * independent-arm mixture trees. Returns @c Infinity when P is not
7948 * absolutely continuous with respect to Q -- an outcome of P that Q
7949 * gives zero mass, mismatched kinds (discrete vs continuous), or a
7950 * region of P's support where Q's density (under)flows to zero. Both
7951 * arguments must resolve to closed-form densities; arithmetic
7952 * composites and conditioned variables raise.
7953 */
7954CREATE OR REPLACE FUNCTION kl(p random_variable, q random_variable)
7955 RETURNS double precision AS $$
7956 SELECT provsql.rv_kl((p)::UUID, (q)::UUID);
7957$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7958
7959/** @brief C entry point behind @ref mutual_information (UUID-level
7960 * binding). */
7961CREATE OR REPLACE FUNCTION rv_mutual_information(x UUID, y UUID)
7962 RETURNS double precision
7963 AS 'provsql','rv_mutual_information' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
7964
7965/**
7966 * @brief Mutual information I(X; Y), in nats.
7967 *
7968 * Exactly @c 0 for structurally independent variables (disjoint
7969 * stochastic-leaf footprints, the same test the moment evaluators use);
7970 * @c H(X) for a discrete variable paired with itself and @c Infinity
7971 * for a continuous one (I(X;X) diverges). A genuinely correlated pair
7972 * (shared leaves) is estimated by a 2-D histogram plug-in over coupled
7973 * joint Monte Carlo draws -- both roots evaluated against the same
7974 * per-iteration cache, so shared leaves keep their joint law -- at the
7975 * @c provsql.rv_mc_samples budget.
7976 */
7977CREATE OR REPLACE FUNCTION mutual_information(
7978 x random_variable, y random_variable)
7979 RETURNS double precision AS $$
7980 SELECT provsql.rv_mutual_information((x)::UUID, (y)::UUID);
7981$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
7982
7983/**
7984 * @brief Compute the Shapley value of an input variable
7985 *
7986 * Measures the contribution of a specific input variable to the
7987 * truth of a provenance expression, using game-theoretic Shapley values.
7988 *
7989 * @param token provenance token to evaluate
7990 * @param variable UUID of the input variable
7991 * @param method knowledge compilation method
7992 * @param arguments additional arguments for the method
7993 * @param banzhaf if true, compute the Banzhaf value instead
7994 */
7995CREATE OR REPLACE FUNCTION shapley(
7996 token UUID,
7997 variable UUID,
7998 method TEXT = NULL,
7999 arguments TEXT = NULL,
8000 banzhaf BOOLEAN = 'f')
8001 RETURNS DOUBLE PRECISION AS
8002 'provsql','shapley' LANGUAGE C STABLE;
8003
8004/** @brief Compute Shapley values for all input variables at once */
8005CREATE OR REPLACE FUNCTION shapley_all_vars(
8006 IN token UUID,
8007 IN method TEXT = NULL,
8008 IN arguments TEXT = NULL,
8009 IN banzhaf BOOLEAN = 'f',
8010 OUT variable UUID,
8011 OUT value DOUBLE PRECISION)
8012 RETURNS SETOF RECORD AS
8013 'provsql', 'shapley_all_vars'
8014 LANGUAGE C STABLE;
8015
8016/** @brief Compute the Banzhaf power index of an input variable */
8017CREATE OR REPLACE FUNCTION banzhaf(
8018 token UUID,
8019 variable UUID,
8020 method TEXT = NULL,
8021 arguments TEXT = NULL)
8022 RETURNS DOUBLE PRECISION AS
8023 $$ SELECT provsql.shapley(token, variable, method, arguments, 't') $$
8024 LANGUAGE SQL;
8025
8026/** @brief Compute Banzhaf power indices for all input variables at once */
8027CREATE OR REPLACE FUNCTION banzhaf_all_vars(
8028 IN token UUID,
8029 IN method TEXT = NULL,
8030 IN arguments TEXT = NULL,
8031 OUT variable UUID,
8032 OUT value DOUBLE PRECISION)
8033 RETURNS SETOF RECORD AS
8034 $$ SELECT * FROM provsql.shapley_all_vars(token, method, arguments, 't') $$
8035 LANGUAGE SQL;
8036
8037/**
8038 * @brief Exact reachability probability over bounded-treewidth data
8039 * (columnar form)
8040 *
8041 * Computes the probability that @p target is reachable from @p source in
8042 * the probabilistic graph given by the parallel edge arrays
8043 * (two-terminal network reliability). Unlike
8044 * @c probability_evaluate(), which compiles the provenance circuit
8045 * built along the relational query plan, this compiles the query
8046 * along a tree decomposition of the *data* graph (in the spirit of the
8047 * provenance refinement of Courcelle's theorem), producing a d-DNNF
8048 * whose size is linear in the number of edges for data of bounded
8049 * treewidth. Exact, and linear-time, on cyclic data as well -- where
8050 * the recursive-query fixpoint cannot terminate structurally.
8051 *
8052 * Edges are independent events. Two array positions may share a token
8053 * only if they are mutual reverses (the natural encoding of an
8054 * undirected edge in a directed edge relation); they are then treated
8055 * as a single bidirectional edge. This is an internal/testing surface:
8056 * the user-facing route is a plain @c WITH @c RECURSIVE reachability
8057 * query under the 'absorptive' (or 'BOOLEAN') provenance class, which
8058 * the query rewriter compiles through @c eval_reachability() /
8059 * @c reachability_materialize().
8060 *
8061 * @param sources source vertex of each edge (dense INTEGER IDs)
8062 * @param destinations destination vertex of each edge
8063 * @param tokens provenance token of each edge tuple
8064 * @param probabilities probability of each edge tuple
8065 * @param source the vertex reachability starts from
8066 * @param target the vertex whose reachability is evaluated
8067 * @param directed if false, each edge can be traversed both ways
8068 */
8069CREATE OR REPLACE FUNCTION reachability_evaluate(
8070 sources INT[],
8071 destinations INT[],
8072 tokens UUID[],
8073 probabilities DOUBLE PRECISION[],
8074 source INT,
8075 target INT,
8076 directed BOOLEAN)
8077 RETURNS DOUBLE PRECISION AS
8078 'provsql','reachability_evaluate' LANGUAGE C IMMUTABLE PARALLEL SAFE;
8079
8080/**
8081 * @brief Reachability probability plus compilation statistics
8082 * (columnar form)
8083 *
8084 * Same compilation as @c reachability_evaluate(), returning the
8085 * probability together with the structural statistics that
8086 * substantiate the bounded-treewidth guarantee: the treewidth of the
8087 * min-fill decomposition of the data graph, its number of bags, the
8088 * maximum number of dynamic-programming states at any decomposition
8089 * node, and the size of the emitted d-DNNF (linear in the number of
8090 * edges for fixed data treewidth).
8091 *
8092 * @param sources source vertex of each edge (dense INTEGER IDs)
8093 * @param destinations destination vertex of each edge
8094 * @param tokens provenance token of each edge tuple
8095 * @param probabilities probability of each edge tuple
8096 * @param source the vertex reachability starts from
8097 * @param target the vertex whose reachability is evaluated
8098 * @param directed if false, each edge can be traversed both ways
8099 * @param[out] probability the reachability probability
8100 * @param[out] data_treewidth treewidth of the min-fill decomposition of the
8101 * data graph
8102 * @param[out] nb_bags number of bags in the decomposition
8103 * @param[out] max_states maximum number of dynamic-programming states at any
8104 * decomposition node
8105 * @param[out] nb_gates number of gates in the emitted d-DNNF
8106 * @param[out] nb_variables number of variables in the emitted d-DNNF
8107 */
8108CREATE OR REPLACE FUNCTION reachability_compile_stats(
8109 IN sources INT[],
8110 IN destinations INT[],
8111 IN tokens UUID[],
8112 IN probabilities DOUBLE PRECISION[],
8113 IN source INT,
8114 IN target INT,
8115 IN directed BOOLEAN,
8116 OUT probability DOUBLE PRECISION,
8117 OUT data_treewidth INT,
8118 OUT nb_bags BIGINT,
8119 OUT max_states BIGINT,
8120 OUT nb_gates BIGINT,
8121 OUT nb_variables BIGINT)
8122 AS 'provsql','reachability_compile_stats'
8123 LANGUAGE C IMMUTABLE PARALLEL SAFE;
8124
8125
8126
8127/**
8128 * @brief Boolean UCQ probability plus compilation statistics
8129 * (columnar form, internal)
8130 *
8131 * Same compilation as @c ucq_joint_compile_stats(query jsonb, ...),
8132 * returning the probability together with the three width columns that
8133 * substantiate thesis Prop. 4.2.11 empirically -- the adversarial family
8134 * has small data and circuit widths but large joint width -- and the
8135 * structural statistics.
8136 *
8137 * @param disjunct_nvars number of query variables of each disjunct
8138 * @param atom_disjunct disjunct index of each atom (parallel to @p atom_rel)
8139 * @param atom_rel relation id of each atom
8140 * @param atom_vars query-variable indices of all atom columns, concatenated
8141 * @param atom_arity number of columns of each atom (slices @p atom_vars)
8142 * @param fact_rel relation id of each fact
8143 * @param fact_elems element ids of all fact columns, concatenated
8144 * @param fact_arity number of columns of each fact (slices @p fact_elems)
8145 * @param fact_tokens provenance token of each fact
8146 * @param fact_probs probability of each fact
8147 * @param[out] probability the exact UCQ probability
8148 * @param[out] joint_treewidth width of the min-fill decomposition found
8149 * @param[out] data_treewidth_lb degeneracy lower bound of the data-only graph
8150 * @param[out] circuit_treewidth_lb degeneracy lower bound of the slice-only graph
8151 * @param[out] n_bags number of bags in the decomposition
8152 * @param[out] max_states peak number of DP states at any node
8153 * @param[out] dd_size number of gates in the emitted d-D
8154 * @param[out] n_enumerating maximum number of essential (enumerating) query
8155 * variables over the disjuncts -- the @c e of the @f$2^{O(k^e)}@f$
8156 * bound, with variables functionally determined by others (via FDs
8157 * mined from the data) removed
8158 */
8159CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
8160 IN disjunct_nvars INT[],
8161 IN atom_disjunct INT[],
8162 IN atom_rel INT[],
8163 IN atom_vars INT[],
8164 IN atom_arity INT[],
8165 IN fact_rel INT[],
8166 IN fact_elems INT[],
8167 IN fact_arity INT[],
8168 IN fact_tokens UUID[],
8169 IN fact_probs DOUBLE PRECISION[],
8170 OUT probability DOUBLE PRECISION,
8171 OUT joint_treewidth INT,
8172 OUT data_treewidth_lb INT,
8173 OUT circuit_treewidth_lb INT,
8174 OUT n_bags BIGINT,
8175 OUT max_states BIGINT,
8176 OUT dd_size BIGINT,
8177 OUT n_enumerating INT)
8178 AS 'provsql','ucq_joint_compile_stats'
8179 LANGUAGE C IMMUTABLE PARALLEL SAFE;
8180
8181
8182/**
8183 * @brief Boolean UCQ probability plus statistics from a JSON specification
8184 *
8185 * JSON-spec wrapper over the columnar @c ucq_joint_compile_stats()
8186 * (see @c ucq_joint_evaluate(query jsonb, ...) for the JSON format).
8187 */
8188CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
8189 IN query JSONB,
8190 IN fact_rel INT[],
8191 IN fact_elems INT[],
8192 IN fact_arity INT[],
8193 IN fact_tokens UUID[],
8194 IN fact_probs DOUBLE PRECISION[],
8195 OUT probability DOUBLE PRECISION,
8196 OUT joint_treewidth INT,
8197 OUT data_treewidth_lb INT,
8198 OUT circuit_treewidth_lb INT,
8199 OUT n_bags BIGINT,
8200 OUT max_states BIGINT,
8201 OUT dd_size BIGINT,
8202 OUT n_enumerating INT)
8203 AS $$
8204DECLARE
8205 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
8206 avars INT[] := '{}'; aarity INT[] := '{}';
8207 d JSONB; a JSONB; v TEXT; didx INT := 0;
8208BEGIN
8209 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
8210 dnv := dnv || (d->>'n_vars')::INT;
8211 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8212 adisj := adisj || didx;
8213 arel := arel || (a->>'rel')::INT;
8214 aarity := aarity || jsonb_array_length(a->'vars');
8215 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8216 avars := avars || v::INT;
8217 END LOOP;
8218 END LOOP;
8219 didx := didx + 1;
8220 END LOOP;
8221 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
8222 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
8223 s.n_enumerating
8224 INTO probability, joint_treewidth, data_treewidth_lb,
8225 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
8226 FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
8227 fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s;
8228END;
8229$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
8230
8231
8232
8233
8234
8235
8236
8237
8238
8239
8240
8241/**
8242 * @brief Correlated Boolean UCQ probability plus compilation statistics
8243 * (columnar form, internal)
8244 *
8245 * Same compilation as @c ucq_joint_evaluate_tracked(); the three width
8246 * columns substantiate thesis Prop. 4.2.11 on real correlated data (the
8247 * data-only and circuit-only degeneracy bounds can be small while the
8248 * joint width is large).
8249 */
8250CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
8251 IN disjunct_nvars INT[],
8252 IN atom_disjunct INT[],
8253 IN atom_rel INT[],
8254 IN atom_vars INT[],
8255 IN atom_arity INT[],
8256 IN fact_rel INT[],
8257 IN fact_elems INT[],
8258 IN fact_arity INT[],
8259 IN fact_tokens UUID[],
8260 OUT probability DOUBLE PRECISION,
8261 OUT joint_treewidth INT,
8262 OUT data_treewidth_lb INT,
8263 OUT circuit_treewidth_lb INT,
8264 OUT n_bags BIGINT,
8265 OUT max_states BIGINT,
8266 OUT dd_size BIGINT,
8267 OUT n_enumerating INT)
8268 AS 'provsql','ucq_joint_compile_stats_tracked'
8269 LANGUAGE C STABLE PARALLEL SAFE;
8270
8271
8272/**
8273 * @brief Correlated Boolean UCQ probability plus statistics from a JSON spec
8274 */
8275CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
8276 IN query JSONB,
8277 IN fact_rel INT[],
8278 IN fact_elems INT[],
8279 IN fact_arity INT[],
8280 IN fact_tokens UUID[],
8281 OUT probability DOUBLE PRECISION,
8282 OUT joint_treewidth INT,
8283 OUT data_treewidth_lb INT,
8284 OUT circuit_treewidth_lb INT,
8285 OUT n_bags BIGINT,
8286 OUT max_states BIGINT,
8287 OUT dd_size BIGINT,
8288 OUT n_enumerating INT)
8289 AS $$
8290DECLARE
8291 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
8292 avars INT[] := '{}'; aarity INT[] := '{}';
8293 d JSONB; a JSONB; v TEXT; didx INT := 0;
8294BEGIN
8295 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
8296 dnv := dnv || (d->>'n_vars')::INT;
8297 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8298 adisj := adisj || didx;
8299 arel := arel || (a->>'rel')::INT;
8300 aarity := aarity || jsonb_array_length(a->'vars');
8301 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8302 avars := avars || v::INT;
8303 END LOOP;
8304 END LOOP;
8305 didx := didx + 1;
8306 END LOOP;
8307 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
8308 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
8309 s.n_enumerating
8310 INTO probability, joint_treewidth, data_treewidth_lb,
8311 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
8312 FROM ucq_joint_compile_stats_tracked(dnv, adisj, arel, avars, aarity,
8313 fact_rel, fact_elems, fact_arity, fact_tokens) s;
8314END;
8315$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
8316
8317/**
8318 * @brief Compile a correlated UCQ and materialise its certified d-D,
8319 * returning the root provenance token (columnar form, internal)
8320 *
8321 * The architecturally-primary route: the compiler builds the
8322 * deterministic, decomposable circuit and materialises it as ordinary
8323 * @c plus / @c times / @c monus provenance gates (carrying the d-DNNF
8324 * certificate); the answer is then obtained through the standard entry
8325 * points on the returned token -- @c probability_evaluate(token),
8326 * @c shapley(token, ...), expectation -- so the joint-width path shares
8327 * the one evaluation pipeline. The token is the exact Boolean
8328 * provenance of the UCQ (no @c 'absorptive' marker).
8329 */
8330CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
8331 disjunct_nvars INT[],
8332 atom_disjunct INT[],
8333 atom_rel INT[],
8334 atom_vars INT[],
8335 atom_arity INT[],
8336 fact_rel INT[],
8337 fact_elems INT[],
8338 fact_arity INT[],
8339 fact_tokens UUID[])
8340 RETURNS UUID AS
8341 'provsql','ucq_joint_materialize_tracked' LANGUAGE C VOLATILE;
8342
8343/**
8344 * @brief Compile a correlated UCQ and materialise its certified d-D
8345 * from a JSON spec, returning the root provenance token
8346 *
8347 * JSON-spec wrapper over @c ucq_joint_materialize_tracked(). Evaluate
8348 * the answer with the standard surface, e.g.
8349 * @c probability_evaluate(ucq_joint_materialize_tracked(query, ...)).
8350 */
8351CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
8352 query JSONB,
8353 fact_rel INT[],
8354 fact_elems INT[],
8355 fact_arity INT[],
8356 fact_tokens UUID[])
8357 RETURNS UUID AS $$
8358DECLARE
8359 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
8360 avars INT[] := '{}'; aarity INT[] := '{}';
8361 d JSONB; a JSONB; v TEXT; didx INT := 0;
8362BEGIN
8363 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
8364 dnv := dnv || (d->>'n_vars')::INT;
8365 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8366 adisj := adisj || didx;
8367 arel := arel || (a->>'rel')::INT;
8368 aarity := aarity || jsonb_array_length(a->'vars');
8369 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8370 avars := avars || v::INT;
8371 END LOOP;
8372 END LOOP;
8373 didx := didx + 1;
8374 END LOOP;
8375 RETURN ucq_joint_materialize_tracked(dnv, adisj, arel, avars, aarity,
8376 fact_rel, fact_elems, fact_arity, fact_tokens);
8377END;
8378$$ LANGUAGE plpgsql VOLATILE;
8379
8380/**
8381 * @brief Compile a UCQ over named relations into a materialised certified
8382 * d-D, gathering the facts from the store -- the descriptor-driven engine
8383 *
8384 * The query-surface bridge for the joint-width compiler: instead of
8385 * hand-built columnar arrays, a JSON @p descriptor names the relations
8386 * and how their columns map to query variables, and this function
8387 * gathers the facts itself (the provenance rewriting is disabled around
8388 * the gather), builds the value-based element dictionary shared across
8389 * the relations (so equal join values get the same dense id), compiles
8390 * and materialises the certified d-D, and returns its provenance token.
8391 * The answer is then any standard evaluation on that token --
8392 * @c probability_evaluate(ucq_joint_provenance(...)),
8393 * @c shapley(...), expectation. This is also the engine the planner-time
8394 * query recogniser drives once it builds the descriptor from a query's
8395 * abstract syntax.
8396 *
8397 * Descriptor shape:
8398 * @verbatim
8399 * { "disjuncts": [ { "n_vars": k,
8400 * "atoms": [ {"rel": <relidx>, "vars": [..]}, ... ] }, ... ],
8401 * "relations": [ "schema.r", "schema.s", ... ], -- relidx -> relation
8402 * "elem_cols": [ ["x"], ["x","y"], ... ] } -- per relation: the
8403 * element columns, in
8404 * the atom's var order
8405 * @endverbatim
8406 *
8407 * @param descriptor the UCQ + the relations and their element columns
8408 * @param fallback token returned if the joint-width compiler declines
8409 * @return the materialised joint-width provenance token (NULL UUID-free
8410 * exact Boolean provenance of the UCQ)
8411 */
8412CREATE OR REPLACE FUNCTION ucq_joint_provenance(
8413 descriptor JSONB, fallback UUID DEFAULT NULL)
8414RETURNS UUID AS $$
8415DECLARE
8416 legs TEXT; sql TEXT; saved TEXT;
8417 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
8418 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
8419 avars INT[]:='{}'; aarity INT[]:='{}';
8420 d jsonb; a jsonb; v TEXT; didx INT:=0;
8421BEGIN
8422 -- Parse the UCQ structure into the columnar query arrays.
8423 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
8424 dnv := dnv || (d->>'n_vars')::INT;
8425 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8426 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
8427 aarity := aarity || jsonb_array_length(a->'vars');
8428 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8429 avars := avars || v::INT;
8430 END LOOP;
8431 END LOOP;
8432 didx := didx + 1;
8433 END LOOP;
8434
8435 -- One UNION ALL leg per relation: (relation index, TEXT element array,
8436 -- provenance token). No temp tables: a single gather query, with the
8437 -- value-based dense element dictionary built inline.
8438 SELECT string_agg(
8439 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
8440 rn - 1,
8441 (SELECT string_agg(format('(%I)::TEXT', c), ',')
8442 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
8443 rel,
8444 -- the lifted single-relation selection (a pre-filter), already
8445 -- deparsed to SQL by the recogniser; '' / absent = unfiltered.
8446 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
8447 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
8448 ELSE '' END),
8449 ' UNION ALL ')
8450 INTO legs
8451 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
8452
8453 sql := format($q$
8454 WITH facts(rel,elems,tok) AS (%s),
8455 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
8456 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
8457 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
8458 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
8459 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
8460 (SELECT array_agg(tok ORDER BY ord) FROM ord),
8461 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
8462 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
8463 JOIN dict dd ON dd.val = e.val)
8464 $q$, legs);
8465
8466 -- Read the raw rows with provenance rewriting disabled (we only read
8467 -- the existing provsql column; this internal gather is not tracked).
8468 saved := current_setting('provsql.active', true);
8469 PERFORM set_config('provsql.active','off', true);
8470 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
8471 PERFORM set_config('provsql.active', saved, true);
8472
8473 RETURN ucq_joint_materialize_tracked(dnv,adisj,arel,avars,aarity,
8474 fact_rel,fact_elems,fact_arity,fact_tokens);
8475EXCEPTION WHEN OTHERS THEN
8476 -- The joint-width compiler declined (unsupported gate type, joint
8477 -- width too large, ...): fall back to the normal provenance so the
8478 -- query never fails. Both give the same probability.
8479 RETURN fallback;
8480END;
8481$$ LANGUAGE plpgsql VOLATILE;
8482
8483-- ===========================================================================
8484-- Safe-UCQ Möbius-inversion route (mobius_evaluate.cpp).
8485--
8486-- The last missing exact route of the Dalvi-Suciu dichotomy: UCQs that are
8487-- safe only because the \#P-hard terms of their inclusion-exclusion expansion
8488-- carry a zero Möbius value on the CNF lattice and cancel (canonical witness:
8489-- QW / q9). Same TID gather as ucq_joint, then the lattice-walking compiler
8490-- materialises a gate_mobius-rooted circuit (a signed combination over
8491-- certified-independent islands), answered in PTIME data complexity by the
8492-- standard probability path.
8493-- ===========================================================================
8494
8495/**
8496 * @brief Materialise the safe-UCQ Möbius circuit and return its root token.
8497 * Columnar (TID) interface; see ucq_mobius_provenance for the gather.
8498 */
8499CREATE OR REPLACE FUNCTION ucq_mobius_materialize_tracked(
8500 disjunct_nvars INT[],
8501 atom_disjunct INT[],
8502 atom_rel INT[],
8503 atom_vars INT[],
8504 atom_arity INT[],
8505 fact_rel INT[],
8506 fact_elems INT[],
8507 fact_arity INT[],
8508 fact_tokens UUID[],
8509 lineage UUID DEFAULT NULL)
8510 RETURNS UUID AS
8511 'provsql','ucq_mobius_materialize_tracked' LANGUAGE C VOLATILE;
8512
8513/**
8514 * @brief Compile the Möbius circuit and return the lattice statistics plus the
8515 * probability (the demonstrability surface). @c cancelled_hard is the
8516 * single number that makes the mechanism legible: for q9 the 1 cancelled
8517 * element is \#P-hard, so the query is easy only because its hard part
8518 * cancels.
8519 */
8520CREATE OR REPLACE FUNCTION ucq_mobius_compile_stats(
8521 IN disjunct_nvars INT[],
8522 IN atom_disjunct INT[],
8523 IN atom_rel INT[],
8524 IN atom_vars INT[],
8525 IN atom_arity INT[],
8526 IN fact_rel INT[],
8527 IN fact_elems INT[],
8528 IN fact_arity INT[],
8529 IN fact_tokens UUID[],
8530 OUT probability DOUBLE PRECISION,
8531 OUT n_components INT,
8532 OUT n_cnf_conjuncts INT,
8533 OUT lattice_size INT,
8534 OUT n_nonzero INT,
8535 OUT n_cancelled INT,
8536 OUT cancelled_hard BOOLEAN,
8537 OUT dd_size BIGINT,
8538 OUT memo_hits BIGINT)
8539 AS 'provsql','ucq_mobius_compile_stats'
8540 LANGUAGE C VOLATILE;
8541
8542/**
8543 * @brief Pass a token through iff it is a @c gate_mobius, else return NULL.
8544 *
8545 * The Möbius-precedence dispatch (see @c make_provenance_expression) wraps the
8546 * Möbius call in this and then @c COALESCE\ s it before the joint-width call:
8547 * a Möbius *success* always roots a @c gate_mobius (the compiler wraps even a
8548 * thin selector around the lineage), so it short-circuits and the joint-width
8549 * compiler never runs; a Möbius *decline* returns the literal lineage (never a
8550 * @c gate_mobius), so this yields NULL and @c COALESCE falls through to
8551 * joint-width. The lineage token is a plain plus/times/input, so the test is
8552 * unambiguous.
8553 */
8554CREATE OR REPLACE FUNCTION mobius_or_null(tok UUID)
8555RETURNS UUID AS $$
8556 SELECT CASE WHEN tok IS NOT NULL AND provsql.get_gate_type(tok) = 'mobius'
8557 THEN tok END
8558$$ LANGUAGE sql STABLE;
8559
8560/**
8561 * @brief Möbius-route provenance from a descriptor (the planner-substituted
8562 * entry point, and the manual one). Same descriptor and TID gather as
8563 * @c ucq_joint_provenance; on any decline (unsafe shape, cap, not TID)
8564 * returns @p fallback, so a recognised query never fails.
8565 */
8566CREATE OR REPLACE FUNCTION ucq_mobius_provenance(
8567 descriptor JSONB, fallback UUID DEFAULT NULL)
8568RETURNS UUID AS $$
8569DECLARE
8570 legs TEXT; sql TEXT; saved TEXT;
8571 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
8572 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
8573 avars INT[]:='{}'; aarity INT[]:='{}';
8574 d jsonb; a jsonb; v TEXT; didx INT:=0;
8575BEGIN
8576 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
8577 dnv := dnv || (d->>'n_vars')::INT;
8578 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8579 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
8580 aarity := aarity || jsonb_array_length(a->'vars');
8581 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8582 avars := avars || v::INT;
8583 END LOOP;
8584 END LOOP;
8585 didx := didx + 1;
8586 END LOOP;
8587
8588 SELECT string_agg(
8589 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
8590 rn - 1,
8591 (SELECT string_agg(format('(%I)::TEXT', c), ',')
8592 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
8593 rel,
8594 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
8595 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
8596 ELSE '' END),
8597 ' UNION ALL ')
8598 INTO legs
8599 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
8600
8601 sql := format($q$
8602 WITH facts(rel,elems,tok) AS (%s),
8603 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
8604 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
8605 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
8606 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
8607 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
8608 (SELECT array_agg(tok ORDER BY ord) FROM ord),
8609 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
8610 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
8611 JOIN dict dd ON dd.val = e.val)
8612 $q$, legs);
8613
8614 saved := current_setting('provsql.active', true);
8615 PERFORM set_config('provsql.active','off', true);
8616 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
8617 PERFORM set_config('provsql.active', saved, true);
8618
8619 -- Pass the normal-provenance fallback as the lineage: it is carried on the
8620 -- gate_mobius so the token still answers Shapley / semiring / PROV on the
8621 -- literal lineage (the Möbius combination is a probability-only shortcut).
8622 RETURN ucq_mobius_materialize_tracked(dnv,adisj,arel,avars,aarity,
8623 fact_rel,fact_elems,fact_arity,fact_tokens, fallback);
8624EXCEPTION WHEN OTHERS THEN
8625 RETURN fallback;
8626END;
8627$$ LANGUAGE plpgsql VOLATILE;
8628
8629/**
8630 * @brief Möbius lattice statistics + probability from a descriptor: the
8631 * demonstrability SRF. Gathers
8632 * the same TID facts as @c ucq_mobius_provenance, then runs the columnar
8633 * @c ucq_mobius_compile_stats.
8634 */
8635CREATE OR REPLACE FUNCTION mobius_compile_stats(
8636 IN descriptor JSONB,
8637 OUT probability DOUBLE PRECISION,
8638 OUT n_components INT,
8639 OUT n_cnf_conjuncts INT,
8640 OUT lattice_size INT,
8641 OUT n_nonzero INT,
8642 OUT n_cancelled INT,
8643 OUT cancelled_hard BOOLEAN,
8644 OUT dd_size BIGINT,
8645 OUT memo_hits BIGINT)
8646RETURNS RECORD AS $$
8647DECLARE
8648 legs TEXT; sql TEXT; saved TEXT;
8649 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
8650 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
8651 avars INT[]:='{}'; aarity INT[]:='{}';
8652 d jsonb; a jsonb; v TEXT; didx INT:=0;
8653BEGIN
8654 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
8655 dnv := dnv || (d->>'n_vars')::INT;
8656 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8657 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
8658 aarity := aarity || jsonb_array_length(a->'vars');
8659 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8660 avars := avars || v::INT;
8661 END LOOP;
8662 END LOOP;
8663 didx := didx + 1;
8664 END LOOP;
8665
8666 SELECT string_agg(
8667 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
8668 rn - 1,
8669 (SELECT string_agg(format('(%I)::TEXT', c), ',')
8670 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
8671 rel,
8672 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
8673 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
8674 ELSE '' END),
8675 ' UNION ALL ')
8676 INTO legs
8677 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
8678
8679 sql := format($q$
8680 WITH facts(rel,elems,tok) AS (%s),
8681 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
8682 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
8683 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
8684 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
8685 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
8686 (SELECT array_agg(tok ORDER BY ord) FROM ord),
8687 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
8688 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
8689 JOIN dict dd ON dd.val = e.val)
8690 $q$, legs);
8691
8692 saved := current_setting('provsql.active', true);
8693 PERFORM set_config('provsql.active','off', true);
8694 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
8695 PERFORM set_config('provsql.active', saved, true);
8696
8697 SELECT s.probability, s.n_components, s.n_cnf_conjuncts, s.lattice_size,
8698 s.n_nonzero, s.n_cancelled, s.cancelled_hard, s.dd_size, s.memo_hits
8699 INTO probability, n_components, n_cnf_conjuncts, lattice_size,
8700 n_nonzero, n_cancelled, cancelled_hard, dd_size, memo_hits
8701 FROM ucq_mobius_compile_stats(dnv,adisj,arel,avars,aarity,
8702 fact_rel,fact_elems,fact_arity,fact_tokens) s;
8703END;
8704$$ LANGUAGE plpgsql VOLATILE;
8705
8706/**
8707 * @brief Internal gather for the per-answer joint route: parse @p descriptor
8708 * into the columnar UCQ arrays and gather every fact (relation index,
8709 * dense element ids, provenance token) with the value dictionary.
8710 *
8711 * Used only by the planner-substituted @c ucq_joint_provenance_answer (the C
8712 * single-DP entry point), which calls it ONCE per query and then computes all
8713 * answers in one sweep. No head pinning: the single DP discovers the answers.
8714 * @c val_by_id maps a dense element id back to its TEXT value (so an answer's
8715 * head ids can be matched to the @c GROUP @c BY head TEXT).
8716 */
8717CREATE OR REPLACE FUNCTION ucq_joint_gather(
8718 descriptor JSONB,
8719 OUT disjunct_nvars INT[], OUT atom_disjunct INT[], OUT atom_rel INT[],
8720 OUT atom_vars INT[], OUT atom_arity INT[],
8721 OUT fact_rel INT[], OUT fact_elems INT[], OUT fact_arity INT[],
8722 OUT fact_tokens UUID[], OUT val_by_id TEXT[])
8723AS $$
8724DECLARE
8725 legs TEXT; sql TEXT; saved TEXT; d jsonb; a jsonb; v TEXT; didx INT := 0;
8726BEGIN
8727 disjunct_nvars:='{}'; atom_disjunct:='{}'; atom_rel:='{}';
8728 atom_vars:='{}'; atom_arity:='{}';
8729 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
8730 disjunct_nvars := disjunct_nvars || (d->>'n_vars')::INT;
8731 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
8732 atom_disjunct := atom_disjunct || didx;
8733 atom_rel := atom_rel || (a->>'rel')::INT;
8734 atom_arity := atom_arity || jsonb_array_length(a->'vars');
8735 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
8736 atom_vars := atom_vars || v::INT;
8737 END LOOP;
8738 END LOOP;
8739 didx := didx + 1;
8740 END LOOP;
8741
8742 SELECT string_agg(
8743 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s', rn - 1,
8744 (SELECT string_agg(format('(%I)::TEXT', c), ',')
8745 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
8746 rel,
8747 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
8748 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
8749 ELSE '' END),
8750 ' UNION ALL ')
8751 INTO legs
8752 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
8753
8754 sql := format($q$
8755 WITH facts(rel,elems,tok) AS (%s),
8756 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
8757 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
8758 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
8759 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
8760 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
8761 (SELECT array_agg(tok ORDER BY ord) FROM ord),
8762 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
8763 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
8764 JOIN dict dd ON dd.val = e.val),
8765 (SELECT array_agg(val ORDER BY id) FROM dict)
8766 $q$, legs);
8767
8768 saved := current_setting('provsql.active', true);
8769 PERFORM set_config('provsql.active','off', true);
8770 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems, val_by_id;
8771 PERFORM set_config('provsql.active', saved, true);
8772END;
8773$$ LANGUAGE plpgsql VOLATILE;
8774
8775/**
8776 * @brief Per-answer joint-width provenance via the TOP-DOWN single DP
8777 * (planner-substituted, C).
8778 *
8779 * The transparent per-answer rewrite substitutes one call per output group.
8780 * On the FIRST call of a query the function gathers the facts once
8781 * (@c ucq_joint_gather), runs the single DP, and materialises EVERY answer's
8782 * certified d-D into the store, caching @c head_vals -> token in @c fn_extra;
8783 * each subsequent group call is an O(1) lookup -- so the whole GROUP BY costs
8784 * one gather + one decomposition + one sweep, not @p k of each. On any
8785 * decline (joint width too large) the @p fallback token (the normal
8786 * per-answer provenance) is returned, so the query never fails. The answer's
8787 * marginal / Shapley / expectation is then the standard evaluation on the
8788 * returned token -- one pipeline for the whole system.
8789 */
8790CREATE OR REPLACE FUNCTION ucq_joint_provenance_answer(
8791 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
8792RETURNS UUID AS 'provsql','ucq_joint_provenance_answer'
8793LANGUAGE C STABLE;
8794
8795/**
8796 * @brief Per-answer safe-UCQ Möbius provenance (planner-substituted): one
8797 * head-pinned Möbius circuit per output group. On the first call the
8798 * facts are gathered once (ucq_joint_gather) and cached; each group pins
8799 * @p head_vars to @p head_vals and compiles, caching head -> token. On
8800 * any decline returns @p fallback. STABLE: it caches per fn-call
8801 * context, so it is not re-evaluated within one scan.
8802 */
8803CREATE OR REPLACE FUNCTION ucq_mobius_provenance_answer(
8804 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
8805RETURNS UUID AS 'provsql','ucq_mobius_provenance_answer'
8806LANGUAGE C STABLE;
8807
8808
8809/**
8810 * @brief Compile and materialise the reachability provenance of every
8811 * vertex (columnar form, internal)
8812 *
8813 * All-targets variant of @c reachability_evaluate(): compiles, along a
8814 * tree decomposition of the data graph, one certified provenance
8815 * circuit per vertex reachable from some source in the all-edges-present
8816 * world, materialises the (shared, linear-size) circuits in the
8817 * provenance store -- @c plus / @c times gates carrying the d-DNNF
8818 * certificate, negated edges as @c monus(one, edge) -- and returns one
8819 * @c (vertex, token) row per such vertex. Sources form a possibly
8820 * *probabilistic source set*: each source arc is gated by the source
8821 * tuple's token, the nil UUID marking a certain (always present)
8822 * source. This is the engine behind the rewriter's
8823 * recursive-reachability route; the returned tokens are ordinary
8824 * provenance tokens usable with the whole evaluation surface, wrapped
8825 * in the 'absorptive' assumption marker (the compiled circuit is the
8826 * exact Boolean lineage but only the absorptive quotient of the
8827 * infinite recursive semiring provenance: probability and absorptive
8828 * semiring evaluations -- e.g. nonnegative min-plus -- are exact,
8829 * counting and why-provenance refuse).
8830 *
8831 * @param sources source vertex of each edge (dense INTEGER IDs)
8832 * @param destinations destination vertex of each edge
8833 * @param tokens provenance token of each edge tuple
8834 * @param probabilities probability of each edge tuple
8835 * @param block_keys per-edge BID key variable (nil UUID = independent
8836 * tuple; alternatives sharing a key are mutually exclusive, e.g.
8837 * from repair_key)
8838 * @param block_indices per-edge outcome index within its block
8839 * @param source_vertices the source vertices
8840 * @param source_tokens per-source provenance token (nil UUID = certain)
8841 * @param source_probabilities per-source probability
8842 * @param directed if false, each edge can be traversed both ways
8843 * @param[out] vertex a vertex reachable from some source
8844 * @param[out] token the materialised reachability provenance token of @c vertex
8845 */
8846CREATE OR REPLACE FUNCTION reachability_materialize(
8847 IN sources INT[],
8848 IN destinations INT[],
8849 IN tokens UUID[],
8850 IN probabilities DOUBLE PRECISION[],
8851 IN block_keys UUID[],
8852 IN block_indices INT[],
8853 IN source_vertices INT[],
8854 IN source_tokens UUID[],
8855 IN source_probabilities DOUBLE PRECISION[],
8856 IN directed BOOLEAN,
8857 OUT vertex INT,
8858 OUT token UUID)
8859 RETURNS SETOF RECORD AS
8860 'provsql','reachability_materialize' LANGUAGE C VOLATILE;
8861
8862
8863/**
8864 * @brief Bounded-hop variant of @c reachability_materialize() (internal)
8865 *
8866 * Compiles, along a tree decomposition of the data graph, one certified
8867 * provenance circuit per (vertex, walk length) pair achievable within
8868 * @p hop_bound edges -- the rows a hop-counting recursive CTE derives,
8869 * row @c (v,h) meaning "some *walk* of exactly @c h edges connects a
8870 * present source to @c v" -- and returns them as @c (vertex, hops,
8871 * token) with @p hop_seed added to the lengths (the CTE base arm's hop
8872 * constant). Also pre-creates, per vertex, the certified gate that a
8873 * hop-discarding query's deduplication will address, wired to the
8874 * compilation's native within-bound root, so the natural "within k
8875 * hops" probability evaluates through the linear certified route.
8876 *
8877 * @param sources source vertex of each edge (dense INTEGER IDs)
8878 * @param destinations destination vertex of each edge
8879 * @param tokens provenance token of each edge tuple
8880 * @param probabilities probability of each edge tuple
8881 * @param block_keys per-edge BID key variable (nil UUID = independent)
8882 * @param block_indices per-edge outcome index within its block
8883 * @param source_vertices the source vertices
8884 * @param source_tokens per-source provenance token (nil UUID = certain)
8885 * @param source_probabilities per-source probability
8886 * @param directed if false, each edge can be traversed both ways
8887 * @param hop_bound maximum walk length
8888 * @param hop_seed hop value of the base arm (added to reported lengths)
8889 * @param[out] vertex a reachable vertex
8890 * @param[out] hops the walk length at which @c vertex is reached
8891 * @param[out] token the materialised provenance token of the @c (vertex, hops) pair
8892 */
8893CREATE OR REPLACE FUNCTION reachability_materialize_hops(
8894 IN sources INT[],
8895 IN destinations INT[],
8896 IN tokens UUID[],
8897 IN probabilities DOUBLE PRECISION[],
8898 IN block_keys UUID[],
8899 IN block_indices INT[],
8900 IN source_vertices INT[],
8901 IN source_tokens UUID[],
8902 IN source_probabilities DOUBLE PRECISION[],
8903 IN directed BOOLEAN,
8904 IN hop_bound INT,
8905 IN hop_seed INT,
8906 OUT vertex INT,
8907 OUT hops INT,
8908 OUT token UUID)
8909 RETURNS SETOF RECORD AS
8910 'provsql','reachability_materialize_hops' LANGUAGE C VOLATILE;
8911
8912
8913/**
8914 * @brief Per-group "some member reachable" compilation (columnar form,
8915 * internal)
8916 *
8917 * For each distinct group in the parallel @p group_ids /
8918 * @p member_vertices arrays, compiles the certified circuit of "some
8919 * member vertex is reachable from a present source" along the data
8920 * decomposition -- the disjunction over the group's *correlated*
8921 * per-vertex reachability events, deterministic by construction
8922 * through the set-reachability state bit -- materialises it, and
8923 * returns one @c (group_id, token) row per group. Engine behind the
8924 * rewriter's cross-vertex aggregation planting.
8925 *
8926 * @param sources source vertex of each edge (dense INTEGER IDs)
8927 * @param destinations destination vertex of each edge
8928 * @param tokens provenance token of each edge tuple
8929 * @param probabilities probability of each edge tuple
8930 * @param block_keys per-edge BID key variable (nil UUID = independent)
8931 * @param block_indices per-edge outcome index within its block
8932 * @param source_vertices the source vertices
8933 * @param source_tokens per-source provenance token (nil UUID = certain)
8934 * @param source_probabilities per-source probability
8935 * @param directed if false, each edge can be traversed both ways
8936 * @param group_ids group identifier of each member row
8937 * @param member_vertices member vertex of each member row
8938 * @param[out] group_id a group whose every member is reachable
8939 * @param[out] token the materialised all-members-reachable provenance token of
8940 * @c group_id
8941 */
8942CREATE OR REPLACE FUNCTION reachability_materialize_any(
8943 IN sources INT[],
8944 IN destinations INT[],
8945 IN tokens UUID[],
8946 IN probabilities DOUBLE PRECISION[],
8947 IN block_keys UUID[],
8948 IN block_indices INT[],
8949 IN source_vertices INT[],
8950 IN source_tokens UUID[],
8951 IN source_probabilities DOUBLE PRECISION[],
8952 IN directed BOOLEAN,
8953 IN group_ids INT[],
8954 IN member_vertices INT[],
8955 OUT group_id INT,
8956 OUT token UUID)
8957 RETURNS SETOF RECORD AS
8958 'provsql','reachability_materialize_any' LANGUAGE C VOLATILE;
8959
8960/**
8961 * @brief Compile and materialise the "every member vertex reachable"
8962 * (k-terminal / coverage) circuit (columnar form, internal)
8963 *
8964 * Arguments as @c reachability_materialize_any() with a single member
8965 * set: compiles the certified circuit of "every member vertex is
8966 * reachable from a present source" -- the conjunction over the
8967 * members' *correlated* per-vertex events, deterministic by
8968 * construction through the pending rescuer-set congruence --
8969 * materialises it, and returns its token, wrapped in the
8970 * @c 'absorptive' assumption marker. Probability evaluation gives the
8971 * k-terminal reliability; nonnegative min-plus the cost of the
8972 * cheapest covering subgraph (directed Steiner cost), shared edges
8973 * paid once. A member vertex absent from the graph is unreachable:
8974 * the circuit is then constant false.
8975 *
8976 * @param sources source vertex of each edge (dense INTEGER IDs)
8977 * @param destinations destination vertex of each edge
8978 * @param tokens provenance token of each edge tuple
8979 * @param probabilities probability of each edge tuple
8980 * @param block_keys per-edge BID key variable (nil UUID = independent)
8981 * @param block_indices per-edge outcome index within its block
8982 * @param source_vertices the source vertices
8983 * @param source_tokens per-source provenance token (nil UUID = certain)
8984 * @param source_probabilities per-source probability
8985 * @param directed if false, each edge can be traversed both ways
8986 * @param member_vertices the member vertices (dense IDs)
8987 */
8988CREATE OR REPLACE FUNCTION reachability_materialize_cover(
8989 sources INT[],
8990 destinations INT[],
8991 tokens UUID[],
8992 probabilities DOUBLE PRECISION[],
8993 block_keys UUID[],
8994 block_indices INT[],
8995 source_vertices INT[],
8996 source_tokens UUID[],
8997 source_probabilities DOUBLE PRECISION[],
8998 directed BOOLEAN,
8999 member_vertices INT[])
9000 RETURNS UUID AS
9001 'provsql','reachability_materialize_cover' LANGUAGE C VOLATILE;
9002
9003/**
9004 * @brief Plant certified any-member-reachable gates for a grouped
9005 * reachability aggregation (internal)
9006 *
9007 * Called (at plan time, over SPI) by the recursive-CTE lowering when
9008 * the outer query aggregates a reachability working table by a column
9009 * of a joined, untracked member relation: @c GROUP @c BY collapses
9010 * each group's per-vertex reach tokens with @c provenance_plus, whose
9011 * disjuncts are correlated (they share edges) and would otherwise
9012 * leave the certified route. For each multi-member group this
9013 * pre-creates, at the canonical address of the group's token multiset,
9014 * a certified single-child plus over the group's native
9015 * any-member-reachable circuit (@c reachability_materialize_any), so
9016 * the natural aggregation stays on the linear evaluation route.
9017 * Best-effort: any failure leaves the generic path untouched (notice
9018 * under verbosity 10).
9019 *
9020 * @param work_name the lowered CTE's working table
9021 * @param node_attribute its vertex column
9022 * @param member_rel the joined member relation (must be untracked)
9023 * @param member_attribute the member relation's join column
9024 * @param group_attribute the member relation's grouping column
9025 * @param edge_rel the tracked edge relation (as for eval_reachability)
9026 * @param source_attribute name of the source-vertex column
9027 * @param destination_attribute name of the destination-vertex column
9028 * @param source_value the base arm's constant, as TEXT
9029 * @param directed if false, each edge can be traversed both ways
9030 * @param edge_quals optional deterministic filter over edge columns
9031 * @param source_rel source relation of a multi-source base arm
9032 * @param source_rel_attribute the source relation's vertex column
9033 * @param edge_sql deparsed edge subquery (join-defined edges)
9034 * @param member_quals optional deterministic filter over the member
9035 * relation's columns (table-qualified as @c t.column), restricting
9036 * which members participate in each group
9037 */
9038CREATE OR REPLACE FUNCTION plant_reach_any_groups(
9039 work_name TEXT,
9040 node_attribute TEXT,
9041 member_rel REGCLASS,
9042 member_attribute TEXT,
9043 group_attribute TEXT,
9044 edge_rel REGCLASS,
9045 source_attribute TEXT,
9046 destination_attribute TEXT,
9047 source_value TEXT,
9048 directed BOOLEAN,
9049 edge_quals TEXT DEFAULT NULL,
9050 source_rel REGCLASS DEFAULT NULL,
9051 source_rel_attribute TEXT DEFAULT NULL,
9052 edge_sql TEXT DEFAULT NULL,
9053 member_quals TEXT DEFAULT NULL)
9054 RETURNS VOID AS
9055$$
9056DECLARE
9057 e RECORD;
9058 grp RECORD;
9059 m RECORD;
9060 sv TEXT[];
9061 st UUID[];
9062 sp double precision[];
9063 gids INT[] := ARRAY[]::INT[];
9064 mids INT[] := ARRAY[]::INT[];
9065 vid INT;
9066 canonical UUID;
9067 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9068BEGIN
9069 BEGIN
9070 -- A tracked member relation would make the aggregated tokens
9071 -- per-row products, not the bare reach tokens: nothing to plant.
9072 IF EXISTS (SELECT 1 FROM pg_attribute
9073 WHERE attrelid = member_rel AND attname = 'provsql'
9074 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped) THEN
9075 RETURN;
9076 END IF;
9077
9078 IF source_rel IS NOT NULL THEN
9079 SELECT g.source_values, g.source_tokens, g.source_probabilities
9080 INTO sv, st, sp
9081 FROM provsql.gather_reachability_sources(source_rel,
9082 source_rel_attribute) g;
9083 IF sv IS NULL THEN
9084 sv := ARRAY[]::TEXT[];
9085 st := ARRAY[]::UUID[];
9086 sp := ARRAY[]::float8[];
9087 END IF;
9088 ELSE
9089 sv := ARRAY[source_value];
9090 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
9091 sp := ARRAY[1.0::float8];
9092 END IF;
9093
9094 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
9095 destination_attribute,
9096 sv, edge_quals, edge_sql);
9097
9098 -- The groups, replicating the user's join semantics: per group, the
9099 -- member vertices and the multiset of their reach tokens (with the
9100 -- multiplicity the join produces). Single-member groups need no
9101 -- planting (provenance_plus passes a single token through).
9102 -- Two steps: materialise the joined rows with their per-row tokens
9103 -- (tracked CTAS, then strip the automatic provsql column), and only
9104 -- then aggregate the now-plain table -- aggregating provenance()
9105 -- inside a grouped tracked query would be rewritten as a
9106 -- provenance-aware aggregation, which is not what the planting
9107 -- needs.
9108 DROP TABLE IF EXISTS provsql_reach_any_flat_tmp;
9109 EXECUTE format(
9110 'CREATE TEMP TABLE provsql_reach_any_flat_tmp AS '
9111 || 'SELECT w.%1$I::TEXT AS node_val, provsql.provenance() AS tok, '
9112 || ' t.%5$I AS grp_key '
9113 || 'FROM %2$I w JOIN %3$s t ON w.%1$I = t.%4$I'
9114 -- The member-relation filter restricts which members participate
9115 -- (deparsed table-qualified as t.column); the working table side
9116 -- carries no provenance distinction here.
9117 || coalesce(' WHERE ' || member_quals, ''),
9118 node_attribute, work_name, member_rel::TEXT, member_attribute,
9119 group_attribute);
9120 PERFORM provsql.remove_provenance('provsql_reach_any_flat_tmp');
9121 DROP TABLE IF EXISTS provsql_reach_any_groups_tmp;
9122 CREATE TEMP TABLE provsql_reach_any_groups_tmp AS
9123 SELECT (row_number() OVER ())::INT AS gid, members, toks FROM (
9124 SELECT array_agg(node_val) AS members, array_agg(tok) AS toks
9125 FROM provsql_reach_any_flat_tmp
9126 GROUP BY grp_key HAVING count(*) >= 2) g;
9127 DROP TABLE provsql_reach_any_flat_tmp;
9128
9129 FOR grp IN SELECT gid, members FROM provsql_reach_any_groups_tmp LOOP
9130 FOR m IN SELECT DISTINCT unnest(grp.members) AS val LOOP
9131 vid := array_position(e.vertices, m.val);
9132 IF vid IS NOT NULL THEN
9133 gids := gids || grp.gid;
9134 mids := mids || vid;
9135 END IF;
9136 END LOOP;
9137 END LOOP;
9138 IF cardinality(gids) = 0 THEN
9139 DROP TABLE provsql_reach_any_groups_tmp;
9140 RETURN;
9141 END IF;
9142
9143 FOR grp IN
9144 SELECT a.group_id, a.token AS any_token, t.toks
9145 FROM provsql.reachability_materialize_any(
9146 e.sources, e.destinations, e.tokens, e.probabilities,
9147 e.block_keys, e.block_indices, e.extra_ids, st, sp,
9148 directed, gids, mids) a
9149 JOIN provsql_reach_any_groups_tmp t ON t.gid = a.group_id
9150 LOOP
9151 canonical := public.uuid_generate_v5(
9152 provsql.uuid_ns_provsql(),
9153 concat('plus-canonical',
9154 (SELECT array_agg(tok ORDER BY tok)
9155 FROM unnest(grp.toks) tok)));
9156 PERFORM provsql.create_gate(canonical, 'plus', ARRAY[grp.any_token]);
9157 PERFORM provsql.set_infos(canonical, 1);
9158 END LOOP;
9159 DROP TABLE provsql_reach_any_groups_tmp;
9160 IF verbosity >= 20 THEN
9161 -- Lift the function-level client_min_messages = warning for the
9162 -- one RAISE; the function-level SET restores the caller's value.
9163 PERFORM set_config('client_min_messages', 'notice', true);
9164 RAISE NOTICE 'ProvSQL: certified any-member gates planted for the aggregation of "%" by %.%',
9165 work_name, member_rel, group_attribute;
9166 PERFORM set_config('client_min_messages', 'warning', true);
9167 END IF;
9168 EXCEPTION WHEN OTHERS THEN
9169 IF verbosity >= 10 THEN
9170 PERFORM set_config('client_min_messages', 'notice', true);
9171 RAISE NOTICE 'ProvSQL: any-member planting for "%" skipped (%)',
9172 work_name, SQLERRM;
9173 PERFORM set_config('client_min_messages', 'warning', true);
9174 END IF;
9175 END;
9176END
9177-- No SET search_path: the deparsed edge subquery must resolve against
9178-- the caller's path; ProvSQL internals are schema-qualified.
9179$$ LANGUAGE plpgsql SET client_min_messages = warning;
9180
9181/**
9182 * @brief Plant the certified all-members-reachable gate for a
9183 * reachability self-join conjunction (internal)
9184 *
9185 * Called (at plan time, over SPI) by the recursive-CTE lowering when
9186 * the outer query self-joins a reachability working table with one
9187 * constant node binding per reference -- "are these k vertices all
9188 * reachable" -- whose row provenance @c provenance_times() computes as
9189 * the product of *correlated* per-vertex reach tokens (they share
9190 * edges). This pre-creates, at the times-canonical address of that
9191 * token multiset, a certified single-child times over the native
9192 * all-members-reachable circuit (@c reachability_materialize_cover),
9193 * so the natural conjunction stays on the linear certified route --
9194 * with the joint-worlds semantics: probability evaluation gives the
9195 * k-terminal reliability, and nonnegative min-plus the cost of the
9196 * cheapest covering subgraph (directed Steiner cost), shared edges
9197 * paid once where the raw product would pay them once per factor.
9198 * Best-effort: any failure leaves the generic path untouched (notice
9199 * under verbosity 10).
9200 *
9201 * @param work_name the lowered CTE's working table
9202 * @param node_attribute its vertex column
9203 * @param edge_rel the tracked edge relation (as for eval_reachability)
9204 * @param source_attribute name of the source-vertex column
9205 * @param destination_attribute name of the destination-vertex column
9206 * @param source_value the base arm's constant, as TEXT
9207 * @param directed if false, each edge can be traversed both ways
9208 * @param node_values the constant node bindings, as TEXT (multiset:
9209 * one per self-join reference)
9210 * @param edge_quals optional deterministic filter over edge columns
9211 * @param source_rel source relation of a multi-source base arm
9212 * @param source_rel_attribute the source relation's vertex column
9213 * @param edge_sql deparsed edge subquery (join-defined edges)
9214 */
9215CREATE OR REPLACE FUNCTION plant_reach_cover(
9216 work_name TEXT,
9217 node_attribute TEXT,
9218 edge_rel REGCLASS,
9219 source_attribute TEXT,
9220 destination_attribute TEXT,
9221 source_value TEXT,
9222 directed BOOLEAN,
9223 node_values TEXT[],
9224 edge_quals TEXT DEFAULT NULL,
9225 source_rel REGCLASS DEFAULT NULL,
9226 source_rel_attribute TEXT DEFAULT NULL,
9227 edge_sql TEXT DEFAULT NULL)
9228 RETURNS VOID AS
9229$$
9230DECLARE
9231 e RECORD;
9232 sv TEXT[];
9233 st UUID[];
9234 sp double precision[];
9235 val TEXT;
9236 vid INT;
9237 vids INT[] := ARRAY[]::INT[];
9238 tok UUID;
9239 toks UUID[] := ARRAY[]::UUID[];
9240 cover_token UUID;
9241 canonical UUID;
9242 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9243BEGIN
9244 BEGIN
9245 IF source_rel IS NOT NULL THEN
9246 SELECT g.source_values, g.source_tokens, g.source_probabilities
9247 INTO sv, st, sp
9248 FROM provsql.gather_reachability_sources(source_rel,
9249 source_rel_attribute) g;
9250 IF sv IS NULL THEN
9251 sv := ARRAY[]::TEXT[];
9252 st := ARRAY[]::UUID[];
9253 sp := ARRAY[]::float8[];
9254 END IF;
9255 ELSE
9256 sv := ARRAY[source_value];
9257 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
9258 sp := ARRAY[1.0::float8];
9259 END IF;
9260
9261 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
9262 destination_attribute,
9263 sv, edge_quals, edge_sql);
9264
9265 -- The bound vertices and their per-row reach tokens, with the
9266 -- multiplicity the self-join produces. A vertex absent from the
9267 -- graph, or from the working table, means the join is empty: no
9268 -- row will exist, nothing to plant.
9269 FOREACH val IN ARRAY node_values LOOP
9270 vid := array_position(e.vertices, val);
9271 IF vid IS NULL THEN
9272 RETURN;
9273 END IF;
9274 vids := vids || vid;
9275 EXECUTE format('SELECT provsql FROM %I WHERE %I::TEXT = $1',
9276 work_name, node_attribute)
9277 INTO tok USING val;
9278 IF tok IS NULL THEN
9279 RETURN;
9280 END IF;
9281 toks := toks || tok;
9282 END LOOP;
9283
9284 cover_token := provsql.reachability_materialize_cover(
9285 e.sources, e.destinations, e.tokens, e.probabilities,
9286 e.block_keys, e.block_indices, e.extra_ids, st, sp,
9287 directed, vids);
9288
9289 SELECT public.uuid_generate_v5(
9290 provsql.uuid_ns_provsql(),
9291 concat('times-canonical', array_agg(t ORDER BY t)))
9292 FROM unnest(toks) t
9293 INTO canonical;
9294 PERFORM provsql.create_gate(canonical, 'times', ARRAY[cover_token]);
9295 PERFORM provsql.set_infos(canonical, 1);
9296 IF verbosity >= 20 THEN
9297 -- Lift the function-level client_min_messages = warning for the
9298 -- one RAISE; the function-level SET restores the caller's value.
9299 PERFORM set_config('client_min_messages', 'notice', true);
9300 RAISE NOTICE 'ProvSQL: certified all-members gate planted for the self-join of "%"',
9301 work_name;
9302 PERFORM set_config('client_min_messages', 'warning', true);
9303 END IF;
9304 EXCEPTION WHEN OTHERS THEN
9305 IF verbosity >= 10 THEN
9306 PERFORM set_config('client_min_messages', 'notice', true);
9307 RAISE NOTICE 'ProvSQL: all-members planting for "%" skipped (%)',
9308 work_name, SQLERRM;
9309 PERFORM set_config('client_min_messages', 'warning', true);
9310 END IF;
9311 END;
9312END
9313-- No SET search_path: the deparsed edge subquery must resolve against
9314-- the caller's path; ProvSQL internals are schema-qualified.
9315$$ LANGUAGE plpgsql SET client_min_messages = warning;
9316
9317/**
9318 * @brief Input leaves of a conjunction-shaped provenance token (internal)
9319 *
9320 * Descends a token's circuit through the conjunctive gate types
9321 * (@c times, and the pass-through @c project / @c eq where-provenance
9322 * wrappers) down to @c input leaves. Returns the distinct leaves, or
9323 * NULL when the circuit contains any other gate type (a disjunctive or
9324 * aggregate shape, which is not a conjunction of independent tuples).
9325 * Used by the reachability gathering to accept join-defined edges:
9326 * a derived edge whose token is a pure conjunction of base tuples.
9327 *
9328 * @param token the provenance token
9329 */
9330CREATE OR REPLACE FUNCTION token_conjunctive_leaves(token UUID)
9331 RETURNS UUID[] AS
9332$$
9333WITH RECURSIVE walk(g) AS (
9334 SELECT token
9335 UNION
9336 SELECT c FROM walk w, unnest(provsql.get_children(w.g)) AS c
9337 WHERE provsql.get_gate_type(w.g) IN ('times', 'project', 'eq', 'annotation')
9338)
9339SELECT CASE WHEN bool_and(provsql.get_gate_type(g)
9340 IN ('times', 'project', 'eq', 'annotation', 'input'))
9341 THEN array_agg(DISTINCT g)
9342 FILTER (WHERE provsql.get_gate_type(g) = 'input')
9343 ELSE NULL END
9344FROM walk;
9345$$ LANGUAGE sql STABLE;
9346
9347/**
9348 * @brief Gather the edges of a tracked relation in the columnar form
9349 * expected by reachability_evaluate (internal)
9350 *
9351 * Materializes the edge relation with its provenance tokens and
9352 * probabilities, maps arbitrary vertex values (compared as TEXT) onto
9353 * dense INTEGER IDs, and checks that every edge tuple carries a base
9354 * input token (independent tuples): reachability compilation along the
9355 * data is only correct when the edges are independent events, so views
9356 * or query results with derived provenance are rejected.
9357 *
9358 * @param rel the provenance-tracked edge relation
9359 * @param source_attribute name of the source-vertex column
9360 * @param destination_attribute name of the destination-vertex column
9361 * @param extra_vertices vertex values (as TEXT) that must be part of
9362 * the dense ID space even when they touch no edge -- the source
9363 * set in particular; their IDs come back in @c extra_ids
9364 * (aligned with the input)
9365 * @param edge_quals optional deterministic filter over the edge
9366 * relation's columns (SQL TEXT, deparsed by the rewriter from
9367 * the recursive arm's WHERE clause), restricting which edges
9368 * participate
9369 * @param rel_sql deparsed edge subquery to gather from instead of
9370 * @p rel (join-defined edges); the tokens are then conjunctions
9371 * of base tuples, validated for shape and disjoint supports
9372 *
9373 * The @c vertices output maps the dense IDs back to the original
9374 * vertex values (as TEXT, 1-indexed), for callers that need to label
9375 * per-vertex results.
9376 *
9377 * @param[out] sources source vertex (dense ID) of each gathered edge
9378 * @param[out] destinations destination vertex (dense ID) of each edge
9379 * @param[out] tokens provenance token of each edge tuple
9380 * @param[out] probabilities probability of each edge tuple
9381 * @param[out] block_keys per-edge BID key variable (nil UUID = independent)
9382 * @param[out] block_indices per-edge outcome index within its block
9383 * @param[out] extra_ids dense IDs assigned to the @p extra_vertices
9384 * @param[out] vertices dense-ID-to-original-value map (TEXT, 1-indexed)
9385 */
9386CREATE OR REPLACE FUNCTION gather_reachability_edges(
9387 IN rel REGCLASS,
9388 IN source_attribute TEXT,
9389 IN destination_attribute TEXT,
9390 IN extra_vertices TEXT[],
9391 IN edge_quals TEXT DEFAULT NULL,
9392 IN rel_sql TEXT DEFAULT NULL,
9393 OUT sources INT[],
9394 OUT destinations INT[],
9395 OUT tokens UUID[],
9396 OUT probabilities DOUBLE PRECISION[],
9397 OUT block_keys UUID[],
9398 OUT block_indices INT[],
9399 OUT extra_ids INT[],
9400 OUT vertices TEXT[])
9401AS
9402$$
9403DECLARE
9404 tkind TEXT;
9405 bkey_expr TEXT;
9406 sel_probs TEXT;
9407 sel_bkeys TEXT;
9408 sel_bidx TEXT;
9409 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9410BEGIN
9411 -- Consult the per-table characterisation registry (TID / BID / OPAQUE,
9412 -- maintained by add_provenance / repair_key and the CTAS lineage hook):
9413 -- a TID relation is certified all-independent-inputs, a BID relation
9414 -- holds input or mulinput rows with the block structure given by the
9415 -- registry's key columns. Derived (OPAQUE), unregistered, or
9416 -- subquery-defined edges take the fully dynamic per-token path.
9417 IF rel IS NOT NULL AND rel_sql IS NULL THEN
9418 tkind := (provsql.get_table_info(rel::oid)).kind;
9419 END IF;
9420 IF tkind NOT IN ('tid', 'bid') THEN
9421 tkind := NULL;
9422 END IF;
9423 IF tkind = 'bid' THEN
9424 SELECT string_agg(quote_ident(a.attname) || '::TEXT', ' || '','' || '
9425 ORDER BY k.ord)
9426 INTO bkey_expr
9427 FROM unnest((provsql.get_table_info(rel::oid)).block_key)
9428 WITH ORDINALITY AS k(attnum, ord)
9429 JOIN pg_attribute a ON a.attrelid = rel AND a.attnum = k.attnum;
9430 -- An empty registry key means the whole table is one block.
9431 bkey_expr := coalesce(bkey_expr, quote_literal(''));
9432 END IF;
9433 IF tkind IS NOT NULL AND verbosity >= 20 THEN
9434 -- The function-level client_min_messages = warning (which silences
9435 -- the CTAS / DROP TABLE chatter) would also swallow this notice;
9436 -- lift it for the one RAISE. The function-level SET restores the
9437 -- caller's value at exit regardless.
9438 PERFORM set_config('client_min_messages', 'notice', true);
9439 RAISE NOTICE 'ProvSQL: catalog characterises % as %', rel, upper(tkind);
9440 PERFORM set_config('client_min_messages', 'warning', true);
9441 END IF;
9442
9443 -- Materialize the edges with their tokens; the planner hook resolves
9444 -- provenance() over the tracked relation, and remove_provenance strips
9445 -- the automatic provsql column so the later aggregation is plain SQL.
9446 -- For a BID relation the synthetic per-block key (a v5 UUID over the
9447 -- registry key columns' values) is computed here, while the columns
9448 -- are in scope.
9449 DROP TABLE IF EXISTS provsql_reachability_edges_tmp;
9450 EXECUTE format(
9451 'CREATE TEMP TABLE provsql_reachability_edges_tmp AS '
9452 || 'SELECT %1$I::TEXT AS u, %2$I::TEXT AS v, '
9453 || 'provsql.strip_annotations(provsql.provenance()) AS token%5$s '
9454 || 'FROM %3$s WHERE %1$I IS NOT NULL AND %2$I IS NOT NULL%4$s',
9455 source_attribute, destination_attribute,
9456 CASE WHEN rel_sql IS NULL THEN rel::TEXT
9457 ELSE '(' || rel_sql || ') AS provsql_edge_subquery' END,
9458 CASE WHEN edge_quals IS NULL THEN ''
9459 ELSE ' AND (' || edge_quals || ')' END,
9460 CASE WHEN tkind = 'bid'
9461 THEN ', public.uuid_generate_v5(provsql.uuid_ns_provsql(), '
9462 || quote_literal('bidblock' || rel::TEXT || ':')
9463 || ' || ' || bkey_expr || ') AS bkey'
9464 ELSE ', NULL::UUID AS bkey' END);
9465 PERFORM provsql.remove_provenance('provsql_reachability_edges_tmp');
9466
9467 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
9468 IF tkind IS NULL THEN
9469 -- Dynamic path: validate the token shapes and, for conjunction-shaped
9470 -- (join-defined) tokens, the pairwise disjointness of their supports.
9471 IF EXISTS (SELECT 1 FROM provsql_reachability_edges_tmp
9472 WHERE provsql.get_gate_type(token) NOT IN ('input', 'mulinput', 'times',
9473 'project', 'eq')) THEN
9474 DROP TABLE provsql_reachability_edges_tmp;
9475 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input, repair_key, or conjunctive join tokens', coalesce(rel::TEXT, 'the edge query');
9476 END IF;
9477 CREATE TEMP TABLE provsql_reachability_support_tmp AS
9478 SELECT t.token, l.leaf
9479 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp
9480 WHERE provsql.get_gate_type(token) IN ('times', 'project', 'eq')) t,
9481 LATERAL unnest(provsql.token_conjunctive_leaves(t.token)) AS l(leaf);
9482 IF EXISTS (SELECT 1
9483 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp) t
9484 WHERE provsql.get_gate_type(t.token) IN ('times', 'project', 'eq')
9485 AND provsql.token_conjunctive_leaves(t.token) IS NULL) THEN
9486 DROP TABLE provsql_reachability_support_tmp;
9487 DROP TABLE provsql_reachability_edges_tmp;
9488 RAISE EXCEPTION 'reachability: a join-defined edge token is not a pure conjunction of base tuples';
9489 END IF;
9490 IF EXISTS (SELECT 1 FROM (
9491 SELECT leaf FROM provsql_reachability_support_tmp
9492 UNION ALL
9493 SELECT DISTINCT token FROM provsql_reachability_edges_tmp
9494 WHERE provsql.get_gate_type(token) = 'input'
9495 ) all_leaves
9496 GROUP BY leaf HAVING count(*) > 1) THEN
9497 DROP TABLE provsql_reachability_support_tmp;
9498 DROP TABLE provsql_reachability_edges_tmp;
9499 RAISE EXCEPTION 'reachability: join-defined edges share base tuples (their supports overlap), so they are not independent';
9500 END IF;
9501 END IF;
9502
9503 -- Per-kind classification expressions for the final aggregation: a TID
9504 -- relation needs no per-row gate introspection at all; a BID relation
9505 -- one get_gate_type per row (the input/mulinput split), block keys from
9506 -- the precomputed column-derived key and indices by numbering within
9507 -- the block; the dynamic path reads the gates.
9508 IF tkind = 'tid' THEN
9509 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
9510 sel_bkeys := $sql$'00000000-0000-0000-0000-000000000000'::UUID$sql$;
9511 sel_bidx := '0';
9512 ELSIF tkind = 'bid' THEN
9513 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
9514 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
9515 THEN e.bkey
9516 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
9517 sel_bidx := 'e.bidx';
9518 ELSE
9519 sel_probs := $sql$CASE WHEN provsql.get_gate_type(e.token) IN ('times','project','eq')
9520 THEN (SELECT CASE WHEN bool_or(coalesce(provsql.get_prob(s.leaf),1.0) = 0)
9521 THEN 0.0
9522 ELSE exp(sum(ln(coalesce(provsql.get_prob(s.leaf),1.0)))) END
9523 FROM provsql_reachability_support_tmp s
9524 WHERE s.token = e.token)
9525 ELSE coalesce(provsql.get_prob(e.token), 1.0) END$sql$;
9526 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
9527 THEN (provsql.get_children(e.token))[1]
9528 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
9529 sel_bidx := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
9530 THEN (provsql.get_infos(e.token)).info1 ELSE 0 END$sql$;
9531 END IF;
9532
9533 EXECUTE format(
9534 $sql$
9535 WITH verts AS (
9536 SELECT u AS x FROM provsql_reachability_edges_tmp
9537 UNION SELECT v FROM provsql_reachability_edges_tmp
9538 UNION SELECT unnest($1)),
9539 ids AS (
9540 SELECT x, (row_number() OVER (ORDER BY x))::INT AS id FROM verts)
9541 SELECT array_agg(iu.id), array_agg(iv.id),
9542 array_agg(e.token),
9543 array_agg(%s),
9544 array_agg(%s),
9545 array_agg(%s),
9546 (SELECT array_agg(i.id ORDER BY ev.ord)
9547 FROM unnest($1) WITH ORDINALITY AS ev(x, ord)
9548 JOIN ids i ON i.x = ev.x),
9549 (SELECT array_agg(x ORDER BY id) FROM ids)
9550 FROM (SELECT t.*,
9551 (row_number() OVER (PARTITION BY t.bkey))::INT AS bidx
9552 FROM provsql_reachability_edges_tmp t) e
9553 JOIN ids iu ON iu.x = e.u
9554 JOIN ids iv ON iv.x = e.v
9555 $sql$, sel_probs, sel_bkeys, sel_bidx)
9556 INTO sources, destinations, tokens, probabilities, block_keys,
9557 block_indices, extra_ids, vertices
9558 USING extra_vertices;
9559
9560 DROP TABLE provsql_reachability_edges_tmp;
9561 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
9562END
9563-- No SET search_path: the deparsed edge subquery (and the REGCLASS
9564-- rendering) must resolve against the caller's search_path; the ProvSQL
9565-- calls above are schema-qualified instead.
9566$$ LANGUAGE plpgsql SET client_min_messages = warning;
9567
9568
9569/**
9570 * @brief Gather a source relation's vertices, tokens and probabilities
9571 * (internal)
9572 *
9573 * For a provenance-tracked source relation, every tuple must carry a
9574 * base @c input token (a *probabilistic source set*); for an untracked
9575 * relation the sources are certain and the tokens come back as the nil
9576 * UUID. Vertex values are returned as TEXT, for the shared dense-ID
9577 * mapping of @c gather_reachability_edges().
9578 *
9579 * @param rel the source relation
9580 * @param source_attribute name of the vertex column
9581 * @param[out] source_values vertex value of each source tuple (as TEXT)
9582 * @param[out] source_tokens per-source base @c input token (nil UUID = certain)
9583 * @param[out] source_probabilities per-source probability
9584 */
9585CREATE OR REPLACE FUNCTION gather_reachability_sources(
9586 IN rel REGCLASS,
9587 IN source_attribute TEXT,
9588 OUT source_values TEXT[],
9589 OUT source_tokens UUID[],
9590 OUT source_probabilities DOUBLE PRECISION[])
9591AS
9592$$
9593DECLARE
9594 tracked BOOLEAN;
9595 tkind TEXT;
9596BEGIN
9597 SELECT EXISTS (
9598 SELECT 1 FROM pg_attribute
9599 WHERE attrelid = rel AND attname = 'provsql'
9600 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped)
9601 INTO tracked;
9602
9603 -- Registry consultation: a TID source relation is certified
9604 -- all-base-input, so the per-row gate check can be skipped; a BID one
9605 -- holds block-correlated tuples, which a probabilistic source set
9606 -- cannot model -- reject it before gathering anything.
9607 IF tracked THEN
9608 tkind := (get_table_info(rel::oid)).kind;
9609 IF tkind = 'bid' THEN
9610 RAISE EXCEPTION 'reachability: % is block-independent (repair_key); block-correlated source sets are not supported', rel;
9611 END IF;
9612 END IF;
9613
9614 DROP TABLE IF EXISTS provsql_reachability_sources_tmp;
9615 IF tracked THEN
9616 EXECUTE format(
9617 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
9618 || 'SELECT %1$I::TEXT AS x, provenance() AS token '
9619 || 'FROM %2$s WHERE %1$I IS NOT NULL',
9620 source_attribute, rel);
9621 PERFORM remove_provenance('provsql_reachability_sources_tmp');
9622 IF tkind IS DISTINCT FROM 'tid'
9623 AND EXISTS (SELECT 1 FROM provsql_reachability_sources_tmp
9624 WHERE get_gate_type(token) <> 'input') THEN
9625 DROP TABLE provsql_reachability_sources_tmp;
9626 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input tokens (independent tuples); views or query results are not supported', rel;
9627 END IF;
9628 SELECT array_agg(x), array_agg(token),
9629 array_agg(coalesce(get_prob(token), 1.0))
9630 INTO source_values, source_tokens, source_probabilities
9631 FROM provsql_reachability_sources_tmp;
9632 DROP TABLE provsql_reachability_sources_tmp;
9633 ELSE
9634 EXECUTE format(
9635 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
9636 || 'SELECT DISTINCT %1$I::TEXT AS x FROM %2$s WHERE %1$I IS NOT NULL',
9637 source_attribute, rel);
9638 SELECT array_agg(x),
9639 array_agg('00000000-0000-0000-0000-000000000000'::UUID),
9640 array_agg(1.0::float8)
9641 INTO source_values, source_tokens, source_probabilities
9642 FROM provsql_reachability_sources_tmp;
9643 DROP TABLE provsql_reachability_sources_tmp;
9644 END IF;
9645END
9646$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SET client_min_messages = warning;
9647
9648/**
9649 * @brief Fixpoint driver for the recursive reachability shape:
9650 * decomposition-aligned compilation with fallback to eval_recursive
9651 *
9652 * Called (at plan time, over SPI) by the recursive-CTE lowering when
9653 * the provenance class is 'absorptive' or 'BOOLEAN'
9654 * (@c provsql.provenance) and the CTE matches the linear
9655 * reachability shape over a tracked base edge relation. Attempts the
9656 * decomposition-aligned route -- gather the edges, compile every
9657 * reachable vertex's certified provenance circuit along a tree
9658 * decomposition of the data graph, materialise them, and fill the
9659 * working table with one tokenised row per reachable vertex. On any
9660 * failure (data treewidth above the cap, per-node state bound, edges
9661 * that are not independent base tuples...), falls back to the generic
9662 * @c eval_recursive() fixpoint, preserving its behaviour exactly.
9663 *
9664 * @param edge_rel the provenance-tracked edge relation
9665 * @param source_attribute name of the source-vertex column
9666 * @param destination_attribute name of the destination-vertex column
9667 * @param source_value the base arm's constant, as TEXT
9668 * @param directed if false, each edge can be traversed both ways
9669 * @param work_name name of the working temp table (the CTE name)
9670 * @param colnames comma-separated user column names (for the fallback)
9671 * @param coldef column definitions of the working table
9672 * @param coltype type of the CTE's single column
9673 * @param body_sql deparsed CTE body (for the fallback)
9674 * @param edge_quals optional deterministic filter over edge columns
9675 * (deparsed from the recursive arm's WHERE clause)
9676 * @param source_rel source relation of a multi-source base arm
9677 * (@c SELECT col FROM sources), NULL for the constant form;
9678 * tracked sources form a probabilistic source set, untracked
9679 * ones are certain
9680 * @param source_rel_attribute the source relation's vertex column
9681 * @param edge_sql deparsed edge subquery when the recursive arm joins a
9682 * derived (join-defined) edge relation instead of a base one;
9683 * NULL for the REGCLASS form
9684 * @param hop_bound maximum number of recursive steps for the
9685 * hop-counting CTE shape (NULL for plain reachability)
9686 * @param hop_seed the base arm's hop constant (hop-counting shape)
9687 * @param hops_position 1-based position of the hop column among the
9688 * CTE's two columns (hop-counting shape)
9689 */
9690CREATE OR REPLACE FUNCTION eval_reachability(
9691 edge_rel REGCLASS,
9692 source_attribute TEXT,
9693 destination_attribute TEXT,
9694 source_value TEXT,
9695 directed BOOLEAN,
9696 work_name TEXT,
9697 colnames TEXT,
9698 coldef TEXT,
9699 coltype TEXT,
9700 body_sql TEXT,
9701 edge_quals TEXT DEFAULT NULL,
9702 source_rel REGCLASS DEFAULT NULL,
9703 source_rel_attribute TEXT DEFAULT NULL,
9704 edge_sql TEXT DEFAULT NULL,
9705 hop_bound INT DEFAULT NULL,
9706 hop_seed INT DEFAULT NULL,
9707 hops_position INT DEFAULT NULL)
9708 RETURNS VOID AS
9709$$
9710DECLARE
9711 e RECORD;
9712 sv TEXT[];
9713 st UUID[];
9714 sp double precision[];
9715 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
9716BEGIN
9717 BEGIN
9718 IF source_rel IS NOT NULL THEN
9719 -- Multi-source: gather the source relation (probabilistic when
9720 -- tracked, certain otherwise).
9721 SELECT g.source_values, g.source_tokens, g.source_probabilities
9722 INTO sv, st, sp
9723 FROM provsql.gather_reachability_sources(source_rel,
9724 source_rel_attribute) g;
9725 IF sv IS NULL THEN
9726 sv := ARRAY[]::TEXT[];
9727 st := ARRAY[]::UUID[];
9728 sp := ARRAY[]::float8[];
9729 END IF;
9730 ELSE
9731 -- Constant base arm: one certain source.
9732 sv := ARRAY[source_value];
9733 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
9734 sp := ARRAY[1.0::float8];
9735 END IF;
9736
9737 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
9738 destination_attribute,
9739 sv, edge_quals, edge_sql);
9740 IF to_regclass(work_name) IS NOT NULL THEN
9741 EXECUTE format('DROP TABLE %I', work_name);
9742 END IF;
9743 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID)', work_name, coldef);
9744 IF hop_bound IS NULL THEN
9745 EXECUTE format(
9746 'INSERT INTO %I SELECT ($1::TEXT[])[m.vertex]::%s, m.token '
9747 || 'FROM provsql.reachability_materialize($2, $3, $4, $5, $6, $7, $8, $9, $10, $11) m',
9748 work_name, coltype)
9749 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
9750 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed;
9751 ELSE
9752 -- Hop-counting shape: one row per (vertex, walk length), the hop
9753 -- column in its CTE position.
9754 EXECUTE format(
9755 'INSERT INTO %I SELECT %s, m.token '
9756 || 'FROM provsql.reachability_materialize_hops($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) m',
9757 work_name,
9758 CASE WHEN hops_position = 1
9759 THEN format('m.hops, ($1::TEXT[])[m.vertex]::%s', coltype)
9760 ELSE format('($1::TEXT[])[m.vertex]::%s, m.hops', coltype) END)
9761 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
9762 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed,
9763 hop_bound, hop_seed;
9764 END IF;
9765 IF verbosity >= 20 THEN
9766 RAISE NOTICE 'ProvSQL: recursive CTE "%" compiled along a tree decomposition of %',
9767 work_name, coalesce(edge_rel::TEXT, 'the join-defined edge query');
9768 END IF;
9769 EXCEPTION WHEN OTHERS THEN
9770 IF verbosity >= 10 THEN
9771 RAISE NOTICE 'ProvSQL: reachability route for "%" fell back to the generic fixpoint (%)',
9772 work_name, SQLERRM;
9773 END IF;
9774 PERFORM provsql.eval_recursive(body_sql, work_name, colnames, coldef);
9775 END;
9776END
9777$$ LANGUAGE plpgsql;
9778
9779
9780
9781/** @} */
9782
9783/** @defgroup provenance_output Provenance output
9784 * Functions for visualizing and exporting provenance circuits
9785 * in various formats.
9786 * @{
9787 */
9788
9789/**
9790 * @brief Return a DOT or TEXT visualization of the provenance circuit
9791 *
9792 * @param token root provenance token
9793 * @param token2desc mapping table for gate descriptions
9794 * @param dbg debug level (0 = normal)
9795 */
9796CREATE OR REPLACE FUNCTION view_circuit(
9797 token UUID,
9798 token2desc REGCLASS,
9799 dbg INT = 0)
9800 RETURNS TEXT AS
9801 'provsql','view_circuit' LANGUAGE C;
9802
9803/**
9804 * @brief Return a DOT visualisation of the d-DNNF compiled from the
9805 * provenance circuit
9806 *
9807 * Runs the requested external knowledge compiler and renders the
9808 * resulting d-DNNF as a GraphViz digraph.
9809 *
9810 * @param token root provenance token
9811 * @param compiler external compiler or in-process meta-route to invoke;
9812 * empty (the default) picks the highest-preference available compiler
9813 */
9814CREATE OR REPLACE FUNCTION compile_to_ddnnf_dot(
9815 token UUID,
9816 compiler TEXT = '')
9817 RETURNS TEXT AS
9818 'provsql','compile_to_ddnnf_dot' LANGUAGE C;
9819
9820/**
9821 * @brief Return the compiled d-DNNF of a provenance circuit in the
9822 * c2d / d4 ".nnf" TEXT interchange format.
9823 *
9824 * Companion to compile_to_ddnnf_dot (DOT, for viewing): this is the
9825 * machine-readable form, suitable for feeding to an external d-DNNF
9826 * reasoner / verifier or saving next to tseytin_cnf (same variable
9827 * numbering). Accepts the same compiler / meta-route names.
9828 *
9829 * @param token root provenance token
9830 * @param compiler compiler or in-process meta-route to use; empty (the
9831 * default) picks the highest-preference available compiler
9832 */
9833CREATE OR REPLACE FUNCTION compile_to_ddnnf(
9834 token UUID,
9835 compiler TEXT = '')
9836 RETURNS TEXT AS
9837 'provsql','compile_to_ddnnf' LANGUAGE C;
9838
9839/**
9840 * @brief Structural statistics of the d-DNNF a compiler produces for a
9841 * provenance circuit.
9842 *
9843 * Compiles the circuit with the given compiler / meta-route (same names
9844 * as compile_to_ddnnf_dot: d4, d4v2, c2d, minic2d, dsharp, panini-*,
9845 * tree-decomposition, interpret-as-dd, default) and returns a jsonb
9846 * object: nodes, edges, and / or / not / inputs counts, smooth, depth
9847 * (longest path), treewidth (null when not computable), and compile_ms.
9848 * Lets clients compare what each compiler produces on the same circuit.
9849 *
9850 * @param token root provenance token
9851 * @param compiler compiler or in-process meta-route to use; empty (the
9852 * default) picks the highest-preference available compiler
9853 */
9854CREATE OR REPLACE FUNCTION ddnnf_stats(
9855 token UUID,
9856 compiler TEXT = '')
9857 RETURNS jsonb AS
9858 'provsql','ddnnf_stats' LANGUAGE C;
9859
9860/**
9861 * @brief Return the DIMACS CNF (Tseytin transformation) of the provenance circuit
9862 *
9863 * Returns the same encoding the extension writes to a temp file before
9864 * invoking d4 / c2d / minic2d / dsharp. With @c weighted true (the
9865 * default), per-input probability weights are appended as @c w lines.
9866 *
9867 * @param token root provenance token
9868 * @param weighted include probability weights when true
9869 * @param mapping prepend "c input <var> <UUID> <prob>" comment lines
9870 * documenting which provenance input each variable stands for
9871 */
9872CREATE OR REPLACE FUNCTION tseytin_cnf(
9873 token UUID,
9874 weighted BOOLEAN = TRUE,
9875 mapping BOOLEAN = TRUE)
9876 RETURNS TEXT AS
9877 'provsql','tseytin_cnf' LANGUAGE C;
9878
9879/**
9880 * @brief Map each DIMACS variable of tseytin_cnf back to its
9881 * provenance input.
9882 *
9883 * Returns one row per input gate: the variable index (matching
9884 * tseytin_cnf and compile_to_ddnnf's NNF), the original-circuit UUID
9885 * of that input, and its probability. Lets a satisfying assignment or
9886 * weighted model count obtained from an external tool be read against
9887 * the provenance circuit.
9888 *
9889 * @param token root provenance token
9890 */
9891CREATE OR REPLACE FUNCTION tseytin_cnf_mapping_json(token UUID)
9892 RETURNS jsonb AS
9893 'provsql','tseytin_cnf_mapping_json' LANGUAGE C;
9894
9895CREATE OR REPLACE FUNCTION tseytin_cnf_mapping(token UUID)
9896 RETURNS TABLE(variable INT, gate UUID, probability FLOAT8) AS $$
9897 SELECT variable, gate, probability
9898 FROM jsonb_to_recordset(tseytin_cnf_mapping_json(token))
9899 AS x(variable INT, gate UUID, probability FLOAT8)
9900 ORDER BY variable
9901$$ LANGUAGE SQL STABLE;
9902
9903/**
9904 * @brief Return a DOT visualisation of the tree decomposition of the
9905 * provenance circuit
9906 *
9907 * Computes the min-fill decomposition used by the in-process
9908 * knowledge compiler. The first line of the output is a comment of
9909 * the form @c "// treewidth=<n>".
9910 *
9911 * @param token root provenance token
9912 */
9913CREATE OR REPLACE FUNCTION tree_decomposition_dot(
9914 token UUID)
9915 RETURNS TEXT AS
9916 'provsql','tree_decomposition_dot' LANGUAGE C;
9917
9918/**
9919 * @brief Report whether an external tool is on the backend's resolved PATH
9920 *
9921 * Uses the same @c find_external_tool() helper that the compilers
9922 * (d4 / c2d / minic2d / dsharp / panini), model counters (ganak /
9923 * sharpsat-td / dpmc via htb+dmc / weightmc), and visualisation
9924 * wrappers (graph-easy, dot) themselves consult, so the result
9925 * reflects exactly what a subsequent @c probability_evaluate or
9926 * @c view_circuit call would see, including the
9927 * @c provsql.tool_search_path GUC prepended to @c $PATH.
9928 *
9929 * Names with a slash are treated as paths and tested directly via
9930 * @c access(X_OK); bare names are resolved through @c /bin/sh's
9931 * @c command -v under the backend's PATH.
9932 *
9933 * @param name bare executable (e.g. @c 'd4') or an absolute path
9934 * @return true iff the tool resolves to an executable file
9935 */
9936CREATE OR REPLACE FUNCTION tool_available(name TEXT)
9937 RETURNS BOOLEAN AS
9938 'provsql','tool_available' LANGUAGE C STRICT;
9939
9940/* ----------------------------------------------------------------------
9941 * External-tool registry
9942 *
9943 * A catalog of the external tools ProvSQL can invoke (the knowledge
9944 * compilers, weighted model counters, and the graph-easy DOT renderer).
9945 * The default tools and their invocations are compiled in (seeded in C), so
9946 * out-of-the-box behaviour is unchanged with no configuration.
9947 *
9948 * Administrators may add / repoint / reorder / disable tools at run time;
9949 * those changes are persisted in the @c provsql.tool_overrides table below
9950 * and overlaid on the compiled seed, so they survive across sessions and
9951 * backends (and dump/restore). An empty overrides table means exactly the
9952 * compiled defaults. The mutators are superuser-only because a tool RECORD
9953 * names an executable run as the PostgreSQL OS user (the same trust level as
9954 * provsql.tool_search_path).
9955 * ---------------------------------------------------------------------- */
9956
9957/**
9958 * @brief Persistent overrides overlaid on the compiled-in tool seed.
9959 *
9960 * Each row is the complete desired RECORD for a tool (added or modified) keyed
9961 * by logical @c name, or a tombstone (@c removed = true) hiding a seeded
9962 * default. The effective registry is the compiled seed with tombstoned names
9963 * removed and the remaining rows upserted over it. Written only by the
9964 * superuser-only register_tool / unregister_tool / set_tool_* functions;
9965 * read back into each backend's in-memory registry on demand. Marked as a
9966 * configuration table so pg_dump carries an operator's registrations.
9967 */
9968CREATE TABLE IF NOT EXISTS tool_overrides(
9969 name TEXT PRIMARY KEY,
9970 removed BOOLEAN NOT NULL DEFAULT false,
9971 kind TEXT,
9972 executable TEXT,
9973 operations TEXT[],
9974 input_formats TEXT[],
9975 output_format TEXT,
9976 parser TEXT,
9977 preference INT,
9978 enabled BOOLEAN,
9979 dependencies TEXT[],
9980 argtpl TEXT,
9981 argtpl_circuit TEXT,
9982 endpoint TEXT
9983);
9984SELECT pg_catalog.pg_extension_config_dump('tool_overrides', '');
9985
9986/**
9987 * @brief Set-returning listing backing the @c provsql.tools view.
9988 *
9989 * @c operations / @c input_formats / @c output_format use the KCMCP
9990 * shared-registry names (see the KCMCP server protocol), so a CLI RECORD and
9991 * a future kcmcp-server RECORD are comparable; @c parser is the CLI-only tag
9992 * for how to decode the tool's raw output. @c argtpl is the command template
9993 * ({in}/{out}/... placeholders). @c available is true iff @c executable
9994 * (when set) and every dependency currently resolve on the backend's PATH.
9995 */
9996CREATE OR REPLACE FUNCTION tool_registry_list()
9997 RETURNS TABLE(name TEXT, kind TEXT, executable TEXT, operations TEXT[],
9998 input_formats TEXT[], output_format TEXT, parser TEXT,
9999 preference INT, enabled BOOLEAN, argtpl TEXT,
10000 argtpl_circuit TEXT, endpoint TEXT, available BOOLEAN) AS
10001 'provsql','tool_registry_list' LANGUAGE C STABLE;
10002
10003/**
10004 * @brief Read-only view of the registered tools.
10005 */
10006CREATE OR REPLACE VIEW tools AS
10007 SELECT name, kind, executable, operations, input_formats, output_format,
10008 parser, preference, enabled, argtpl, argtpl_circuit, endpoint,
10009 available
10010 FROM tool_registry_list();
10011
10012/**
10013 * @brief Register a tool, or replace the RECORD with the same logical name.
10014 *
10015 * @param name logical id (e.g. @c 'd4-jm62300'); also the value
10016 * @c provsql.fallback_compiler / the wmc tool selector use
10017 * @param executable executable to resolve on PATH (defaults to @c name)
10018 * @param kind @c 'cli' (spawn @c executable) or @c 'kcmcp' (talk to
10019 * the KCMCP server at @c endpoint)
10020 * @param operations capabilities (KCMCP names): @c 'compile' / @c 'wmc'
10021 * (and ProvSQL-local @c 'render')
10022 * @param input_formats accepted inputs (KCMCP names): @c 'dimacs-cnf',
10023 * @c 'circuit-bcs12' (listing @c 'circuit-bcs12' enables
10024 * the native-circuit fast path)
10025 * @param output_format result encoding (KCMCP names): @c 'ddnnf-nnf',
10026 * @c 'decimal', @c 'rational', ... (local @c 'panini-dd'
10027 * / @c 'ascii' where KCMCP has no code)
10028 * @param parser CLI-only decode tag: @c 'nnf' (the tolerant d4 / c2d
10029 * NNF reader), @c 'panini-dd', @c 'wmc-line',
10030 * @c 'weightmc', @c 'ascii'
10031 * @param argtpl command template; placeholders @c {in} / @c {out}
10032 * (and @c {binary} / @c {tmpdir} / @c {pivotAC}). When
10033 * it omits @c {binary}, the executable is prepended.
10034 * @param argtpl_circuit command used when the @c 'circuit-bcs12' input is
10035 * selected (a BC-S1.2 circuit rather than a CNF); only a
10036 * tool accepting that input needs it
10037 * @param preference ordering within an operation (higher first)
10038 * @param enabled whether the dispatchers may select it
10039 * @param endpoint for a @c 'kcmcp' RECORD, the server address:
10040 * @c 'unix:/path' or @c 'host:port'
10041 *
10042 * Superuser-only: a CLI RECORD runs an arbitrary command as the PostgreSQL
10043 * OS user, and a kcmcp RECORD names a socket the server connects to.
10044 */
10045CREATE OR REPLACE FUNCTION register_tool(
10046 name TEXT,
10047 executable TEXT DEFAULT NULL,
10048 kind TEXT DEFAULT 'cli',
10049 operations TEXT[] DEFAULT NULL,
10050 input_formats TEXT[] DEFAULT NULL,
10051 output_format TEXT DEFAULT NULL,
10052 parser TEXT DEFAULT NULL,
10053 argtpl TEXT DEFAULT NULL,
10054 argtpl_circuit TEXT DEFAULT NULL,
10055 preference INT DEFAULT 0,
10056 enabled BOOLEAN DEFAULT true,
10057 endpoint TEXT DEFAULT NULL)
10058 RETURNS VOID AS
10059 'provsql','tool_registry_register' LANGUAGE C;
10060
10061/** @brief Unregister a tool; errors on an unknown tool name. Superuser-only. */
10062CREATE OR REPLACE FUNCTION unregister_tool(name TEXT)
10063 RETURNS VOID AS
10064 'provsql','tool_registry_unregister' LANGUAGE C STRICT;
10065
10066/** @brief Enable/disable a tool; errors on an unknown tool name. Superuser-only. */
10067CREATE OR REPLACE FUNCTION set_tool_enabled(name TEXT, enabled BOOLEAN)
10068 RETURNS VOID AS
10069 'provsql','tool_registry_set_enabled' LANGUAGE C STRICT;
10070
10071/** @brief Set a tool's preference; errors on an unknown tool name. Superuser-only. */
10072CREATE OR REPLACE FUNCTION set_tool_preference(name TEXT, preference INT)
10073 RETURNS VOID AS
10074 'provsql','tool_registry_set_preference' LANGUAGE C STRICT;
10075
10076-- The mutators guard at the C level too, but revoke from PUBLIC so the
10077-- superuser requirement is visible in the catalog.
10078REVOKE ALL ON FUNCTION register_tool(TEXT, TEXT, TEXT, TEXT[], TEXT[], TEXT, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT) FROM PUBLIC;
10079REVOKE ALL ON FUNCTION unregister_tool(TEXT) FROM PUBLIC;
10080REVOKE ALL ON FUNCTION set_tool_enabled(TEXT, BOOLEAN) FROM PUBLIC;
10081REVOKE ALL ON FUNCTION set_tool_preference(TEXT, INT) FROM PUBLIC;
10082
10083/**
10084 * @brief Return an XML representation of the provenance circuit
10085 *
10086 * @param token root provenance token
10087 * @param token2desc optional mapping table for gate descriptions
10088 */
10089CREATE OR REPLACE FUNCTION to_provxml(
10090 token UUID,
10091 token2desc REGCLASS = NULL)
10092 RETURNS TEXT AS
10093 'provsql','to_provxml' LANGUAGE C;
10094
10095/** @brief Return the provenance token of the current query result tuple */
10096CREATE OR REPLACE FUNCTION provenance() RETURNS UUID AS
10097 'provsql', 'provenance' LANGUAGE C;
10098
10099/**
10100 * @brief Compute where-provenance for a result tuple
10101 *
10102 * Returns a TEXT representation showing which input columns
10103 * contributed to each output column.
10104 */
10105CREATE OR REPLACE FUNCTION where_provenance(token UUID)
10106 RETURNS TEXT AS
10107 'provsql','where_provenance' LANGUAGE C;
10108
10109/** @} */
10110
10111/** @defgroup circuit_init Circuit initialization
10112 * Functions and statements executed at extension load time to
10113 * reset internal caches and create the constant zero/one gates.
10114 * @{
10115 */
10116
10117/** @brief Reset the internal cache of OID constants used by the query rewriter */
10118CREATE OR REPLACE FUNCTION reset_constants_cache()
10119 RETURNS VOID AS
10120 'provsql', 'reset_constants_cache' LANGUAGE C;
10121
10122SELECT reset_constants_cache();
10123
10124SELECT create_gate(gate_zero(), 'zero');
10125SELECT create_gate(gate_one(), 'one');
10126
10127/** @} */
10128
10129/** @brief Types of update operations tracked for temporal provenance */
10130CREATE TYPE QUERY_TYPE_ENUM AS ENUM (
10131 'INSERT', -- Row was inserted
10132 'DELETE', -- Row was deleted
10133 'UPDATE', -- Row was updated
10134 'UNDO' -- Previous operation was undone
10135 );
10136
10137/** @defgroup compiled_semirings Compiled semirings
10138 * Definitions of compiled semirings
10139 * @{
10140 */
10141
10142/** @brief Evaluate provenance as a symbolic formula (e.g., "a ⊗ b ⊕ c") */
10143-- The mapping is optional (as for sr_boolexpr): formula renders whatever
10144-- circuit it is given, and the measure-carrier circuits it is most useful
10145-- on (random variables, arithmetic, mixtures) have no leaf mapping at all.
10146-- Without one, input leaves render as the semiring's 𝟙.
10147CREATE FUNCTION sr_formula(token ANYELEMENT, token2value REGCLASS = NULL)
10148 RETURNS VARCHAR AS
10149$$
10150BEGIN
10151 IF token IS NULL THEN
10152 RETURN NULL;
10153 END IF;
10154 RETURN provsql.provenance_evaluate_compiled(
10155 token,
10156 token2value,
10157 'formula',
10158 '𝟙'::VARCHAR
10159 );
10160END
10161$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
10162
10163/** @brief Evaluate provenance over the counting semiring (ℕ) */
10164CREATE FUNCTION sr_counting(token ANYELEMENT, token2value REGCLASS)
10165 RETURNS INT AS
10166$$
10167BEGIN
10168 RETURN provsql.provenance_evaluate_compiled(
10169 token,
10170 token2value,
10171 'counting',
10172 1
10173 );
10174END
10175$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10176
10177/** @brief Evaluate provenance as why-provenance (set of witness sets) */
10178CREATE FUNCTION sr_why(token ANYELEMENT, token2value REGCLASS)
10179 RETURNS VARCHAR AS
10180$$
10181BEGIN
10182 RETURN provsql.provenance_evaluate_compiled(
10183 token,
10184 token2value,
10185 'why',
10186 '{}'::VARCHAR
10187 );
10188END
10189$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10190
10191/** @brief Evaluate provenance as how-provenance (canonical polynomial provenance ℕ[X], universal commutative-semiring provenance) */
10192CREATE FUNCTION sr_how(token ANYELEMENT, token2value REGCLASS)
10193 RETURNS VARCHAR AS
10194$$
10195BEGIN
10196 RETURN provsql.provenance_evaluate_compiled(
10197 token,
10198 token2value,
10199 'how',
10200 '{}'::VARCHAR
10201 );
10202END
10203$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10204
10205/** @brief Evaluate provenance as which-provenance (lineage: a single set of contributing labels) */
10206CREATE FUNCTION sr_which(token ANYELEMENT, token2value REGCLASS)
10207 RETURNS VARCHAR AS
10208$$
10209BEGIN
10210 RETURN provsql.provenance_evaluate_compiled(
10211 token,
10212 token2value,
10213 'which',
10214 '{}'::VARCHAR
10215 );
10216END
10217$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10218
10219/** @brief Evaluate provenance as a Boolean expression
10220 *
10221 * The optional @p token2value mapping labels the leaves of the
10222 * formula: when omitted, leaves are rendered as bare @c x@<id@>
10223 * placeholders.
10224 */
10225CREATE FUNCTION sr_boolexpr(token ANYELEMENT, token2value REGCLASS = NULL)
10226 RETURNS VARCHAR AS
10227$$
10228BEGIN
10229 IF token IS NULL THEN
10230 RETURN NULL;
10231 END IF;
10232 RETURN provsql.provenance_evaluate_compiled(
10233 token,
10234 token2value,
10235 'boolexpr',
10236 '⊤'::VARCHAR
10237 );
10238END
10239$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
10240
10241/** @brief Evaluate provenance over the Boolean semiring (true/false) */
10242CREATE FUNCTION sr_boolean(token ANYELEMENT, token2value REGCLASS)
10243 RETURNS BOOLEAN AS
10244$$
10245BEGIN
10246 RETURN provsql.provenance_evaluate_compiled(
10247 token,
10248 token2value,
10249 'BOOLEAN',
10250 TRUE
10251 );
10252END
10253$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10254
10255/** @brief Structural universal-zero test (C backend of nonzero's default mode) */
10256CREATE FUNCTION true_nonzero(token UUID)
10257 RETURNS BOOLEAN AS
10258 'provsql', 'true_nonzero' LANGUAGE C PARALLEL SAFE STABLE;
10259
10260/**
10261 * @brief Test whether a provenance annotation is nonzero.
10262 *
10263 * Returns false only on a *proof* that the annotation is zero; true
10264 * otherwise, so filtering with <tt>WHERE nonzero(provenance())</tt> never
10265 * discards a row whose annotation could be nonzero.
10266 *
10267 * The default mode (@p semiring NULL) tests *universal* zero-ness: zero in
10268 * every (m-)semiring under every leaf valuation, decided by sound
10269 * structural rules (zero propagation through the gates; a comparison gate
10270 * whose satisfying-world set is empty). Filtering on it can never
10271 * contradict any downstream semiring evaluation.
10272 *
10273 * A named @p semiring evaluates the circuit there and tests against that
10274 * semiring's zero: 'BOOLEAN' is presence in the vanilla SQL answer on this
10275 * instance (the mode that filters, e.g., the null-padded arm of a
10276 * difference), 'counting' is bag multiplicity. An absent @p mapping reads
10277 * every leaf as the semiring's one (true / 1); with a mapping, leaves take
10278 * their mapped values.
10279 *
10280 * A NULL @p token reads as the neutral 1 (an untracked row): true.
10281 *
10282 * @param token provenance token to test
10283 * @param semiring NULL (universal zero test), 'BOOLEAN', or 'counting'
10284 * @param mapping optional mapping table from tokens to leaf values
10285 */
10286CREATE FUNCTION nonzero(token UUID,
10287 semiring TEXT DEFAULT NULL,
10288 mapping REGCLASS DEFAULT NULL)
10289 RETURNS BOOLEAN AS
10290$$
10291BEGIN
10292 IF token IS NULL THEN
10293 RETURN true;
10294 END IF;
10295 IF semiring IS NULL THEN
10296 RETURN provsql.true_nonzero(token);
10297 ELSIF semiring = 'BOOLEAN' THEN
10298 RETURN provsql.provenance_evaluate_compiled(token, mapping, 'BOOLEAN', TRUE);
10299 ELSIF semiring = 'counting' THEN
10300 RETURN provsql.provenance_evaluate_compiled(token, mapping, 'counting', 1) <> 0;
10301 ELSE
10302 RAISE EXCEPTION 'nonzero: unsupported semiring "%" (supported: BOOLEAN, counting; NULL for the universal zero test)', semiring;
10303 END IF;
10304END
10305$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
10306
10307/**
10308 * @brief Presence in the vanilla SQL answer on this instance.
10309 *
10310 * Shorthand for <tt>nonzero(token, 'BOOLEAN')</tt> with every leaf true:
10311 * <tt>WHERE present(provenance())</tt> restores the result set the query
10312 * has without provenance tracking, filtering the zero-annotated extras
10313 * (antijoin arms, failed HAVING groups, unknown comparisons) that the
10314 * rewriting keeps visible.
10315 */
10316CREATE FUNCTION present(token UUID)
10317 RETURNS BOOLEAN AS
10318$$
10319 SELECT provsql.nonzero(token, 'BOOLEAN');
10320$$ LANGUAGE sql PARALLEL SAFE STABLE;
10321
10322/** @brief Evaluate provenance over the tropical (min-plus) m-semiring
10323 *
10324 * Inputs are read as %float8 cost values; the additive identity
10325 * is <tt>'Infinity'::%float8</tt> and the multiplicative identity is 0.
10326 * Returns the cost of the cheapest derivation.
10327 *
10328 * With @p nonnegative, input costs are checked nonnegative and the
10329 * semiring is *absorptive*: evaluation then also accepts circuits
10330 * carrying the @c 'absorptive' assumption marker -- notably cyclic
10331 * recursive queries truncated at the absorptive value fixpoint, giving
10332 * exact min-cost reachability on cyclic data.
10333 */
10334CREATE FUNCTION sr_tropical(token ANYELEMENT, token2value REGCLASS,
10335 nonnegative BOOLEAN = false)
10336 RETURNS FLOAT AS
10337$$
10338BEGIN
10339 RETURN provsql.provenance_evaluate_compiled(
10340 token,
10341 token2value,
10342 CASE WHEN nonnegative THEN 'tropical_nonneg' ELSE 'tropical' END,
10343 0::FLOAT
10344 );
10345END
10346$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10347
10348/** @brief Evaluate provenance over the Viterbi (max-times) m-semiring
10349 *
10350 * Inputs are read as %float8 probability values in @f$[0,1]@f$.
10351 * Returns the probability of the most likely derivation.
10352 */
10353CREATE FUNCTION sr_viterbi(token ANYELEMENT, token2value REGCLASS)
10354 RETURNS FLOAT AS
10355$$
10356BEGIN
10357 RETURN provsql.provenance_evaluate_compiled(
10358 token,
10359 token2value,
10360 'viterbi',
10361 1::FLOAT
10362 );
10363END
10364$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10365
10366/** @brief Evaluate provenance over the Łukasiewicz fuzzy m-semiring
10367 *
10368 * Inputs are read as %float8 graded-truth values in @f$[0,1]@f$.
10369 * Addition is @f$\max@f$; multiplication is the Łukasiewicz t-norm
10370 * @f$\max(a + b - 1, 0)@f$, which preserves crisp truth and avoids
10371 * the near-zero collapse of long product chains.
10372 */
10373CREATE FUNCTION sr_lukasiewicz(token ANYELEMENT, token2value REGCLASS)
10374 RETURNS FLOAT AS
10375$$
10376BEGIN
10377 RETURN provsql.provenance_evaluate_compiled(
10378 token,
10379 token2value,
10380 'lukasiewicz',
10381 1::FLOAT
10382 );
10383END
10384$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10385
10386/** @brief Evaluate provenance over the min-max m-semiring on a user ENUM
10387 *
10388 * Inputs are read as values of a user-defined ENUM carrier; addition
10389 * is ENUM-min, multiplication is ENUM-max. Bottom and top of the ENUM
10390 * are derived from @c pg_enum.enumsortorder. The third argument is a
10391 * sample value of the carrier ENUM, used only for type inference; its
10392 * value is ignored.
10393 *
10394 * The security shape: alternative derivations combine to the least
10395 * sensitive label, joins combine to the most sensitive label.
10396 *
10397 * @param token Provenance token to evaluate.
10398 * @param token2value Mapping from input gates to ENUM values.
10399 * @param element_one Sample value of the carrier ENUM (any value works).
10400 */
10401CREATE FUNCTION sr_minmax(token UUID, token2value REGCLASS, element_one ANYENUM)
10402 RETURNS ANYENUM AS
10403$$
10404BEGIN
10405 RETURN provsql.provenance_evaluate_compiled(
10406 token,
10407 token2value,
10408 'minmax',
10409 element_one
10410 );
10411END
10412$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10413
10414/** @brief Evaluate provenance over the max-min m-semiring on a user ENUM
10415 *
10416 * Dual of :sqlfunc:`sr_minmax`: addition is ENUM-max, multiplication
10417 * is ENUM-min. The fuzzy / availability / trust shape: alternatives
10418 * combine to the most permissive label, joins combine to the strictest
10419 * label. The third argument is a sample value of the carrier ENUM,
10420 * used only for type inference; its value is ignored.
10421 *
10422 * @param token Provenance token to evaluate.
10423 * @param token2value Mapping from input gates to ENUM values.
10424 * @param element_one Sample value of the carrier ENUM (any value works).
10425 */
10426CREATE FUNCTION sr_maxmin(token UUID, token2value REGCLASS, element_one ANYENUM)
10427 RETURNS ANYENUM AS
10428$$
10429BEGIN
10430 RETURN provsql.provenance_evaluate_compiled(
10431 token,
10432 token2value,
10433 'maxmin',
10434 element_one
10435 );
10436END
10437$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10438
10439/** @} */
10440
10441/** @defgroup choose_aggregate choose aggregate
10442 * Choose one value among many, used in particular to code a mutually
10443 * exclusive choice as an aggregate.
10444 * @{
10445 */
10446
10447/** @brief Transition function for the choose aggregate (keeps first non-NULL value) */
10448CREATE FUNCTION choose_function(state ANYELEMENT, data ANYELEMENT)
10449 RETURNS ANYELEMENT AS
10450$$
10451BEGIN
10452 IF state IS NULL THEN
10453 RETURN data;
10454 ELSE
10455 RETURN state;
10456 END IF;
10457END
10458$$ LANGUAGE plpgsql PARALLEL SAFE IMMUTABLE;
10459
10460/** @brief Aggregate that returns an arbitrary non-NULL value from a group */
10461CREATE AGGREGATE choose(ANYELEMENT) (
10462 SFUNC = choose_function,
10463 STYPE = ANYELEMENT
10464);
10465
10466/** @brief Explodes a table column containing aggregated provenance into multiple rows.
10467 *
10468 * For each row in the input table, this function unnests the children of the
10469 * specified aggregate token column and produces one output row per child.
10470 * It reconstructs the corresponding value and provenance (`provsql`) for
10471 * each resulting row.
10472 *
10473 * The original table is replaced by the transformed table.
10474 *
10475 * @param _tbl Name of the table to transform.
10476 * @param AGG_TOKEN Name of the column containing the aggregate to explode.
10477 */
10478CREATE OR REPLACE FUNCTION explode_table(_tbl TEXT, AGG_TOKEN TEXT)
10479RETURNS VOID AS $$
10480DECLARE
10481 _nsp TEXT;
10482BEGIN
10483 -- Resolve the schema actually holding _tbl so the rebuilt table is
10484 -- recreated in place (the provsql helper functions are schema-qualified
10485 -- so this works whatever the caller's search_path is).
10486 SELECT n.nspname INTO _nsp
10487 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
10488 WHERE c.oid = _tbl::REGCLASS;
10489
10490 EXECUTE format('
10491 CREATE TABLE %1$I.temp_exploded AS
10492 SELECT
10493 %2$I.*,
10494 provsql.get_extra(children[2]) AS new_t,
10495 provsql.provenance_times(children[1], provsql) AS new_provsql
10496 FROM %1$I.%2$I,
10497 LATERAL (
10498 SELECT provsql.get_children(sm) AS children
10499 FROM UNNEST(provsql.get_children(%3$I)) AS sm
10500 ) AS sub', _nsp, _tbl, AGG_TOKEN);
10501 EXECUTE format('DROP TABLE %I.%I', _nsp, _tbl);
10502 EXECUTE format('ALTER TABLE %I.temp_exploded DROP COLUMN %I, DROP COLUMN provsql', _nsp, AGG_TOKEN);
10503 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_t TO %I', _nsp, AGG_TOKEN);
10504 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_provsql TO provsql', _nsp);
10505 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME TO %I', _nsp, _tbl);
10506END;
10507$$ LANGUAGE plpgsql;
10508
10509/** @} */
10510
10511/**
10512 * @brief Append @c provsql to this database's default search_path, if missing.
10513 *
10514 * ProvSQL's operators and functions live in the @c provsql schema and
10515 * are resolved through @c search_path. When @c provsql is absent from
10516 * the path some surfaces fail with a clear error (RV/AGG_TOKEN
10517 * arithmetic), but others can be silently misrouted by an implicit
10518 * cross-domain cast. This helper makes the common case painless: it
10519 * reads the current <em>database-level</em> search_path setting from
10520 * @c pg_db_role_setting, appends @c provsql if not already present
10521 * (never replacing or reordering the existing entries), and applies the
10522 * result with @c ALTER @c DATABASE. It is idempotent and emits a
10523 * @c NOTICE describing what it did.
10524 *
10525 * Only @b new sessions pick up the change; the calling session keeps its
10526 * current path. Role-level settings (if any) take precedence over the
10527 * database-level setting and are left untouched. The caller must be the
10528 * database owner or a superuser (the privilege model of @c ALTER
10529 * @c DATABASE). Returns the resulting search_path value.
10530 */
10531CREATE OR REPLACE FUNCTION setup_search_path()
10532 RETURNS TEXT
10533 LANGUAGE plpgsql AS $$
10534DECLARE
10535 db TEXT := current_database();
10536 cfg TEXT[];
10537 cur TEXT; -- existing database-level search_path value
10538 new_path TEXT;
10539BEGIN
10540 -- setrole = 0 selects the database-wide default, not a per-role override.
10541 SELECT s.setconfig INTO cfg
10542 FROM pg_db_role_setting s
10543 JOIN pg_database d ON d.oid = s.setdatabase
10544 WHERE d.datname = db AND s.setrole = 0;
10545
10546 IF cfg IS NOT NULL THEN
10547 SELECT substr(e, length('search_path=') + 1) INTO cur
10548 FROM unnest(cfg) AS e
10549 WHERE e LIKE 'search_path=%';
10550 END IF;
10551
10552 IF cur IS NULL THEN
10553 -- No database-level search_path at all: install the documented
10554 -- default with provsql appended.
10555 new_path := '"$user", public, provsql';
10556 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
10557 RAISE NOTICE 'ProvSQL: set search_path = % for database "%" (no previous database-level setting). Only new sessions are affected.',
10558 new_path, db;
10559 RETURN new_path;
10560 END IF;
10561
10562 -- Already contains provsql as a path element? Idempotent no-op.
10563 IF EXISTS (
10564 SELECT 1 FROM unnest(string_to[](cur, ',')) AS p
10565 WHERE btrim(btrim(p), '"') = 'provsql')
10566 THEN
10567 RAISE NOTICE 'ProvSQL: search_path for database "%" already contains provsql (= %); no change.',
10568 db, cur;
10569 RETURN cur;
10570 END IF;
10571
10572 new_path := cur || ', provsql';
10573 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
10574 RAISE NOTICE 'ProvSQL: appended provsql to search_path for database "%" (now: %). Only new sessions are affected.',
10575 db, new_path;
10576 RETURN new_path;
10577END;
10578$$;
10579
10580GRANT USAGE ON SCHEMA provsql TO PUBLIC;
10581
10582SET search_path TO public;
10583
10584-- Installation-time advisory: if provsql is not in the database's default
10585-- search_path, point the user at setup_search_path(). reset_val reflects
10586-- the configured session default (postgresql.conf / ALTER DATABASE / ALTER
10587-- ROLE), unaffected by the SET search_path statements this script ran.
10588-- CREATE EXTENSION raises client_min_messages to WARNING for the duration
10589-- of the script, so we lower it around the RAISE NOTICE. SET LOCAL only:
10590-- it unwinds by itself when CREATE EXTENSION's transaction ends. An
10591-- explicit save/restore here would capture the WARNING clamp (already in
10592-- force when this block runs) and restore *that* at session level,
10593-- leaving the whole installing session with NOTICEs suppressed.
10594DO $$
10595DECLARE
10596 rp TEXT;
10597 has_provsql BOOLEAN;
10598BEGIN
10599 SELECT reset_val INTO rp FROM pg_settings WHERE name = 'search_path';
10600 SELECT bool_or(btrim(btrim(p), '"') = 'provsql')
10601 INTO has_provsql
10602 FROM unnest(string_to[](coalesce(rp, ''), ',')) AS p;
10603 IF NOT coalesce(has_provsql, false) THEN
10604 SET LOCAL client_min_messages = notice;
10605 RAISE NOTICE 'ProvSQL: schema "provsql" is not in your default search_path (currently: %).', rp;
10606 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());
10607 END IF;
10608END;
10609$$;
10610
10611-- Final constants-cache refresh. The planned SELECT statements earlier in
10612-- this script (reset_constants_cache itself, the zero/one create_gate calls)
10613-- make the installing session memoize the OID constants *mid-script*, while
10614-- objects defined later (notably the choose aggregate, used by the
10615-- scalar-subquery decorrelation) do not exist yet. Their optional lookups
10616-- then stay InvalidOid for the rest of the session, silently disabling the
10617-- corresponding rewrites (e.g. IN/NOT IN over a tracked relation would raise
10618-- "Subqueries ... not supported") until a new connection. Refreshing here,
10619-- after every object exists, repairs the installing session's cache.
10620SELECT provsql.reset_constants_cache();
10621SET search_path TO provsql;
10622
10623/** @defgroup update_provenance Update provenance (PostgreSQL 14+)
10624 * Extended provenance tracking for INSERT, UPDATE, DELETE, and UNDO
10625 * operations, including temporal validity ranges.
10626 * @{
10627 */
10628
10629/**
10630 * @brief Table recording the history of INSERT, UPDATE, DELETE, and UNDO operations
10631 *
10632 * Each row records one provenance-tracked modification, linking the
10633 * operation's provenance token to metadata (query TEXT, type, user,
10634 * TIMESTAMP) and the temporal validity range of the affected rows.
10635 */
10636CREATE TABLE update_provenance (
10637 provsql UUID,
10638 query TEXT,
10639 query_type QUERY_TYPE_ENUM,
10640 username TEXT,
10641 ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
10642 valid_time TSTZMULTIRANGE DEFAULT TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL))
10643);
10644
10645/** @cond INTERNAL */
10646/* Enable provenance tracking on an existing table (PostgreSQL 14+ version).
10647 * Overrides the common version; documented via add_provenance in provsql.common.sql. */
10648CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
10649 RETURNS VOID AS
10650$$
10651BEGIN
10652 -- Idempotence: a second add_provenance on an already-tracked table is
10653 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
10654 -- re-run freely.
10655 IF EXISTS (
10656 SELECT 1 FROM pg_attribute
10657 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
10658 ) THEN
10659 RAISE NOTICE 'table % already has provenance tracking', _tbl;
10660 RETURN;
10661 END IF;
10662 -- See the common-version body for the rationale of dropping the
10663 -- column DEFAULT and UNIQUE in favour of provenance_guard + a
10664 -- plain index.
10665 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
10666 EXECUTE format(
10667 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
10668 _tbl);
10669 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
10670 EXECUTE format(
10671 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
10672 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
10673 _tbl);
10674
10675 EXECUTE format('CREATE TRIGGER insert_statement AFTER INSERT ON %s REFERENCING NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE PROCEDURE provsql.insert_statement_trigger()', _tbl);
10676 EXECUTE format('CREATE TRIGGER delete_statement AFTER DELETE ON %s REFERENCING OLD TABLE AS OLD_TABLE FOR EACH STATEMENT EXECUTE PROCEDURE provsql.delete_statement_trigger()', _tbl);
10677 EXECUTE format('CREATE TRIGGER update_statement AFTER UPDATE ON %s REFERENCING OLD TABLE AS OLD_TABLE NEW TABLE AS NEW_TABLE FOR EACH STATEMENT EXECUTE PROCEDURE provsql.update_statement_trigger()', _tbl);
10678
10679 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
10680 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
10681END
10682$$ LANGUAGE plpgsql SECURITY DEFINER;
10683/** @endcond */
10684
10685/** @cond INTERNAL */
10686/* Trigger function for DELETE statement provenance tracking (PostgreSQL 14+).
10687 * Overrides the common version; documented via delete_statement_trigger in provsql.common.sql. */
10688CREATE OR REPLACE FUNCTION delete_statement_trigger()
10689 RETURNS TRIGGER AS
10690$$
10691DECLARE
10692 query_text TEXT;
10693 delete_token UUID;
10694 old_token UUID;
10695 new_token UUID;
10696 r RECORD;
10697 enable_trigger BOOL;
10698BEGIN
10699 enable_trigger := current_setting('provsql.update_provenance', true);
10700 IF enable_trigger = 'f' THEN
10701 RETURN NULL;
10702 END IF;
10703 delete_token := public.uuid_generate_v4();
10704
10705 PERFORM create_gate(delete_token, 'update');
10706
10707 SELECT query
10708 INTO query_text
10709 FROM pg_stat_activity
10710 WHERE pid = pg_backend_pid();
10711
10712 INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time)
10713 VALUES (delete_token, query_text, 'DELETE', current_user, CURRENT_TIMESTAMP, TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)));
10714
10715 PERFORM set_config('provsql.update_provenance', 'off', false);
10716 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
10717 PERFORM set_config('provsql.update_provenance', 'on', false);
10718
10719 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
10720 old_token := r.provsql;
10721 new_token := provenance_monus(old_token, delete_token);
10722
10723 PERFORM set_config('provsql.update_provenance', 'off', false);
10724 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
10725 USING new_token, old_token;
10726 PERFORM set_config('provsql.update_provenance', 'on', false);
10727 END LOOP;
10728
10729 RETURN NULL;
10730END
10731$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
10732/** @endcond */
10733
10734/**
10735 * @brief Trigger function for INSERT statement provenance tracking
10736 *
10737 * Records the insertion in update_provenance and multiplies provenance
10738 * tokens of inserted rows with the insert token.
10739 */
10740CREATE OR REPLACE FUNCTION insert_statement_trigger()
10741 RETURNS TRIGGER AS
10742$$
10743DECLARE
10744 query_text TEXT;
10745 insert_token UUID;
10746 old_token UUID;
10747 new_token UUID;
10748 r RECORD;
10749 enable_trigger BOOL;
10750BEGIN
10751 enable_trigger := current_setting('provsql.update_provenance', true);
10752 IF enable_trigger = 'f' THEN
10753 RETURN NULL;
10754 END IF;
10755
10756 insert_token := public.uuid_generate_v4();
10757
10758 PERFORM create_gate(insert_token, 'update');
10759
10760 SELECT query
10761 INTO query_text
10762 FROM pg_stat_activity
10763 WHERE pid = pg_backend_pid();
10764
10765 INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time)
10766 VALUES (insert_token, query_text, 'INSERT', current_user, CURRENT_TIMESTAMP, TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)));
10767
10768 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
10769 old_token := r.provsql;
10770 new_token := provenance_times(old_token, insert_token);
10771 PERFORM set_config('provsql.update_provenance', 'off', false);
10772 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
10773 USING new_token, old_token;
10774 PERFORM set_config('provsql.update_provenance', 'on', false);
10775 END LOOP;
10776
10777 RETURN NULL;
10778END
10779$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
10780
10781/**
10782 * @brief Trigger function for UPDATE statement provenance tracking
10783 *
10784 * Records the update in update_provenance. Multiplies new-row tokens
10785 * with the update token and applies monus to old-row tokens.
10786 */
10787CREATE OR REPLACE FUNCTION update_statement_trigger()
10788 RETURNS TRIGGER AS
10789$$
10790DECLARE
10791 query_text TEXT;
10792 update_token UUID;
10793 old_token UUID;
10794 new_token UUID;
10795 r RECORD;
10796 enable_trigger BOOL;
10797BEGIN
10798 enable_trigger := current_setting('provsql.update_provenance', true);
10799 IF enable_trigger = 'f' THEN
10800 RETURN NULL;
10801 END IF;
10802 update_token := public.uuid_generate_v4();
10803
10804 PERFORM create_gate(update_token, 'update');
10805
10806 SELECT query
10807 INTO query_text
10808 FROM pg_stat_activity
10809 WHERE pid = pg_backend_pid();
10810
10811 INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time)
10812 VALUES (update_token, query_text, 'UPDATE', current_user, CURRENT_TIMESTAMP, TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)));
10813
10814 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
10815 old_token := r.provsql;
10816 new_token := provenance_times(old_token, update_token);
10817
10818 PERFORM set_config('provsql.update_provenance', 'off', false);
10819 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
10820 USING new_token, old_token;
10821 PERFORM set_config('provsql.update_provenance', 'on', false);
10822 END LOOP;
10823
10824 PERFORM set_config('provsql.update_provenance', 'off', false);
10825 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
10826 PERFORM set_config('provsql.update_provenance', 'on', false);
10827
10828 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
10829 old_token := r.provsql;
10830 new_token := provenance_monus(old_token, update_token);
10831
10832 PERFORM set_config('provsql.update_provenance', 'off', false);
10833 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
10834 USING new_token, old_token;
10835 PERFORM set_config('provsql.update_provenance', 'on', false);
10836 END LOOP;
10837
10838 RETURN NULL;
10839END
10840$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
10841
10842
10843/** @} */
10844
10845/** @defgroup temporal_db Temporal DB (PostgreSQL 14+)
10846 * Functions for temporal database support. These use provenance
10847 * evaluation over the multirange semiring to track temporal validity
10848 * of tuples.
10849 * @{
10850 */
10851
10852SET search_path TO provsql;
10853
10854/**
10855 * @brief Evaluate provenance over the temporal (interval-union) m-semiring
10856 *
10857 * Inputs are read as %TSTZMULTIRANGE validity intervals; the additive
10858 * identity is <tt>'{}'::%TSTZMULTIRANGE</tt> (empty), the multiplicative
10859 * identity is <tt>'{(,)}'::%TSTZMULTIRANGE</tt> (universal). Returns the union
10860 * of intervals supporting the result, computed via the compiled circuit
10861 * traversal.
10862 *
10863 * @param token Provenance token to evaluate.
10864 * @param token2value Mapping from input gates to validity multiranges.
10865 */
10866CREATE FUNCTION sr_temporal(token ANYELEMENT, token2value REGCLASS)
10867 RETURNS TSTZMULTIRANGE AS
10868$$
10869BEGIN
10870 RETURN provsql.provenance_evaluate_compiled(
10871 token,
10872 token2value,
10873 'interval_union',
10874 '{(,)}'::TSTZMULTIRANGE
10875 );
10876END
10877$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10878
10879/**
10880 * @brief Evaluate provenance over the interval-union m-semiring
10881 * with a NUMERIC multirange carrier
10882 *
10883 * Inputs are read as %nummultirange validity ranges over a NUMERIC
10884 * domain (e.g. sensor measurement-validity ranges). Addition is
10885 * multirange union, multiplication is intersection, monus is set
10886 * difference; the additive identity is <tt>'{}'::%nummultirange</tt>
10887 * and the multiplicative identity is <tt>'{(,)}'::%nummultirange</tt>
10888 * (universal range).
10889 *
10890 * @param token Provenance token to evaluate.
10891 * @param token2value Mapping from input gates to NUMERIC multiranges.
10892 */
10893CREATE FUNCTION sr_interval_num(token ANYELEMENT, token2value REGCLASS)
10894 RETURNS nummultirange AS
10895$$
10896BEGIN
10897 RETURN provsql.provenance_evaluate_compiled(
10898 token,
10899 token2value,
10900 'interval_union',
10901 '{(,)}'::nummultirange
10902 );
10903END
10904$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10905
10906/**
10907 * @brief Evaluate provenance over the interval-union m-semiring
10908 * with an int4 multirange carrier
10909 *
10910 * Inputs are read as %int4multirange validity ranges over the
10911 * integers (e.g. page or line ranges of supporting documents).
10912 * Addition is multirange union, multiplication is intersection,
10913 * monus is set difference; the additive identity is
10914 * <tt>'{}'::%int4multirange</tt> and the multiplicative identity is
10915 * <tt>'{(,)}'::%int4multirange</tt>.
10916 *
10917 * @param token Provenance token to evaluate.
10918 * @param token2value Mapping from input gates to int4 multiranges.
10919 */
10920CREATE FUNCTION sr_interval_int(token ANYELEMENT, token2value REGCLASS)
10921 RETURNS int4multirange AS
10922$$
10923BEGIN
10924 RETURN provsql.provenance_evaluate_compiled(
10925 token,
10926 token2value,
10927 'interval_union',
10928 '{(,)}'::int4multirange
10929 );
10930END
10931$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
10932
10933/**
10934 * @brief Evaluate temporal provenance as a TIMESTAMP multirange
10935 *
10936 * Thin wrapper around :sqlfunc:`sr_temporal` retained for backward
10937 * compatibility; both compute the same union of validity intervals.
10938 *
10939 * @param token provenance token to evaluate
10940 * @param token2value mapping table from tokens to temporal validity ranges
10941 */
10942CREATE OR REPLACE FUNCTION union_tstzintervals(
10943 token UUID,
10944 token2value REGCLASS
10945)
10946RETURNS TSTZMULTIRANGE AS
10947$$
10948 SELECT sr_temporal(token, token2value)
10949$$ LANGUAGE SQL PARALLEL SAFE STABLE;
10950
10951/**
10952 * @brief Query a table as it was at a specific point in time
10953 *
10954 * Returns all rows whose temporal validity includes the given TIMESTAMP.
10955 *
10956 * @param tablename name of the provenance-tracked table
10957 * @param at_time the point in time to query
10958 */
10959CREATE OR REPLACE FUNCTION timetravel(
10960 tablename TEXT,
10961 at_time TIMESTAMPTZ
10962)
10963RETURNS SETOF RECORD
10964LANGUAGE plpgsql
10965AS
10966$$
10967BEGIN
10968 RETURN QUERY EXECUTE format(
10969 '
10970 SELECT
10971 %1$I.*,
10972 sr_temporal(provenance(), %2$L)
10973 FROM
10974 %1$I
10975 WHERE
10976 sr_temporal(provenance(), %2$L) @> %3$L::TIMESTAMPTZ
10977 ',
10978 tablename,
10979 'provsql.time_validity_view',
10980 at_time::TEXT
10981 );
10982END;
10983$$;
10984
10985/**
10986 * @brief Query a table for rows valid during a time interval
10987 *
10988 * Returns all rows whose temporal validity overlaps the given range.
10989 *
10990 * @param tablename name of the provenance-tracked table
10991 * @param from_time start of the time interval
10992 * @param to_time end of the time interval
10993 */
10994CREATE OR REPLACE FUNCTION timeslice(
10995 tablename TEXT,
10996 from_time TIMESTAMPTZ,
10997 to_time TIMESTAMPTZ
10998)
10999RETURNS SETOF RECORD
11000LANGUAGE plpgsql
11001AS
11002$$
11003BEGIN
11004 RETURN QUERY EXECUTE format(
11005 '
11006 SELECT
11007 %1$I.*,
11008 sr_temporal(provenance(), %2$L)
11009 FROM
11010 %1$I
11011 WHERE
11012 sr_temporal(provenance(), %2$L)
11013 && tstzrange(%3$L::TIMESTAMPTZ, %4$L::TIMESTAMPTZ)
11014 ',
11015 tablename,
11016 'provsql.time_validity_view',
11017 from_time::TEXT,
11018 to_time::TEXT
11019 );
11020END;
11021$$;
11022
11023/**
11024 * @brief Query the full temporal history of specific rows
11025 *
11026 * Returns all versions of rows matching the given column values,
11027 * with their temporal validity ranges.
11028 *
11029 * @param tablename name of the provenance-tracked table
11030 * @param col_names array of column names to filter on
11031 * @param col_values array of corresponding values to match
11032 */
11033CREATE OR REPLACE FUNCTION history(
11034 tablename TEXT,
11035 col_names TEXT[],
11036 col_values TEXT[]
11037)
11038RETURNS SETOF RECORD
11039LANGUAGE plpgsql
11040AS
11041$$
11042DECLARE
11043 condition TEXT := '';
11044 i INT;
11045BEGIN
11046 IF array_length(col_names, 1) IS NULL
11047 OR array_length(col_values, 1) IS NULL
11048 OR array_length(col_names, 1) != array_length(col_values, 1)
11049 THEN
11050 RAISE EXCEPTION 'col_names and col_values must have the same (non-null) length';
11051 END IF;
11052
11053 FOR i IN 1..array_length(col_names, 1)
11054 LOOP
11055 IF i > 1 THEN
11056 condition := condition || ' AND ';
11057 END IF;
11058 condition := condition || format('%I = %L', col_names[i], col_values[i]);
11059 END LOOP;
11060
11061 RETURN QUERY EXECUTE format(
11062 '
11063 SELECT
11064 %I.*,
11065 sr_temporal(provenance(), %L)
11066 FROM
11067 %I
11068 WHERE
11069 %s
11070 ',
11071 tablename,
11072 'provsql.time_validity_view',
11073 tablename,
11074 condition
11075 );
11076END;
11077$$;
11078
11079/**
11080 * @brief Get the valid time range for a specific tuple
11081 *
11082 * @param token provenance token of the tuple
11083 * @param tablename name of the table containing the tuple
11084 */
11085CREATE OR REPLACE FUNCTION get_valid_time(
11086 token UUID,
11087 tablename TEXT
11088)
11089RETURNS TSTZMULTIRANGE
11090LANGUAGE plpgsql
11091AS $$
11092DECLARE
11093 result TSTZMULTIRANGE;
11094BEGIN
11095 EXECUTE format(
11096 '
11097 SELECT
11098 sr_temporal(provenance(), %L)
11099 FROM
11100 %I
11101 WHERE
11102 provsql = %L
11103 ',
11104 'provsql.time_validity_view',
11105 tablename,
11106 token
11107 )
11108 INTO result;
11109
11110 RETURN result;
11111END;
11112$$;
11113
11114/**
11115 * @brief Undo a previously recorded update operation
11116 *
11117 * Traverses all provenance-tracked tables and rewrites their circuits
11118 * to apply monus with respect to the given update token, effectively
11119 * undoing the operation.
11120 *
11121 * @param c UUID of the update operation to undo (from update_provenance)
11122 */
11123CREATE OR REPLACE FUNCTION undo(
11124 c UUID
11125)
11126RETURNS UUID
11127LANGUAGE plpgsql
11128AS $$
11129DECLARE
11130 undo_query TEXT;
11131 undone_query TEXT;
11132 undo_token UUID;
11133 schema_rec RECORD;
11134 table_rec RECORD;
11135 row_rec RECORD;
11136 new_x UUID;
11137BEGIN
11138 SELECT query INTO undone_query
11139 FROM update_provenance
11140 WHERE provsql = c
11141 LIMIT 1;
11142
11143 IF undone_query IS NULL THEN
11144 RAISE NOTICE 'Unable to find % in update_provenance', c;
11145 RETURN c;
11146 END IF;
11147
11148 SELECT query
11149 INTO undo_query
11150 FROM pg_stat_activity
11151 WHERE pid = pg_backend_pid();
11152
11153 undo_token := public.uuid_generate_v4();
11154 PERFORM create_gate(undo_token, 'update');
11155 INSERT INTO update_provenance(provsql, query, query_type, username, ts, valid_time)
11156 VALUES (
11157 undo_token,
11158 undo_query,
11159 'UNDO',
11160 current_user,
11161 CURRENT_TIMESTAMP,
11162 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL))
11163 );
11164
11165 PERFORM set_config('provsql.update_provenance', 'off', false);
11166
11167 FOR schema_rec IN
11168 SELECT nspname
11169 FROM pg_namespace
11170 WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1')
11171 LOOP
11172 FOR table_rec IN
11173 EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname)
11174 LOOP
11175 IF EXISTS (
11176 SELECT 1
11177 FROM information_schema.columns
11178 WHERE table_schema = schema_rec.nspname
11179 AND table_name = table_rec.tname
11180 AND table_name <> 'update_provenance'
11181 AND column_name = 'provsql'
11182 ) THEN
11183 FOR row_rec IN
11184 EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname)
11185 LOOP
11186 new_x := replace_the_circuit(row_rec.x, c, undo_token);
11187 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2',
11188 schema_rec.nspname, table_rec.tname)
11189 USING new_x, row_rec.x;
11190 END LOOP;
11191 END IF;
11192 END LOOP;
11193 END LOOP;
11194
11195 PERFORM set_config('provsql.update_provenance', 'on', false);
11196
11197 RETURN undo_token;
11198END;
11199$$;
11200
11201/**
11202 * @brief Recursively rewrite a circuit to undo a specific operation
11203 *
11204 * Helper for undo(). Walks the circuit and replaces occurrences of
11205 * the target update gate with its monus.
11206 *
11207 * @param x provenance token to rewrite
11208 * @param c UUID of the update operation to undo
11209 * @param u UUID of the undo operation
11210 */
11211CREATE OR REPLACE FUNCTION replace_the_circuit(
11212 x UUID,
11213 c UUID,
11214 u UUID
11215)
11216RETURNS UUID
11217LANGUAGE plpgsql
11218AS $$
11219DECLARE
11220 nchildren UUID[];
11221 child UUID;
11222 ntoken UUID;
11223 ntype PROVENANCE_GATE;
11224BEGIN
11225 IF x = c THEN
11226 RETURN provenance_monus(c, u);
11227 -- update and input gates cannot have children
11228 ELSIF get_gate_type(x) = 'update' OR get_gate_type(x) = 'input' THEN
11229 RETURN x;
11230 ELSE
11231 nchildren := '{}';
11232 FOREACH child IN ARRAY get_children(x)
11233 LOOP
11234 nchildren := array_append(nchildren, replace_the_circuit(child, c, u));
11235 END LOOP;
11236
11237 ntoken := public.uuid_generate_v4();
11238 ntype := get_gate_type(x);
11239
11240 PERFORM create_gate(ntoken, ntype, nchildren);
11241 RETURN ntoken;
11242 END IF;
11243END;
11244$$;
11245
11246-- The base validity mapping is a plain view over the data-modification log:
11247-- update_provenance is append-only and never has its provsql rewritten, so a
11248-- view stays correct (unlike a tracked table's mapping, which must be a
11249-- maintained mapping table -- see create_provenance_mapping(maintained)).
11250CREATE VIEW provsql.time_validity_view AS
11251 SELECT valid_time AS value, provsql AS provenance FROM provsql.update_provenance;
11252
11253/** @} */
11254
11255SET search_path TO public;
11256
11257-- Final constants-cache refresh: same rationale as at the end of
11258-- provsql.common.sql. On PG14+ this file is appended after the common
11259-- script, so this is the last statement of the generated install script;
11260-- the refresh must come after every object has been created for the
11261-- installing session's memoized constants to be complete.
11262SELECT provsql.reset_constants_cache();