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 );
83
84/** @defgroup gate_manipulation Circuit gate manipulation
85 * Low-level functions for creating and querying provenance circuit gates.
86 * @{
87 */
88
89/**
90 * @brief Create a new gate in the provenance circuit
91 *
92 * @param token UUID identifying the new gate
93 * @param type gate type (see PROVENANCE_GATE)
94 * @param children optional array of child gate UUIDs
95 */
96CREATE OR REPLACE FUNCTION create_gate(
97 token UUID,
98 type PROVENANCE_GATE,
99 children UUID[] DEFAULT NULL)
100 RETURNS VOID AS
101 'provsql','create_gate' LANGUAGE C PARALLEL SAFE;
102/**
103 * @brief Return the gate type of a provenance token
104 *
105 * Returns @c 'input' for any token not yet materialized in the circuit,
106 * since input is the default semantics of an unmaterialized provenance token.
107 */
108CREATE OR REPLACE FUNCTION get_gate_type(
109 token UUID)
110 RETURNS PROVENANCE_GATE AS
111 'provsql','get_gate_type' LANGUAGE C IMMUTABLE PARALLEL SAFE;
112/** @brief Return the children of a provenance gate */
113CREATE OR REPLACE FUNCTION get_children(
114 token UUID)
115 RETURNS UUID[] AS
116 'provsql','get_children' LANGUAGE C IMMUTABLE PARALLEL SAFE;
117/**
118 * @brief Set the probability of an input gate
119 *
120 * @param token UUID of the input gate
121 * @param p probability value in [0,1]
122 */
123CREATE OR REPLACE FUNCTION set_prob(
124 token UUID, p DOUBLE PRECISION)
125 RETURNS VOID AS
126 'provsql','set_prob' LANGUAGE C PARALLEL SAFE;
127/** @brief Get the probability associated with an input gate */
128CREATE OR REPLACE FUNCTION get_prob(
129 token UUID)
130 RETURNS DOUBLE PRECISION AS
131 'provsql','get_prob' LANGUAGE C STABLE PARALLEL SAFE;
132
133/**
134 * @brief Set additional INTEGER values on provenance circuit gate
135 *
136 * This function sets two INTEGER values associated to a circuit gate, used in
137 * different ways by different gate types:
138 * - for mulinput, info1 indicates the value of this multivalued variable
139 * - for eq, info1 and info2 indicate the attribute index of the
140 equijoin in, respectively, the first and second columns
141 * - for agg, info1 is the oid of the aggregate function and info2 the
142 oid of the aggregate result type
143 * - for cmp, info1 is the oid of the comparison operator
145 * @param token UUID of the circuit gate
146 * @param info1 first INTEGER value
147 * @param info2 second INTEGER value
148 */
149CREATE OR REPLACE FUNCTION set_infos(
150 token UUID, info1 INT, info2 INT DEFAULT NULL)
151 RETURNS VOID AS
152 'provsql','set_infos' LANGUAGE C PARALLEL SAFE;
153
154/** @brief Get the INTEGER info values associated with a circuit gate */
155CREATE OR REPLACE FUNCTION get_infos(
156 token UUID, OUT info1 INT, OUT info2 INT)
157 RETURNS RECORD AS
158 'provsql','get_infos' LANGUAGE C STABLE PARALLEL SAFE;
159
160/**
161 * @brief Wrap @p token in a fresh @c gate_assumed carrying @p assumption
162 * as its label, and return the wrapper's UUID.
163 *
164 * Public primitive callable from any rewrite or driver that needs to
165 * flag a sub-circuit as sound only under an evaluation assumption:
166 *
167 * - @c 'BOOLEAN' -- the sub-circuit only preserves the Boolean function
168 * of the lineage (e.g. the safe-query rewrite collapses derivation
169 * multiplicities); transparent for semirings admitting a homomorphism
170 * from Boolean functions.
171 * - @c 'absorptive' -- the sub-circuit was truncated at the absorptive
172 * value fixpoint (cyclic recursive query); transparent for absorptive
173 * semirings (probability, BOOLEAN, min-plus over nonnegative
174 * costs...), fatal for the rest (counting, why-provenance).
175 *
176 * Incompatible evaluators raise a @c CircuitException. Always kept as
177 * an explicit node in PROV-XML export.
178 *
179 * The wrapper UUID is content-derived via @c uuid_generate_v5 on the
180 * assumption and the child, so identical children always wrap to the
181 * same outer UUID per assumption. No-op (returns NULL) on a NULL
182 * input.
183 */
184CREATE OR REPLACE FUNCTION provenance_assume(token UUID, assumption TEXT)
185 RETURNS UUID AS
186$$
187DECLARE
188 wrapped UUID;
189BEGIN
190 IF token IS NULL THEN
191 RETURN NULL;
192 END IF;
193 IF assumption NOT IN ('BOOLEAN', 'absorptive') THEN
194 RAISE EXCEPTION 'provenance_assume: unknown assumption %', assumption;
195 END IF;
196 wrapped := public.uuid_generate_v5(uuid_ns_provsql(),
197 concat('assumed', assumption, token));
198 PERFORM create_gate(wrapped, 'assumed', ARRAY[token]);
199 PERFORM set_extra(wrapped, assumption);
200 RETURN wrapped;
201END
202$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
203 SECURITY DEFINER PARALLEL SAFE;
204
205/**
206 * @brief Wrap @p token in a Boolean-assumption marker (compatibility
207 * name; see @c provenance_assume).
208 */
209CREATE OR REPLACE FUNCTION assume_boolean(token UUID) RETURNS UUID AS
210$$
211SELECT provsql.provenance_assume(token, 'BOOLEAN');
212$$ LANGUAGE sql SECURITY DEFINER PARALLEL SAFE;
213
214/**
215 * @brief Wrap @p token in a fresh transparent @c gate_annotation carrying
216 * @p extra, and return the wrapper's UUID.
217 *
218 * Unlike every other gate, the annotation wrapper's UUID folds in @p extra
219 * (not just the child): @c uuid_generate_v5 over @c concat('annotation',
220 * token, extra). This is deliberate -- two annotations over the same child
221 * with different @p extra must be distinct gates (e.g. the same input tuple
222 * carrying different per-occurrence order keys, or two queries attaching
223 * different certificates to a shared root). The wrapper is transparent
224 * (identity) for EVERY evaluator; @p extra is inert metadata read only by the
225 * code that placed it. No-op (returns NULL) on a NULL input.
226 */
227CREATE OR REPLACE FUNCTION annotate(token UUID, extra TEXT) RETURNS UUID AS
228$$
229DECLARE
230 annotated UUID;
231BEGIN
232 IF token IS NULL THEN
233 RETURN NULL;
234 END IF;
235 annotated := public.uuid_generate_v5(uuid_ns_provsql(),
236 concat('annotation', token, extra));
237 PERFORM create_gate(annotated, 'annotation', ARRAY[token]);
238 PERFORM set_extra(annotated, extra);
239 RETURN annotated;
240END
241$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
242 SECURITY DEFINER PARALLEL SAFE;
243
244/**
245 * @brief Condition a provenance token (a Boolean event) on another.
246 *
247 * Builds the terminal @c gate_conditioned that the measure evaluators read
248 * as @c "P(target ∧ evidence) / P(evidence)". This is the backing function
249 * of the binary @c | operator (@c "target | evidence", value-level
250 * conditioning of the UUID carrier).
251 *
252 * The gate stores three children @c [target, evidence, joint] with
253 * @c joint @c = @c times(target, @c evidence); evaluation is then the plain
254 * ratio @c P(joint)/P(evidence), and content-addressing makes a base tuple
255 * shared by @p target and @p evidence the same input gate in both circuits,
256 * so the conditional is exact and correlation-aware.
257 *
258 * Conventions:
259 * - Conditioning on a certain or absent event is a no-op: @c evidence NULL
260 * or @c gate_one() returns @p target unchanged (@c "P(X|true)=P(X)").
261 * - A @p target with no provenance defaults to the certain event 1, so
262 * @c "1 | c" is the well-defined certain-row posterior.
263 * - Nested conditioning folds (sequential Bayesian update):
264 * @c "(X | A) | B = X | (A ∧ B)" -- the gate never nests, it stays one
265 * level deep with the evidence accumulated by @c times.
266 *
267 * The result is TERMINAL: a conditioned token may not become a child of a
268 * @c plus / @c times / @c monus / @c agg gate (those constructors refuse
269 * it); the only operation it admits is more conditioning.
270 */
271CREATE OR REPLACE FUNCTION cond(target UUID, evidence UUID) RETURNS UUID AS
272$$
273DECLARE
274 tgt UUID;
275 ev UUID;
276 jnt UUID;
277 result UUID;
278 ch UUID[];
279BEGIN
280 -- P(X | true) = P(X): conditioning on a certain / absent event is inert.
281 IF evidence IS NULL OR evidence = gate_one() THEN
282 RETURN target;
283 END IF;
284
285 -- A row with no provenance defaults to the certain event 1.
286 tgt := coalesce(target, gate_one());
287
288 IF get_gate_type(tgt) = 'conditioned' THEN
289 -- Sequential update (X | A) | B = X | (A ∧ B): fold B into both the
290 -- evidence and the joint of the inner gate so the result stays a single
291 -- gate_conditioned over the ORIGINAL target.
292 ch := get_children(tgt);
293 tgt := ch[1]; -- original target X
294 ev := provenance_times(ch[2], evidence); -- A ∧ B
295 jnt := provenance_times(ch[3], evidence); -- (X ∧ A) ∧ B
296 ELSE
297 ev := evidence;
298 jnt := provenance_times(tgt, evidence); -- X ∧ C
299 END IF;
300
301 result := public.uuid_generate_v5(uuid_ns_provsql(),
302 concat('conditioned', tgt, ev, jnt));
303 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev, jnt]);
304 RETURN result;
305END
306$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
307 SECURITY DEFINER PARALLEL SAFE;
308
309/**
310 * @brief Binary @c | : value-level conditioning, @c "target | evidence".
311 *
312 * Carrier-parametric in its left operand; the UUID form builds the terminal
313 * @c gate_conditioned via @c cond. Does not collide with core PostgreSQL's
314 * INTEGER bitwise @c | (different argument types).
315 */
316CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=UUID, PROCEDURE=cond);
317
318/**
319 * @brief Placeholder for @c "X | (predicate)" on a UUID event.
320 *
321 * Lets the conditioning event be written as a natural Boolean combination of
322 * random_variable / aggregate comparisons (e.g. @c "event | (sensor > 3)")
323 * instead of a hand-built gate. Never executes: the ProvSQL planner hook
324 * converts the Boolean operand into a condition gate and emits @c cond.
325 */
326CREATE OR REPLACE FUNCTION cond_predicate(target UUID, predicate BOOLEAN)
327 RETURNS UUID AS
328$$
329BEGIN
330 RAISE EXCEPTION 'UUID | (predicate) must be rewritten by the ProvSQL '
331 'planner hook: the right operand must be a Boolean combination of '
332 'random_variable / aggregate comparisons (is provsql.active off?)';
333END
334$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
335
336CREATE OPERATOR | (LEFTARG=UUID, RIGHTARG=BOOLEAN, PROCEDURE=cond_predicate);
337
338/**
339 * @brief Deterministic indicator gate for an ordinary (regular) comparison.
340 *
341 * The predicate-provenance of an ordinary comparison (both sides of regular
342 * type, e.g. @c "region = 'north'") is the deterministic indicator
343 * @c "χ(cond)": @c gate_one() when the comparison holds on the row,
344 * @c gate_zero() otherwise (Definition in the HAVING-provenance semantics).
345 * The planner emits this for a regular comparison appearing inside a MIXED
346 * conditioning predicate (one that also has a random_variable / aggregate
347 * comparison); @c cond is evaluated per row, so the indicator is the row's
348 * own truth value, combined by @c ⊗ / @c ⊕ with the probabilistic gates.
349 */
350CREATE OR REPLACE FUNCTION regular_indicator(cond BOOLEAN) RETURNS UUID AS
351$$
352 SELECT CASE WHEN cond THEN provsql.gate_one() ELSE provsql.gate_zero() END;
353$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
354
355/**
356 * @brief Whole-tuple output conditioning directive: @c "given(evidence)".
357 *
358 * Written as a term in the select list, @c given(c) conditions the OUTPUT
359 * provenance of the current query's rows on @p c:
360 *
361 * @code
362 * SELECT a, b, given((SELECT provenance() FROM tests
363 * WHERE patient_id = s.id AND result = 'positive'))
364 * FROM source s;
365 * -- visible columns: a, b (the given(...) term is stripped)
366 * -- per-row output provenance: provenance() | <that row's evidence>
367 * @endcode
368 *
369 * The query rewriter recognises the marker, STRIPS it from the visible
370 * projection, and wraps each output row's provenance expression in
371 * @c cond(row_provenance, c) -- deriving a new conditioned relation, never
372 * mutating any stored provenance. @p c is evaluated per output row and may
373 * correlate with the row's columns, so each tuple is conditioned on its own
374 * evidence. When the rewriter is inactive the call is a harmless identity
375 * (it returns @p evidence as an ordinary column).
376 */
377CREATE OR REPLACE FUNCTION given(evidence UUID) RETURNS UUID AS
378$$
379 SELECT evidence;
380$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
381
382/**
383 * @brief Prefix unary @c | : alias for @c given, @c "| evidence".
384 *
385 * Disambiguated from the binary @c | by the absence of a left operand
386 * (@c "a, | c" parses @c "| c" as the prefix form). PostgreSQL keeps
387 * prefix operators on every supported version (postfix operators were
388 * removed in PG14), so @c "| c" is safe across the CI matrix.
389 */
390CREATE OPERATOR | (RIGHTARG=UUID, PROCEDURE=given);
391
392/**
393 * @brief Placeholder for the prefix @c "| (predicate)" whole-tuple form.
394 *
395 * Lets the whole-tuple conditioning event be a natural Boolean predicate
396 * (e.g. @c "SELECT a, | (sensor > 3) FROM readings") instead of a hand-built
397 * gate. Never executes: the planner converts the Boolean operand into a
398 * condition gate and emits @c given, which the rewriter then strips and folds
399 * into each output row's provenance.
400 */
401CREATE OR REPLACE FUNCTION given_predicate(predicate BOOLEAN) RETURNS UUID AS
402$$
403BEGIN
404 RAISE EXCEPTION 'prefix | (predicate) must be rewritten by the ProvSQL '
405 'planner hook: the operand must be a Boolean combination of '
406 'random_variable / aggregate comparisons (is provsql.active off?)';
407END
408$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
409
410CREATE OPERATOR | (RIGHTARG=BOOLEAN, PROCEDURE=given_predicate);
411
412/**
413 * @brief Event negation: @c "! event" / @c "provenance_not(event)".
414 *
415 * The complement of a Boolean provenance event: @c "!x" holds in exactly the
416 * worlds where @p x does not. It is sugar for @c "monus(one, x)" -- an
417 * ordinary m-semiring expression (Boolean @c NOT, probability @c "1 - P(x)"),
418 * NOT a measure-only marker -- so it composes like any @c monus, and a
419 * conditioned / terminal token is refused as its child (so @c "!(x | c)"
420 * errors, as conditioning cannot be buried under further algebra).
421 *
422 * The motivating use is conditioning on the NON-occurrence of an arbitrary
423 * violation query @p W (a denial constraint), where @p W itself is built with
424 * ordinary idioms and needs no hand-rolled gates:
425 *
426 * @code
427 * -- W = "some pair of overlapping same-room bookings is present"
428 * WITH w AS (SELECT provenance() AS tok
429 * FROM bookings a JOIN bookings b
430 * ON a.id < b.id AND a.room = b.room
431 * AND a.lo < b.hi AND b.lo < a.hi
432 * GROUP BY ())
433 * SELECT probability_evaluate((SELECT provenance() FROM bookings WHERE id=1)
434 * | !w.tok) -- P(booking 1 | no overlap)
435 * FROM w;
436 * @endcode
437 *
438 * Named @c provenance_not, after the @c "provenance_times / _plus / _monus"
439 * family; the prefix @c ! operator is the ergonomic form (SQL's reserved
440 * @c NOT keyword cannot serve as a function name).
441 */
442CREATE OR REPLACE FUNCTION provenance_not(event UUID) RETURNS UUID AS
444 SELECT provsql.provenance_monus(provsql.gate_one(), event);
445$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
446 SET search_path=provsql,pg_temp,public;
447
448/**
449 * @brief Prefix unary @c ! : alias for @c provenance_not, @c "! event".
450 *
451 * Prefix operators are kept on every supported PostgreSQL version (postfix
452 * operators were removed in PG14), and core PG defines no prefix @c ! on
453 * @c UUID, so @c "! event" is safe across the CI matrix.
454 */
455CREATE OPERATOR ! (RIGHTARG=UUID, PROCEDURE=provenance_not);
456
457/**
458 * @brief Build a per-input order-key string for the inversion-free path.
460 * Emitted by the planner per certified atom: @c K-prefixed, length-prefixed
461 * @c "K<factor> <octet_length(root)>:<root><octet_length(sec)>:<sec>", parsed
462 * back at evaluation by @c safe_cert_key_parse. @p root / @p sec are the
463 * tuple's root- and secondary-class column values (TEXT-cast by the caller);
464 * the byte-length prefixes keep the values unambiguous for @em any column type,
465 * including TEXT containing spaces, colons or digits. @p factor is the atom's
466 * factor id (or -1 for the shared self-join guard). @c IMMUTABLE so the planner
467 * can fold it and the marker dedups by content-addressing.
468 */
469CREATE OR REPLACE FUNCTION inversion_free_key(root TEXT, sec TEXT, factor INT)
470 RETURNS TEXT AS
471$$ SELECT 'K' || factor::TEXT || ' '
472 || octet_length(root) || ':' || root
473 || octet_length(sec) || ':' || sec $$
474 LANGUAGE sql IMMUTABLE PARALLEL SAFE;
475
476/**
477 * @brief Set extra TEXT information on provenance circuit gate
479 * This function sets TEXT-encoded data associated to a circuit gate, used in
480 * different ways by different gate types:
481 * - for project, it is a TEXT-encoded ARRAY of two-element ARRAYs that
482 * indicate mappings between input attribute (first element) and output
483 * attribute (second element)
484 * - for value and agg, it is the TEXT-encoded (base for value, computed
485 * for agg) scalar value
486 *
487 * @param token UUID of the circuit gate
488 * @param data TEXT-encoded information
489 */
490CREATE OR REPLACE FUNCTION set_extra(
491 token UUID, data TEXT)
492 RETURNS VOID AS
493 'provsql','set_extra' LANGUAGE C PARALLEL SAFE STRICT;
494/** @brief Get the TEXT-encoded extra data associated with a circuit gate */
495CREATE OR REPLACE FUNCTION get_extra(token UUID)
496 RETURNS TEXT AS
497 'provsql','get_extra' LANGUAGE C STABLE PARALLEL SAFE RETURNS NULL ON NULL INPUT;
498
499/**
500 * @brief Return the total number of materialized gates in the provenance circuit
501 *
502 * Input gates for provenance-tracked table rows are created lazily on
503 * first reference; rows that have never appeared in a query result are
504 * not counted.
505 */
506CREATE OR REPLACE FUNCTION get_nb_gates() RETURNS BIGINT AS
507 'provsql', 'get_nb_gates' LANGUAGE C PARALLEL SAFE;
508
509/** @} */
510
511/** @defgroup table_management Provenance table management
512 * Functions for enabling, disabling, and configuring provenance
513 * tracking on user tables.
514 * @{
515 */
516
517
518/**
519 * @brief Trigger function for DELETE statement provenance tracking
520 *
521 * Records the deletion and applies monus to provenance tokens of
522 * deleted rows. This is the version for PostgreSQL < 14.
523 */
524CREATE OR REPLACE FUNCTION delete_statement_trigger()
525 RETURNS TRIGGER AS
526$$
527DECLARE
528 query_text TEXT;
529 delete_token UUID;
530 old_token UUID;
531 new_token UUID;
532 r RECORD;
533BEGIN
534 delete_token := public.uuid_generate_v4();
535
536 PERFORM create_gate(delete_token, 'input');
537
538 SELECT query
539 INTO query_text
540 FROM pg_stat_activity
541 WHERE pid = pg_backend_pid();
542
543 INSERT INTO delete_provenance (delete_token, query, deleted_by, deleted_at)
544 VALUES (delete_token, query_text, current_user, CURRENT_TIMESTAMP);
545
546 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
548 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
549 old_token := r.provsql;
550 new_token := provenance_monus(old_token, delete_token);
551
552 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
553 USING new_token, old_token;
554 END LOOP;
555
556 RETURN NULL;
557END
558$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
559
560
561/**
562 * @brief Record per-relation provenance metadata used by the
563 * safe-query optimisation.
564 *
565 * Stores a @c (relid, kind, block_key) RECORD in the persistent
566 * mmap-backed table-info store. @p kind is one of:
567 * - @c 'tid' -- independent input leaves (post-@c add_provenance default)
568 * - @c 'bid' -- block-correlated leaves; rows sharing the same value
569 * of @p block_key are mutually exclusive. An empty
570 * @p block_key means the whole table is one block.
571 * - @c 'opaque' -- arbitrary correlations from a derived source
572 * (CREATE TABLE AS SELECT, INSERT INTO SELECT,
573 * UPDATE under provsql.update_provenance); the
574 * safe-query rewriter must bail on these.
575 *
576 * @param relid pg_class OID of the relation.
577 * @param kind One of @c 'tid' / @c 'bid' / @c 'opaque'.
578 * @param block_key Block-key column numbers (only meaningful for
579 * @c 'bid'; ignored otherwise but conventionally
580 * passed empty).
581 */
582CREATE OR REPLACE FUNCTION set_table_info(
583 relid OID, kind TEXT, block_key INT2[] DEFAULT ARRAY[]::INT2[])
584 RETURNS VOID AS
585 'provsql','set_table_info' LANGUAGE C PARALLEL SAFE;
586
587/** @brief Remove per-relation provenance metadata. No-op when missing. */
588CREATE OR REPLACE FUNCTION remove_table_info(relid OID)
589 RETURNS VOID AS
590 'provsql','remove_table_info' LANGUAGE C PARALLEL SAFE;
591
592/**
593 * @brief Read per-relation provenance metadata.
594 *
595 * Returns NULL if no RECORD exists. @c kind is one of @c 'tid' /
596 * @c 'bid' / @c 'opaque'; @c block_key is the (possibly empty) array
597 * of block-key column numbers, only meaningful when @c kind = @c 'bid'.
598 * Used by the planner-time hierarchy detector to gate the safe-query
599 * rewrite.
600 */
601CREATE OR REPLACE FUNCTION get_table_info(
602 relid OID, OUT kind TEXT, OUT block_key INT2[])
603 RETURNS RECORD AS
604 'provsql','get_table_info' LANGUAGE C STABLE PARALLEL SAFE;
606/**
607 * @brief Record the base-relation ancestor set of a tracked relation.
608 *
609 * Base tables created with @c add_provenance / @c repair_key carry
610 * @c {self}; CTAS-derived tables inherit the union of their sources'
611 * ancestor sets. The safe-query rewriter consults the registry to
612 * enforce that joined FROM entries have disjoint base ancestors
613 * before firing the read-once factoring.
614 *
615 * The worker preserves the relation's existing @c kind / @c block_key
616 * half on update; it silently no-ops when no kind RECORD exists for
617 * @p relid (callers should run @c add_provenance / @c repair_key
618 * first). The ancestor list is capped at 64 entries (clear error if
619 * exceeded).
620 *
621 * @param relid pg_class OID of the relation.
622 * @param ancestors Sorted, deduplicated base-relation OIDs.
623 */
624CREATE OR REPLACE FUNCTION set_ancestors(
625 relid OID, ancestors OID[] DEFAULT ARRAY[]::OID[])
626 RETURNS VOID AS
627 'provsql','set_ancestors' LANGUAGE C PARALLEL SAFE;
628
629/** @brief Clear the ancestor half of a per-relation RECORD (keeps kind/block_key).
630 * No-op when missing. */
631CREATE OR REPLACE FUNCTION remove_ancestors(relid OID)
632 RETURNS VOID AS
633 'provsql','remove_ancestors' LANGUAGE C PARALLEL SAFE;
634
635/**
636 * @brief Read the base-relation ancestor set of a tracked relation.
637 *
638 * Returns @c NULL when no ancestor RECORD exists for @p relid (or the
639 * RECORD is empty -- both cases make the safe-query rewriter take
640 * its conservative refuse path, so they collapse here).
641 */
642CREATE OR REPLACE FUNCTION get_ancestors(relid OID)
643 RETURNS OID[] AS
644 'provsql','get_ancestors' LANGUAGE C STABLE PARALLEL SAFE;
645
646/**
647 * @brief BEFORE INSERT OR UPDATE OF provsql row trigger installed by
648 * @c add_provenance.
649 *
650 * Two jobs:
651 *
652 * 1. Fill @c NEW.provsql with a fresh @c uuid_generate_v4 leaf when
653 * the user did not supply one (a column DEFAULT would not do here:
654 * it fires before the trigger sees the row, so we could not tell
655 * "user omitted the column" from "user supplied a value").
656 * 2. When the user does supply a non-NULL @c provsql on @c INSERT,
657 * or changes it on @c UPDATE, flip the table's per-table
658 * metadata to @c OPAQUE. The user is free to write whatever
659 * UUIDs they want (cross-table reuse, compound tokens minted
660 * via @c create_gate, ...); the cost is that the safe-query
661 * rewriter then refuses to fire on this table, because TID
662 * independence can no longer be assumed.
663 */
664CREATE OR REPLACE FUNCTION provenance_guard()
665 RETURNS TRIGGER AS $$
666BEGIN
667 IF TG_OP = 'INSERT' THEN
668 IF NEW.provsql IS NULL THEN
669 NEW.provsql := public.uuid_generate_v4();
670 ELSE
671 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
672 END IF;
673 ELSIF TG_OP = 'UPDATE' THEN
674 IF NEW.provsql IS DISTINCT FROM OLD.provsql THEN
675 PERFORM provsql.set_table_info(TG_RELID, 'opaque');
676 END IF;
677 END IF;
678 RETURN NEW;
679END;
680$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
681 SECURITY DEFINER;
682
683/**
684 * @brief Enable provenance tracking on an existing table
685 *
686 * Adds a <tt>provsql</tt> UUID column to the table, an index for
687 * fast UUID-keyed lookups, and a BEFORE INSERT/UPDATE row trigger
688 * (@c provenance_guard) that mints a fresh @c uuid_generate_v4
689 * leaf when the user omits the column on INSERT, or flips the
690 * table's metadata to @c OPAQUE when the user supplies their own
691 * value. Input gates for existing rows are created lazily when
692 * first referenced by a query.
693 *
694 * @param _tbl the table to add provenance tracking to
695 */
696CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
697 RETURNS VOID AS
698$$
699BEGIN
700 -- Idempotence: a second add_provenance on an already-tracked table is
701 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
702 -- re-run freely.
703 IF EXISTS (
704 SELECT 1 FROM pg_attribute
705 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
706 ) THEN
707 RAISE NOTICE 'table % already has provenance tracking', _tbl;
708 RETURN;
709 END IF;
710 -- No DEFAULT: the guard trigger mints the UUID, so the trigger can
711 -- distinguish "user omitted" (NULL) from "user supplied a value".
712 -- No UNIQUE: we no longer rely on it to keep the table TID -- the
713 -- guard does that semantically -- and a UNIQUE would reject the
714 -- legitimate cross-table UUID copy that just flips the table to
715 -- OPAQUE. We keep a plain index for fast UUID-keyed lookups.
716 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
717 EXECUTE format(
718 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
719 _tbl);
720 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
721 EXECUTE format(
722 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
723 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
724 _tbl);
725 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
726 -- Seed the base-ancestor set to {self}: a base TID table's atoms
727 -- come from itself and no other relation. CTAS-derived tables
728 -- inherit unions of source ancestor sets; that is handled by the
729 -- CTAS hook (a separate slice), not here.
730 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
731END
732$$ LANGUAGE plpgsql SECURITY DEFINER;
733
734/**
735 * @brief Remove provenance tracking from a table
736 *
737 * Drops the <tt>provsql</tt> column and associated triggers.
738 *
739 * @param _tbl the table to remove provenance tracking from
740 */
741CREATE OR REPLACE FUNCTION remove_provenance(_tbl REGCLASS)
742 RETURNS VOID AS
743$$
744DECLARE
745BEGIN
746 PERFORM provsql.remove_table_info(_tbl::oid);
747 -- Drop the BEFORE INSERT/UPDATE guard first: it has a column
748 -- dependency on provsql (via the OF provsql clause), so the
749 -- subsequent DROP COLUMN would otherwise raise.
750 BEGIN
751 EXECUTE format('DROP TRIGGER provenance_guard on %s', _tbl);
752 EXCEPTION WHEN undefined_object THEN
753 END;
754 EXECUTE format('ALTER TABLE %s DROP COLUMN provsql', _tbl);
755 BEGIN
756 EXECUTE format('DROP TRIGGER add_gate on %s', _tbl);
757 EXCEPTION WHEN undefined_object THEN
758 END;
759 BEGIN
760 EXECUTE format('DROP TRIGGER insert_statement on %s', _tbl);
761 EXECUTE format('DROP TRIGGER update_statement on %s', _tbl);
762 EXECUTE format('DROP TRIGGER delete_statement on %s', _tbl);
763 EXCEPTION WHEN undefined_object THEN
764 END;
765END
766$$ LANGUAGE plpgsql;
767
768/**
769 * @brief Set up provenance for a table with duplicate key values
770 *
771 * When a table has duplicate rows for a given key, this function
772 * replaces simple input gates with multivalued input (mulinput) gates
773 * that model a uniform distribution over duplicates.
774 *
775 * @param _tbl the table to repair
776 * @param key_att the key attribute(s) as a comma-separated string, or
777 * empty string if the whole table is one group
778 */
779CREATE OR REPLACE FUNCTION repair_key(_tbl REGCLASS, key_att TEXT)
780 RETURNS VOID AS
781$$
782DECLARE
783 r RECORD;
784 rows_query TEXT;
785 block_key_cols INT2[];
786BEGIN
787 -- Resolve the (possibly comma-separated) key_att TEXT into the
788 -- corresponding pg_attribute.attnum values for the safe-query
789 -- metadata. Names are trimmed; quoting is not supported because
790 -- repair_key has never accepted quoted identifiers in key_att.
791 IF key_att = '' THEN
792 block_key_cols := ARRAY[]::INT2[];
793 ELSE
794 SELECT array_agg(a.attnum ORDER BY t.ord)::INT2[]
795 INTO block_key_cols
796 FROM unnest(string_to[](key_att, ',')) WITH ORDINALITY AS t(name, ord)
797 JOIN pg_attribute a
798 ON a.attrelid = _tbl
799 AND a.attname = trim(t.name)
800 AND a.attnum > 0
801 AND NOT a.attisdropped;
802 IF block_key_cols IS NULL OR array_length(block_key_cols, 1) IS NULL THEN
803 RAISE EXCEPTION 'repair_key: could not resolve key columns from "%"', key_att;
804 END IF;
805 IF array_length(block_key_cols, 1) > 16 THEN
806 RAISE EXCEPTION 'repair_key: block key wider than 16 columns is not supported';
807 END IF;
808 END IF;
809
810 -- Same column shape as add_provenance: no UNIQUE, no DEFAULT past
811 -- the initial backfill (the guard trigger added after the rename
812 -- takes over both jobs once the column has been renamed to its
813 -- final name). The DEFAULT is kept here only so the second pass
814 -- below can read provsql_temp from the user-visible rows
815 -- without a separate UPDATE.
816 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql_temp UUID DEFAULT public.uuid_generate_v4()', _tbl);
817
818 -- Build a per-group mapping (key columns + a fresh key_token + the
819 -- group size) once, then use it for both the create_gate(key_token,
820 -- 'input') first pass and the per-row mulinput second pass. Going
821 -- through a temp table avoids re-running uuid_generate_v4() (which
822 -- would produce different UUIDs the second time). USING (%1$s) on
823 -- the second pass handles the multi-column case uniformly.
824 -- ON COMMIT DROP plus the explicit DROP TABLE at the end of this
825 -- function leave the temp table cleaned up across transactions and
826 -- across repeated calls in the same transaction.
827 IF key_att = '' THEN
828 EXECUTE format(
829 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
830 SELECT public.uuid_generate_v4() AS provsql_key_token,
831 COUNT(*) AS provsql_group_size
832 FROM %s', _tbl);
833 rows_query := format(
834 'SELECT t.provsql_temp,
835 k.provsql_key_token AS key_token,
836 ROW_NUMBER() OVER (ORDER BY t.ctid) AS within_group,
837 k.provsql_group_size AS group_size
838 FROM %s t CROSS JOIN provsql_repair_key_tmp k', _tbl);
839 ELSE
840 EXECUTE format(
841 'CREATE TEMP TABLE provsql_repair_key_tmp ON COMMIT DROP AS
842 SELECT %1$s,
843 public.uuid_generate_v4() AS provsql_key_token,
844 COUNT(*) AS provsql_group_size
845 FROM %2$s
846 GROUP BY %1$s', key_att, _tbl);
847 rows_query := format(
848 'SELECT t.provsql_temp,
849 k.provsql_key_token AS key_token,
850 ROW_NUMBER() OVER (PARTITION BY k.provsql_key_token
851 ORDER BY t.ctid) AS within_group,
852 k.provsql_group_size AS group_size
853 FROM %2$s t
854 JOIN provsql_repair_key_tmp k USING (%1$s)', key_att, _tbl);
855 END IF;
856
857 -- Pass 1: one input gate per group key.
858 FOR r IN SELECT provsql_key_token FROM provsql_repair_key_tmp LOOP
859 PERFORM provsql.create_gate(r.provsql_key_token, 'input');
860 END LOOP;
861
862 -- Pass 2: per row, attach a mulinput gate to its group's key token.
863 FOR r IN EXECUTE rows_query LOOP
864 PERFORM provsql.create_gate(r.provsql_temp, 'mulinput', ARRAY[r.key_token]);
865 PERFORM provsql.set_prob(r.provsql_temp, 1./r.group_size);
866 PERFORM provsql.set_infos(r.provsql_temp, r.within_group::INT);
867 END LOOP;
868
869 DROP TABLE provsql_repair_key_tmp;
870
871 EXECUTE format('ALTER TABLE %s ALTER COLUMN provsql_temp DROP DEFAULT', _tbl);
872 EXECUTE format('ALTER TABLE %s RENAME COLUMN provsql_temp TO provsql', _tbl);
873 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
874 EXECUTE format(
875 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
876 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
877 _tbl);
878 PERFORM provsql.set_table_info(_tbl::oid, 'bid', block_key_cols);
879 -- Base BID tables also have themselves as their sole ancestor. Same
880 -- rationale as the @c add_provenance branch above.
881 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
882END
883$$ LANGUAGE plpgsql;
884
885/**
886 * @brief Event trigger that purges per-table provenance metadata when
887 * a tracked relation is dropped outside of remove_provenance().
888 *
889 * Plain DROP TABLE bypasses remove_provenance() and would otherwise
890 * leave a stale entry in the table-info store keyed by a now-recycled
891 * OID, with confusing consequences for the safe-query rewriter the
892 * next time the OID is reused. This trigger forwards every dropped
893 * relation OID to provsql.remove_table_info(), which is a no-op for
894 * relations that were not tracked.
895 */
896CREATE OR REPLACE FUNCTION cleanup_table_info()
897 RETURNS event_trigger AS
898$$
899DECLARE
900 r RECORD;
901BEGIN
902 FOR r IN
903 SELECT objid FROM pg_event_trigger_dropped_objects()
904 WHERE object_type IN ('table', 'foreign table', 'materialized view')
905 LOOP
906 PERFORM provsql.remove_table_info(r.objid);
907 END LOOP;
908END
909$$ LANGUAGE plpgsql;
910
911DROP EVENT TRIGGER IF EXISTS provsql_cleanup_table_info;
912-- @c EXECUTE @c PROCEDURE (rather than the PG 11+ @c EXECUTE
913-- @c FUNCTION alias) so the extension installs on PG 10 too.
914CREATE EVENT TRIGGER provsql_cleanup_table_info ON sql_drop
915 EXECUTE PROCEDURE provsql.cleanup_table_info();
916
917/**
918 * @brief Create a provenance mapping table from an attribute
919 *
920 * Creates a new table mapping provenance tokens to values of a given
921 * attribute, for use with semiring evaluation functions.
922 * Idempotent: if the mapping table already exists, raises a NOTICE and
923 * changes nothing (drop it first to rebuild).
924 *
925 * @param newtbl name of the mapping table to create
926 * @param oldtbl source table with provenance tracking
927 * @param att attribute whose values populate the mapping
928 * @param preserve_case if true, quote the table name to preserve case
929 */
930CREATE OR REPLACE FUNCTION create_provenance_mapping(
931 newtbl TEXT,
932 oldtbl REGCLASS,
933 att TEXT,
934 preserve_case BOOL DEFAULT 'f'
935) RETURNS VOID AS
937DECLARE
938BEGIN
939 -- Idempotence: when the mapping table already exists, leave it alone
940 -- with a NOTICE (re-runnable setup scripts / notebook cells). Drop it
941 -- first to rebuild a stale mapping.
942 IF (CASE WHEN preserve_case THEN to_regclass(format('%I', newtbl))
943 ELSE to_regclass(newtbl) END) IS NOT NULL THEN
944 RAISE NOTICE 'mapping table % already exists', newtbl;
945 RETURN;
946 END IF;
947 -- ON COMMIT DROP only fires at COMMIT: several mapping creations in
948 -- one transaction (a notebook cell, a setup script run via psql -1)
949 -- would otherwise collide on the leftover temp table. The to_regclass
950 -- probe (rather than DROP IF EXISTS) keeps the first call NOTICE-free.
951 IF to_regclass('pg_temp.tmp_provsql') IS NOT NULL THEN
952 DROP TABLE tmp_provsql;
953 END IF;
954 EXECUTE format('CREATE TEMP TABLE tmp_provsql ON COMMIT DROP AS TABLE %s', oldtbl);
955 ALTER TABLE tmp_provsql RENAME provsql TO provenance;
956 IF preserve_case THEN
957 EXECUTE format('CREATE TABLE %I AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
958 EXECUTE format('CREATE INDEX ON %I(provenance)', newtbl);
959 ELSE
960 EXECUTE format('CREATE TABLE %s AS SELECT %s AS value, provenance FROM tmp_provsql', newtbl, att);
961 EXECUTE format('CREATE INDEX ON %s(provenance)', newtbl);
962 END IF;
963END
964$$ LANGUAGE plpgsql;
965
966/**
967 * @brief Create a view mapping provenance tokens to attribute values
968 *
969 * Like create_provenance_mapping but creates a view instead of a table,
970 * so it always reflects the current state of the source table.
971 *
972 * @param newview name of the view to create
973 * @param oldtbl source table with provenance tracking
974 * @param att attribute whose values populate the mapping
975 * @param preserve_case if true, quote the view name to preserve case
976 */
977CREATE OR REPLACE FUNCTION create_provenance_mapping_view(
978 newview TEXT,
979 oldtbl REGCLASS,
980 att TEXT,
981 preserve_case BOOL DEFAULT false
982)
983RETURNS VOID
984LANGUAGE plpgsql
985AS
986$$
987BEGIN
988 IF preserve_case THEN
989 EXECUTE format(
990 'CREATE OR REPLACE VIEW %I AS SELECT %s AS value, provsql AS provenance FROM %s',
991 newview,
992 att,
993 oldtbl
994 );
995 ELSE
996 EXECUTE format(
997 'CREATE OR REPLACE VIEW %s AS SELECT %s AS value, provsql AS provenance FROM %s',
998 newview,
999 att,
1000 oldtbl
1002 END IF;
1003END;
1004$$;
1005
1006/** @} */
1008/** @defgroup internal_constants Internal constants
1009 * UUID namespace and identity element functions used for
1010 * deterministic gate generation.
1011 * @{
1012 */
1013
1014/** @brief Return the ProvSQL UUID namespace (used for deterministic gate UUIDs) */
1015CREATE OR REPLACE FUNCTION uuid_ns_provsql() RETURNS UUID AS
1016$$
1017 -- uuid_generate_v5(uuid_ns_url(),'http://pierre.senellart.com/software/provsql/')
1018 SELECT '920d4f02-8718-5319-9532-d4ab83a64489'::UUID
1019$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1020
1021/** @brief Return the UUID of the semiring zero gate */
1022CREATE OR REPLACE FUNCTION gate_zero() RETURNS UUID AS
1023$$
1024 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'zero');
1025$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1026
1027/** @brief Return the UUID of the semiring one gate */
1028CREATE OR REPLACE FUNCTION gate_one() RETURNS UUID AS
1029$$
1030 SELECT public.uuid_generate_v5(provsql.uuid_ns_provsql(),'one');
1031$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1032
1033/** @brief Return the epsilon threshold used for probability comparisons */
1034CREATE OR REPLACE FUNCTION epsilon() RETURNS DOUBLE PRECISION AS
1036 SELECT CAST(0.001 AS DOUBLE PRECISION)
1037$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
1038
1039/** @} */
1040
1041/** @defgroup semiring_operations Semiring operations
1042 * Functions that build provenance circuit gates for semiring operations.
1043 * These are called internally by the query rewriter.
1044 * @{
1045 */
1046
1047/**
1048 * @brief Create a times (product) gate from multiple provenance tokens
1049 *
1050 * Filters out NULL and one-gates; returns gate_one() if all tokens
1051 * are trivial, or a single token if only one remains.
1052 *
1053 * Before creating an ordinary gate, the *times-canonical* address of
1054 * the surviving multiset -- @c uuid5('times-canonical{sorted tokens}')
1055 * -- is probed: the reachability rewriter pre-creates there, for
1056 * self-join conjunctions of reachability tokens, a certified
1057 * equivalent (the all-members-reachable circuit; see
1058 * @c plant_reach_cover). Ordinary creation never writes under that
1059 * recipe, so a hit is always a deliberate plant; the ordinary
1060 * order-dependent recipe is used otherwise, so ordinary
1061 * times gates (and their formula rendering) are untouched.
1062 */
1063CREATE OR REPLACE FUNCTION provenance_times(VARIADIC tokens UUID[])
1064 RETURNS UUID AS
1065$$
1066DECLARE
1067 times_token UUID;
1068 filtered_tokens UUID[];
1069 canonical UUID;
1070BEGIN
1071 SELECT array_agg(t) FROM unnest(tokens) t WHERE t IS NOT NULL AND t <> gate_one() INTO filtered_tokens;
1072
1073 -- Dispatch on the FILTERED count: a single survivor short-circuits
1074 -- to that token directly (no useless single-child times gate); zero
1075 -- survivors collapse to the identity. Using array_length(tokens, 1)
1076 -- here would miss the [one, cmp] → [cmp] case, leaving the cmp wrapped
1077 -- in a one-child times when its only sibling was gate_one().
1078 CASE coalesce(array_length(filtered_tokens, 1), 0)
1079 WHEN 0 THEN
1080 times_token:=gate_one();
1081 WHEN 1 THEN
1082 times_token:=filtered_tokens[1];
1083 ELSE
1084 -- Computed separately from the filtering aggregate above: an
1085 -- ORDER BY aggregate there would make the planner feed *both*
1086 -- aggregates sorted input, scrambling the stored children order.
1087 SELECT uuid_generate_v5(uuid_ns_provsql(),
1088 concat('times-canonical', array_agg(t ORDER BY t)))
1089 FROM unnest(filtered_tokens) t
1090 INTO canonical;
1091 IF get_gate_type(canonical) = 'times' THEN
1092 -- A deliberate pre-creation at the canonical address: same
1093 -- children, same product.
1094 times_token := canonical;
1095 ELSE
1096 times_token := uuid_generate_v5(uuid_ns_provsql(),concat('times',filtered_tokens));
1097
1098 PERFORM create_gate(times_token, 'times', ARRAY_AGG(t)) FROM UNNEST(filtered_tokens) AS t WHERE t IS NOT NULL;
1099 END IF;
1100 END CASE;
1101
1102 RETURN times_token;
1103END
1104$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
1105
1106/**
1107 * @brief Create a monus (difference) gate from two provenance tokens
1108 *
1109 * Implements m-semiring monus. Returns token1 if token2 is NULL
1110 * (used for LEFT OUTER JOIN semantics in the EXCEPT rewriting).
1111 */
1112CREATE OR REPLACE FUNCTION provenance_monus(token1 UUID, token2 UUID)
1113 RETURNS UUID AS
1114$$
1115DECLARE
1116 monus_token UUID;
1117BEGIN
1118 IF token1 IS NULL THEN
1119 RAISE EXCEPTION USING MESSAGE='provenance_monus is called with first argument NULL';
1120 END IF;
1122 IF token2 IS NULL THEN
1123 -- Special semantics, because of a LEFT OUTER JOIN used by the
1124 -- difference operator: token2 NULL means there is no second argument
1125 RETURN token1;
1126 END IF;
1127
1128 IF token1 = token2 THEN
1129 -- X-X=0
1130 monus_token:=gate_zero();
1131 ELSIF token1 = gate_zero() THEN
1132 -- 0-X=0
1133 monus_token:=gate_zero();
1134 ELSIF token2 = gate_zero() THEN
1135 -- X-0=X
1136 monus_token:=token1;
1137 ELSE
1138 monus_token:=uuid_generate_v5(uuid_ns_provsql(),concat('monus',token1,token2));
1139 PERFORM create_gate(monus_token, 'monus', ARRAY[token1::UUID, token2::UUID]);
1140 END IF;
1141
1142 RETURN monus_token;
1143END
1144$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1145
1146/**
1147 * @brief Create a project gate for where-provenance tracking
1148 *
1149 * Records the mapping between input and output attribute positions.
1150 *
1151 * @param token child provenance token
1152 * @param positions array encoding attribute position mappings
1153 */
1154CREATE OR REPLACE FUNCTION provenance_project(token UUID, VARIADIC positions INT[])
1155 RETURNS UUID AS
1156$$
1157DECLARE
1158 project_token UUID;
1159 rec RECORD;
1160BEGIN
1161 project_token:=uuid_generate_v5(uuid_ns_provsql(),concat('project', token, positions));
1162 PERFORM create_gate(project_token, 'project', ARRAY[token]);
1163 PERFORM set_extra(project_token, ARRAY_AGG(pair)::TEXT)
1164 FROM (
1165 SELECT ARRAY[(CASE WHEN info=0 THEN NULL ELSE info END), idx] AS pair
1166 FROM unnest(positions) WITH ORDINALITY AS a(info, idx)
1167 ORDER BY idx
1168 ) t;
1169
1170 RETURN project_token;
1171END
1172$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1173
1174/**
1175 * @brief Create an equijoin gate for where-provenance tracking
1177 * @param token child provenance token
1178 * @param pos1 attribute index in the first relation
1179 * @param pos2 attribute index in the second relation
1180 */
1181CREATE OR REPLACE FUNCTION provenance_eq(token UUID, pos1 INT, pos2 INT)
1182 RETURNS UUID AS
1183$$
1184DECLARE
1185 eq_token UUID;
1186 rec RECORD;
1187BEGIN
1188 eq_token:=uuid_generate_v5(uuid_ns_provsql(),concat('eq',token,pos1,',',pos2));
1189
1190 PERFORM create_gate(eq_token, 'eq', ARRAY[token::UUID]);
1191 PERFORM set_infos(eq_token, pos1, pos2);
1192 RETURN eq_token;
1193END
1194$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1195
1196/**
1197 * @brief Create a plus (sum) gate from an array of provenance tokens
1198 *
1199 * Filters out NULL and zero-gates; returns gate_zero() if all tokens
1200 * are trivial, or a single token if only one remains. Before creating
1201 * a gate, probes the *canonical* address of the multiset -- a dedicated
1202 * v5 recipe namespace over the sorted tokens (plus is commutative), in
1203 * which this function never creates anything, so a gate found there is
1204 * always a deliberate pre-creation computing the same sum. That is the
1205 * bounded-hop reachability route's hook: it plants, at the canonical
1206 * address of a vertex's per-length tokens, a certified gate over its
1207 * native within-bound circuit, keeping the natural hop-discarding query
1208 * on the linear evaluation route. Absent a canonical gate, the
1209 * ordinary order-dependent recipe is used, so ordinary plus
1210 * gates (and their formula rendering) are untouched.
1211 */
1212CREATE OR REPLACE FUNCTION provenance_plus(tokens UUID[])
1213 RETURNS UUID AS
1214$$
1215DECLARE
1216 c INTEGER;
1217 plus_token UUID;
1218 filtered_tokens UUID[];
1219 canonical UUID;
1220BEGIN
1221 SELECT array_agg(t) FROM unnest(tokens) t
1222 WHERE t IS NOT NULL AND t <> gate_zero()
1223 INTO filtered_tokens;
1224
1225 c:=array_length(filtered_tokens, 1);
1226
1227 IF c = 0 THEN
1228 plus_token := gate_zero();
1229 ELSIF c = 1 THEN
1230 plus_token := filtered_tokens[1];
1231 ELSE
1232 -- Computed separately from the filtering aggregate above: an ORDER
1233 -- BY aggregate there would make the planner feed *both* aggregates
1234 -- sorted input, scrambling the stored (aggregation-order) children.
1235 SELECT uuid_generate_v5(uuid_ns_provsql(),
1236 concat('plus-canonical', array_agg(t ORDER BY t)))
1237 FROM unnest(filtered_tokens) t
1238 INTO canonical;
1239 IF get_gate_type(canonical) = 'plus' THEN
1240 -- A deliberate pre-creation at the canonical address: same
1241 -- children, same sum.
1242 plus_token := canonical;
1243 ELSE
1244 plus_token := uuid_generate_v5(
1245 uuid_ns_provsql(),
1246 concat('plus', filtered_tokens));
1247
1248 PERFORM create_gate(plus_token, 'plus', filtered_tokens);
1249 END IF;
1250 END IF;
1251
1252 RETURN plus_token;
1253END
1254$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE IMMUTABLE;
1255
1256/**
1257 * @brief Driver for provenance over recursive queries (WITH RECURSIVE).
1258 *
1259 * Invoked by the planner hook (@c lower_recursive_cte in @c provsql.c) when it
1260 * lowers a recursive CTE whose body touches provenance-tracked relations. The
1261 * hook deparses the CTE body to SQL and calls this function, which runs naive
1262 * bottom-up (fixpoint) evaluation: each round re-evaluates the body
1263 * @c base @c UNION @c recursive over a tracked working table until the
1264 * provenance tokens stop changing. Every round goes through ProvSQL's normal
1265 * rewriting, so the recursive join yields @c times gates, the untracked base
1266 * branch yields @c gate_one, and the @c UNION yields the @c plus merge of
1267 * alternative derivations -- no provenance is plumbed by hand here. The result
1268 * is left in a tracked temp table named @p work_name, which the hook then scans
1269 * in place of the CTE.
1270 *
1271 * The working tables (@p work_name and a scratch @c _new) are created once and
1272 * reused across rounds (TRUNCATE + INSERT), so the round count never
1273 * accumulates relation locks. Because content-addressed gate UUIDs make
1274 * structurally identical sub-circuits share, the fixpoint test is an exact
1275 * relational @c EXCEPT and the circuit stays the shared (polynomial) form.
1276 *
1277 * Scope: UNION (set) recursion. On *acyclic* input the structural fixpoint is
1278 * reached and the resulting circuit is the universal provenance, sound for any
1279 * semiring. On *cyclic* input the circuit never stabilises structurally; when
1280 * the session's provenance class (@c provsql.provenance) is @c 'absorptive' or
1281 * @c 'BOOLEAN' we instead stop at the value-fixpoint bound (number of
1282 * derivable tuples) -- every minimal, tuple-repetition-free derivation is then
1283 * covered, and the longer ones are absorbed in any absorptive semiring (after
1284 * Deutch, Milo, Roy & Tannen, ICDT 2014) -- and wrap the resulting tokens in
1285 * the @c 'absorptive' assumption marker, so that non-absorptive semiring
1286 * evaluations (counting, why-provenance: genuinely infinite on cyclic data)
1287 * refuse them while probability, Boolean, formula-as-circuit and min-plus
1288 * evaluations proceed. Under the general classes, cyclic input trips the
1289 * @p max_iter guard.
1290 *
1291 * This function has no @c SET @c search_path on purpose: @p body_sql is the
1292 * caller's deparsed query and must resolve relation names in the caller's path.
1293 *
1294 * @param body_sql the recursive CTE body, e.g.
1295 * @c 'SELECT 1 UNION SELECT e.dst FROM edge e JOIN reach r ON e.src=r.node'
1296 * @param work_name the working relation name @p body_sql references (the CTE name)
1297 * @param colnames comma-separated user columns, e.g. @c 'node'
1298 * @param coldef column definitions for the working table, e.g. @c 'node INTEGER'
1299 * @param max_iter safety bound on fixpoint rounds (non-termination guard)
1300 */
1301CREATE OR REPLACE FUNCTION eval_recursive(
1302 body_sql TEXT,
1303 work_name TEXT,
1304 colnames TEXT,
1305 coldef TEXT,
1306 max_iter INT DEFAULT 1000)
1307 RETURNS VOID AS
1308$$
1309DECLARE
1310 changed BOOLEAN; -- circuit changed structurally this round
1311 set_stable BOOLEAN; -- user-column tuple set unchanged this round
1312 iters INT := 0;
1313 new_count INT; -- rows in _new this round (INSERT ROW_COUNT)
1314 -- Under an absorptive semiring the provenance *value* converges on cyclic
1315 -- data even though the circuit keeps growing structurally. A minimal
1316 -- derivation cannot repeat a tuple, so it has depth <= (number of derivable
1317 -- tuples); after that many naive rounds the value equals the least fixpoint,
1318 -- and the surplus (longer, cyclic) derivations are absorbed at evaluation
1319 -- time. We learn that bound from the tuple-set fixpoint, stop there, and
1320 -- mark the resulting tokens with the 'absorptive' assumption so evaluation
1321 -- under a non-absorptive semiring refuses rather than silently returning a
1322 -- truncated value.
1323 absorptive_mode BOOLEAN :=
1324 coalesce(current_setting('provsql.provenance', true), 'semiring')
1325 IN ('absorptive', 'BOOLEAN');
1326 truncated BOOLEAN := false; -- exited at the value fixpoint (cyclic data)
1327 ntuples INT := NULL; -- the bound above, set once the tuple set stabilises
1328BEGIN
1329 EXECUTE format('DROP TABLE IF EXISTS %I', work_name);
1330 DROP TABLE IF EXISTS _new;
1331
1332 -- Tracked working table (carries provsql), initially empty, plus a scratch
1333 -- table of the same shape; both reused across rounds.
1334 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID)', work_name, coldef);
1335 EXECUTE format('CREATE TEMP TABLE _new (LIKE %I)', work_name);
1336
1337 LOOP
1338 iters := iters + 1;
1339 -- Hard safety bound (also catches genuinely unbounded recursion, e.g. an
1340 -- unbounded counter, where even the tuple set never stabilises).
1341 IF iters > max_iter THEN
1342 RAISE EXCEPTION 'eval_recursive: no fixpoint after % rounds (cyclic data?)', max_iter;
1343 END IF;
1344
1345 -- One round of naive evaluation: re-run the CTE body over the current
1346 -- working table. INSERT targets a tracked table, so ProvSQL fills provsql.
1347 -- Take the row count from the INSERT itself (counting _new directly would be
1348 -- an aggregate over a provenance-tracked table -> an AGG_TOKEN).
1349 EXECUTE 'TRUNCATE _new';
1350 EXECUTE format('INSERT INTO _new(%s) %s', colnames, body_sql);
1351 GET DIAGNOSTICS new_count = ROW_COUNT;
1352
1353 -- Exact structural fixpoint test (content-addressed tokens => set equality).
1354 EXECUTE format(
1355 'SELECT EXISTS((TABLE _new EXCEPT TABLE %1$I) UNION ALL (TABLE %1$I EXCEPT TABLE _new))',
1356 work_name) INTO changed;
1357
1358 -- In an absorptive class, learn the round bound from the tuple-set
1359 -- fixpoint (the set always stabilises after finitely many rounds, even on
1360 -- cyclic data).
1361 IF absorptive_mode AND ntuples IS NULL THEN
1362 EXECUTE format(
1363 'SELECT NOT EXISTS('
1364 || '(SELECT %2$s FROM _new EXCEPT SELECT %2$s FROM %1$I) UNION ALL '
1365 || '(SELECT %2$s FROM %1$I EXCEPT SELECT %2$s FROM _new))',
1366 work_name, colnames) INTO set_stable;
1367 IF set_stable THEN
1368 ntuples := new_count;
1369 END IF;
1370 END IF;
1371
1372 -- Copy _new into the working table (tracked -> tracked carries the tokens).
1373 EXECUTE format('TRUNCATE %I', work_name);
1374 EXECUTE format('INSERT INTO %1$I(%2$s) SELECT %2$s FROM _new', work_name, colnames);
1375
1376 -- Structural fixpoint: done (acyclic / fully converged) -- sound for any
1377 -- semiring.
1378 EXIT WHEN NOT changed;
1379
1380 -- Absorptive class on cyclic data: once the value-fixpoint bound is
1381 -- reached (plus one confirming round, so that acyclic circuits whose
1382 -- token depth lags the tuple-set saturation still exit through the
1383 -- structural test above, untagged) we stop, even though the circuit
1384 -- is not structurally stable.
1385 IF absorptive_mode AND ntuples IS NOT NULL AND iters >= ntuples + 1 THEN
1386 truncated := true;
1387 EXIT;
1388 END IF;
1389 END LOOP;
1390
1391 -- Tokens of a truncated (cyclic) fixpoint are sound only under absorptive
1392 -- evaluation: RECORD that in the circuit itself.
1393 IF truncated THEN
1394 EXECUTE format(
1395 'UPDATE %I SET provsql = provsql.provenance_assume(provsql, ''absorptive'')',
1396 work_name);
1397 END IF;
1398END
1399$$ LANGUAGE plpgsql SET client_min_messages = warning;
1400
1401/**
1402 * @brief Create a comparison gate for HAVING clause provenance
1403 *
1404 * @param left_token provenance token for the left operand
1405 * @param comparison_op OID of the comparison operator
1406 * @param right_token provenance token for the right operand
1408CREATE OR REPLACE FUNCTION provenance_cmp(
1409 left_token UUID,
1410 comparison_op OID,
1411 right_token UUID
1412)
1413RETURNS UUID AS
1414$$
1415DECLARE
1416 cmp_token UUID;
1417BEGIN
1418 -- deterministic v5 namespace id
1419 cmp_token := public.uuid_generate_v5(
1420 uuid_ns_provsql(),
1421 concat('cmp', left_token::TEXT, comparison_op::TEXT, right_token::TEXT)
1422 );
1423 -- wire it up in the circuit
1424 PERFORM create_gate(cmp_token, 'cmp', ARRAY[left_token, right_token]);
1425 PERFORM set_infos(cmp_token, comparison_op::INTEGER);
1426 RETURN cmp_token;
1427END
1428$$ LANGUAGE plpgsql
1429 SET search_path=provsql,pg_temp,public
1430 SECURITY DEFINER
1431 IMMUTABLE
1432 PARALLEL SAFE
1433 STRICT;
1434
1435/**
1436 * @brief Create an arithmetic gate over scalar-valued provenance children
1437 *
1438 * Builds a deterministic @c gate_arith from an operator tag and an
1439 * ordered list of children. The tag is one of the @c provsql_arith_op
1440 * ENUM values declared in @c src/provsql_utils.h
1441 * (@c PLUS=0, @c TIMES=1, @c MINUS=2, @c DIV=3, @c NEG=4) and is
1442 * stored in the gate's @c info1 field. Children must be UUIDs of
1443 * scalar-producing gates (@c gate_rv, @c gate_value, or another
1444 * @c gate_arith). The token UUID is derived deterministically from
1445 * @p op and @p children so identical sub-expressions share their gate.
1446 *
1447 * @param op Operator tag (@c provsql_arith_op).
1448 * @param children Ordered list of child gate UUIDs.
1449 * @return UUID of the (possibly pre-existing) @c gate_arith.
1450 */
1451CREATE OR REPLACE FUNCTION provenance_arith(
1452 op INTEGER,
1453 children UUID[]
1454)
1455RETURNS UUID AS
1456$$
1457DECLARE
1458 arith_token UUID;
1459BEGIN
1460 arith_token := public.uuid_generate_v5(
1461 uuid_ns_provsql(),
1462 concat('arith', op::TEXT, children::TEXT)
1463 );
1464 PERFORM create_gate(arith_token, 'arith', children);
1465 PERFORM set_infos(arith_token, op);
1466 RETURN arith_token;
1467END
1468$$ LANGUAGE plpgsql
1469 SET search_path=provsql,pg_temp,public
1470 SECURITY DEFINER
1471 IMMUTABLE
1472 PARALLEL SAFE
1473 STRICT;
1474
1475/** @} */
1476
1477/** @defgroup semiring_evaluation Semiring evaluation
1478 * Functions for evaluating provenance circuits over semirings,
1479 * both user-defined (via function references) and compiled (built-in).
1480 * @{
1482
1483/**
1484 * @brief Evaluate provenance using a compiled (built-in) semiring
1485 *
1486 * This C function handles semiring evaluation entirely in C++ for
1487 * better performance. The semiring is specified by name.
1488 *
1489 * @param token provenance token to evaluate
1490 * @param token2value mapping table from tokens to semiring values
1491 * @param semiring name of the compiled semiring (e.g., "formula", "counting")
1492 * @param element_one identity element of the semiring
1493 */
1494CREATE OR REPLACE FUNCTION provenance_evaluate_compiled(
1495 token UUID,
1496 token2value REGCLASS,
1497 semiring TEXT,
1498 element_one ANYELEMENT)
1499RETURNS ANYELEMENT AS
1500 'provsql', 'provenance_evaluate_compiled' LANGUAGE C PARALLEL SAFE STABLE;
1501
1502
1503/**
1504 * @brief Evaluate provenance over a user-defined semiring (PL/pgSQL version)
1505 *
1506 * Recursively walks the provenance circuit and evaluates each gate
1507 * using the provided semiring operations. This is the generic version
1508 * that accepts semiring operations as function references.
1509 *
1510 * @param token provenance token to evaluate
1511 * @param token2value mapping table from tokens to semiring values
1512 * @param element_one identity element of the semiring
1513 * @param value_type OID of the semiring value type
1514 * @param plus_function semiring addition (aggregate)
1515 * @param times_function semiring multiplication (aggregate)
1516 * @param monus_function semiring monus (binary), or NULL
1517 * @param delta_function δ-semiring operator, or NULL
1518 */
1519CREATE OR REPLACE FUNCTION provenance_evaluate(
1520 token UUID,
1521 token2value REGCLASS,
1522 element_one ANYELEMENT,
1523 value_type REGTYPE,
1524 plus_function REGPROC,
1525 times_function REGPROC,
1526 monus_function REGPROC,
1527 delta_function REGPROC)
1528 RETURNS ANYELEMENT AS
1529$$
1530DECLARE
1531 gate_type PROVENANCE_GATE;
1532 result ALIAS FOR $0;
1533 children UUID[];
1534-- cmp_value ANYELEMENT;
1535-- temp_result ANYELEMENT;
1536 value_text TEXT;
1537BEGIN
1538 SELECT get_gate_type(token) INTO gate_type;
1539
1540 IF gate_type IS NULL THEN
1541 RETURN NULL;
1542
1543 ELSIF gate_type = 'input' THEN
1544 EXECUTE format('SELECT value FROM %s WHERE provenance=%L', token2value, token)
1545 INTO result;
1546 IF result IS NULL THEN
1547 result := element_one;
1548 END IF;
1549 ELSIF gate_type = 'mulinput' THEN
1550 SELECT concat('{',(get_children(token))[1]::TEXT,'=',(get_infos(token)).info1,'}')
1551 INTO result;
1552 ELSIF gate_type='update' THEN
1553 EXECUTE format('SELECT value FROM %s WHERE provenance=%L',token2value,token) INTO result;
1554 IF result IS NULL THEN
1555 result:=element_one;
1556 END IF;
1557 ELSIF gate_type = 'plus' THEN
1558 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
1559 plus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
1560 INTO result;
1561
1562 ELSIF gate_type = 'times' THEN
1563 EXECUTE format('SELECT %s(provsql.provenance_evaluate(t,%L,%L::%s,%L,%L,%L,%L,%L)) FROM unnest(get_children(%L)) AS t',
1564 times_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
1565 INTO result;
1566
1567 ELSIF gate_type = 'monus' THEN
1568 IF monus_function IS NULL THEN
1569 RAISE EXCEPTION USING MESSAGE='Provenance with negation evaluated over a semiring without monus function';
1570 ELSE
1571 EXECUTE format('SELECT %s(a1,a2) FROM (SELECT provsql.provenance_evaluate(c[1],%L,%L::%s,%L,%L,%L,%L,%L) AS a1, ' ||
1572 'provsql.provenance_evaluate(c[2],%L,%L::%s,%L,%L,%L,%L,%L) AS a2 FROM get_children(%L) c) tmp',
1573 monus_function, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function,
1574 token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function, token)
1575 INTO result;
1576 END IF;
1577
1578 ELSIF gate_type = 'eq' THEN
1579 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1580 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1581 INTO result;
1582
1583/* elsif gate_type = 'cmp' then
1584
1585 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1586 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1587 INTO temp_result;
1588
1589 EXECUTE format('SELECT get_extra((get_children(%L))[2])', token)
1590 INTO cmp_value;
1591
1592 IF temp_result::TEXT = cmp_value::TEXT THEN
1593 SELECT concat('{',temp_result::TEXT,'=',cmp_value::TEXT,'}')
1594 INTO result;
1595 ELSE
1596 RETURN gate_zero()
1597 */
1598
1599
1600
1601 ELSIF gate_type = 'delta' THEN
1602 IF delta_function IS NULL THEN
1603 RAISE EXCEPTION USING MESSAGE='Provenance with aggregation evaluated over a semiring without delta function';
1604 ELSE
1605 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',
1606 delta_function, token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1607 INTO result;
1608 END IF;
1609
1610 ELSIF gate_type = 'zero' THEN
1611 EXECUTE format('SELECT %I(a) FROM (SELECT %L::%I AS a WHERE FALSE) temp', plus_function, element_one, value_type)
1612 INTO result;
1613
1614 ELSIF gate_type = 'one' THEN
1615 EXECUTE format('SELECT %L::%I', element_one, value_type)
1616 INTO result;
1617
1618 ELSIF gate_type = 'project' THEN
1619 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1620 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1621 INTO result;
1622
1623 ELSIF gate_type = 'annotation' THEN
1624 -- Transparent single-child wrapper (carries the inversion-free certificate
1625 -- / per-input order keys in extra, inert for every semiring): evaluate
1626 -- through to the child, like 'project'.
1627 EXECUTE format('SELECT provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L)',
1628 token, token2value, element_one, value_type, value_type, plus_function, times_function, monus_function, delta_function)
1629 INTO result;
1630
1631 ELSE
1632 RAISE EXCEPTION USING MESSAGE='provenance_evaluate cannot be called on formulas using ' || gate_type || ' gates; use compiled semirings instead';
1633 END IF;
1635 RETURN result;
1636END
1637$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
1638
1640/**
1641 * @brief Evaluate aggregate provenance over a user-defined semiring (PL/pgSQL version)
1642 *
1643 * Handles agg and semimod gates produced by GROUP BY queries.
1644 *
1645 * @param token provenance token to evaluate
1646 * @param token2value mapping table from tokens to semiring values
1647 * @param agg_function_final finalization function for the aggregate
1648 * @param agg_function aggregate combination function
1649 * @param semimod_function semimodule scalar multiplication function
1650 * @param element_one identity element of the semiring
1651 * @param value_type OID of the semiring value type
1652 * @param plus_function semiring addition
1653 * @param times_function semiring multiplication
1654 * @param monus_function semiring monus, or NULL
1655 * @param delta_function δ-semiring operator, or NULL
1657CREATE OR REPLACE FUNCTION aggregation_evaluate(
1658 token UUID,
1659 token2value REGCLASS,
1660 agg_function_final REGPROC,
1661 agg_function REGPROC,
1662 semimod_function REGPROC,
1663 element_one ANYELEMENT,
1664 value_type REGTYPE,
1665 plus_function REGPROC,
1666 times_function REGPROC,
1667 monus_function REGPROC,
1668 delta_function REGPROC)
1669 RETURNS ANYELEMENT AS
1670$$
1671DECLARE
1672 gt PROVENANCE_GATE;
1673 result ALIAS FOR $0;
1674BEGIN
1675 SELECT get_gate_type(token) INTO gt;
1676
1677 IF gt IS NULL THEN
1678 RETURN NULL;
1679 ELSIF gt='agg' THEN
1680 EXECUTE format('SELECT %I(%I(provsql.aggregation_evaluate(t,%L,%L,%L,%L,%L::%s,%L,%L,%L,%L,%L)),pp.proname::varchar) FROM
1681 unnest(get_children(%L)) AS t, pg_proc pp
1682 WHERE pp.oid=(get_infos(%L)).info1
1683 GROUP BY pp.proname',
1684 agg_function_final, agg_function,token2value,agg_function_final,agg_function,semimod_function,element_one,value_type,value_type,plus_function,times_function,
1685 monus_function,delta_function,token,token)
1686 INTO result;
1687 ELSE
1688 -- gt='semimod'
1689 EXECUTE format('SELECT %I(get_extra((get_children(%L))[2]),provsql.provenance_evaluate((get_children(%L))[1],%L,%L::%s,%L,%L,%L,%L,%L))',
1690 semimod_function,token,token,token2value,element_one,value_type,value_type,plus_function,times_function,monus_function,delta_function)
1691 INTO result;
1692 END IF;
1693 RETURN result;
1694END
1695$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
1697/**
1698 * @brief Evaluate provenance over a user-defined semiring (C version)
1699 *
1700 * Optimized C implementation of provenance_evaluate. Infers the
1701 * value type from element_one. Monus and delta functions are optional.
1702 *
1703 * @param token provenance token to evaluate
1704 * @param token2value mapping table from tokens to semiring values
1705 * @param element_one identity element of the semiring
1706 * @param plus_function semiring addition (aggregate)
1707 * @param times_function semiring multiplication (aggregate)
1708 * @param monus_function semiring monus, or NULL if not needed
1709 * @param delta_function δ-semiring operator, or NULL if not needed
1710 */
1711CREATE OR REPLACE FUNCTION provenance_evaluate(
1712 token UUID,
1713 token2value REGCLASS,
1714 element_one ANYELEMENT,
1715 plus_function REGPROC,
1716 times_function REGPROC,
1717 monus_function REGPROC = NULL,
1718 delta_function REGPROC = NULL)
1719 RETURNS ANYELEMENT AS
1720 'provsql','provenance_evaluate' LANGUAGE C STABLE;
1721
1722/** @brief Evaluate aggregate provenance over a user-defined semiring (C version) */
1723CREATE OR REPLACE FUNCTION aggregation_evaluate(
1724 token UUID,
1725 token2value REGCLASS,
1726 agg_function_final REGPROC,
1727 agg_function REGPROC,
1728 semimod_function REGPROC,
1729 element_one ANYELEMENT,
1730 plus_function REGPROC,
1731 times_function REGPROC,
1732 monus_function REGPROC = NULL,
1733 delta_function REGPROC = NULL)
1734 RETURNS ANYELEMENT AS
1735 'provsql','aggregation_evaluate' LANGUAGE C STABLE;
1736
1737/** @} */
1738
1739/** @defgroup circuit_introspection Circuit introspection
1740 * Functions for examining the structure of provenance circuits,
1741 * used by visualization and where-provenance features.
1742 * @{
1743 */
1744
1745/** @brief Row type for sub_circuit_with_desc results */
1746CREATE TYPE GATE_WITH_DESC AS (f UUID, t UUID, gate_type PROVENANCE_GATE, desc_str CHARACTER VARYING, infos INTEGER[], extra TEXT);
1747
1748/**
1749 * @brief Return the sub-circuit reachable from a token, with descriptions
1751 * Recursively traverses the provenance circuit from the given token and
1752 * returns all edges together with input gate descriptions from the
1753 * mapping table.
1754 *
1755 * @param token root provenance token
1756 * @param token2desc mapping table providing descriptions for input gates
1758CREATE OR REPLACE FUNCTION sub_circuit_with_desc(
1759 token UUID,
1760 token2desc REGCLASS) RETURNS SETOF GATE_WITH_DESC AS
1761$$
1762BEGIN
1763 RETURN QUERY EXECUTE
1764 'WITH RECURSIVE transitive_closure(f,t,gate_type) AS (
1765 SELECT $1,t,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) AS t
1766 UNION ALL
1767 SELECT p1.t,u,provsql.get_gate_type(p1.t) FROM transitive_closure p1, unnest(provsql.get_children(p1.t)) AS u)
1768 SELECT *, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
1769 SELECT f::UUID,t::UUID,gate_type,NULL FROM transitive_closure
1770 UNION ALL
1771 SELECT p2.provenance::UUID as f, NULL::UUID, ''input'', CAST (p2.value AS varchar) FROM transitive_closure p1 JOIN ' || token2desc || ' AS p2
1772 ON p2.provenance=t
1773 UNION ALL
1774 SELECT provenance::UUID as f, NULL::UUID, ''input'', CAST (value AS varchar) FROM ' || token2desc || ' WHERE provenance=$1
1775 ) t'
1776 USING token LOOP;
1777 RETURN;
1778END
1779$$ LANGUAGE plpgsql PARALLEL SAFE;
1780
1781/**
1782 * @brief Identify which table and how many columns a provenance token belongs to
1783 *
1784 * Searches all provenance-tracked tables for a row matching the given
1785 * token and returns the table name and column count.
1786 *
1787 * @param token provenance token to look up
1788 * @param table_name (OUT) the table containing this token
1789 * @param nb_columns (OUT) number of non-provenance columns in that table
1790 */
1791CREATE OR REPLACE FUNCTION identify_token(
1792 token UUID, OUT table_name REGCLASS, OUT nb_columns INTEGER) AS
1793$$
1794DECLARE
1795 t RECORD;
1796 result RECORD;
1797BEGIN
1798 table_name:=NULL;
1799 nb_columns:=-1;
1800 FOR t IN
1801 SELECT relname,
1802 (SELECT count(*) FROM pg_attribute a2 WHERE a2.attrelid=a1.attrelid AND attnum>0 AND atttypid<>0)-1 c
1803 FROM pg_attribute a1 JOIN pg_type ON atttypid=pg_type.oid
1804 JOIN pg_class ON attrelid=pg_class.oid
1805 JOIN pg_namespace ON relnamespace=pg_namespace.oid
1806 WHERE typname='UUID' AND relkind='r'
1807 AND nspname<>'provsql'
1808 AND attname='provsql'
1809 LOOP
1810 EXECUTE format('SELECT * FROM %I WHERE provsql=%L',t.relname,token) INTO result;
1811 -- Test result.provsql rather than the whole RECORD: "RECORD IS NOT NULL"
1812 -- is true only when every field is non-null, so a matched row that has any
1813 -- NULL data column would be wrongly skipped. The provsql column is the
1814 -- (non-null) token we matched on, so it is set iff a row was found.
1815 IF result.provsql IS NOT NULL THEN
1816 table_name:=t.relname;
1817 nb_columns:=t.c;
1818 EXIT;
1819 END IF;
1820 END LOOP;
1822$$ LANGUAGE plpgsql STRICT;
1823
1824/**
1825 * @brief Return the sub-circuit for where-provenance computation
1826 *
1827 * Similar to sub_circuit_with_desc but resolves input gates to their
1828 * source table and column count for where-provenance evaluation.
1829 */
1830CREATE OR REPLACE FUNCTION sub_circuit_for_where(token UUID)
1831 RETURNS TABLE(f UUID, t UUID, gate_type PROVENANCE_GATE, table_name REGCLASS, nb_columns INTEGER, infos INTEGER[], extra TEXT) AS
1832$$
1833 WITH RECURSIVE transitive_closure(f,t,idx,gate_type) AS (
1834 SELECT $1,t,id,provsql.get_gate_type($1) FROM unnest(provsql.get_children($1)) WITH ORDINALITY AS a(t,id)
1835 UNION ALL
1836 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)
1837 ) SELECT f, t, gate_type, table_name, nb_columns, ARRAY[(get_infos(f)).info1, (get_infos(f)).info2], get_extra(f) FROM (
1838 -- One row per distinct (parent, child, child-position) edge. The
1839 -- recursive closure (UNION ALL) re-emits a gate's outgoing edges once per
1840 -- path that reaches it, so a *shared* non-input gate would otherwise be
1841 -- reported with duplicate edges; DISTINCT on the (f,t,idx) triple
1842 -- collapses those while keeping genuine repeated children (same f,t,
1843 -- different idx, e.g. a self-product). Without this, a shared
1844 -- single-child gate (notably an inversion-free order-marker annotation)
1845 -- gets its child wired k times in the where-circuit -> the locator sets
1846 -- are duplicated k-fold.
1847 SELECT DISTINCT f, t::UUID, idx, gate_type, NULL::REGCLASS AS table_name, NULL::INTEGER AS nb_columns FROM transitive_closure
1848 UNION ALL
1849 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
1850 UNION ALL
1851 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
1852 ) t
1853 -- order each parent's edges by child position so the where-circuit's TIMES
1854 -- concatenation reproduces the column order (input rows have idx NULL).
1855 ORDER BY f, idx
1856$$
1857LANGUAGE sql;
1858
1859/**
1860 * @brief BFS expansion of a provenance circuit, capped at @p max_depth
1861 *
1862 * Returns one row per (parent, child) edge in the BFS-bounded subgraph
1863 * rooted at @p root, plus one row for the root with <tt>parent</tt> and
1864 * <tt>child_pos</tt> NULL. Provenance circuits are DAGs, so a child gate
1865 * may have several parents within the bound; each such edge is reported
1866 * as a separate row, so callers must deduplicate on <tt>node</tt> if they
1867 * need a one-row-per-node view.
1868 *
1869 * <tt>depth</tt> is the node's BFS depth (its shortest distance from
1870 * @p root), so for an edge (parent, child) it is always the case that
1871 * <tt>parent.depth + 1 &gt;= child.depth</tt>; equality holds only on
1872 * shortest-path edges. A node at <tt>depth = max_depth</tt> is not
1873 * expanded; callers can detect a partial expansion by comparing
1874 * <tt>provsql.get_children</tt> length against the number of outgoing
1875 * edges reported.
1876 *
1877 * <tt>info1</tt> and <tt>info2</tt> are the INTEGER values stored on
1878 * the gate by <tt>provsql.set_infos</tt>, formatted as TEXT; their
1879 * meaning is gate-type-specific (see <tt>provsql.set_infos</tt>).
1880 *
1881 * @param root root provenance token
1882 * @param max_depth maximum BFS depth (default 8)
1883 */
1884CREATE OR REPLACE FUNCTION circuit_subgraph(root UUID, max_depth INT DEFAULT 8)
1885 RETURNS TABLE(node UUID, parent UUID, child_pos INT, gate_type TEXT, info1 TEXT, info2 TEXT, depth INT) AS
1886$$
1887 WITH RECURSIVE bfs(node, parent, child_pos, depth) AS (
1888 SELECT root, NULL::UUID, NULL::INT, 0
1889 UNION ALL
1890 SELECT c.t, b.node, c.idx::INT, b.depth + 1
1891 FROM bfs b
1892 CROSS JOIN LATERAL unnest(provsql.get_children(b.node))
1893 WITH ORDINALITY AS c(t, idx)
1894 WHERE b.depth < max_depth
1895 ),
1896 -- Each node's canonical depth is its longest-path distance from the
1897 -- root (the standard circuit-depth notion: the longest chain of
1898 -- gates separating the node from the output). The recursive CTE
1899 -- enumerates paths up to @c max_depth, so MAX over those is the
1900 -- longest path of length at most @c max_depth.
1901 node_depth AS (
1902 SELECT node, MAX(depth) AS depth FROM bfs GROUP BY node
1903 ),
1904 -- All distinct (parent, child, child_pos) triples seen during the BFS.
1905 -- A child reached from k parents within the bound contributes k rows.
1906 -- Self-joins (times(x, x)) contribute one row per child position.
1907 edges AS (
1908 SELECT DISTINCT parent, node AS child, child_pos
1909 FROM bfs WHERE parent IS NOT NULL
1910 )
1911 SELECT
1912 d.node,
1913 e.parent,
1914 e.child_pos,
1915 provsql.get_gate_type(d.node)::TEXT,
1916 i.info1::TEXT,
1917 i.info2::TEXT,
1918 d.depth
1919 FROM node_depth d
1920 LEFT JOIN edges e ON e.child = d.node
1921 LEFT JOIN LATERAL provsql.get_infos(d.node) i ON TRUE
1922 ORDER BY d.depth, d.node, e.parent;
1923$$ LANGUAGE sql STABLE PARALLEL SAFE;
1924
1925/**
1926 * @brief BFS subgraph of the IN-MEMORY simplified circuit rooted at @p root.
1927 *
1928 * Same row shape as @ref circuit_subgraph plus an inline @c extra
1929 * column, but built from the @c GenericCircuit returned by
1930 * @c getGenericCircuit -- i.e. AFTER @c provsql.simplify_on_load
1931 * passes (RangeCheck, ...) have rewritten any decidable @c gate_cmp
1932 * into Bernoulli @c gate_input / @c gate_zero / @c gate_one leaves.
1933 * Lets a renderer show the user what the evaluator actually sees,
1934 * without mutating the persisted DAG.
1935 *
1936 * Returns @c jsonb (an array of objects) rather than @c SETOF RECORD
1937 * to keep the C++ implementation free of SRF / @c FuncCallContext
1938 * boilerplate; callers either consume the array directly or expand
1939 * it via @c jsonb_array_elements.
1940 *
1941 * @param root Root provenance token.
1942 * @param max_depth Maximum BFS depth (default 8).
1943 */
1944CREATE OR REPLACE FUNCTION simplified_circuit_subgraph(
1945 root UUID, max_depth INT DEFAULT 8) RETURNS jsonb
1946 AS 'provsql','simplified_circuit_subgraph'
1947 LANGUAGE C STABLE PARALLEL SAFE;
1948
1949/**
1950 * @brief Empirical histogram of a scalar sub-circuit
1951 *
1952 * Returns a jsonb array of @c {bin_lo, bin_hi, count} objects covering
1953 * the observed @c [min, max] range of @p bins equal-width samples from
1954 * the sub-circuit rooted at @p token. Sample count is taken from
1955 * @c provsql.rv_mc_samples; pinning @c provsql.monte_carlo_seed makes
1956 * the result reproducible.
1957 *
1958 * Accepted root gate types are the scalar ones: @c gate_value (Dirac
1959 * at the constant, single bin), @c gate_rv (sampled from the leaf's
1960 * distribution), and @c gate_arith (sampled by recursing through the
1961 * arithmetic DAG, with shared @c gate_rv leaves correctly correlated
1962 * within an iteration). Any other gate type raises.
1963 *
1964 * @param token Root provenance token of a scalar sub-circuit.
1965 * @param bins Number of equal-width histogram bins (default 30).
1966 * @param prov Conditioning event (defaults to @c gate_one() = no
1967 * conditioning). When non-trivial, the histogram is
1968 * over the conditional distribution recovered by
1969 * rejection sampling on the joint circuit with @p token.
1970 */
1971CREATE OR REPLACE FUNCTION rv_histogram(
1972 token UUID, bins INT DEFAULT 30, prov UUID DEFAULT gate_one())
1973 RETURNS jsonb
1974 AS 'provsql','rv_histogram'
1975 LANGUAGE C VOLATILE PARALLEL SAFE;
1976
1977/**
1978 * @brief Sample the closed-form PDF and CDF of a (possibly truncated)
1979 * scalar distribution.
1980 *
1981 * Returns @c {"pdf": [{x, p}, ...], "cdf": [{x, p}, ...]} with @p samples
1982 * evenly-spaced points spanning the distribution's natural display
1983 * range (intersected with the conditioning event's interval when
1984 * @c prov is non-trivial). Used by ProvSQL Studio's Distribution
1985 * profile panel to overlay the analytical curve on the empirical
1986 * histogram from :sqlfunc:`rv_histogram` -- the simplifier's
1987 * analytical wins (e.g. @c c·Exp(λ) folding to @c Exp(λ/c)) become
1988 * visible as a smooth curve riding over the MC-sampled bars.
1989 *
1990 * Returns @c NULL when the root sub-circuit is not a closed-form
1991 * shape (V1: only bare @c gate_rv of Normal / Uniform / Exponential
1992 * / INTEGER-Erlang). The frontend reads @c NULL as "skip overlay"
1993 * without erroring, so the caller can dispatch this in parallel with
1994 * @c rv_histogram regardless of the underlying shape.
1995 *
1996 * @param token Scalar gate token (random_variable's UUID).
1997 * @param samples Number of (x, p) points; must be >= 2.
1998 * @param prov Conditioning event (defaults to @c gate_one() = no
1999 * conditioning). When non-trivial, the curves are
2000 * over the truncated distribution.
2001 */
2002CREATE OR REPLACE FUNCTION rv_analytical_curves(
2003 token UUID, samples INT DEFAULT 100, prov UUID DEFAULT gate_one())
2004 RETURNS jsonb
2005 AS 'provsql','rv_analytical_curves'
2006 LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2007
2008/**
2009 * @brief Draw conditional Monte Carlo samples from a scalar gate.
2010 *
2011 * Returns up to @c n samples of the scalar value at @c token; when
2012 * @c prov is not the trivial @c gate_one() event, draws are accepted
2013 * only on iterations where @c prov evaluates true (rejection
2014 * sampling). Shared @c gate_rv leaves between @c token and @c prov
2015 * are loaded into a single joint circuit so the indicator's draw
2016 * and the value's draw share their per-iteration state.
2017 *
2018 * @param token Scalar sub-circuit root.
2019 * @param n Number of accepted samples to attempt.
2020 * @param prov Conditioning event (defaults to @c gate_one() = no
2021 * conditioning).
2022 *
2023 * Emits a @c NOTICE when the conditional acceptance rate yields fewer
2024 * than @c n samples within the @c provsql.rv_mc_samples budget so the
2025 * caller can choose to widen the budget.
2026 */
2027CREATE OR REPLACE FUNCTION rv_sample(
2028 token UUID, n INTEGER, prov UUID DEFAULT gate_one())
2029 RETURNS SETOF float8
2030 AS 'provsql','rv_sample'
2031 LANGUAGE C VOLATILE PARALLEL SAFE;
2032
2033/**
2034 * @brief Resolve an input gate UUID back to its source row
2035 *
2036 * Searches every provenance-tracked relation for a row whose
2037 * <tt>provsql</tt> column equals @p UUID and returns the relation's
2038 * REGCLASS together with the row encoded as JSONB. Returns zero
2039 * rows when @p UUID is not the provenance token of any tracked row,
2040 * including when it identifies an internal gate (<tt>plus</tt>,
2041 * <tt>times</tt>, ...) rather than an input.
2042 *
2043 * Ordinarily exactly one row is returned, but if the same UUID
2044 * happens to appear as a <tt>provsql</tt> value in several tracked
2045 * tables, all matches are returned.
2046 *
2047 * @param UUID token to resolve
2048 */
2049CREATE OR REPLACE FUNCTION resolve_input(UUID UUID)
2050 RETURNS TABLE(relation REGCLASS, row_data JSONB) AS
2051$$
2052DECLARE
2053 t RECORD;
2054 rel REGCLASS;
2055 rd JSONB;
2056 -- ProvSQL's rewriter unconditionally appends a provsql column to the
2057 -- targetlist of any SELECT reading from a tracked relation; capture and
2058 -- discard it here rather than disabling the rewriter for the whole call.
2059 ign UUID;
2060BEGIN
2061 FOR t IN
2062 SELECT c.oid::REGCLASS AS regc
2063 FROM pg_attribute a
2064 JOIN pg_class c ON a.attrelid = c.oid
2065 JOIN pg_namespace ns ON c.relnamespace = ns.oid
2066 JOIN pg_type ty ON a.atttypid = ty.oid
2067 WHERE a.attname = 'provsql'
2068 AND ty.typname = 'UUID'
2069 AND c.relkind = 'r'
2070 AND ns.nspname <> 'provsql'
2071 AND a.attnum > 0
2072 LOOP
2073 FOR rel, rd, ign IN
2074 EXECUTE format(
2075 'SELECT %L::REGCLASS, to_jsonb(t) - ''provsql'', t.provsql FROM %s AS t WHERE provsql = $1',
2076 t.regc, t.regc)
2077 USING UUID
2078 LOOP
2079 relation := rel;
2080 row_data := rd;
2081 RETURN NEXT;
2082 END LOOP;
2083 END LOOP;
2084END
2085$$ LANGUAGE plpgsql STABLE;
2086
2087/** @} */
2088
2089/** @defgroup agg_token_type Type for the result of aggregate queries
2091 * Custom type <tt>AGG_TOKEN</tt> for a provenance semimodule value, to
2092 * be used in attributes that are computed as a result of aggregation.
2093 * As for provenance tokens, this is simply a UUID, but this UUID is
2094 * displayed in a specific way (as the result of the aggregation
2095 * followed by a "(*)") to help with readability.
2096 *
2097 * The TEXT output is controlled by the
2098 * <tt>provsql.aggtoken_text_as_uuid</tt> GUC. By default it is off and
2099 * the cell renders as <tt>"value (*)"</tt>. When set to on (typical
2100 * for UI layers such as ProvSQL Studio), the cell renders as the
2101 * underlying UUID instead, so the caller can click through to the
2102 * provenance circuit; the value side is then recovered via
2103 * <tt>provsql.agg_token_value_text(UUID)</tt>.
2104 *
2105 * @{
2106 */
2108CREATE TYPE AGG_TOKEN;
2109
2110/** @brief Input function for the AGG_TOKEN type (parses TEXT representation) */
2111CREATE OR REPLACE FUNCTION agg_token_in(CSTRING)
2112 RETURNS AGG_TOKEN
2113 AS 'provsql','agg_token_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2115/**
2116 * @brief Output function for the AGG_TOKEN type
2117 *
2118 * Default: produces the human-friendly @c "value (*)" form, where
2119 * @c value is the running aggregate state.
2120 *
2121 * When the @c provsql.aggtoken_text_as_uuid GUC is on, returns the
2122 * underlying provenance UUID instead. UI layers (notably ProvSQL
2123 * Studio) flip this on per session so aggregate cells expose the
2124 * circuit root UUID for click-through; the @c "value (*)" display
2125 * string is recovered via @c provsql.agg_token_value_text(UUID).
2126 *
2127 * Marked STABLE rather than IMMUTABLE because the chosen output
2128 * shape now depends on a GUC that the same session can flip at
2129 * runtime.
2131CREATE OR REPLACE FUNCTION agg_token_out(AGG_TOKEN)
2132 RETURNS CSTRING
2133 AS 'provsql','agg_token_out' LANGUAGE C STABLE STRICT PARALLEL SAFE;
2134
2135/** @brief Cast an AGG_TOKEN to its TEXT representation */
2136CREATE OR REPLACE FUNCTION agg_token_cast(AGG_TOKEN)
2137 RETURNS TEXT
2138 AS 'provsql','agg_token_cast' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2139
2140CREATE TYPE AGG_TOKEN (
2141 internallength = 117,
2142 input = agg_token_in,
2143 output = agg_token_out,
2144 alignment = char
2145);
2146
2147/** @brief Extract the UUID from an AGG_TOKEN (implicit cast to UUID) */
2148CREATE OR REPLACE FUNCTION agg_token_uuid(aggtok AGG_TOKEN)
2149 RETURNS UUID AS
2150$$
2151BEGIN
2152 RETURN agg_token_cast(aggtok)::UUID;
2153END
2154$$ LANGUAGE plpgsql STRICT SET search_path=provsql,pg_temp,public SECURITY DEFINER IMMUTABLE PARALLEL SAFE;
2155
2156/** @brief Implicit PostgreSQL cast from AGG_TOKEN to UUID (delegates to agg_token_uuid()) */
2157CREATE CAST (AGG_TOKEN AS UUID) WITH FUNCTION agg_token_uuid(AGG_TOKEN) AS IMPLICIT;
2158
2159/**
2160 * @brief Recover the @c "value (*)" display string for an aggregation gate
2161 *
2162 * Companion helper to the @c provsql.aggtoken_text_as_uuid GUC. With
2163 * the GUC on, an @c AGG_TOKEN cell prints as the underlying provenance
2164 * UUID, which is convenient for tooling that wants to click through to
2165 * the circuit but loses the human-readable aggregate value. This
2166 * function takes such a UUID and returns the original @c "value (*)"
2167 * string by reading the gate's @c extra (set by aggregate evaluation
2168 * for @c agg gates, and by @c agg_arith_make for the @c arith gates
2169 * that AGG_TOKEN arithmetic mints). Returns @c NULL if @p token does
2170 * not resolve to an @c agg or @c arith gate.
2172 * @param token UUID of an @c agg gate (typically obtained from an
2173 * @c AGG_TOKEN cell when @c aggtoken_text_as_uuid is on,
2174 * or via a manual UUID cast otherwise).
2175 */
2176CREATE OR REPLACE FUNCTION agg_token_value_text(token UUID)
2177 RETURNS TEXT AS
2178$$
2179 SELECT CASE
2180 -- agg gates: extra is set by aggregate evaluation; arith gates
2181 -- (AGG_TOKEN arithmetic): extra is recorded by agg_arith_make.
2182 WHEN provsql.get_gate_type(token) IN ('agg', 'arith')
2183 THEN provsql.get_extra(token) || ' (*)'
2184 ELSE NULL
2185 END;
2186$$ LANGUAGE sql STABLE STRICT PARALLEL SAFE;
2188/** @brief Cast an AGG_TOKEN to NUMERIC (extracts the aggregate value, loses provenance) */
2189CREATE OR REPLACE FUNCTION agg_token_to_numeric(AGG_TOKEN)
2190 RETURNS NUMERIC
2191 AS 'provsql','agg_token_to_numeric' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2192
2193/** @brief Cast an AGG_TOKEN to double precision (extracts the aggregate value, loses provenance) */
2194CREATE OR REPLACE FUNCTION agg_token_to_float8(AGG_TOKEN)
2195 RETURNS double precision
2196 AS 'provsql','agg_token_to_float8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2197
2198/** @brief Cast an AGG_TOKEN to INTEGER (extracts the aggregate value, loses provenance) */
2199CREATE OR REPLACE FUNCTION agg_token_to_int4(AGG_TOKEN)
2200 RETURNS INTEGER
2201 AS 'provsql','agg_token_to_int4' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2202
2203/** @brief Cast an AGG_TOKEN to bigint (extracts the aggregate value, loses provenance) */
2204CREATE OR REPLACE FUNCTION agg_token_to_int8(AGG_TOKEN)
2205 RETURNS bigint
2206 AS 'provsql','agg_token_to_int8' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2207
2208/** @brief Cast an AGG_TOKEN to TEXT (extracts the aggregate value, loses provenance) */
2209CREATE OR REPLACE FUNCTION agg_token_to_text(AGG_TOKEN)
2210 RETURNS TEXT
2211 AS 'provsql','agg_token_to_text' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2212
2213/** @brief Assignment cast from AGG_TOKEN to NUMERIC (extracts the scalar
2214 * value, dropping provenance). ASSIGNMENT, not IMPLICIT: provenance-
2215 * preserving arithmetic on aggregates is provided by the native
2216 * AGG_TOKEN operators below, so an implicit NUMERIC coercion would only
2217 * silently steal `s + 1` away from them (and reroute it differently
2218 * depending on whether provsql is in search_path). Write `s::NUMERIC`
2219 * to opt into the lossy scalar. */
2220CREATE CAST (AGG_TOKEN AS NUMERIC) WITH FUNCTION agg_token_to_numeric(AGG_TOKEN) AS ASSIGNMENT;
2221
2222-- ---------------------------------------------------------------------
2223-- Arithmetic on aggregates (AGG_TOKEN)
2224--
2225-- Mirrors the random_variable arithmetic surface: the operators build a
2226-- `gate_arith` over the operand provenance UUIDs (via provenance_arith,
2227-- info1 = PROVSQL_ARITH_*), so the arithmetic is recorded symbolically
2228-- in the circuit and can be resolved when a comparison (gate_cmp) over
2229-- the result is evaluated. Unlike random_variable (a bare UUID), an
2230-- AGG_TOKEN also carries a running scalar value, so each operator
2231-- additionally computes the resulting value and bundles it back with the
2232-- new gate.
2233-- ---------------------------------------------------------------------
2234
2235/** @brief Running value of an AGG_TOKEN as NUMERIC, without the
2236 * provenance-loss warning the public cast emits (internal use). */
2237CREATE OR REPLACE FUNCTION agg_token_value(AGG_TOKEN)
2238 RETURNS NUMERIC
2239 AS 'provsql','agg_token_value' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2240
2241/** @brief Bundle a provenance gate UUID with a running value into an
2242 * AGG_TOKEN (inverse of the agg_token_uuid / agg_token_value
2243 * accessors). */
2244CREATE OR REPLACE FUNCTION agg_token_make(tok UUID, val NUMERIC)
2245 RETURNS AGG_TOKEN AS
2247 SELECT format('( %s , %s )', tok::TEXT, val::TEXT)::provsql.AGG_TOKEN;
2248$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
2249 SET search_path=provsql,pg_temp,public;
2251/** @brief Lift a scalar NUMERIC constant into a gate_value leaf and
2252 * return its UUID, so it can be a child of a gate_arith (the agg-side
2253 * analogue of as_random for random_variable). */
2254CREATE OR REPLACE FUNCTION agg_value_gate(v NUMERIC)
2255 RETURNS UUID AS
2256$$
2257DECLARE
2258 token UUID := public.uuid_generate_v5(
2259 provsql.uuid_ns_provsql(), concat('value', v::TEXT));
2260BEGIN
2261 PERFORM provsql.create_gate(token, 'value');
2262 PERFORM provsql.set_extra(token, v::TEXT);
2263 RETURN token;
2264END
2265$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE
2266 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
2267
2268/** @brief Mint (or reuse) the gate_arith for an AGG_TOKEN arithmetic
2269 * result and return the AGG_TOKEN carrying it.
2270 *
2271 * Also records the computed scalar in the gate's @c extra -- exactly
2272 * what aggregate evaluation does for @c agg gates -- so
2273 * @c agg_token_value_text can recover the @c "value (*)" display from
2274 * the bare UUID (as ProvSQL Studio does for result cells under
2275 * @c provsql.aggtoken_text_as_uuid). The gate UUID is deterministic in
2276 * (op, children), so re-recording the (identical) value is idempotent. */
2277CREATE OR REPLACE FUNCTION agg_arith_make(op INT, children UUID[], val NUMERIC)
2278 RETURNS AGG_TOKEN AS
2279$$
2280DECLARE
2281 token UUID := provsql.provenance_arith(op, children);
2282BEGIN
2283 PERFORM provsql.set_extra(token, val::TEXT);
2284 RETURN provsql.agg_token_make(token, val);
2285END
2286$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
2287 SET search_path=provsql,pg_temp,public SECURITY DEFINER;
2288
2289-- AGG_TOKEN <op> AGG_TOKEN --------------------------------------------
2290/** @brief AGG_TOKEN + AGG_TOKEN (gate_arith PLUS). */
2291CREATE OR REPLACE FUNCTION agg_token_plus(a AGG_TOKEN, b AGG_TOKEN)
2292 RETURNS AGG_TOKEN AS
2293$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, (b)::UUID],
2294 provsql.agg_token_value(a) + provsql.agg_token_value(b)); $$
2295 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2296
2297/** @brief AGG_TOKEN - AGG_TOKEN (gate_arith MINUS). */
2298CREATE OR REPLACE FUNCTION agg_token_minus(a AGG_TOKEN, b AGG_TOKEN)
2299 RETURNS AGG_TOKEN AS
2300$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, (b)::UUID],
2301 provsql.agg_token_value(a) - provsql.agg_token_value(b)); $$
2302 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2303
2304/** @brief AGG_TOKEN * AGG_TOKEN (gate_arith TIMES). */
2305CREATE OR REPLACE FUNCTION agg_token_times(a AGG_TOKEN, b AGG_TOKEN)
2306 RETURNS AGG_TOKEN AS
2307$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, (b)::UUID],
2308 provsql.agg_token_value(a) * provsql.agg_token_value(b)); $$
2309 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2310
2311/** @brief AGG_TOKEN / AGG_TOKEN (gate_arith DIV). */
2312CREATE OR REPLACE FUNCTION agg_token_div(a AGG_TOKEN, b AGG_TOKEN)
2313 RETURNS AGG_TOKEN AS
2314$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, (b)::UUID],
2315 provsql.agg_token_value(a) / provsql.agg_token_value(b)); $$
2316 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2317
2318/** @brief Unary -AGG_TOKEN (gate_arith NEG). */
2319CREATE OR REPLACE FUNCTION agg_token_neg(a AGG_TOKEN)
2320 RETURNS AGG_TOKEN AS
2321$$ SELECT provsql.agg_arith_make(4, ARRAY[(a)::UUID],
2322 - provsql.agg_token_value(a)); $$
2323 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2324
2325-- AGG_TOKEN <op> NUMERIC ----------------------------------------------
2326/** @brief AGG_TOKEN + NUMERIC (gate_arith PLUS, constant lifted to a value gate). */
2327CREATE OR REPLACE FUNCTION agg_token_plus_numeric(a AGG_TOKEN, b NUMERIC)
2328 RETURNS AGG_TOKEN AS
2329$$ SELECT provsql.agg_arith_make(0, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2330 provsql.agg_token_value(a) + b); $$
2331 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2332
2333/** @brief AGG_TOKEN - NUMERIC. */
2334CREATE OR REPLACE FUNCTION agg_token_minus_numeric(a AGG_TOKEN, b NUMERIC)
2335 RETURNS AGG_TOKEN AS
2336$$ SELECT provsql.agg_arith_make(2, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2337 provsql.agg_token_value(a) - b); $$
2338 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2340/** @brief AGG_TOKEN * NUMERIC. */
2341CREATE OR REPLACE FUNCTION agg_token_times_numeric(a AGG_TOKEN, b NUMERIC)
2342 RETURNS AGG_TOKEN AS
2343$$ SELECT provsql.agg_arith_make(1, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2344 provsql.agg_token_value(a) * b); $$
2345 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2346
2347/** @brief AGG_TOKEN / NUMERIC. */
2348CREATE OR REPLACE FUNCTION agg_token_div_numeric(a AGG_TOKEN, b NUMERIC)
2349 RETURNS AGG_TOKEN AS
2350$$ SELECT provsql.agg_arith_make(3, ARRAY[(a)::UUID, provsql.agg_value_gate(b)],
2351 provsql.agg_token_value(a) / b); $$
2352 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2353
2354-- NUMERIC <op> AGG_TOKEN ----------------------------------------------
2355/** @brief NUMERIC + AGG_TOKEN. */
2356CREATE OR REPLACE FUNCTION numeric_plus_agg_token(a NUMERIC, b AGG_TOKEN)
2357 RETURNS AGG_TOKEN AS
2358$$ SELECT provsql.agg_arith_make(0, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2359 a + provsql.agg_token_value(b)); $$
2360 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2361
2362/** @brief NUMERIC - AGG_TOKEN. */
2363CREATE OR REPLACE FUNCTION numeric_minus_agg_token(a NUMERIC, b AGG_TOKEN)
2364 RETURNS AGG_TOKEN AS
2365$$ SELECT provsql.agg_arith_make(2, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2366 a - provsql.agg_token_value(b)); $$
2367 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2368
2369/** @brief NUMERIC * AGG_TOKEN. */
2370CREATE OR REPLACE FUNCTION numeric_times_agg_token(a NUMERIC, b AGG_TOKEN)
2371 RETURNS AGG_TOKEN AS
2372$$ SELECT provsql.agg_arith_make(1, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2373 a * provsql.agg_token_value(b)); $$
2374 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2375
2376/** @brief NUMERIC / AGG_TOKEN. */
2377CREATE OR REPLACE FUNCTION numeric_div_agg_token(a NUMERIC, b AGG_TOKEN)
2378 RETURNS AGG_TOKEN AS
2379$$ SELECT provsql.agg_arith_make(3, ARRAY[provsql.agg_value_gate(a), (b)::UUID],
2380 a / provsql.agg_token_value(b)); $$
2381 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2383-- Operator declarations -----------------------------------------------
2384CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_plus, COMMUTATOR = +);
2385CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_minus);
2386CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_times, COMMUTATOR = *);
2387CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_div);
2388CREATE OPERATOR - (RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_neg);
2389
2390CREATE OPERATOR + (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_plus_numeric, COMMUTATOR = +);
2391CREATE OPERATOR - (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_minus_numeric);
2392CREATE OPERATOR * (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_times_numeric, COMMUTATOR = *);
2393CREATE OPERATOR / (LEFTARG=AGG_TOKEN, RIGHTARG=NUMERIC, PROCEDURE=agg_token_div_numeric);
2394
2395CREATE OPERATOR + (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_plus_agg_token, COMMUTATOR = +);
2396CREATE OPERATOR - (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_minus_agg_token);
2397CREATE OPERATOR * (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_times_agg_token, COMMUTATOR = *);
2398CREATE OPERATOR / (LEFTARG=NUMERIC, RIGHTARG=AGG_TOKEN, PROCEDURE=numeric_div_agg_token);
2399
2400/** @brief Assignment cast from AGG_TOKEN to double precision */
2401CREATE CAST (AGG_TOKEN AS double precision) WITH FUNCTION agg_token_to_float8(AGG_TOKEN) AS ASSIGNMENT;
2402/** @brief Assignment cast from AGG_TOKEN to INTEGER */
2403CREATE CAST (AGG_TOKEN AS INTEGER) WITH FUNCTION agg_token_to_int4(AGG_TOKEN) AS ASSIGNMENT;
2404/** @brief Assignment cast from AGG_TOKEN to bigint */
2405CREATE CAST (AGG_TOKEN AS bigint) WITH FUNCTION agg_token_to_int8(AGG_TOKEN) AS ASSIGNMENT;
2406/** @brief Assignment cast from AGG_TOKEN to TEXT (extracts value, not UUID) */
2407CREATE CAST (AGG_TOKEN AS TEXT) WITH FUNCTION agg_token_to_text(AGG_TOKEN) AS ASSIGNMENT;
2408
2409/**
2410 * @brief Condition a discrete aggregate's distribution on an event:
2411 * @c "SUM(x) | C".
2413 * Mirrors @c random_variable_cond for the @c AGG_TOKEN carrier: returns a
2414 * conditioned @c AGG_TOKEN that flows onward, its provenance token wrapped in
2415 * the composable two-child @c gate_conditioned @c [agg_target, condition]
2416 * while its running value is preserved. The moment / support dispatchers
2417 * unpack it (@c agg_conditioned_target + @c rv_conditioned_prov) and route
2418 * through the existing @c agg_raw_moment with the condition conjoined into the
2419 * @c prov argument, so @c expected(SUM(x)|C) / @c variance(SUM(x)|C) report
2420 * the conditional aggregate distribution. Nested conditioning folds.
2421 */
2422CREATE OR REPLACE FUNCTION agg_token_cond(a AGG_TOKEN, cond UUID)
2423 RETURNS AGG_TOKEN AS
2424$$
2425DECLARE
2426 tok UUID;
2427 ev UUID;
2428 result UUID;
2429 ch UUID[];
2430BEGIN
2431 IF cond IS NULL OR cond = gate_one() THEN
2432 RETURN a;
2433 END IF;
2434
2435 tok := (a)::UUID;
2436 IF get_gate_type(tok) = 'conditioned'
2437 AND array_length(get_children(tok), 1) = 2 THEN
2438 ch := get_children(tok);
2439 tok := ch[1];
2440 ev := provenance_times(ch[2], cond);
2441 ELSE
2442 ev := cond;
2443 END IF;
2444
2445 result := public.uuid_generate_v5(uuid_ns_provsql(),
2446 concat('conditioned', tok, ev));
2447 PERFORM create_gate(result, 'conditioned', ARRAY[tok, ev]);
2448 RETURN agg_token_make(result, agg_token_value(a));
2449END
2450$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
2451 SECURITY DEFINER PARALLEL SAFE;
2452
2453CREATE OPERATOR | (
2454 LEFTARG = AGG_TOKEN,
2455 RIGHTARG = UUID,
2456 PROCEDURE = agg_token_cond
2457);
2458
2459/**
2460 * @brief Placeholder for @c "SUM(x) | (predicate)" on an AGG_TOKEN.
2462 * Lets the conditioning event be a natural Boolean predicate (e.g.
2463 * @c "SUM(x) | (SUM(x) > 5)") instead of a hand-built gate. Never executes:
2464 * the planner converts the Boolean operand into a condition gate and emits
2465 * @c agg_token_cond.
2466 */
2467CREATE OR REPLACE FUNCTION agg_token_cond_predicate(
2468 a AGG_TOKEN, predicate BOOLEAN) RETURNS AGG_TOKEN AS
2469$$
2470BEGIN
2471 RAISE EXCEPTION 'AGG_TOKEN | (predicate) must be rewritten by the ProvSQL '
2472 'planner hook: the right operand must be a Boolean combination of '
2473 'aggregate / random_variable comparisons (is provsql.active off?)';
2474END
2475$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
2477CREATE OPERATOR | (
2478 LEFTARG = AGG_TOKEN,
2479 RIGHTARG = BOOLEAN,
2480 PROCEDURE = agg_token_cond_predicate
2481);
2482
2483/**
2484 * @brief Unpack the target of a conditioned @c AGG_TOKEN.
2485 *
2486 * For a @c "SUM(x) | C" whose provenance token is the two-child
2487 * @c gate_conditioned @c [agg_target, condition] returns the AGG_TOKEN over
2488 * @c agg_target (same running value); for any other AGG_TOKEN returns it
2489 * unchanged. The conditioning event itself is recovered separately via
2490 * @c rv_conditioned_prov on the token's UUID.
2491 */
2492CREATE OR REPLACE FUNCTION agg_conditioned_target(a AGG_TOKEN)
2493 RETURNS AGG_TOKEN AS
2494$$
2495 SELECT CASE
2496 WHEN provsql.get_gate_type((a)::UUID) = 'conditioned'
2497 AND array_length(provsql.get_children((a)::UUID), 1) = 2
2498 THEN provsql.agg_token_make(
2499 (provsql.get_children((a)::UUID))[1], provsql.agg_token_value(a))
2500 ELSE a
2501 END;
2502$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
2503
2504/**
2505 * @brief Placeholder comparison of AGG_TOKEN with NUMERIC
2506 *
2507 * This function is never actually called; it exists so the SQL parser
2508 * accepts comparison operators between AGG_TOKEN and NUMERIC values.
2509 * The ProvSQL query rewriter replaces these comparisons at plan time.
2510 */
2511CREATE OR REPLACE FUNCTION agg_token_comp_numeric(a AGG_TOKEN, b NUMERIC)
2512RETURNS BOOLEAN
2513LANGUAGE plpgsql
2514IMMUTABLE STRICT PARALLEL SAFE
2515AS $$
2516BEGIN
2517 RAISE EXCEPTION 'Comparison AGG_TOKEN-NUMERIC not implemented, should be replaced by ProvSQL behavior';
2518END;
2519$$;
2520
2521/**
2522 * @brief Placeholder comparison of NUMERIC with AGG_TOKEN
2523 *
2524 * Symmetric to agg_token_comp_numeric; never actually called.
2525 * The ProvSQL query rewriter replaces these comparisons at plan time.
2526 */
2527CREATE OR REPLACE FUNCTION numeric_comp_agg_token(a NUMERIC, b AGG_TOKEN)
2528RETURNS BOOLEAN
2529LANGUAGE plpgsql
2530IMMUTABLE STRICT PARALLEL SAFE
2531AS $$
2532BEGIN
2533 RAISE EXCEPTION 'Comparison NUMERIC-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
2534END;
2535$$;
2536
2537/** @brief SQL operator AGG_TOKEN < NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2538CREATE OPERATOR < (
2539 LEFTARG = AGG_TOKEN,
2540 RIGHTARG = NUMERIC,
2541 PROCEDURE = agg_token_comp_numeric,
2542 COMMUTATOR = >,
2543 NEGATOR = >=
2544);
2545/** @brief SQL operator NUMERIC < AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2546CREATE OPERATOR < (
2547 LEFTARG = NUMERIC,
2548 RIGHTARG = AGG_TOKEN,
2549 PROCEDURE = numeric_comp_agg_token,
2550 COMMUTATOR = >,
2551 NEGATOR = >=
2552);
2553
2554/** @brief SQL operator AGG_TOKEN <= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2555CREATE OPERATOR <= (
2556 LEFTARG = AGG_TOKEN,
2557 RIGHTARG = NUMERIC,
2558 PROCEDURE = agg_token_comp_numeric,
2559 COMMUTATOR = >=,
2560 NEGATOR = >
2561);
2562/** @brief SQL operator NUMERIC <= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2563CREATE OPERATOR <= (
2564 LEFTARG = NUMERIC,
2565 RIGHTARG = AGG_TOKEN,
2566 PROCEDURE = numeric_comp_agg_token,
2567 COMMUTATOR = >=,
2568 NEGATOR = >
2569);
2570
2571/** @brief SQL operator AGG_TOKEN = NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2572CREATE OPERATOR = (
2573 LEFTARG = AGG_TOKEN,
2574 RIGHTARG = NUMERIC,
2575 PROCEDURE = agg_token_comp_numeric,
2576 COMMUTATOR = =,
2577 NEGATOR = <>
2578);
2579/** @brief SQL operator NUMERIC = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2580CREATE OPERATOR = (
2581 LEFTARG = NUMERIC,
2582 RIGHTARG = AGG_TOKEN,
2583 PROCEDURE = numeric_comp_agg_token,
2584 COMMUTATOR = =,
2585 NEGATOR = <>
2587
2588/** @brief SQL operator AGG_TOKEN <> NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2589CREATE OPERATOR <> (
2590 LEFTARG = AGG_TOKEN,
2591 RIGHTARG = NUMERIC,
2592 PROCEDURE = agg_token_comp_numeric,
2593 COMMUTATOR = <>,
2594 NEGATOR = =
2595);
2596/** @brief SQL operator NUMERIC <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2597CREATE OPERATOR <> (
2598 LEFTARG = NUMERIC,
2599 RIGHTARG = AGG_TOKEN,
2600 PROCEDURE = numeric_comp_agg_token,
2601 COMMUTATOR = <>,
2602 NEGATOR = =
2603);
2605/** @brief SQL operator AGG_TOKEN >= NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2606CREATE OPERATOR >= (
2607 LEFTARG = AGG_TOKEN,
2608 RIGHTARG = NUMERIC,
2609 PROCEDURE = agg_token_comp_numeric,
2610 COMMUTATOR = <=,
2611 NEGATOR = <
2612);
2613/** @brief SQL operator NUMERIC >= AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2614CREATE OPERATOR >= (
2615 LEFTARG = NUMERIC,
2616 RIGHTARG = AGG_TOKEN,
2617 PROCEDURE = numeric_comp_agg_token,
2618 COMMUTATOR = <=,
2619 NEGATOR = <
2620);
2621
2622/** @brief SQL operator AGG_TOKEN > NUMERIC (placeholder rewritten by ProvSQL at plan time) */
2623CREATE OPERATOR > (
2624 LEFTARG = AGG_TOKEN,
2625 RIGHTARG = NUMERIC,
2626 PROCEDURE = agg_token_comp_numeric,
2627 COMMUTATOR = <,
2628 NEGATOR = <=
2629);
2630/** @brief SQL operator NUMERIC > AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2631CREATE OPERATOR > (
2632 LEFTARG = NUMERIC,
2633 RIGHTARG = AGG_TOKEN,
2634 PROCEDURE = numeric_comp_agg_token,
2635 COMMUTATOR = <,
2636 NEGATOR = <=
2637);
2638
2639/**
2640 * @brief Placeholder comparison of two AGG_TOKEN values (the diagonal)
2641 *
2642 * Never actually called; lets the parser accept AGG_TOKEN \<op\> AGG_TOKEN
2643 * (e.g. sum(x) > sum(y) on materialised tokens), which the ProvSQL
2644 * rewriter lowers to a gate_cmp at plan time. Declaring this diagonal
2645 * also disambiguates `s = s2` (otherwise "operator is not unique",
2646 * because both AGG_TOKEN -> UUID and AGG_TOKEN -> NUMERIC casts apply).
2647 */
2648CREATE OR REPLACE FUNCTION agg_token_comp_agg_token(a AGG_TOKEN, b AGG_TOKEN)
2649RETURNS BOOLEAN
2650LANGUAGE plpgsql
2651IMMUTABLE STRICT PARALLEL SAFE
2652AS $$
2653BEGIN
2654 RAISE EXCEPTION 'Comparison AGG_TOKEN-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
2655END;
2656$$;
2657
2658/** @brief SQL operator AGG_TOKEN < AGG_TOKEN (placeholder rewritten at plan time) */
2659CREATE OPERATOR < (
2660 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2661 COMMUTATOR = >, NEGATOR = >=
2662);
2663/** @brief SQL operator AGG_TOKEN <= AGG_TOKEN (placeholder rewritten at plan time) */
2664CREATE OPERATOR <= (
2665 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2666 COMMUTATOR = >=, NEGATOR = >
2667);
2668/** @brief SQL operator AGG_TOKEN > AGG_TOKEN (placeholder rewritten at plan time) */
2669CREATE OPERATOR > (
2670 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2671 COMMUTATOR = <, NEGATOR = <=
2672);
2673/** @brief SQL operator AGG_TOKEN >= AGG_TOKEN (placeholder rewritten at plan time) */
2674CREATE OPERATOR >= (
2675 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2676 COMMUTATOR = <=, NEGATOR = <
2677);
2678/** @brief SQL operator AGG_TOKEN = AGG_TOKEN (placeholder rewritten at plan time) */
2679CREATE OPERATOR = (
2680 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2681 COMMUTATOR = =, NEGATOR = <>
2682);
2683/** @brief SQL operator AGG_TOKEN <> AGG_TOKEN (placeholder rewritten at plan time) */
2684CREATE OPERATOR <> (
2685 LEFTARG=AGG_TOKEN, RIGHTARG=AGG_TOKEN, PROCEDURE=agg_token_comp_agg_token,
2686 COMMUTATOR = <>, NEGATOR = =
2687);
2688
2689/**
2690 * @brief Placeholder comparison of AGG_TOKEN with TEXT
2691 *
2692 * This function is never actually called; it exists so the SQL parser
2693 * accepts comparison operators between AGG_TOKEN and TEXT values.
2694 * The ProvSQL query rewriter replaces these comparisons at plan time.
2695 */
2696CREATE OR REPLACE FUNCTION agg_token_comp_text(a AGG_TOKEN, b TEXT)
2697RETURNS BOOLEAN
2698LANGUAGE plpgsql
2699IMMUTABLE STRICT PARALLEL SAFE
2700AS $$
2701BEGIN
2702 RAISE EXCEPTION 'Comparison AGG_TOKEN-TEXT not implemented, should be replaced by ProvSQL behavior';
2703END;
2704$$;
2705
2706/**
2707 * @brief Placeholder comparison of TEXT with AGG_TOKEN
2708 *
2709 * Symmetric to agg_token_comp_text; never actually called.
2710 * The ProvSQL query rewriter replaces these comparisons at plan time.
2711 */
2712CREATE OR REPLACE FUNCTION text_comp_agg_token(a TEXT, b AGG_TOKEN)
2713RETURNS BOOLEAN
2714LANGUAGE plpgsql
2715IMMUTABLE STRICT PARALLEL SAFE
2716AS $$
2717BEGIN
2718 RAISE EXCEPTION 'Comparison TEXT-AGG_TOKEN not implemented, should be replaced by ProvSQL behavior';
2719END;
2720$$;
2721
2722/** @brief SQL operator AGG_TOKEN = TEXT (placeholder rewritten by ProvSQL at plan time) */
2723CREATE OPERATOR = (
2724 LEFTARG = AGG_TOKEN,
2725 RIGHTARG = TEXT,
2726 PROCEDURE = agg_token_comp_text,
2727 COMMUTATOR = =,
2728 NEGATOR = <>
2729);
2730/** @brief SQL operator TEXT = AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2731CREATE OPERATOR = (
2732 LEFTARG = TEXT,
2733 RIGHTARG = AGG_TOKEN,
2734 PROCEDURE = text_comp_agg_token,
2735 COMMUTATOR = =,
2736 NEGATOR = <>
2737);
2738
2739/** @brief SQL operator AGG_TOKEN <> TEXT (placeholder rewritten by ProvSQL at plan time) */
2740CREATE OPERATOR <> (
2741 LEFTARG = AGG_TOKEN,
2742 RIGHTARG = TEXT,
2743 PROCEDURE = agg_token_comp_text,
2744 COMMUTATOR = <>,
2745 NEGATOR = =
2747/** @brief SQL operator TEXT <> AGG_TOKEN (placeholder rewritten by ProvSQL at plan time) */
2748CREATE OPERATOR <> (
2749 LEFTARG = TEXT,
2750 RIGHTARG = AGG_TOKEN,
2751 PROCEDURE = text_comp_agg_token,
2752 COMMUTATOR = <>,
2753 NEGATOR = =
2754);
2755
2756/** @} */
2757
2758/** @defgroup random_variable_type Type for continuous random variables
2759 *
2760 * Custom type <tt>random_variable</tt>: a thin wrapper around a
2761 * provenance gate UUID, used to expose continuous probabilistic
2762 * c-tables in SQL. The UUID indexes either a <tt>gate_rv</tt>
2763 * (an actual distribution) or a <tt>gate_value</tt> (a
2764 * zero-variance constant produced by <tt>provsql.as_random</tt>).
2765 * Binary-coercible with <tt>UUID</tt> (same 16-byte layout), so an
2766 * <tt>rv</tt>-typed expression flows directly into any function
2767 * expecting a UUID at zero runtime cost.
2768 *
2769 * Constructors live in this group: <tt>provsql.normal(μ, σ)</tt>,
2770 * <tt>provsql.uniform(a, b)</tt>, <tt>provsql.exponential(λ)</tt>,
2771 * <tt>provsql.erlang(k, λ)</tt>, and <tt>provsql.as_random(c)</tt>.
2772 * Operator overloads
2773 * (<tt>+ - * /</tt> and the six comparators) are defined further
2774 * below, alongside direct <tt>rv_cmp_*</tt> UUID constructors for
2775 * callers that want a <tt>gate_cmp</tt> token without going through
2776 * the planner hook.
2777 * @{
2778 */
2779
2780CREATE TYPE random_variable;
2781
2782/** @brief Input function for the random_variable type */
2783CREATE OR REPLACE FUNCTION random_variable_in(CSTRING)
2784 RETURNS random_variable
2785 AS 'provsql','random_variable_in' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2786
2787/** @brief Output function for the random_variable type */
2788CREATE OR REPLACE FUNCTION random_variable_out(random_variable)
2789 RETURNS CSTRING
2790 AS 'provsql','random_variable_out' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2791
2792CREATE TYPE random_variable (
2793 internallength = 16,
2794 input = random_variable_in,
2795 output = random_variable_out,
2796 alignment = char
2797);
2798
2799/** @brief Build a random_variable from a UUID (internal). */
2800CREATE OR REPLACE FUNCTION random_variable_make(tok UUID)
2801 RETURNS random_variable
2802 AS 'provsql','random_variable_make' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
2803
2804/** @brief Binary-coercible cast random_variable -> UUID.
2805 * A random_variable is byte-for-byte a pg_uuid_t (alignment char,
2806 * length 16), so WITHOUT FUNCTION lets PostgreSQL reinterpret the
2807 * bytes at zero runtime cost. The cast is ASSIGNMENT (not IMPLICIT):
2808 * an implicit cross-domain cast would silently reroute a comparison
2809 * such as `v < w` to `UUID < UUID` (raw byte comparison) whenever
2810 * `provsql` is not in search_path, since operators are resolved
2811 * through search_path but casts are not. Demoting to ASSIGNMENT
2812 * turns that silent wrong result into a clean parse error. Passing a
2813 * random_variable to a UUID-taking function now needs an explicit
2814 * `v::UUID` (function resolution never applies assignment casts). */
2815CREATE CAST (random_variable AS UUID) WITHOUT FUNCTION AS ASSIGNMENT;
2816CREATE CAST (UUID AS random_variable) WITHOUT FUNCTION;
2817
2818/**
2819 * @brief Internal: true iff @p x is a finite (non-NaN, non-±∞) float8.
2820 *
2821 * PostgreSQL's <tt>isnan</tt> is defined for <tt>NUMERIC</tt> only,
2822 * not for <tt>double precision</tt>; we use the inequality form,
2823 * which works because PG defines <tt>NaN = NaN</tt> as <tt>TRUE</tt>
2824 * for floats (so <tt>NaN <> 'NaN'::float8</tt> is <tt>FALSE</tt>).
2825 */
2826CREATE OR REPLACE FUNCTION is_finite_float8(x double precision)
2827 RETURNS BOOL AS
2828$$
2829 SELECT $1 <> 'NaN'::float8 AND $1 <> 'Infinity'::float8 AND $1 <> '-Infinity'::float8;
2830$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
2831
2832/**
2833 * @brief Construct a normal-distribution random variable
2835 * Creates a fresh <tt>gate_rv</tt> with @c "normal:μ,σ" stored in
2836 * the gate's @c extra field, and returns a <tt>random_variable</tt>
2837 * pointing at it.
2838 *
2839 * Validation:
2840 * - @p mu and @p sigma must be finite (no @c NaN, no @c ±Infinity).
2841 * - @p sigma must be non-negative.
2842 * - When @p sigma is zero the distribution degenerates to the Dirac
2843 * at @p mu; the call is silently routed through @c as_random(mu),
2844 * producing a @c gate_value rather than a zero-variance @c gate_rv.
2845 * This keeps the sampler / moment / boundcheck paths free of σ=0
2846 * special cases and lets <tt>normal(x, 0)</tt> share its gate with
2847 * <tt>as_random(x)</tt>.
2848 *
2849 * @warning The <tt>VOLATILE</tt> marking is load-bearing and must
2850 * not be weakened. Each call mints a fresh <tt>uuid_generate_v4</tt>
2851 * token because two calls to <tt>normal(0, 1)</tt> are *independent*
2852 * random variables; if PostgreSQL were allowed to fold the function
2853 * (which it would under <tt>STABLE</tt> / <tt>IMMUTABLE</tt>), two
2854 * calls in the same query would share a UUID and collapse into a
2855 * single dependent RV, silently breaking the c-table semantics.
2856 * Same warning applies to @c uniform and @c exponential below.
2857 *
2858 * @sa <a href="https://en.wikipedia.org/wiki/Normal_distribution">Wikipedia: Normal distribution</a>
2859 */
2860CREATE OR REPLACE FUNCTION normal(mu double precision, sigma double precision)
2861 RETURNS random_variable AS
2862$$
2863DECLARE
2864 token UUID;
2865BEGIN
2866 IF NOT provsql.is_finite_float8(mu) OR NOT provsql.is_finite_float8(sigma) THEN
2867 RAISE EXCEPTION 'provsql.normal: parameters must be finite (got mu=%, sigma=%)', mu, sigma;
2868 END IF;
2869 IF sigma < 0 THEN
2870 RAISE EXCEPTION 'provsql.normal: sigma must be non-negative (got %)', sigma;
2871 END IF;
2872 IF sigma = 0 THEN
2873 RETURN provsql.as_random(mu);
2874 END IF;
2875 token := public.uuid_generate_v4();
2876 PERFORM provsql.create_gate(token, 'rv');
2877 PERFORM provsql.set_extra(token, 'normal:' || mu || ',' || sigma);
2878 RETURN provsql.random_variable_make(token);
2879END
2880$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
2881
2882/**
2883 * @brief Construct a uniform-distribution random variable on [a, b]
2884 *
2885 * Validation:
2886 * - @p a and @p b must be finite.
2887 * - @p a must be ≤ @p b (reversed bounds are rejected).
2888 * - When <tt>a = b</tt> the distribution is the Dirac at @p a; the
2889 * call is silently routed through @c as_random(a) for the same
2890 * reason as @c normal with @p sigma = 0.
2891 *
2892 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
2893 * @ref normal.
2894 *
2895 * @sa <a href="https://en.wikipedia.org/wiki/Continuous_uniform_distribution">Wikipedia: Continuous uniform distribution</a>
2896 */
2897CREATE OR REPLACE FUNCTION uniform(a double precision, b double precision)
2898 RETURNS random_variable AS
2899$$
2900DECLARE
2901 token UUID;
2902BEGIN
2903 IF NOT provsql.is_finite_float8(a) OR NOT provsql.is_finite_float8(b) THEN
2904 RAISE EXCEPTION 'provsql.uniform: bounds must be finite (got a=%, b=%)', a, b;
2905 END IF;
2906 IF a > b THEN
2907 RAISE EXCEPTION 'provsql.uniform: a must be <= b (got a=%, b=%)', a, b;
2908 END IF;
2909 IF a = b THEN
2910 RETURN provsql.as_random(a);
2911 END IF;
2912 token := public.uuid_generate_v4();
2913 PERFORM provsql.create_gate(token, 'rv');
2914 PERFORM provsql.set_extra(token, 'uniform:' || a || ',' || b);
2915 RETURN provsql.random_variable_make(token);
2916END
2917$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
2918
2919/**
2920 * @brief Construct an exponential-distribution random variable with rate λ
2921 *
2922 * Validation:
2923 * - @p lambda must be finite and strictly positive. No degenerate
2924 * form exists for the exponential distribution, so there is no
2925 * silent route through @c as_random.
2926 *
2927 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
2928 * @ref normal.
2929 *
2930 * @sa <a href="https://en.wikipedia.org/wiki/Exponential_distribution">Wikipedia: Exponential distribution</a>
2931 */
2932CREATE OR REPLACE FUNCTION exponential(lambda double precision)
2933 RETURNS random_variable AS
2934$$
2935DECLARE
2936 token UUID;
2937BEGIN
2938 IF NOT provsql.is_finite_float8(lambda) THEN
2939 RAISE EXCEPTION 'provsql.exponential: lambda must be finite (got %)', lambda;
2940 END IF;
2941 IF lambda <= 0 THEN
2942 RAISE EXCEPTION 'provsql.exponential: lambda must be strictly positive (got %)', lambda;
2943 END IF;
2944 token := public.uuid_generate_v4();
2945 PERFORM provsql.create_gate(token, 'rv');
2946 PERFORM provsql.set_extra(token, 'exponential:' || lambda);
2947 RETURN provsql.random_variable_make(token);
2948END
2949$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
2950
2951/**
2952 * @brief Construct an Erlang-distribution random variable, sum of
2953 * @p k i.i.d. exponentials with shared rate @p lambda
2954 *
2955 * The Erlang distribution is the sum of @p k independent
2956 * <tt>Exp(λ)</tt> random variables (equivalently the gamma with
2957 * INTEGER shape). It is the natural closure of i.i.d.
2958 * exponentials under addition, and is materialised here as a single
2959 * <tt>gate_rv</tt> so the analytic CDF and closed-form moments fire
2960 * directly (rather than the sampler having to draw and sum @p k
2961 * exponential leaves per Monte-Carlo iteration).
2962 *
2963 * Validation:
2964 * - @p k must be ≥ 1. The degenerate @c k=1 case is silently routed
2965 * through @c exponential so <tt>erlang(1, λ)</tt> shares its gate
2966 * with <tt>exponential(λ)</tt>.
2967 * - @p lambda must be finite and strictly positive.
2968 *
2969 * @warning <tt>VOLATILE</tt> is load-bearing; see the warning on
2970 * @ref normal.
2971 *
2972 * @sa <a href="https://en.wikipedia.org/wiki/Erlang_distribution">Wikipedia: Erlang distribution</a>
2973 */
2974CREATE OR REPLACE FUNCTION erlang(k INTEGER, lambda double precision)
2975 RETURNS random_variable AS
2976$$
2977DECLARE
2978 token UUID;
2979BEGIN
2980 IF k < 1 THEN
2981 RAISE EXCEPTION 'provsql.erlang: k must be >= 1 (got %)', k;
2982 END IF;
2983 IF NOT provsql.is_finite_float8(lambda) THEN
2984 RAISE EXCEPTION 'provsql.erlang: lambda must be finite (got %)', lambda;
2985 END IF;
2986 IF lambda <= 0 THEN
2987 RAISE EXCEPTION 'provsql.erlang: lambda must be strictly positive (got %)', lambda;
2988 END IF;
2989 IF k = 1 THEN
2990 RETURN provsql.exponential(lambda);
2991 END IF;
2992 token := public.uuid_generate_v4();
2993 PERFORM provsql.create_gate(token, 'rv');
2994 PERFORM provsql.set_extra(token, 'erlang:' || k || ',' || lambda);
2995 RETURN provsql.random_variable_make(token);
2996END
2997$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
2998
2999/**
3000 * @brief Construct a probabilistic-mixture random variable.
3001 *
3002 * Returns a @c random_variable whose distribution is a Bernoulli
3003 * mixture of two scalar RV roots: with probability <tt>P(p = true)</tt>
3004 * the mixture samples @p x, with the complementary probability it
3005 * samples @p y. The mixing token @p p is a @c gate_input Bernoulli
3006 * whose probability has been pinned with @c set_prob, and the same
3007 * @p p can be shared with other branches of the circuit -- the
3008 * Monte-Carlo sampler's per-iteration cache couples every reference
3009 * to the same draw, so users can build joint conditional structures
3010 * (e.g. <tt>mixture(p, X1, Y1) + mixture(p, X2, Y2)</tt> samples
3011 * X1 + X2 with prob π and Y1 + Y2 with prob 1-π).
3012 *
3013 * @p x and @p y may be any scalar RV root: a base @c gate_rv
3014 * (@c normal / @c uniform / @c exponential / @c erlang), a
3015 * @c gate_value Dirac (@c as_random), a @c gate_arith expression, or
3016 * another @c mixture. N-ary mixtures are built by composition --
3017 * <tt>mixture(p1, A, mixture(p2, B, C))</tt> realises a 3-component
3018 * mixture with effective weights <tt>π1, (1-π1)·π2, (1-π1)·(1-π2)</tt>.
3019 *
3020 * Validation:
3021 * - @p p must point to a Boolean gate (@c input, @c mulinput,
3022 * @c update, @c plus, @c times, @c monus, @c project, @c eq,
3023 * @c cmp, @c zero, @c one). Compound Boolean gates derive their
3024 * probability from their atoms via the active probability-evaluation
3025 * method; a bare @c gate_input's probability is whatever @c set_prob
3026 * pinned (@c set_prob is responsible for keeping it in [0, 1]).
3027 * - @p x and @p y must be scalar RV roots; aggregate / Boolean roots
3028 * are rejected at construction.
3029 *
3030 * Two calls to @c mixture with the same @c (p, x, y) operands collapse
3031 * to the same @c gate_mixture node by v5-hash, exactly like
3032 * @c arith(PLUS, X, Y). Draw independence is controlled by @p p:
3033 * sharing @p p couples branch selection across consumers via the
3034 * sampler's @c bool_cache_; minting independent Bernoullis (e.g. via
3035 * the @c mixture(p_value, …) overload) decouples them.
3037 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
3038 */
3039CREATE OR REPLACE FUNCTION mixture(
3040 p UUID, x random_variable, y random_variable)
3041 RETURNS random_variable AS
3042$$
3043DECLARE
3044 token UUID;
3045 p_kind provsql.PROVENANCE_GATE;
3046 x_uuid UUID;
3047 y_uuid UUID;
3048 x_kind provsql.PROVENANCE_GATE;
3049 y_kind provsql.PROVENANCE_GATE;
3050BEGIN
3051 p_kind := provsql.get_gate_type(p);
3052 IF p_kind NOT IN ('input','mulinput','update',
3053 'plus','times','monus',
3054 'project','eq','cmp',
3055 'zero','one') THEN
3056 RAISE EXCEPTION 'provsql.mixture: p must be a Boolean gate '
3057 '(input/mulinput/update/plus/times/monus/project/eq/cmp/zero/one), got %', p_kind;
3058 END IF;
3059
3060 x_uuid := (x)::UUID;
3061 y_uuid := (y)::UUID;
3062 x_kind := provsql.get_gate_type(x_uuid);
3063 y_kind := provsql.get_gate_type(y_uuid);
3064 IF x_kind NOT IN ('rv','value','arith','mixture') THEN
3065 RAISE EXCEPTION 'provsql.mixture: x must be a scalar RV root (rv / value / arith / mixture), got %', x_kind;
3066 END IF;
3067 IF y_kind NOT IN ('rv','value','arith','mixture') THEN
3068 RAISE EXCEPTION 'provsql.mixture: y must be a scalar RV root (rv / value / arith / mixture), got %', y_kind;
3069 END IF;
3070
3071 token := public.uuid_generate_v5(
3072 provsql.uuid_ns_provsql(),
3073 concat('mixture', p, x_uuid, y_uuid));
3074 PERFORM provsql.create_gate(token, 'mixture', ARRAY[p, x_uuid, y_uuid]);
3075 RETURN provsql.random_variable_make(token);
3076END
3077$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
3078
3079/**
3080 * @brief Ad-hoc mixture constructor that mints a fresh anonymous
3081 * @c gate_input Bernoulli with probability @p p_value.
3082 *
3083 * Sugar over the @c mixture(UUID, x, y) form: when the caller doesn't
3084 * care about reusing the Bernoulli token elsewhere in the circuit
3085 * (which is the common case &ndash; "give me a 0.3 / 0.7 weighted GMM,
3086 * I don't need to share the coin"), this overload creates the
3087 * underlying @c gate_input on the fly with a fresh
3088 * @c uuid_generate_v4() token, pins @p p_value via @c set_prob, and
3089 * threads everything into the UUID-keyed constructor.
3090 *
3091 * Each call mints a NEW Bernoulli, so two calls to
3092 * <tt>mixture(0.5, X, Y)</tt> are *independent* mixtures whose branch
3093 * selections are uncorrelated. When coupling is desired (e.g. two
3094 * mixtures sharing a coin), use the @c mixture(UUID, x, y) form with a
3095 * user-managed @c gate_input token.
3096 *
3097 * @warning <tt>VOLATILE</tt> is load-bearing for the same reason as
3098 * @ref normal and the other RV constructors -- folding under
3099 * @c STABLE / @c IMMUTABLE would collapse two independent draws into
3100 * one shared gate.
3101 *
3102 * @sa <a href="https://en.wikipedia.org/wiki/Mixture_distribution">Wikipedia: Mixture distribution</a>
3104CREATE OR REPLACE FUNCTION mixture(
3105 p_value double precision,
3106 x random_variable,
3107 y random_variable)
3108 RETURNS random_variable AS
3109$$
3110DECLARE
3111 p_token UUID;
3112BEGIN
3113 IF p_value IS NULL OR p_value <> p_value OR p_value < 0 OR p_value > 1 THEN
3114 RAISE EXCEPTION 'provsql.mixture: probability must be in [0,1] (got %)', p_value;
3115 END IF;
3116 p_token := public.uuid_generate_v4();
3117 PERFORM provsql.create_gate(p_token, 'input');
3118 PERFORM provsql.set_prob(p_token, p_value);
3119 RETURN provsql.mixture(p_token, x, y);
3120END
3121$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3123/**
3124 * @brief Categorical-RV constructor over explicit (probabilities,
3125 * values) arrays.
3126 *
3127 * Builds a categorical-form @c gate_mixture directly: a fresh
3128 * @c gate_input "key" anchor and one @c gate_mulinput per outcome with
3129 * positive mass, all sharing the key. The wires
3130 * <tt>[key, mul_1, ..., mul_n]</tt> are what downstream evaluators
3131 * (@c Expectation, @c MonteCarloSampler, @c AnalyticEvaluator,
3132 * @c RangeCheck) recognise via @c isCategoricalMixture and treat as a
3133 * scalar RV with the categorical distribution @p probs over
3134 * @p outcomes.
3135 *
3136 * Validation:
3137 * - @p probs and @p outcomes must be non-null, same length, length &ge; 1.
3138 * - Each @c probs[i] must be finite, in <tt>[0, 1]</tt>, and the array
3139 * must sum to 1 within @c 1e-9.
3140 * - Each @c outcomes[i] must be finite.
3141 *
3142 * Each call mints a fresh key gate and a fresh set of mulinputs, so
3143 * two calls to @c categorical with the same arrays are *independent*
3144 * categorical RVs. The marking is @c VOLATILE accordingly.
3145 *
3146 * Degenerate case: a categorical with exactly one positive-mass
3147 * outcome reduces to @c as_random(v) at construction (the block would
3148 * just be a single mulinput, which is operationally a Dirac point
3149 * mass). Two such calls share the @c gate_value UUID via the v5
3150 * convention @c as_random already uses.
3151 *
3152 * @sa @c mixture for the Bernoulli-weighted choice constructor.
3153 * @sa <a href="https://en.wikipedia.org/wiki/Categorical_distribution">Wikipedia: Categorical distribution</a>
3154 */
3155CREATE OR REPLACE FUNCTION categorical(
3156 probs double precision[],
3157 outcomes double precision[])
3158 RETURNS random_variable AS
3159$$
3160DECLARE
3161 n INTEGER;
3162 p_sum double precision := 0.0;
3163 i INTEGER;
3164 key_token UUID;
3165 mix_token UUID;
3166 mul_token UUID;
3167 mul_tokens UUID[] := ARRAY[]::UUID[];
3168 mix_wires UUID[];
3169 pi_i double precision;
3170 vi_i double precision;
3171BEGIN
3172 IF probs IS NULL OR outcomes IS NULL THEN
3173 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must be non-null';
3174 END IF;
3175 n := array_length(probs, 1);
3176 IF n IS NULL OR n < 1 THEN
3177 RAISE EXCEPTION 'provsql.categorical: probs must be non-empty';
3178 END IF;
3179 IF array_length(outcomes, 1) <> n THEN
3180 RAISE EXCEPTION 'provsql.categorical: probs and outcomes must have the same length (got % and %)',
3181 n, array_length(outcomes, 1);
3182 END IF;
3183
3184 FOR i IN 1..n LOOP
3185 pi_i := probs[i];
3186 vi_i := outcomes[i];
3187 -- PostgreSQL diverges from IEEE 754: NaN = NaN is TRUE there, so
3188 -- the canonical x <> x NaN test doesn't fire. Compare against the
3189 -- literal 'NaN'::float8 instead, and reject ±Infinity for outcomes
3190 -- explicitly.
3191 IF pi_i IS NULL OR pi_i = 'NaN'::float8 OR pi_i < 0 OR pi_i > 1 THEN
3192 RAISE EXCEPTION 'provsql.categorical: probs[%] must be in [0,1] (got %)', i, pi_i;
3193 END IF;
3194 IF vi_i IS NULL OR vi_i = 'NaN'::float8
3195 OR vi_i = 'Infinity'::float8 OR vi_i = '-Infinity'::float8 THEN
3196 RAISE EXCEPTION 'provsql.categorical: outcomes[%] must be finite (got %)', i, vi_i;
3197 END IF;
3198 p_sum := p_sum + pi_i;
3199 END LOOP;
3200 IF abs(p_sum - 1.0) > 1e-9 THEN
3201 RAISE EXCEPTION 'provsql.categorical: probs must sum to 1 within 1e-9 (got %)', p_sum;
3202 END IF;
3203
3204 -- Degenerate case: exactly one positive-mass outcome (the rest are
3205 -- zero). The "categorical" is then a Dirac point mass; skip the
3206 -- block-allocation entirely and return @c as_random(v), which yields
3207 -- a shared, v5-keyed gate_value -- exactly what downstream
3208 -- evaluators (rv_moment, AnalyticEvaluator, rv_support) treat
3209 -- specially. Saves a key gate and a mulinput per call, and lets
3210 -- two calls to @c categorical({1.0}, {v}) collide on the same
3211 -- gate_value UUID instead of producing distinct anonymous blocks.
3212 DECLARE
3213 nb_positive INTEGER := 0;
3214 only_idx INTEGER := 0;
3215 BEGIN
3216 FOR i IN 1..n LOOP
3217 IF probs[i] > 0.0 THEN
3218 nb_positive := nb_positive + 1;
3219 only_idx := i;
3220 END IF;
3221 END LOOP;
3222 IF nb_positive = 1 THEN
3223 RETURN provsql.as_random(outcomes[only_idx]);
3224 END IF;
3225 END;
3226
3227 -- Mint the block's key anchor. Probability 1.0 matches the
3228 -- joint-table convention: the categorical mass lives on the
3229 -- mulinputs, the key just identifies the block.
3230 key_token := public.uuid_generate_v4();
3231 PERFORM provsql.create_gate(key_token, 'input');
3232 PERFORM provsql.set_prob(key_token, 1.0);
3233
3234 -- One mulinput per positive-probability outcome. Zero-probability
3235 -- entries contribute no mass and are skipped: the gate_mixture's
3236 -- wire vector is otherwise polluted with no-op leaves.
3237 FOR i IN 1..n LOOP
3238 pi_i := probs[i];
3239 IF pi_i <= 0.0 THEN CONTINUE; END IF;
3240 mul_token := public.uuid_generate_v4();
3241 PERFORM provsql.create_gate(mul_token, 'mulinput', ARRAY[key_token]);
3242 PERFORM provsql.set_prob(mul_token, pi_i);
3243 PERFORM provsql.set_infos(mul_token, (i - 1));
3244 PERFORM provsql.set_extra(mul_token, outcomes[i]::TEXT);
3245 mul_tokens := mul_tokens || mul_token;
3246 END LOOP;
3247
3248 mix_wires := ARRAY[key_token] || mul_tokens;
3249 mix_token := public.uuid_generate_v4();
3250 PERFORM provsql.create_gate(mix_token, 'mixture', mix_wires);
3251 RETURN provsql.random_variable_make(mix_token);
3252END
3253$$ LANGUAGE plpgsql STRICT VOLATILE PARALLEL SAFE;
3254
3255/**
3256 * @brief Lift a deterministic constant into a random_variable
3257 *
3258 * Creates a <tt>gate_value</tt> carrying the constant's TEXT form so
3259 * that comparisons against a <tt>random_variable</tt> column produce
3260 * the same circuit shape regardless of whether the operand is an
3261 * actual RV or a literal constant.
3262 *
3263 * Marked <tt>IMMUTABLE</tt>: the gate UUID is derived deterministically
3264 * from the constant via the same v5 convention as <tt>provenance_semimod</tt>'s
3265 * inline value gate (<tt>concat('value', CAST(c AS VARCHAR))</tt>), so
3266 * <tt>as_random(2)</tt> always resolves to the same gate, and any other
3267 * code path that already creates a value gate for the same constant
3268 * (e.g. <tt>provenance_semimod</tt>) shares the UUID.
3269 * <tt>create_gate</tt> is idempotent on already-mapped tokens, so
3270 * repeat invocations are harmless.
3271 *
3272 * @sa <a href="https://en.wikipedia.org/wiki/Degenerate_distribution">Wikipedia: Degenerate distribution (Dirac point mass)</a>
3273 */
3274CREATE OR REPLACE FUNCTION as_random(c double precision)
3275 RETURNS random_variable AS
3276$$
3277DECLARE
3278 -- Canonicalise -0.0 to +0.0: IEEE 754 defines x + 0.0 = +0.0 for
3279 -- both signed zeros, and is identity for finite, NaN, and ±Infinity.
3280 -- Without this, as_random(-0.0) and as_random(+0.0) would produce
3281 -- different gate UUIDs (their CAST AS VARCHAR TEXT representations
3282 -- differ: '-0' vs '0') even though they denote the same constant.
3283 c_canon double precision := c + 0.0;
3284 c_text varchar := CAST(c_canon AS VARCHAR);
3285 token UUID := public.uuid_generate_v5(
3286 provsql.uuid_ns_provsql(), concat('value', c_text));
3287BEGIN
3288 PERFORM provsql.create_gate(token, 'value');
3289 PERFORM provsql.set_extra(token, c_text);
3290 RETURN provsql.random_variable_make(token);
3291END
3292$$ LANGUAGE plpgsql STRICT IMMUTABLE PARALLEL SAFE;
3293
3294/**
3295 * @brief Implicit cast double precision -> random_variable (lifts a
3296 * scalar literal to a constant RV).
3297 *
3298 * Lets users write <tt>WHERE reading > 2.5::float8</tt> instead of
3299 * <tt>WHERE reading > provsql.as_random(2.5)</tt>; the planner-hook
3300 * rewriter then sees a uniform <tt>random_variable</tt> on both sides.
3301 * Sibling casts below cover @c INTEGER and @c NUMERIC literals so
3302 * plain <tt>WHERE reading > 2</tt> and <tt>WHERE reading > 2.5</tt>
3303 * also work; PostgreSQL's operator resolution does not chain casts
3304 * across more than one step, so each NUMERIC-source type needs its
3305 * own direct cast.
3306 */
3307CREATE CAST (double precision AS random_variable)
3308 WITH FUNCTION as_random(double precision) AS IMPLICIT;
3309
3310/** @brief @c as_random for @c INTEGER (delegates to the @c float8 form). */
3311CREATE OR REPLACE FUNCTION as_random(c INTEGER)
3312 RETURNS random_variable AS
3313$$ SELECT provsql.as_random(c::double precision); $$
3314LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
3315
3316/** @brief @c as_random for @c NUMERIC (delegates to the @c float8 form). */
3317CREATE OR REPLACE FUNCTION as_random(c NUMERIC)
3318 RETURNS random_variable AS
3319$$ SELECT provsql.as_random(c::double precision); $$
3320LANGUAGE sql STRICT IMMUTABLE PARALLEL SAFE;
3321
3322/** @brief Implicit cast INTEGER -> random_variable. */
3323CREATE CAST (INTEGER AS random_variable)
3324 WITH FUNCTION as_random(INTEGER) AS IMPLICIT;
3325
3326/** @brief Implicit cast NUMERIC -> random_variable. */
3327CREATE CAST (NUMERIC AS random_variable)
3328 WITH FUNCTION as_random(NUMERIC) AS IMPLICIT;
3329
3331 * @name Arithmetic and comparison on random_variable
3332 *
3333 * Each binary operator below is declared on @c (random_variable,
3334 * random_variable) only; mixed shapes such as <tt>rv + 2</tt> or
3335 * <tt>2.5 > rv</tt> resolve through the implicit casts from
3336 * @c INTEGER / @c NUMERIC / @c double @c precision to
3337 * @c random_variable declared above. This avoids the resolution
3338 * ambiguity that would arise if both <tt>(rv, NUMERIC)</tt> and
3339 * <tt>(rv, rv)</tt> overloads were declared while implicit casts also
3340 * existed.
3341 *
3342 * Arithmetic operators build a @c gate_arith via @c provenance_arith
3343 * and return a new @c random_variable wrapping its UUID.
3344 *
3345 * Comparison operators are placeholders that return @c BOOLEAN and
3346 * raise if executed -- the @c BOOLEAN return type is required so that
3347 * PostgreSQL accepts <tt>WHERE rv > 2</tt> at parse-analyze. The
3348 * planner hook intercepts every such @c OpExpr (matched by
3349 * @c opfuncid against @c constants_t::OID_FUNCTION_RV_CMP) and rewrites
3350 * it into a @c provenance_cmp call whose UUID is conjoined into the
3351 * tuple's @c provsql column via @c provenance_times. Code that needs
3352 * a @c gate_cmp UUID directly (without going through the planner hook)
3353 * uses the @c rv_cmp_* family below, which call @c provenance_cmp
3354 * with the matching float8-comparator OID.
3355 *
3356 * @{
3357 */
3358
3359/** @brief @c random_variable + @c random_variable (gate_arith PLUS). */
3360CREATE OR REPLACE FUNCTION random_variable_plus(
3361 a random_variable, b random_variable)
3362 RETURNS random_variable AS
3363$$
3364 SELECT provsql.random_variable_make(
3365 provsql.provenance_arith(
3366 0, -- PROVSQL_ARITH_PLUS
3367 ARRAY[(a)::UUID,
3368 (b)::UUID]));
3369$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3370
3371/** @brief @c random_variable - @c random_variable (gate_arith MINUS). */
3372CREATE OR REPLACE FUNCTION random_variable_minus(
3373 a random_variable, b random_variable)
3374 RETURNS random_variable AS
3375$$
3376 SELECT provsql.random_variable_make(
3377 provsql.provenance_arith(
3378 2, -- PROVSQL_ARITH_MINUS
3379 ARRAY[(a)::UUID,
3380 (b)::UUID]));
3381$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3382
3383/** @brief @c random_variable * @c random_variable (gate_arith TIMES). */
3384CREATE OR REPLACE FUNCTION random_variable_times(
3385 a random_variable, b random_variable)
3386 RETURNS random_variable AS
3387$$
3388 SELECT provsql.random_variable_make(
3389 provsql.provenance_arith(
3390 1, -- PROVSQL_ARITH_TIMES
3391 ARRAY[(a)::UUID,
3392 (b)::UUID]));
3393$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3394
3395/** @brief @c random_variable / @c random_variable (gate_arith DIV). */
3396CREATE OR REPLACE FUNCTION random_variable_div(
3397 a random_variable, b random_variable)
3398 RETURNS random_variable AS
3399$$
3400 SELECT provsql.random_variable_make(
3401 provsql.provenance_arith(
3402 3, -- PROVSQL_ARITH_DIV
3403 ARRAY[(a)::UUID,
3404 (b)::UUID]));
3405$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3406
3407/** @brief Unary @c -random_variable (gate_arith NEG). */
3408CREATE OR REPLACE FUNCTION random_variable_neg(a random_variable)
3409 RETURNS random_variable AS
3410$$
3411 SELECT provsql.random_variable_make(
3412 provsql.provenance_arith(
3413 4, -- PROVSQL_ARITH_NEG
3414 ARRAY[(a)::UUID]));
3415$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3416
3417/**
3418 * @brief Internal helper: float8-comparator OID for a given symbol.
3419 *
3420 * Wraps the @c '&lt;sym&gt;(double precision,double precision)'::regoperator
3421 * lookup so the per-comparator functions read uniformly. Marked
3422 * @c IMMUTABLE because the resolved OID is fixed at catalog level
3423 * (the float8 comparators are core PG and never re-installed).
3424 */
3425CREATE OR REPLACE FUNCTION random_variable_cmp_oid(sym TEXT)
3426 RETURNS oid AS
3427$$
3428 SELECT (sym || '(double precision,double precision)')::regoperator::oid;
3429$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3430
3431/* The six @c random_variable_{lt,le,eq,ne,ge,gt} functions below are
3432 * BOOLEAN placeholders -- they exist only so the @c (rv, rv) operators
3433 * can be declared at all (PostgreSQL needs a procedure to bind to the
3434 * operator definition, and a procedure returning anything but @c BOOLEAN
3435 * would be rejected by parse-analyze in a WHERE position). They MUST
3436 * NOT be invoked directly: the planner hook in @c src/provsql.c
3437 * intercepts every @c OpExpr whose @c opfuncid matches one of these and
3438 * rewrites it into a @c provenance_cmp() call against the row's
3439 * provenance. If the executor ever reaches one of these, it means the
3440 * planner hook was bypassed (e.g. @c provsql.active was off), in which
3441 * case raising is the right behaviour. */
3442
3443/** @brief Placeholder body shared by every <tt>random_variable_*</tt>
3444 * comparison procedure. Raises with a uniform message. */
3445CREATE OR REPLACE FUNCTION random_variable_cmp_placeholder(
3446 a random_variable, b random_variable)
3447 RETURNS BOOLEAN AS
3448$$
3449BEGIN
3450 RAISE EXCEPTION 'random_variable comparison must be rewritten by the '
3451 'ProvSQL planner hook (is provsql.active off?)';
3452END
3453$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
3454
3455CREATE OR REPLACE FUNCTION random_variable_lt(
3456 a random_variable, b random_variable) RETURNS BOOLEAN AS
3457$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
3458LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3459
3460CREATE OR REPLACE FUNCTION random_variable_le(
3461 a random_variable, b random_variable) RETURNS BOOLEAN AS
3462$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
3463LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3464
3465CREATE OR REPLACE FUNCTION random_variable_eq(
3466 a random_variable, b random_variable) RETURNS BOOLEAN AS
3467$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
3468LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3469
3470CREATE OR REPLACE FUNCTION random_variable_ne(
3471 a random_variable, b random_variable) RETURNS BOOLEAN AS
3472$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
3473LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3474
3475CREATE OR REPLACE FUNCTION random_variable_ge(
3476 a random_variable, b random_variable) RETURNS BOOLEAN AS
3477$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
3478LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3479
3480CREATE OR REPLACE FUNCTION random_variable_gt(
3481 a random_variable, b random_variable) RETURNS BOOLEAN AS
3482$$ SELECT provsql.random_variable_cmp_placeholder(a, b); $$
3483LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3484
3485/* Direct UUID constructors -- used by tests and any caller that wants
3486 * a @c gate_cmp without going through the planner hook (e.g. building
3487 * a circuit fragment in a SELECT list). Each delegates to
3488 * @c provenance_cmp with the matching float8-comparator OID. */
3489
3490/** @brief Build a @c gate_cmp for <tt>a &lt; b</tt> and return its UUID. */
3491CREATE OR REPLACE FUNCTION rv_cmp_lt(
3492 a random_variable, b random_variable) RETURNS UUID AS
3493$$
3494 SELECT provsql.provenance_cmp(
3495 (a)::UUID,
3496 provsql.random_variable_cmp_oid('<'),
3497 (b)::UUID);
3498$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3499
3500/** @brief Build a @c gate_cmp for <tt>a &le; b</tt> and return its UUID. */
3501CREATE OR REPLACE FUNCTION rv_cmp_le(
3502 a random_variable, b random_variable) RETURNS UUID AS
3503$$
3504 SELECT provsql.provenance_cmp(
3505 (a)::UUID,
3506 provsql.random_variable_cmp_oid('<='),
3507 (b)::UUID);
3508$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3509
3510/** @brief Build a @c gate_cmp for <tt>a = b</tt> and return its UUID. */
3511CREATE OR REPLACE FUNCTION rv_cmp_eq(
3512 a random_variable, b random_variable) RETURNS UUID AS
3513$$
3514 SELECT provsql.provenance_cmp(
3515 (a)::UUID,
3516 provsql.random_variable_cmp_oid('='),
3517 (b)::UUID);
3518$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3519
3520/** @brief Build a @c gate_cmp for <tt>a &lt;&gt; b</tt> and return its UUID. */
3521CREATE OR REPLACE FUNCTION rv_cmp_ne(
3522 a random_variable, b random_variable) RETURNS UUID AS
3523$$
3524 SELECT provsql.provenance_cmp(
3525 (a)::UUID,
3526 provsql.random_variable_cmp_oid('<>'),
3527 (b)::UUID);
3528$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3529
3530/** @brief Build a @c gate_cmp for <tt>a &ge; b</tt> and return its UUID. */
3531CREATE OR REPLACE FUNCTION rv_cmp_ge(
3532 a random_variable, b random_variable) RETURNS UUID AS
3533$$
3534 SELECT provsql.provenance_cmp(
3535 (a)::UUID,
3536 provsql.random_variable_cmp_oid('>='),
3537 (b)::UUID);
3538$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3539
3540/** @brief Build a @c gate_cmp for <tt>a &gt; b</tt> and return its UUID. */
3541CREATE OR REPLACE FUNCTION rv_cmp_gt(
3542 a random_variable, b random_variable) RETURNS UUID AS
3543$$
3544 SELECT provsql.provenance_cmp(
3545 (a)::UUID,
3546 provsql.random_variable_cmp_oid('>'),
3547 (b)::UUID);
3548$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3549
3550CREATE OPERATOR + (
3551 LEFTARG = random_variable,
3552 RIGHTARG = random_variable,
3553 PROCEDURE = random_variable_plus,
3554 COMMUTATOR = +
3555);
3556
3557CREATE OPERATOR - (
3558 LEFTARG = random_variable,
3559 RIGHTARG = random_variable,
3560 PROCEDURE = random_variable_minus
3561);
3562
3563CREATE OPERATOR * (
3564 LEFTARG = random_variable,
3565 RIGHTARG = random_variable,
3566 PROCEDURE = random_variable_times,
3567 COMMUTATOR = *
3568);
3569
3570CREATE OPERATOR / (
3571 LEFTARG = random_variable,
3572 RIGHTARG = random_variable,
3573 PROCEDURE = random_variable_div
3574);
3575
3576/** @brief Prefix unary minus on @c random_variable. */
3577CREATE OPERATOR - (
3578 RIGHTARG = random_variable,
3579 PROCEDURE = random_variable_neg
3580);
3581
3582CREATE OPERATOR < (
3583 LEFTARG = random_variable,
3584 RIGHTARG = random_variable,
3585 PROCEDURE = random_variable_lt,
3586 COMMUTATOR = >,
3587 NEGATOR = >=
3588);
3589
3590CREATE OPERATOR <= (
3591 LEFTARG = random_variable,
3592 RIGHTARG = random_variable,
3593 PROCEDURE = random_variable_le,
3594 COMMUTATOR = >=,
3595 NEGATOR = >
3596);
3597
3598CREATE OPERATOR = (
3599 LEFTARG = random_variable,
3600 RIGHTARG = random_variable,
3601 PROCEDURE = random_variable_eq,
3602 COMMUTATOR = =,
3603 NEGATOR = <>
3604);
3605
3606CREATE OPERATOR <> (
3607 LEFTARG = random_variable,
3608 RIGHTARG = random_variable,
3609 PROCEDURE = random_variable_ne,
3610 COMMUTATOR = <>,
3611 NEGATOR = =
3612);
3613
3614CREATE OPERATOR >= (
3615 LEFTARG = random_variable,
3616 RIGHTARG = random_variable,
3617 PROCEDURE = random_variable_ge,
3618 COMMUTATOR = <=,
3619 NEGATOR = <
3620);
3621
3622CREATE OPERATOR > (
3623 LEFTARG = random_variable,
3624 RIGHTARG = random_variable,
3625 PROCEDURE = random_variable_gt,
3626 COMMUTATOR = <,
3627 NEGATOR = <=
3628);
3629
3630/**
3631 * @brief Condition a random variable on an event: @c "X | C".
3632 *
3633 * Returns a conditioned distribution that flows onward like any other
3634 * @c random_variable: it can be stored, re-conditioned, and queried with
3635 * @c expected / @c variance / @c moment / @c support, which then report the
3636 * conditional distribution. @p cond is a Boolean-event provenance token,
3637 * typically a comparison over the variable itself (@c "X | rv_cmp_gt(X,
3638 * as_random(3))" -- a truncation) or any external event.
3639 *
3640 * Unlike the UUID carrier's terminal @c cond, the random-variable form is a
3641 * composable two-child @c gate_conditioned @c [target, condition]: the moment
3642 * / support dispatchers unpack it and route through the existing conditional
3643 * evaluator (@c rv_moment over the joint of the target and the condition).
3644 * Nested conditioning folds: @c "(X|A)|B = X|(A∧B)".
3645 */
3646CREATE OR REPLACE FUNCTION random_variable_cond(rv random_variable, cond UUID)
3647 RETURNS random_variable AS
3648$$
3649DECLARE
3650 tgt UUID;
3651 ev UUID;
3652 result UUID;
3653 ch UUID[];
3654BEGIN
3655 IF cond IS NULL OR cond = gate_one() THEN
3656 RETURN rv;
3657 END IF;
3658
3659 tgt := (rv)::UUID;
3660 IF get_gate_type(tgt) = 'conditioned'
3661 AND array_length(get_children(tgt), 1) = 2 THEN
3662 -- Fold (X|A)|B = X|(A∧B): the rv-carrier conditioned gate is the
3663 -- two-child [target, condition] shape; accumulate the new event.
3664 ch := get_children(tgt);
3665 tgt := ch[1];
3666 ev := provenance_times(ch[2], cond);
3667 ELSE
3668 ev := cond;
3669 END IF;
3670
3671 result := public.uuid_generate_v5(uuid_ns_provsql(),
3672 concat('conditioned', tgt, ev));
3673 PERFORM create_gate(result, 'conditioned', ARRAY[tgt, ev]);
3674 RETURN (result)::random_variable;
3675END
3676$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public
3677 SECURITY DEFINER PARALLEL SAFE;
3678
3679CREATE OPERATOR | (
3680 LEFTARG = random_variable,
3681 RIGHTARG = UUID,
3682 PROCEDURE = random_variable_cond
3683);
3685/**
3686 * @brief Placeholder for @c "X | (predicate)" -- conditioning a random
3687 * variable on a Boolean comparison written naturally.
3688 *
3689 * Lets one write @c "X | (X > 3)" instead of
3690 * @c "X | rv_cmp_gt(X, as_random(3))". Never executes: the ProvSQL planner
3691 * hook rewrites the Boolean operand (a combination of random_variable
3692 * comparisons) into the corresponding condition gate and emits
3693 * @c random_variable_cond. Reaching it at runtime means the rewriter was
3694 * inactive or the predicate was not a random_variable comparison.
3695 */
3696CREATE OR REPLACE FUNCTION random_variable_cond_predicate(
3697 rv random_variable, predicate BOOLEAN) RETURNS random_variable AS
3698$$
3699BEGIN
3700 RAISE EXCEPTION 'random_variable | (predicate) must be rewritten by the '
3701 'ProvSQL planner hook: the right operand must be a Boolean combination '
3702 'of random_variable comparisons (is provsql.active off?)';
3703END
3704$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
3705
3706CREATE OPERATOR | (
3707 LEFTARG = random_variable,
3708 RIGHTARG = BOOLEAN,
3709 PROCEDURE = random_variable_cond_predicate
3710);
3711
3712/**
3713 * @brief Unpack the target of a random-variable conditioning gate.
3714 *
3715 * For a two-child @c gate_conditioned @c [target, condition] (the @c "X | C"
3716 * shape) returns @p target; for any other token returns it unchanged. Used
3717 * by the moment / support dispatchers to route a conditioned distribution
3718 * through the existing conditional evaluator.
3719 */
3720CREATE OR REPLACE FUNCTION rv_conditioned_target(token UUID) RETURNS UUID AS
3722 SELECT CASE
3723 WHEN provsql.get_gate_type(token) = 'conditioned'
3724 AND array_length(provsql.get_children(token), 1) = 2
3725 THEN (provsql.get_children(token))[1]
3726 ELSE token
3727 END;
3728$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3729
3730/**
3731 * @brief Combine a conditioning gate's event with an explicit @p prov.
3732 *
3733 * For a two-child @c gate_conditioned @c [target, condition] returns
3734 * @c "condition ∧ prov"; otherwise returns @p prov unchanged. Lets a stored
3735 * @c "X | C" be queried as @c expected(X|C) (prov defaulting to one) or have
3736 * an extra condition conjoined as @c expected(X|C, extra_prov).
3737 */
3738CREATE OR REPLACE FUNCTION rv_conditioned_prov(token UUID, prov UUID)
3739 RETURNS UUID AS
3740$$
3741 SELECT CASE
3742 WHEN provsql.get_gate_type(token) = 'conditioned'
3743 AND array_length(provsql.get_children(token), 1) = 2
3744 THEN provsql.provenance_times((provsql.get_children(token))[2], prov)
3745 ELSE prov
3746 END;
3747$$ LANGUAGE sql STABLE PARALLEL SAFE SET search_path=provsql,pg_temp,public;
3748
3749/** @} */
3750
3752 * @name Aggregates over random_variable
3753 *
3754 * An overload of the standard
3755 * @c sum aggregate that takes a @c random_variable per row and returns
3756 * the @c random_variable representing the (provenance-weighted) sum.
3757 * Lives in the @c provsql schema so a @c sum(random_variable) call
3758 * resolves to it without colliding with the built-in NUMERIC @c sum
3759 * overloads in @c pg_catalog.
3760 *
3761 * Direct calls outside a provenance-tracked query treat each row's
3762 * contribution unconditionally (no per-row Boolean selector). When
3763 * the planner hook sees a @c provsql.sum @c Aggref over a
3764 * provenance-tracked query, it wraps the per-row argument @c x in
3765 * <tt>provsql.mixture(prov_token, x, provsql.as_random(0))</tt> so the
3766 * aggregate's effective semantics become
3767 * @f$\mathrm{SUM}(x) = \sum_i \mathbf{1}\{\varphi_i\} \cdot X_i@f$,
3768 * the natural extension of semimodule-provenance to RV-valued M.
3769 *
3770 * The internal state is the array of UUIDs of the per-row mixtures.
3771 * The final function builds a single @c gate_arith @c PLUS over them
3772 * (or returns @c as_random(0) for an empty group, the additive
3773 * identity). Sharing on @c provenance_arith's v5 hash means two
3774 * @c sum invocations over the same set of rows collide on the same
3775 * gate.
3776 *
3777 * @{
3778 */
3779
3780/**
3781 * @brief Per-row helper: wrap an RV in @c mixture(prov, rv, as_random(0)).
3782 *
3783 * Internal helper used by the planner-hook rewriter to lift a
3784 * @c sum(random_variable) argument into its provenance-aware form.
3785 * Encodes one row's contribution to the SUM as a Bernoulli mixture
3786 * over the row's provenance: with probability @c P(prov) the mixture
3787 * samples @c rv, otherwise it samples the additive identity
3788 * @c as_random(0). Exposed as a regular SQL function so the planner
3789 * can construct a @c FuncExpr by name without needing to disambiguate
3790 * @c mixture / @c as_random overloads at OID-lookup time.
3791 */
3792CREATE OR REPLACE FUNCTION rv_aggregate_semimod(
3793 prov UUID, rv random_variable)
3794 RETURNS random_variable AS
3795$$
3796 SELECT provsql.mixture(prov, rv, provsql.as_random(0::double precision));
3797$$ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
3798
3799/**
3800 * @brief State-transition function for @c sum(random_variable).
3801 *
3802 * Appends the input RV's UUID to the running array. NULL inputs are
3803 * skipped (matching standard SUM semantics). The aggregate's INITCOND
3804 * is @c '{}' so the FINALFUNC always runs even on an empty group, which
3805 * is what lets us return @c as_random(0) (the additive identity) for
3806 * an empty SUM rather than NULL.
3807 */
3808CREATE OR REPLACE FUNCTION sum_rv_sfunc(
3809 state UUID[], rv random_variable)
3810 RETURNS UUID[] AS
3811$$
3812 SELECT CASE
3813 WHEN rv IS NULL THEN state
3814 ELSE array_append(state, (rv)::UUID)
3815 END;
3816$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
3817
3818/**
3819 * @brief Final function for @c sum(random_variable): build a
3820 * @c gate_arith PLUS root.
3821 *
3822 * Empty group (@c state = @c '{}'): return @c as_random(0), the
3823 * additive identity, so SUM over zero rows is the deterministic
3824 * scalar 0 -- matches the AGG_TOKEN convention in @c agg_raw_moment.
3825 *
3826 * Singleton group: return the single child directly without minting a
3827 * useless single-child @c gate_arith.
3828 *
3829 * Otherwise: build @c gate_arith(PLUS, state) via @c provenance_arith.
3830 */
3831CREATE OR REPLACE FUNCTION sum_rv_ffunc(state UUID[])
3832 RETURNS random_variable AS
3834DECLARE
3835 arith_token UUID;
3836BEGIN
3837 IF state IS NULL OR array_length(state, 1) IS NULL THEN
3838 RETURN provsql.as_random(0::double precision);
3839 END IF;
3840 IF array_length(state, 1) = 1 THEN
3841 RETURN provsql.random_variable_make(state[1]);
3842 END IF;
3843 arith_token := provsql.provenance_arith(0, state); -- 0 = PROVSQL_ARITH_PLUS
3844 RETURN provsql.random_variable_make(arith_token);
3846$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
3847
3848CREATE AGGREGATE sum(random_variable) (
3849 SFUNC = sum_rv_sfunc,
3850 STYPE = UUID[],
3851 INITCOND = '{}',
3852 FINALFUNC = sum_rv_ffunc
3853);
3854
3855/**
3856 * @brief Final function for @c avg(random_variable).
3857 *
3858 * Builds the natural lift of @c "AVG = SUM / COUNT" into the
3859 * @c random_variable algebra:
3860 * @f[
3861 * \mathrm{AVG}(x) \;=\; \frac{\sum_i \mathbf{1}\{\varphi_i\} \cdot X_i}
3862 * {\sum_i \mathbf{1}\{\varphi_i\}}
3863 * @f]
3864 * realised as @c gate_arith(DIV, num, denom) where @c num is the
3865 * @c sum(random_variable) gate over the per-row mixtures and @c denom
3866 * is the @c sum(random_variable) gate over the same provenance gates
3867 * weighted by a per-row @c as_random(1) -- exactly the SQL pattern
3868 * "@c sum(x) @c / @c sum(as_random(1))" emitted as a single
3869 * @c random_variable token.
3870 *
3871 * Reuses @c sum_rv_sfunc as the state-transition function so the
3872 * array of per-row UUIDs is collected identically to
3873 * @c sum(random_variable). In a provenance-tracked query the
3874 * planner-hook rewriter routes RV-returning aggregates through
3875 * @c make_rv_aggregate_expression, which wraps each per-row argument
3876 * in @c mixture(prov_i, x_i, as_random(0)); the FFUNC then recovers
3877 * @c prov_i from each mixture's first child to construct the matching
3878 * @c mixture(prov_i, as_random(1), as_random(0)) for the denominator.
3879 * Outside a tracked query the per-row UUIDs are plain RV roots, in
3880 * which case each row contributes an unconditional @c as_random(1)
3881 * to the denominator -- the natural extension of "no provenance =
3882 * every row counts" used elsewhere in the extension.
3883 *
3884 * Empty group: returns @c NULL, matching the standard SQL @c AVG
3885 * convention. This differs from @c sum(random_variable), which
3886 * returns the additive identity @c as_random(0) for an empty group;
3887 * for AVG the multiplicative identity is not the right answer and
3888 * the caller has no way to disambiguate "0 rows" from "rows that
3889 * sum to 0".
3890 */
3891CREATE OR REPLACE FUNCTION avg_rv_ffunc(state UUID[])
3892 RETURNS random_variable AS
3893$$
3894DECLARE
3895 n INTEGER;
3896 i INTEGER;
3897 num_token UUID;
3898 denom_token UUID;
3899 denom_state UUID[] := '{}';
3900 one_uuid UUID;
3901 gtype provsql.PROVENANCE_GATE;
3902 children UUID[];
3903 prov_i UUID;
3904BEGIN
3905 IF state IS NULL THEN
3906 RETURN NULL;
3907 END IF;
3908 n := array_length(state, 1);
3909 IF n IS NULL THEN
3910 RETURN NULL;
3911 END IF;
3912
3913 one_uuid := (
3914 provsql.as_random(1::double precision))::UUID;
3915
3916 FOR i IN 1..n LOOP
3917 gtype := provsql.get_gate_type(state[i]);
3918 IF gtype = 'mixture'::provsql.PROVENANCE_GATE THEN
3919 children := provsql.get_children(state[i]);
3920 prov_i := children[1];
3921 denom_state := array_append(
3922 denom_state,
3923 (
3924 provsql.rv_aggregate_semimod(
3925 prov_i, provsql.as_random(1::double precision)))::UUID);
3926 ELSE
3927 denom_state := array_append(denom_state, one_uuid);
3928 END IF;
3929 END LOOP;
3930
3931 IF n = 1 THEN
3932 num_token := state[1];
3933 denom_token := denom_state[1];
3934 ELSE
3935 num_token := provsql.provenance_arith(0, state); -- 0 = PLUS
3936 denom_token := provsql.provenance_arith(0, denom_state); -- 0 = PLUS
3937 END IF;
3938
3939 RETURN provsql.random_variable_make(
3940 provsql.provenance_arith(
3941 3, -- 3 = PROVSQL_ARITH_DIV
3942 ARRAY[num_token, denom_token]));
3943END
3944$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
3945
3946CREATE AGGREGATE avg(random_variable) (
3947 SFUNC = sum_rv_sfunc,
3948 STYPE = UUID[],
3949 INITCOND = '{}',
3950 FINALFUNC = avg_rv_ffunc
3951);
3953/**
3954 * @brief Final function for @c product(random_variable).
3955 *
3956 * Multiplicative analogue of @c sum(random_variable):
3957 * @f[
3958 * \mathrm{PRODUCT}(x) \;=\; \prod_i \big(\mathbf{1}\{\varphi_i\} \cdot X_i
3959 * + \mathbf{1}\{\neg\varphi_i\} \cdot 1\big)
3960 * \;=\; \prod_{i : \varphi_i} X_i
3961 * @f]
3962 * realised as @c gate_arith(TIMES, mixtures) over per-row contributions
3963 * whose @em else-branch is @c as_random(1) (the multiplicative
3964 * identity), so rows whose provenance is false contribute @c 1 to the
3965 * product instead of @c 0.
3966 *
3967 * The C-side wrap shared with @c sum / @c avg always builds
3968 * @c mixture(prov_i, X_i, as_random(0)); the PRODUCT FFUNC patches each
3969 * mixture's else-branch to @c as_random(1) by reconstructing the
3970 * mixture with the corrected else-arg. Going through
3971 * @c provsql.mixture (rather than @c create_gate directly) keeps the
3972 * gate v5-hash consistent with any other mixture sharing the same
3973 * @c (prov_i, X_i, as_random(1)) triple.
3974 *
3975 * Reuses @c sum_rv_sfunc as the state-transition function. Empty
3976 * group: returns the multiplicative identity @c as_random(1) -- the
3977 * natural counterpart to @c sum(random_variable)'s empty-group
3978 * @c as_random(0).
3979 *
3980 * Singleton group: returns the single patched child directly without
3981 * minting a useless single-child @c gate_arith TIMES root.
3982 *
3983 * Direct (untracked) call: state entries are raw RV uuids rather than
3984 * mixtures; pass them through unchanged so PRODUCT degenerates to the
3985 * straight RV product over all rows, the natural "no provenance =
3986 * every row counts" behaviour.
3987 */
3988CREATE OR REPLACE FUNCTION product_rv_ffunc(state UUID[])
3989 RETURNS random_variable AS
3991DECLARE
3992 n INTEGER;
3993 i INTEGER;
3994 prod_state UUID[] := '{}';
3995 one_rv provsql.random_variable;
3996 gtype provsql.PROVENANCE_GATE;
3997 children UUID[];
3998 prov_i UUID;
3999 x_uuid UUID;
4000BEGIN
4001 one_rv := provsql.as_random(1::double precision);
4002
4003 IF state IS NULL THEN
4004 RETURN one_rv;
4005 END IF;
4006 n := array_length(state, 1);
4007 IF n IS NULL THEN
4008 RETURN one_rv;
4009 END IF;
4010
4011 FOR i IN 1..n LOOP
4012 gtype := provsql.get_gate_type(state[i]);
4013 IF gtype = 'mixture'::provsql.PROVENANCE_GATE THEN
4014 children := provsql.get_children(state[i]);
4015 prov_i := children[1];
4016 x_uuid := children[2];
4017 prod_state := array_append(
4018 prod_state,
4019 (
4020 provsql.mixture(
4021 prov_i,
4022 provsql.random_variable_make(x_uuid),
4023 one_rv))::UUID);
4024 ELSE
4025 prod_state := array_append(prod_state, state[i]);
4026 END IF;
4027 END LOOP;
4028
4029 IF n = 1 THEN
4030 RETURN provsql.random_variable_make(prod_state[1]);
4031 END IF;
4032 RETURN provsql.random_variable_make(
4033 provsql.provenance_arith(1, prod_state)); -- 1 = PROVSQL_ARITH_TIMES
4034END
4035$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
4036
4037CREATE AGGREGATE product(random_variable) (
4038 SFUNC = sum_rv_sfunc,
4039 STYPE = UUID[],
4040 INITCOND = '{}',
4041 FINALFUNC = product_rv_ffunc
4042);
4043
4044/** @} */
4045
4046/** @} */
4047
4048/** @defgroup aggregate_provenance Aggregate provenance
4049 * Functions for building and evaluating aggregate (GROUP BY) provenance,
4050 * including the δ-semiring operator and semimodule multiplication.
4051 * @{
4052 */
4054/**
4055 * @brief Create a δ-semiring gate wrapping a provenance token
4056 *
4057 * Used internally for aggregate provenance. Returns the token unchanged
4058 * if it is gate_zero() or gate_one(), and gate_one() if the token is NULL.
4059 */
4060CREATE OR REPLACE FUNCTION provenance_delta
4061 (token UUID)
4062 RETURNS UUID AS
4063$$
4064DECLARE
4065 delta_token UUID;
4066BEGIN
4067 IF token = gate_zero() OR token = gate_one() THEN
4068 return token;
4069 END IF;
4070
4071 IF token IS NULL THEN
4072 return gate_one();
4073 END IF;
4074
4075 delta_token:=uuid_generate_v5(uuid_ns_provsql(),concat('delta',token));
4076
4077 PERFORM create_gate(delta_token,'delta',ARRAY[token::UUID]);
4078
4079 RETURN delta_token;
4080END
4081$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SECURITY DEFINER PARALLEL SAFE;
4082
4083/**
4084 * @brief Build an aggregate provenance gate from grouped tokens
4085 *
4086 * Called internally by the query rewriter for GROUP BY queries.
4087 * Creates an agg gate linking all contributing tokens and records
4088 * the aggregate function OID and the computed scalar value.
4089 *
4090 * @param aggfnoid OID of the SQL aggregate function
4091 * @param aggtype OID of the aggregate result type
4092 * @param val computed aggregate value
4093 * @param tokens array of provenance tokens being aggregated
4094 * @param is_scalar true for a scalar (no GROUP BY) aggregation, whose
4095 * output row exists even when no tuple is present; stored in the
4096 * high bit of info2
4097 */
4098CREATE OR REPLACE FUNCTION provenance_aggregate(
4099 aggfnoid INTEGER,
4100 aggtype INTEGER,
4101 val ANYELEMENT,
4102 tokens UUID[],
4103 is_scalar BOOLEAN DEFAULT false)
4104 RETURNS AGG_TOKEN AS
4105$$
4106DECLARE
4107 c INTEGER;
4108 agg_tok UUID;
4109 agg_val varchar;
4110BEGIN
4111 -- Drop the NULL placeholders array_agg keeps for rows that did not produce a
4112 -- semimod gate (provenance_semimod returns NULL for a NULL aggregated value),
4113 -- so a NULL input never participates in the aggregate.
4114 tokens := array_remove(tokens, NULL);
4115 c:=COALESCE(array_length(tokens, 1), 0);
4116
4117 agg_val = CAST(val as VARCHAR);
4118
4119 IF c = 0 THEN
4120 agg_tok := gate_zero();
4121 ELSE
4122 -- aggfnoid must be part of the UUID: SUM(id) and AVG(id) over the
4123 -- same children would otherwise collapse to a single gate, and
4124 -- their concurrent set_infos calls would overwrite each other's
4125 -- aggregation operator (resulting in the wrong agg_kind being
4126 -- read by provsql_having under cross-backend contention). The
4127 -- scalar-aggregation flag must likewise be hashed: a scalar and a
4128 -- grouped aggregate over identical children carry different info2 and
4129 -- must stay distinct gates, else the concurrent set_infos calls would
4130 -- clobber the flag. The flag is stored in the high bit of info2 (the
4131 -- low 31 bits keep the result-type OID); aggtype itself is passed clean
4132 -- so the AGG_TOKEN->scalar cast still finds a valid type.
4133 agg_tok := uuid_generate_v5(
4134 uuid_ns_provsql(),
4135 concat('agg',aggfnoid,tokens,CASE WHEN is_scalar THEN 'S' ELSE '' END));
4136 PERFORM create_gate(agg_tok, 'agg', tokens);
4137 PERFORM set_infos(agg_tok, aggfnoid,
4138 CASE WHEN is_scalar THEN aggtype | (-2147483648) ELSE aggtype END);
4139 PERFORM set_extra(agg_tok, agg_val);
4140 END IF;
4141
4142 RETURN '( '||agg_tok||' , '||agg_val||' )';
4143END
4144$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql,pg_temp,public SECURITY DEFINER;
4145
4146/**
4147 * @brief Create a semimodule scalar multiplication gate
4148 *
4149 * Pairs a scalar value with a provenance token, used internally by
4150 * the query rewriter for aggregate provenance.
4151 *
4152 * @param val the scalar value
4153 * @param token the provenance token to multiply
4154 */
4155CREATE OR REPLACE FUNCTION provenance_semimod(val ANYELEMENT, token UUID)
4156 RETURNS UUID AS
4157$$
4158DECLARE
4159 semimod_token UUID;
4160 value_token UUID;
4161BEGIN
4162 -- A NULL value means this row does not participate in the aggregate (SQL
4163 -- aggregates ignore NULL inputs; only count(*) counts rows unconditionally,
4164 -- and it passes a constant 1 here). Produce no semimod gate so the row is
4165 -- skipped when provenance_aggregate builds the agg gate.
4166 IF val IS NULL THEN
4167 RETURN NULL;
4168 END IF;
4170 SELECT uuid_generate_v5(uuid_ns_provsql(),concat('value',CAST(val AS VARCHAR)))
4171 INTO value_token;
4172 SELECT uuid_generate_v5(uuid_ns_provsql(),concat('semimod',value_token,token))
4173 INTO semimod_token;
4174
4175 --create value gates
4176 PERFORM create_gate(value_token,'value');
4177 PERFORM set_extra(value_token, CAST(val AS VARCHAR));
4178
4179 --create semimod gate
4180 PERFORM create_gate(semimod_token,'semimod',ARRAY[token::UUID,value_token]);
4181
4182 RETURN semimod_token;
4183END
4184$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql,pg_temp,public SECURITY DEFINER;
4185
4186/** @} */
4187
4188/** @defgroup probability Probability and Shapley values
4189 * Functions for computing probabilities, expected values, and
4190 * game-theoretic contribution measures (Shapley/Banzhaf values)
4191 * from provenance circuits.
4192 * @{
4193 */
4194
4196 * @brief Compute the probability of a provenance token
4197 *
4198 * Compiles the provenance circuit to d-DNNF and evaluates the
4199 * probability. The compilation method can be selected explicitly.
4200 *
4201 * @param token provenance token to evaluate
4202 * @param method knowledge compilation method (NULL for default)
4203 * @param arguments additional arguments for the method
4204 */
4205CREATE OR REPLACE FUNCTION probability_evaluate(
4206 token UUID,
4207 method TEXT = NULL,
4208 arguments TEXT = NULL)
4209 RETURNS DOUBLE PRECISION AS
4210 'provsql','probability_evaluate' LANGUAGE C STABLE;
4211
4212/**
4213 * @brief Cheap certified probability interval of a DNF-shaped circuit.
4214 *
4215 * Returns @c [lower,upper] with @c lower <= probability_evaluate(token) <=
4216 * @c upper, computed without compiling the circuit (the Olteanu-Huang d-tree
4217 * leaf bound). Errors when @p token is not a monotone DNF over input leaves.
4218 */
4219CREATE OR REPLACE FUNCTION probability_bounds(
4220 token UUID,
4221 OUT lower DOUBLE PRECISION,
4222 OUT upper DOUBLE PRECISION) AS
4223 'provsql','probability_bounds' LANGUAGE C STABLE;
4224
4225/**
4226 * @brief Compute the expected value of a probabilistic scalar
4227 *
4228 * Computes E[input | prov] for either an @c AGG_TOKEN (discrete
4229 * SUM/MIN/MAX aggregation over Boolean-input gate_agg circuits, with
4230 * @c prov as the Boolean conditioning event) or a @c random_variable
4231 * (continuous distribution, traversed by the analytical / MC
4232 * evaluator from @c Expectation.cpp).
4233 *
4234 * Implementation: thin wrapper over @c moment(input, 1, prov, method,
4235 * arguments). Both branches converge on the same machinery; the
4236 * AGG_TOKEN side computes E[X] as the @f$k=1@f$ instance of the
4237 * @f$n^k@f$-tuple enumeration in @c agg_raw_moment, the
4238 * random_variable side calls @c compute_expectation through
4239 * @c rv_moment.
4240 *
4241 * @param input aggregate expression or random variable to compute E[·] of
4242 * @param prov provenance condition (defaults to gate_one(), i.e., unconditional)
4243 * @param method knowledge compilation method (AGG_TOKEN path only)
4244 * @param arguments additional arguments for the method (AGG_TOKEN path only)
4245 */
4246CREATE OR REPLACE FUNCTION expected(
4247 input ANYELEMENT,
4248 prov UUID = gate_one(),
4249 method TEXT = NULL,
4250 arguments TEXT = NULL)
4251 RETURNS DOUBLE PRECISION AS $$
4252 SELECT moment(input, 1, prov, method, arguments);
4253$$ LANGUAGE sql PARALLEL SAFE STABLE SET search_path=provsql SECURITY DEFINER;
4254
4255/**
4256 * @brief Internal: shared C entry point for variance / moment / central_moment.
4258 * The @c expected() SQL function reaches the Expectation evaluator
4259 * through @c provenance_evaluate_compiled(..., 'expectation', ...).
4260 * The variance / raw-moment / central-moment SQL functions need an
4261 * extra @p k INTEGER argument that does not fit that dispatcher's
4262 * signature, so they go through this dedicated entry point. Returns
4263 * E[X^k] when @p central is FALSE, or E[(X - E[X])^k] when TRUE.
4264 */
4265CREATE OR REPLACE FUNCTION rv_moment(
4266 token UUID, k INTEGER, central BOOLEAN,
4267 prov UUID DEFAULT gate_one())
4268 RETURNS double precision
4269 AS 'provsql','rv_moment' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
4270
4271/**
4272 * @brief Compute the raw moment E[X^k | prov] of an AGG_TOKEN aggregate
4273 *
4274 * Sister of @c expected() for the AGG_TOKEN side of the polymorphic
4275 * @c moment / @c variance / @c central_moment dispatch. Supports the
4276 * same aggregation functions as @c expected: SUM (which COUNT
4277 * normalises to at the gate level via @c Aggregation.cpp:322), MIN,
4278 * and MAX.
4279 *
4280 * Strategy:
4281 * - <b>SUM</b>: with X = Σᵢ Iᵢ·vᵢ (Iᵢ the per-row inclusion indicator,
4282 * vᵢ the row's value), expanding X^k and taking expectation gives
4283 * @f$E[X^k] = \sum_{(i_1,\ldots,i_k) \in \{1..n\}^k} v_{i_1}\cdots v_{i_k}
4284 * \cdot P(\bigwedge_{i \in \TEXT{distinct}(i_1..i_k)} I_i)@f$.
4285 * We enumerate the @f$n^k@f$ tuples, conjoin the distinct inclusion
4286 * tokens (and @p prov when conditioning), and evaluate the
4287 * probability via @c probability_evaluate.
4288 * - <b>MIN / MAX</b>: replace @c v with @c v^k in the rank-based
4289 * enumeration that @c expected already uses; @c MAX is handled by
4290 * sign-flipping per the existing trick (negate vs. rerank), with
4291 * the outer multiplier becoming @f$(-1)^k@f$ instead of just @f$-1@f$.
4293 * Cost: SUM is @f$O(n^k)@f$ probability evaluations -- tractable for
4294 * small @p k or small @p n; for larger sizes, prefer reaching for the
4295 * sampler. MIN / MAX stay linear in @p n.
4296 */
4297CREATE OR REPLACE FUNCTION agg_raw_moment(
4298 token AGG_TOKEN,
4299 k INTEGER,
4300 prov UUID = gate_one(),
4301 method TEXT = NULL,
4302 arguments TEXT = NULL)
4303 RETURNS DOUBLE PRECISION AS $$
4304DECLARE
4305 aggregation_function VARCHAR;
4306 child_pairs UUID[];
4307 pair_children UUID[];
4308 n INTEGER;
4309 i INTEGER;
4310 j INTEGER;
4311 vals float8[];
4312 toks UUID[];
4313 total float8;
4314 total_probability float8;
4315 tup INTEGER[];
4316 d INTEGER;
4317 prod_v float8;
4318 distinct_tok UUID[];
4319 conj_token UUID;
4320 prob float8;
4321 sign_max float8;
4322BEGIN
4323 IF token IS NULL OR k IS NULL THEN
4324 RETURN NULL;
4325 END IF;
4326 IF k < 0 THEN
4327 RAISE EXCEPTION 'agg_raw_moment(): k must be non-negative (got %)', k;
4328 END IF;
4329 IF get_gate_type(token) <> 'agg' THEN
4330 IF get_gate_type(token) IN ('arith', 'conditioned') THEN
4331 RAISE EXCEPTION 'expected / variance / moment over an arithmetic '
4332 'combination of aggregates (e.g. SUM(x) + SUM(y) or SUM(x) + 5), or a '
4333 'conditioning of one, is not yet supported: a moment can be taken only '
4334 'over a single aggregate (SUM / COUNT / MIN / MAX), optionally '
4335 'conditioned (SUM(x) | C)'
4336 USING HINT = 'Take the moment of each aggregate separately, or condition '
4337 'the bare aggregate.';
4338 ELSE
4339 RAISE EXCEPTION USING MESSAGE='Wrong gate type for agg_raw_moment computation';
4340 END IF;
4341 END IF;
4342 IF k = 0 THEN
4343 RETURN 1;
4344 END IF;
4345
4346 SELECT pp.proname::varchar FROM pg_proc pp
4347 WHERE oid=(get_infos(token)).info1
4348 INTO aggregation_function;
4350 child_pairs := get_children(token);
4351 n := COALESCE(array_length(child_pairs, 1), 0);
4352
4353 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
4354 -- count(col) keeps the COUNT identity at the gate level but its value is a
4355 -- SUM of per-row 0/1 indicators, so its moments are computed exactly like
4356 -- SUM (and its empty group is the real value 0, like SUM). count(*)
4357 -- arrives here as 'sum' (it normalises to F_SUM_INT4); count(col) as 'count'.
4358 -- Trivial empty aggregation: SUM = 0, so SUM^k = 0 for k >= 1.
4359 -- Note: AGG_TOKEN semantics treat the "no row included" world as
4360 -- SUM = 0, so this stays consistent with k = 1 (= expected()).
4361 IF n = 0 THEN
4362 RETURN 0;
4363 END IF;
4364
4365 -- Extract per-child token + value arrays.
4366 vals := ARRAY[]::float8[];
4367 toks := ARRAY[]::UUID[];
4368 FOR i IN 1..n LOOP
4369 pair_children := get_children(child_pairs[i]);
4370 toks := toks || pair_children[1];
4371 vals := vals || CAST(get_extra(pair_children[2]) AS float8);
4372 END LOOP;
4373
4374 -- Enumerate all k-tuples (i_1, ..., i_k) in {1..n}^k. tup is the
4375 -- current tuple; we step through them in lexicographic order.
4376 total := 0;
4377 tup := array_fill(1, ARRAY[k]);
4378 LOOP
4379 prod_v := 1;
4380 FOR j IN 1..k LOOP
4381 prod_v := prod_v * vals[tup[j]];
4382 END LOOP;
4383
4384 SELECT array_agg(DISTINCT toks[idx]) INTO distinct_tok
4385 FROM unnest(tup) AS idx;
4386
4387 IF prov <> gate_one() THEN
4388 distinct_tok := distinct_tok || prov;
4389 END IF;
4390 conj_token := provenance_times(VARIADIC distinct_tok);
4391 prob := probability_evaluate(conj_token, method, arguments);
4392
4393 total := total + prod_v * prob;
4394
4395 d := k;
4396 WHILE d >= 1 AND tup[d] = n LOOP
4397 tup[d] := 1;
4398 d := d - 1;
4399 END LOOP;
4400 EXIT WHEN d = 0;
4401 tup[d] := tup[d] + 1;
4402 END LOOP;
4403 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
4404 -- Rank enumeration: per distinct value v, P(MIN = v) is the
4405 -- probability that some t_i with v_i=v is true and all t_j with
4406 -- smaller v are false. For MAX we negate values so the same
4407 -- "smaller-than" rank logic computes MIN-of-negated, then flip.
4408 -- The outer multiplier picks up the right sign for the k-th moment
4409 -- of MAX: E[MAX^k] = (-1)^k * E[MIN(-v)^k], so sign_max = (-1)^k.
4410 sign_max := CASE
4411 WHEN aggregation_function = 'max'
4412 THEN power(-1::float8, k)
4413 ELSE 1
4414 END;
4415
4416 -- MIN/MAX over the empty input world are NULL (no elements), not ±Infinity:
4417 -- SQL returns one row with a NULL value. The moment is therefore CONDITIONAL
4418 -- on the aggregate being defined (non-empty) -- the empty world is excluded
4419 -- and the result renormalised by P(prov AND non-empty). (count, whose empty
4420 -- value 0 is a real value, keeps the empty world; sum keeps it too, as 0.)
4421 IF n = 0 THEN
4422 RETURN NULL; -- structurally empty: MIN/MAX undefined
4423 END IF;
4424
4425 -- Numerator E[MIN^k . 1{prov AND non-empty}] (the rank sum naturally omits
4426 -- the empty world, since every term requires a present token).
4427 WITH tok_value AS (
4428 SELECT (get_children(c))[1] AS tok,
4429 (CASE WHEN aggregation_function='max' THEN -1 ELSE 1 END)
4430 * CAST(get_extra((get_children(c))[2]) AS DOUBLE PRECISION) AS v
4431 FROM UNNEST(child_pairs) AS c
4432 ) SELECT sign_max * COALESCE(SUM(p * power(v, k)), 0) FROM (
4433 SELECT t1.v AS v,
4434 probability_evaluate(
4435 CASE WHEN prov = gate_one()
4436 THEN provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
4437 provenance_plus(ARRAY_AGG(t2.tok)))
4438 ELSE provenance_times(prov,
4439 provenance_monus(provenance_plus(ARRAY_AGG(t1.tok)),
4440 provenance_plus(ARRAY_AGG(t2.tok)))) END,
4441 method, arguments) AS p
4442 FROM tok_value t1 LEFT OUTER JOIN tok_value t2 ON t1.v > t2.v
4443 GROUP BY t1.v) tmp
4444 INTO total;
4445
4446 -- Denominator P(prov AND non-empty) = P(prov (x) (+) tokens).
4447 SELECT probability_evaluate(
4448 CASE WHEN prov = gate_one()
4449 THEN provenance_plus(ARRAY_AGG(tok))
4450 ELSE provenance_times(prov, provenance_plus(ARRAY_AGG(tok))) END,
4451 method, arguments)
4452 FROM (SELECT (get_children(c))[1] AS tok FROM UNNEST(child_pairs) AS c) s
4453 INTO total_probability;
4454
4455 IF total_probability <= epsilon() THEN
4456 RETURN NULL; -- never defined under prov: MIN/MAX undefined
4457 END IF;
4458 RETURN total / total_probability; -- already conditional; skip generic norm
4459 ELSE
4460 RAISE EXCEPTION USING MESSAGE=
4461 'Cannot compute moment for aggregation function ' || aggregation_function;
4462 END IF;
4463
4464 -- Conditional normalisation: E[X^k · 1_A] / P(A) = E[X^k | A].
4465 IF prov <> gate_one()
4466 AND total <> 0
4467 AND total <> 'Infinity'::float8
4468 AND total <> '-Infinity'::float8 THEN
4469 total := total / probability_evaluate(prov, method, arguments);
4470 END IF;
4471
4472 RETURN total;
4473END
4474$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
4475
4476/**
4477 * @brief Compute the variance Var[X | prov] of a probabilistic scalar
4478 *
4479 * Polymorphic dispatcher that mirrors @c expected: @c random_variable
4480 * inputs go through the analytical / MC evaluator
4481 * (@c rv_moment(UUID, 2, true)); @c AGG_TOKEN inputs go through the
4482 * @c agg_raw_moment helper, computing
4483 * @f$\mathrm{Var}[X|A] = E[X^2|A] - E[X|A]^2@f$. Conditioning on
4484 * @c prov is supported for @c AGG_TOKEN (matching @c expected) but
4485 * not yet for @c random_variable.
4486 */
4487CREATE OR REPLACE FUNCTION variance(
4488 input ANYELEMENT,
4489 prov UUID = gate_one(),
4490 method TEXT = NULL,
4491 arguments TEXT = NULL)
4492 RETURNS DOUBLE PRECISION AS $$
4493DECLARE
4494 m1 float8;
4495 m2 float8;
4496BEGIN
4497 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
4498 IF input IS NULL THEN
4499 RETURN NULL;
4500 END IF;
4501 -- Conditioning on prov is handled inside rv_moment: when prov
4502 -- resolves to gate_one() (the default, or load-time
4503 -- simplification of any always-true sub-circuit) the
4504 -- unconditional analytical path runs unchanged; otherwise the
4505 -- joint-circuit loader unifies shared gate_rv leaves between
4506 -- input and prov, and the conditional path runs either
4507 -- truncated-distribution closed form or MC rejection.
4508 RETURN provsql.rv_moment(
4509 rv_conditioned_target((input::random_variable)::UUID), 2, true,
4510 rv_conditioned_prov((input::random_variable)::UUID, prov));
4511 END IF;
4512
4513 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
4514 IF input IS NULL THEN
4515 RETURN NULL;
4516 END IF;
4517 m1 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
4518 rv_conditioned_prov(input::UUID, prov), method, arguments);
4519 m2 := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 2,
4520 rv_conditioned_prov(input::UUID, prov), method, arguments);
4521 IF m1 IS NULL OR m2 IS NULL THEN
4522 RETURN NULL;
4523 END IF;
4524 RETURN m2 - m1 * m1;
4525 END IF;
4526
4527 RAISE EXCEPTION 'variance() is not yet supported for input type %', pg_typeof(input);
4528END
4529$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
4530
4531/**
4532 * @brief Compute the raw moment E[X^k | prov] of a probabilistic scalar
4533 *
4534 * @c k must be a non-negative INTEGER. @c k = 0 returns 1; @c k = 1
4535 * is equivalent to @c expected(input). Polymorphic dispatcher: routes
4536 * @c random_variable through @c rv_moment (analytical / MC) and
4537 * @c AGG_TOKEN through @c agg_raw_moment (SUM via tuple enumeration,
4538 * MIN / MAX via rank enumeration).
4539 */
4540CREATE OR REPLACE FUNCTION moment(
4541 input ANYELEMENT,
4542 k INTEGER,
4543 prov UUID = gate_one(),
4544 method TEXT = NULL,
4545 arguments TEXT = NULL)
4546 RETURNS DOUBLE PRECISION AS $$
4547BEGIN
4548 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
4549 IF input IS NULL OR k IS NULL THEN
4550 RETURN NULL;
4551 END IF;
4552 -- See variance() above: rv_moment handles the conditional/unconditional
4553 -- dispatch internally based on the resolved prov gate type.
4554 RETURN provsql.rv_moment(
4555 rv_conditioned_target((input::random_variable)::UUID), k, false,
4556 rv_conditioned_prov((input::random_variable)::UUID, prov));
4557 END IF;
4558
4559 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
4560 RETURN agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), k,
4561 rv_conditioned_prov(input::UUID, prov), method, arguments);
4562 END IF;
4563
4564 RAISE EXCEPTION 'moment() is not yet supported for input type %', pg_typeof(input);
4565END
4566$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
4567
4568/**
4569 * @brief Internal: rv-side support computation
4570 *
4571 * Lifts @c provsql.compute_support out of @c RangeCheck.cpp -- the
4572 * same interval-arithmetic propagation @c runRangeCheck uses to
4573 * decide @c gate_cmps. Returns @c [-Infinity, +Infinity] when the
4574 * tightest bound is the conservative all-real interval (e.g. for a
4575 * normal RV, or any sub-circuit that mixes a normal in).
4576 */
4577CREATE OR REPLACE FUNCTION rv_support(
4578 token UUID, prov UUID DEFAULT gate_one(),
4579 OUT lo float8, OUT hi float8)
4580 AS 'provsql','rv_support' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
4581
4582/**
4583 * @brief Compute the support interval @c [lo, hi] of a probabilistic
4584 * (or deterministic) scalar
4585 *
4586 * Polymorphic dispatcher mirroring @c expected / @c variance /
4587 * @c moment / @c central_moment, with two extra "free" branches:
4588 *
4589 * - <b>Plain NUMERIC</b> (@c smallint / @c INTEGER / @c bigint /
4590 * @c NUMERIC / @c real / @c double @c precision): degenerate
4591 * point support @f$[c, c]@f$. Lets callers ask for the support
4592 * of a literal without round-tripping through @c as_random.
4593 * - <b>@c random_variable / bare @c UUID</b> (any provenance gate
4594 * token; the @c random_variable branch reinterprets the value via
4595 * the binary-coercible @c random_variable @c -> @c UUID cast):
4596 * routes to @c rv_support, which propagates distribution
4597 * supports (uniform exact, exponential @c [0,+∞), normal
4598 * @c (-∞,+∞)) through @c gate_arith via interval arithmetic.
4599 * @c gate_value gives the same @f$[c, c]@f$ point support as the
4600 * NUMERIC branch; any non-scalar gate (Boolean gates, aggregates,
4601 * ...) safely falls back to the conservative all-real interval
4602 * without raising. Conditioning on @c prov is not yet supported.
4603 *
4604 * - @c AGG_TOKEN: closed-form per aggregation function:
4605 * - @c SUM : @f$[\sum_i \min(0,v_i), \sum_i \max(0,v_i)]@f$
4606 * (every row is independently in or out of the included set; the
4607 * extreme SUMs are reached by including only positive or only
4608 * negative-valued rows).
4609 * - @c MIN : @f$[\min_i v_i, \max_i v_i]@f$ in the non-empty
4610 * subsets, plus @c +Infinity if the empty subset has positive
4611 * probability under @c prov.
4612 * - @c MAX : symmetric -- @c -Infinity if empty has positive
4613 * probability under @c prov, otherwise @c min_i v_i; @c hi is
4614 * always @c max_i v_i.
4615 *
4616 * Other aggregation functions raise.
4617 *
4618 * Returns the composite RECORD @c (lo, hi) via the function's
4619 * @c OUT parameters, with @c -Infinity / @c +Infinity marking
4620 * unbounded ends.
4621 */
4622CREATE OR REPLACE FUNCTION support(
4623 input ANYELEMENT,
4624 prov UUID = gate_one(),
4625 method TEXT = NULL,
4626 arguments TEXT = NULL,
4627 OUT lo float8,
4628 OUT hi float8)
4629 AS $$
4630DECLARE
4631 aggregation_function VARCHAR;
4632 child_pairs UUID[];
4633 values_arr float8[];
4634 total_probability float8;
4635BEGIN
4636 IF input IS NULL THEN
4637 lo := NULL; hi := NULL; RETURN;
4638 END IF;
4639
4640 -- Plain NUMERIC: degenerate point support. Lets `support(2.5)` /
4641 -- `support(42)` / etc. return (2.5, 2.5) without making the user
4642 -- wrap in `as_random`.
4643 IF pg_typeof(input) IN (
4644 'smallint'::REGTYPE, 'INTEGER'::REGTYPE, 'bigint'::REGTYPE,
4645 'NUMERIC'::REGTYPE, 'real'::REGTYPE, 'double precision'::REGTYPE) THEN
4646 lo := input::double precision;
4647 hi := input::double precision;
4648 RETURN;
4649 END IF;
4650
4651 -- random_variable is binary-coercible to UUID (explicit cast
4652 -- below), so a single rv_support call covers both shapes.
4653 -- rv_support handles
4654 -- gate_value (point), gate_rv (distribution), gate_arith
4655 -- (propagated), and falls back to the conservative all-real
4656 -- interval for any other gate kind. Conditioning on prov is not
4657 -- supported (would require restricting the underlying joint
4658 -- distribution by the indicator of prov, which has no closed form
4659 -- for the basic distributions we ship).
4660 IF pg_typeof(input) IN ('random_variable'::REGTYPE, 'UUID'::REGTYPE) THEN
4661 -- Conditional support: rv_support folds the AND-conjunct interval
4662 -- constraints from prov into the unconditional support. When
4663 -- prov is gate_one() the unconditional support is returned
4664 -- unchanged.
4665 SELECT r.lo, r.hi INTO lo, hi
4666 FROM provsql.rv_support(
4667 rv_conditioned_target(input::UUID),
4668 rv_conditioned_prov(input::UUID, prov)) r;
4669 RETURN;
4670 END IF;
4671
4672 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
4673 -- A conditioned aggregate SUM(x)|C: the value-range support is that of
4674 -- the target aggregate (conditioning can only tighten it; the
4675 -- conservative range stays valid), so unpack to the target gate.
4676 DECLARE
4677 atok AGG_TOKEN := agg_conditioned_target(input::AGG_TOKEN);
4678 BEGIN
4679 IF get_gate_type(atok) <> 'agg' THEN
4680 RAISE EXCEPTION USING MESSAGE='Wrong gate type for support computation';
4681 END IF;
4682 SELECT pp.proname::varchar FROM pg_proc pp
4683 WHERE oid=(get_infos(atok)).info1
4684 INTO aggregation_function;
4685 child_pairs := get_children(atok);
4686
4687 IF aggregation_function = 'sum' OR aggregation_function = 'count' THEN
4688 -- count(col) is a SUM of per-row 0/1 indicators (empty group = 0), so its
4689 -- support is computed like SUM; count(*) arrives as 'sum'.
4690 -- Empty AGG_TOKEN: SUM is identically 0.
4691 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
4692 lo := 0; hi := 0; RETURN;
4693 END IF;
4694 SELECT sum(LEAST(v, 0::float8)), sum(GREATEST(v, 0::float8))
4695 INTO lo, hi
4696 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
4697 FROM unnest(child_pairs) AS c) sub;
4698 ELSIF aggregation_function = 'min' OR aggregation_function = 'max' THEN
4699 -- MIN/MAX over the empty input world are NULL, not ±Infinity (matching the
4700 -- moment surface): the empty world carries no value, so the support is just
4701 -- the range of the per-row values [min(v), max(v)]. A structurally empty
4702 -- aggregate has no defined value at all -> NULL support.
4703 IF COALESCE(array_length(child_pairs, 1), 0) = 0 THEN
4704 lo := NULL; hi := NULL; RETURN;
4705 END IF;
4706
4707 SELECT min(v), max(v)
4708 INTO lo, hi
4709 FROM (SELECT CAST(get_extra((get_children(c))[2]) AS float8) AS v
4710 FROM UNNEST(child_pairs) AS c) sub;
4711 ELSE
4712 RAISE EXCEPTION USING MESSAGE=
4713 'Cannot compute support for aggregation function ' || aggregation_function;
4714 END IF;
4715 RETURN;
4716 END;
4717 END IF;
4718
4719 RAISE EXCEPTION 'support() is not yet supported for input type %', pg_typeof(input);
4720END
4721$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
4722
4723/**
4724 * @brief Compute the central moment E[(X - E[X|prov])^k | prov]
4725 *
4726 * @c k = 0 returns 1; @c k = 1 returns 0; @c k = 2 is equivalent to
4727 * @c variance(input, prov, ...). Polymorphic dispatcher: routes
4728 * @c random_variable through @c rv_moment, and @c AGG_TOKEN through
4729 * the binomial expansion
4730 * @f$E[(X-\mu)^k|A] = \sum_{i=0}^{k} \binom{k}{i} (-\mu)^{k-i} E[X^i|A]@f$
4731 * with @f$\mu = E[X|A]@f$, where each @f$E[X^i|A]@f$ comes from
4732 * @c agg_raw_moment.
4733 */
4734CREATE OR REPLACE FUNCTION central_moment(
4735 input ANYELEMENT,
4736 k INTEGER,
4737 prov UUID = gate_one(),
4738 method TEXT = NULL,
4739 arguments TEXT = NULL)
4740 RETURNS DOUBLE PRECISION AS $$
4741DECLARE
4742 mu float8;
4743 total float8;
4744 i INTEGER;
4745 raw_i float8;
4746 binom float8;
4747 -- iterative binomial coefficient C(k, i)
4748 k_double float8;
4749BEGIN
4750 IF pg_typeof(input) = 'random_variable'::REGTYPE THEN
4751 IF input IS NULL OR k IS NULL THEN
4752 RETURN NULL;
4753 END IF;
4754 -- See variance() above: rv_moment handles the conditional/unconditional
4755 -- dispatch internally based on the resolved prov gate type.
4756 RETURN provsql.rv_moment(
4757 rv_conditioned_target((input::random_variable)::UUID), k, true,
4758 rv_conditioned_prov((input::random_variable)::UUID, prov));
4759 END IF;
4760
4761 IF pg_typeof(input) = 'AGG_TOKEN'::REGTYPE THEN
4762 IF input IS NULL OR k IS NULL THEN
4763 RETURN NULL;
4764 END IF;
4765 IF k < 0 THEN
4766 RAISE EXCEPTION 'central_moment(): k must be non-negative (got %)', k;
4767 END IF;
4768 IF k = 0 THEN RETURN 1; END IF;
4769 IF k = 1 THEN RETURN 0; END IF;
4770
4771 mu := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), 1,
4772 rv_conditioned_prov(input::UUID, prov), method, arguments);
4773 IF mu IS NULL THEN RETURN NULL; END IF;
4774 -- mu may be ±Infinity for empty MIN / MAX with positive empty
4775 -- probability; central_moment is undefined in that case.
4776 IF mu = 'Infinity'::float8 OR mu = '-Infinity'::float8 THEN
4777 RETURN mu;
4778 END IF;
4779
4780 total := 0;
4781 binom := 1; -- C(k, 0)
4782 k_double := k;
4783 FOR i IN 0..k LOOP
4784 raw_i := agg_raw_moment(agg_conditioned_target(input::AGG_TOKEN), i,
4785 rv_conditioned_prov(input::UUID, prov), method, arguments);
4786 IF raw_i IS NULL THEN RETURN NULL; END IF;
4787 total := total + binom * power(-mu, k - i) * raw_i;
4788 -- C(k, i+1) = C(k, i) * (k - i) / (i + 1)
4789 IF i < k THEN
4790 binom := binom * (k_double - i) / (i + 1);
4791 END IF;
4792 END LOOP;
4793 RETURN total;
4794 END IF;
4795
4796 RAISE EXCEPTION 'central_moment() is not yet supported for input type %', pg_typeof(input);
4797END
4798$$ LANGUAGE plpgsql PARALLEL SAFE SET search_path=provsql SECURITY DEFINER;
4799
4800/**
4801 * @brief Compute the Shapley value of an input variable
4802 *
4803 * Measures the contribution of a specific input variable to the
4804 * truth of a provenance expression, using game-theoretic Shapley values.
4805 *
4806 * @param token provenance token to evaluate
4807 * @param variable UUID of the input variable
4808 * @param method knowledge compilation method
4809 * @param arguments additional arguments for the method
4810 * @param banzhaf if true, compute the Banzhaf value instead
4811 */
4812CREATE OR REPLACE FUNCTION shapley(
4813 token UUID,
4814 variable UUID,
4815 method TEXT = NULL,
4816 arguments TEXT = NULL,
4817 banzhaf BOOLEAN = 'f')
4818 RETURNS DOUBLE PRECISION AS
4819 'provsql','shapley' LANGUAGE C STABLE;
4820
4821/** @brief Compute Shapley values for all input variables at once */
4822CREATE OR REPLACE FUNCTION shapley_all_vars(
4823 IN token UUID,
4824 IN method TEXT = NULL,
4825 IN arguments TEXT = NULL,
4826 IN banzhaf BOOLEAN = 'f',
4827 OUT variable UUID,
4828 OUT value DOUBLE PRECISION)
4829 RETURNS SETOF RECORD AS
4830 'provsql', 'shapley_all_vars'
4831 LANGUAGE C STABLE;
4832
4833/** @brief Compute the Banzhaf power index of an input variable */
4834CREATE OR REPLACE FUNCTION banzhaf(
4835 token UUID,
4836 variable UUID,
4837 method TEXT = NULL,
4838 arguments TEXT = NULL)
4839 RETURNS DOUBLE PRECISION AS
4840 $$ SELECT provsql.shapley(token, variable, method, arguments, 't') $$
4841 LANGUAGE SQL;
4842
4843/** @brief Compute Banzhaf power indices for all input variables at once */
4844CREATE OR REPLACE FUNCTION banzhaf_all_vars(
4845 IN token UUID,
4846 IN method TEXT = NULL,
4847 IN arguments TEXT = NULL,
4848 OUT variable UUID,
4849 OUT value DOUBLE PRECISION)
4850 RETURNS SETOF RECORD AS
4851 $$ SELECT * FROM provsql.shapley_all_vars(token, method, arguments, 't') $$
4852 LANGUAGE SQL;
4853
4854/**
4855 * @brief Exact reachability probability over bounded-treewidth data
4856 * (columnar form)
4857 *
4858 * Computes the probability that @p target is reachable from @p source in
4859 * the probabilistic graph given by the parallel edge arrays
4860 * (two-terminal network reliability). Unlike
4861 * @c probability_evaluate(), which compiles the provenance circuit
4862 * built along the relational query plan, this compiles the query
4863 * along a tree decomposition of the *data* graph (in the spirit of the
4864 * provenance refinement of Courcelle's theorem), producing a d-DNNF
4865 * whose size is linear in the number of edges for data of bounded
4866 * treewidth. Exact, and linear-time, on cyclic data as well -- where
4867 * the recursive-query fixpoint cannot terminate structurally.
4868 *
4869 * Edges are independent events. Two array positions may share a token
4870 * only if they are mutual reverses (the natural encoding of an
4871 * undirected edge in a directed edge relation); they are then treated
4872 * as a single bidirectional edge. This is an internal/testing surface:
4873 * the user-facing route is a plain @c WITH @c RECURSIVE reachability
4874 * query under the 'absorptive' (or 'BOOLEAN') provenance class, which
4875 * the query rewriter compiles through @c eval_reachability() /
4876 * @c reachability_materialize().
4877 *
4878 * @param sources source vertex of each edge (dense INTEGER IDs)
4879 * @param destinations destination vertex of each edge
4880 * @param tokens provenance token of each edge tuple
4881 * @param probabilities probability of each edge tuple
4882 * @param source the vertex reachability starts from
4883 * @param target the vertex whose reachability is evaluated
4884 * @param directed if false, each edge can be traversed both ways
4885 */
4886CREATE OR REPLACE FUNCTION reachability_evaluate(
4887 sources INT[],
4888 destinations INT[],
4889 tokens UUID[],
4890 probabilities DOUBLE PRECISION[],
4891 source INT,
4892 target INT,
4893 directed BOOLEAN)
4894 RETURNS DOUBLE PRECISION AS
4895 'provsql','reachability_evaluate' LANGUAGE C IMMUTABLE PARALLEL SAFE;
4896
4897/**
4898 * @brief Reachability probability plus compilation statistics
4899 * (columnar form)
4900 *
4901 * Same compilation as @c reachability_evaluate(), returning the
4902 * probability together with the structural statistics that
4903 * substantiate the bounded-treewidth guarantee: the treewidth of the
4904 * min-fill decomposition of the data graph, its number of bags, the
4905 * maximum number of dynamic-programming states at any decomposition
4906 * node, and the size of the emitted d-DNNF (linear in the number of
4907 * edges for fixed data treewidth).
4908 *
4909 * @param sources source vertex of each edge (dense INTEGER IDs)
4910 * @param destinations destination vertex of each edge
4911 * @param tokens provenance token of each edge tuple
4912 * @param probabilities probability of each edge tuple
4913 * @param source the vertex reachability starts from
4914 * @param target the vertex whose reachability is evaluated
4915 * @param directed if false, each edge can be traversed both ways
4916 * @param[out] probability the reachability probability
4917 * @param[out] data_treewidth treewidth of the min-fill decomposition of the
4918 * data graph
4919 * @param[out] nb_bags number of bags in the decomposition
4920 * @param[out] max_states maximum number of dynamic-programming states at any
4921 * decomposition node
4922 * @param[out] nb_gates number of gates in the emitted d-DNNF
4923 * @param[out] nb_variables number of variables in the emitted d-DNNF
4924 */
4925CREATE OR REPLACE FUNCTION reachability_compile_stats(
4926 IN sources INT[],
4927 IN destinations INT[],
4928 IN tokens UUID[],
4929 IN probabilities DOUBLE PRECISION[],
4930 IN source INT,
4931 IN target INT,
4932 IN directed BOOLEAN,
4933 OUT probability DOUBLE PRECISION,
4934 OUT data_treewidth INT,
4935 OUT nb_bags BIGINT,
4936 OUT max_states BIGINT,
4937 OUT nb_gates BIGINT,
4938 OUT nb_variables BIGINT)
4939 AS 'provsql','reachability_compile_stats'
4940 LANGUAGE C IMMUTABLE PARALLEL SAFE;
4941
4942
4943
4944/**
4945 * @brief Boolean UCQ probability plus compilation statistics
4946 * (columnar form, internal)
4947 *
4948 * Same compilation as @c ucq_joint_compile_stats(query jsonb, ...),
4949 * returning the probability together with the three width columns that
4950 * substantiate thesis Prop. 4.2.11 empirically -- the adversarial family
4951 * has small data and circuit widths but large joint width -- and the
4952 * structural statistics.
4953 *
4954 * @param disjunct_nvars number of query variables of each disjunct
4955 * @param atom_disjunct disjunct index of each atom (parallel to @p atom_rel)
4956 * @param atom_rel relation id of each atom
4957 * @param atom_vars query-variable indices of all atom columns, concatenated
4958 * @param atom_arity number of columns of each atom (slices @p atom_vars)
4959 * @param fact_rel relation id of each fact
4960 * @param fact_elems element ids of all fact columns, concatenated
4961 * @param fact_arity number of columns of each fact (slices @p fact_elems)
4962 * @param fact_tokens provenance token of each fact
4963 * @param fact_probs probability of each fact
4964 * @param[out] probability the exact UCQ probability
4965 * @param[out] joint_treewidth width of the min-fill decomposition found
4966 * @param[out] data_treewidth_lb degeneracy lower bound of the data-only graph
4967 * @param[out] circuit_treewidth_lb degeneracy lower bound of the slice-only graph
4968 * @param[out] n_bags number of bags in the decomposition
4969 * @param[out] max_states peak number of DP states at any node
4970 * @param[out] dd_size number of gates in the emitted d-D
4971 * @param[out] n_enumerating maximum number of essential (enumerating) query
4972 * variables over the disjuncts -- the @c e of the @f$2^{O(k^e)}@f$
4973 * bound, with variables functionally determined by others (via FDs
4974 * mined from the data) removed
4975 */
4976CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
4977 IN disjunct_nvars INT[],
4978 IN atom_disjunct INT[],
4979 IN atom_rel INT[],
4980 IN atom_vars INT[],
4981 IN atom_arity INT[],
4982 IN fact_rel INT[],
4983 IN fact_elems INT[],
4984 IN fact_arity INT[],
4985 IN fact_tokens UUID[],
4986 IN fact_probs DOUBLE PRECISION[],
4987 OUT probability DOUBLE PRECISION,
4988 OUT joint_treewidth INT,
4989 OUT data_treewidth_lb INT,
4990 OUT circuit_treewidth_lb INT,
4991 OUT n_bags BIGINT,
4992 OUT max_states BIGINT,
4993 OUT dd_size BIGINT,
4994 OUT n_enumerating INT)
4995 AS 'provsql','ucq_joint_compile_stats'
4996 LANGUAGE C IMMUTABLE PARALLEL SAFE;
4997
4998
4999/**
5000 * @brief Boolean UCQ probability plus statistics from a JSON specification
5001 *
5002 * JSON-spec wrapper over the columnar @c ucq_joint_compile_stats()
5003 * (see @c ucq_joint_evaluate(query jsonb, ...) for the JSON format).
5004 */
5005CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
5006 IN query JSONB,
5007 IN fact_rel INT[],
5008 IN fact_elems INT[],
5009 IN fact_arity INT[],
5010 IN fact_tokens UUID[],
5011 IN fact_probs DOUBLE PRECISION[],
5012 OUT probability DOUBLE PRECISION,
5013 OUT joint_treewidth INT,
5014 OUT data_treewidth_lb INT,
5015 OUT circuit_treewidth_lb INT,
5016 OUT n_bags BIGINT,
5017 OUT max_states BIGINT,
5018 OUT dd_size BIGINT,
5019 OUT n_enumerating INT)
5020 AS $$
5021DECLARE
5022 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
5023 avars INT[] := '{}'; aarity INT[] := '{}';
5024 d JSONB; a JSONB; v TEXT; didx INT := 0;
5025BEGIN
5026 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
5027 dnv := dnv || (d->>'n_vars')::INT;
5028 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5029 adisj := adisj || didx;
5030 arel := arel || (a->>'rel')::INT;
5031 aarity := aarity || jsonb_array_length(a->'vars');
5032 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5033 avars := avars || v::INT;
5034 END LOOP;
5035 END LOOP;
5036 didx := didx + 1;
5037 END LOOP;
5038 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
5039 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
5040 s.n_enumerating
5041 INTO probability, joint_treewidth, data_treewidth_lb,
5042 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
5043 FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
5044 fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s;
5045END;
5046$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
5057
5058/**
5059 * @brief Correlated Boolean UCQ probability plus compilation statistics
5060 * (columnar form, internal)
5061 *
5062 * Same compilation as @c ucq_joint_evaluate_tracked(); the three width
5063 * columns substantiate thesis Prop. 4.2.11 on real correlated data (the
5064 * data-only and circuit-only degeneracy bounds can be small while the
5065 * joint width is large).
5066 */
5067CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
5068 IN disjunct_nvars INT[],
5069 IN atom_disjunct INT[],
5070 IN atom_rel INT[],
5071 IN atom_vars INT[],
5072 IN atom_arity INT[],
5073 IN fact_rel INT[],
5074 IN fact_elems INT[],
5075 IN fact_arity INT[],
5076 IN fact_tokens UUID[],
5077 OUT probability DOUBLE PRECISION,
5078 OUT joint_treewidth INT,
5079 OUT data_treewidth_lb INT,
5080 OUT circuit_treewidth_lb INT,
5081 OUT n_bags BIGINT,
5082 OUT max_states BIGINT,
5083 OUT dd_size BIGINT,
5084 OUT n_enumerating INT)
5085 AS 'provsql','ucq_joint_compile_stats_tracked'
5086 LANGUAGE C STABLE PARALLEL SAFE;
5087
5088
5089/**
5090 * @brief Correlated Boolean UCQ probability plus statistics from a JSON spec
5091 */
5092CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
5093 IN query JSONB,
5094 IN fact_rel INT[],
5095 IN fact_elems INT[],
5096 IN fact_arity INT[],
5097 IN fact_tokens UUID[],
5098 OUT probability DOUBLE PRECISION,
5099 OUT joint_treewidth INT,
5100 OUT data_treewidth_lb INT,
5101 OUT circuit_treewidth_lb INT,
5102 OUT n_bags BIGINT,
5103 OUT max_states BIGINT,
5104 OUT dd_size BIGINT,
5105 OUT n_enumerating INT)
5106 AS $$
5107DECLARE
5108 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
5109 avars INT[] := '{}'; aarity INT[] := '{}';
5110 d JSONB; a JSONB; v TEXT; didx INT := 0;
5111BEGIN
5112 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
5113 dnv := dnv || (d->>'n_vars')::INT;
5114 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5115 adisj := adisj || didx;
5116 arel := arel || (a->>'rel')::INT;
5117 aarity := aarity || jsonb_array_length(a->'vars');
5118 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5119 avars := avars || v::INT;
5120 END LOOP;
5121 END LOOP;
5122 didx := didx + 1;
5123 END LOOP;
5124 SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
5125 s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
5126 s.n_enumerating
5127 INTO probability, joint_treewidth, data_treewidth_lb,
5128 circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
5129 FROM ucq_joint_compile_stats_tracked(dnv, adisj, arel, avars, aarity,
5130 fact_rel, fact_elems, fact_arity, fact_tokens) s;
5131END;
5132$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
5133
5134/**
5135 * @brief Compile a correlated UCQ and materialise its certified d-D,
5136 * returning the root provenance token (columnar form, internal)
5137 *
5138 * The architecturally-primary route: the compiler builds the
5139 * deterministic, decomposable circuit and materialises it as ordinary
5140 * @c plus / @c times / @c monus provenance gates (carrying the d-DNNF
5141 * certificate); the answer is then obtained through the standard entry
5142 * points on the returned token -- @c probability_evaluate(token),
5143 * @c shapley(token, ...), expectation -- so the joint-width path shares
5144 * the one evaluation pipeline. The token is the exact Boolean
5145 * provenance of the UCQ (no @c 'absorptive' marker).
5146 */
5147CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
5148 disjunct_nvars INT[],
5149 atom_disjunct INT[],
5150 atom_rel INT[],
5151 atom_vars INT[],
5152 atom_arity INT[],
5153 fact_rel INT[],
5154 fact_elems INT[],
5155 fact_arity INT[],
5156 fact_tokens UUID[])
5157 RETURNS UUID AS
5158 'provsql','ucq_joint_materialize_tracked' LANGUAGE C VOLATILE;
5159
5160/**
5161 * @brief Compile a correlated UCQ and materialise its certified d-D
5162 * from a JSON spec, returning the root provenance token
5163 *
5164 * JSON-spec wrapper over @c ucq_joint_materialize_tracked(). Evaluate
5165 * the answer with the standard surface, e.g.
5166 * @c probability_evaluate(ucq_joint_materialize_tracked(query, ...)).
5167 */
5168CREATE OR REPLACE FUNCTION ucq_joint_materialize_tracked(
5169 query JSONB,
5170 fact_rel INT[],
5171 fact_elems INT[],
5172 fact_arity INT[],
5173 fact_tokens UUID[])
5174 RETURNS UUID AS $$
5175DECLARE
5176 dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
5177 avars INT[] := '{}'; aarity INT[] := '{}';
5178 d JSONB; a JSONB; v TEXT; didx INT := 0;
5179BEGIN
5180 FOR d IN SELECT * FROM jsonb_array_elements(query->'disjuncts') LOOP
5181 dnv := dnv || (d->>'n_vars')::INT;
5182 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5183 adisj := adisj || didx;
5184 arel := arel || (a->>'rel')::INT;
5185 aarity := aarity || jsonb_array_length(a->'vars');
5186 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5187 avars := avars || v::INT;
5188 END LOOP;
5189 END LOOP;
5190 didx := didx + 1;
5191 END LOOP;
5192 RETURN ucq_joint_materialize_tracked(dnv, adisj, arel, avars, aarity,
5193 fact_rel, fact_elems, fact_arity, fact_tokens);
5194END;
5195$$ LANGUAGE plpgsql VOLATILE;
5196
5197/**
5198 * @brief Compile a UCQ over named relations into a materialised certified
5199 * d-D, gathering the facts from the store -- the descriptor-driven engine
5200 *
5201 * The query-surface bridge for the joint-width compiler: instead of
5202 * hand-built columnar arrays, a JSON @p descriptor names the relations
5203 * and how their columns map to query variables, and this function
5204 * gathers the facts itself (the provenance rewriting is disabled around
5205 * the gather), builds the value-based element dictionary shared across
5206 * the relations (so equal join values get the same dense id), compiles
5207 * and materialises the certified d-D, and returns its provenance token.
5208 * The answer is then any standard evaluation on that token --
5209 * @c probability_evaluate(ucq_joint_provenance(...)),
5210 * @c shapley(...), expectation. This is also the engine the planner-time
5211 * query recogniser drives once it builds the descriptor from a query's
5212 * abstract syntax.
5213 *
5214 * Descriptor shape:
5215 * @verbatim
5216 * { "disjuncts": [ { "n_vars": k,
5217 * "atoms": [ {"rel": <relidx>, "vars": [..]}, ... ] }, ... ],
5218 * "relations": [ "schema.r", "schema.s", ... ], -- relidx -> relation
5219 * "elem_cols": [ ["x"], ["x","y"], ... ] } -- per relation: the
5220 * element columns, in
5221 * the atom's var order
5222 * @endverbatim
5223 *
5224 * @param descriptor the UCQ + the relations and their element columns
5225 * @param fallback token returned if the joint-width compiler declines
5226 * @return the materialised joint-width provenance token (NULL UUID-free
5227 * exact Boolean provenance of the UCQ)
5228 */
5229CREATE OR REPLACE FUNCTION ucq_joint_provenance(
5230 descriptor JSONB, fallback UUID DEFAULT NULL)
5231RETURNS UUID AS $$
5232DECLARE
5233 legs TEXT; sql TEXT; saved TEXT;
5234 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
5235 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
5236 avars INT[]:='{}'; aarity INT[]:='{}';
5237 d jsonb; a jsonb; v TEXT; didx INT:=0;
5238BEGIN
5239 -- Parse the UCQ structure into the columnar query arrays.
5240 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
5241 dnv := dnv || (d->>'n_vars')::INT;
5242 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5243 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
5244 aarity := aarity || jsonb_array_length(a->'vars');
5245 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5246 avars := avars || v::INT;
5247 END LOOP;
5248 END LOOP;
5249 didx := didx + 1;
5250 END LOOP;
5251
5252 -- One UNION ALL leg per relation: (relation index, TEXT element array,
5253 -- provenance token). No temp tables: a single gather query, with the
5254 -- value-based dense element dictionary built inline.
5255 SELECT string_agg(
5256 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
5257 rn - 1,
5258 (SELECT string_agg(format('(%I)::TEXT', c), ',')
5259 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
5260 rel,
5261 -- the lifted single-relation selection (a pre-filter), already
5262 -- deparsed to SQL by the recogniser; '' / absent = unfiltered.
5263 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
5264 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
5265 ELSE '' END),
5266 ' UNION ALL ')
5267 INTO legs
5268 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
5269
5270 sql := format($q$
5271 WITH facts(rel,elems,tok) AS (%s),
5272 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
5273 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
5274 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
5275 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
5276 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
5277 (SELECT array_agg(tok ORDER BY ord) FROM ord),
5278 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
5279 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
5280 JOIN dict dd ON dd.val = e.val)
5281 $q$, legs);
5282
5283 -- Read the raw rows with provenance rewriting disabled (we only read
5284 -- the existing provsql column; this internal gather is not tracked).
5285 saved := current_setting('provsql.active', true);
5286 PERFORM set_config('provsql.active','off', true);
5287 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
5288 PERFORM set_config('provsql.active', saved, true);
5289
5290 RETURN ucq_joint_materialize_tracked(dnv,adisj,arel,avars,aarity,
5291 fact_rel,fact_elems,fact_arity,fact_tokens);
5292EXCEPTION WHEN OTHERS THEN
5293 -- The joint-width compiler declined (unsupported gate type, joint
5294 -- width too large, ...): fall back to the normal provenance so the
5295 -- query never fails. Both give the same probability.
5296 RETURN fallback;
5297END;
5298$$ LANGUAGE plpgsql VOLATILE;
5299
5300-- ===========================================================================
5301-- Safe-UCQ Möbius-inversion route (mobius_evaluate.cpp).
5302--
5303-- The last missing exact route of the Dalvi-Suciu dichotomy: UCQs that are
5304-- safe only because the \#P-hard terms of their inclusion-exclusion expansion
5305-- carry a zero Möbius value on the CNF lattice and cancel (canonical witness:
5306-- QW / q9). Same TID gather as ucq_joint, then the lattice-walking compiler
5307-- materialises a gate_mobius-rooted circuit (a signed combination over
5308-- certified-independent islands), answered in PTIME data complexity by the
5309-- standard probability path.
5310-- ===========================================================================
5311
5312/**
5313 * @brief Materialise the safe-UCQ Möbius circuit and return its root token.
5314 * Columnar (TID) interface; see ucq_mobius_provenance for the gather.
5315 */
5316CREATE OR REPLACE FUNCTION ucq_mobius_materialize_tracked(
5317 disjunct_nvars INT[],
5318 atom_disjunct INT[],
5319 atom_rel INT[],
5320 atom_vars INT[],
5321 atom_arity INT[],
5322 fact_rel INT[],
5323 fact_elems INT[],
5324 fact_arity INT[],
5325 fact_tokens UUID[],
5326 lineage UUID DEFAULT NULL)
5327 RETURNS UUID AS
5328 'provsql','ucq_mobius_materialize_tracked' LANGUAGE C VOLATILE;
5329
5330/**
5331 * @brief Compile the Möbius circuit and return the lattice statistics plus the
5332 * probability (the demonstrability surface). @c cancelled_hard is the
5333 * single number that makes the mechanism legible: for q9 the 1 cancelled
5334 * element is \#P-hard, so the query is easy only because its hard part
5335 * cancels.
5336 */
5337CREATE OR REPLACE FUNCTION ucq_mobius_compile_stats(
5338 IN disjunct_nvars INT[],
5339 IN atom_disjunct INT[],
5340 IN atom_rel INT[],
5341 IN atom_vars INT[],
5342 IN atom_arity INT[],
5343 IN fact_rel INT[],
5344 IN fact_elems INT[],
5345 IN fact_arity INT[],
5346 IN fact_tokens UUID[],
5347 OUT probability DOUBLE PRECISION,
5348 OUT n_components INT,
5349 OUT n_cnf_conjuncts INT,
5350 OUT lattice_size INT,
5351 OUT n_nonzero INT,
5352 OUT n_cancelled INT,
5353 OUT cancelled_hard BOOLEAN,
5354 OUT dd_size BIGINT,
5355 OUT memo_hits BIGINT)
5356 AS 'provsql','ucq_mobius_compile_stats'
5357 LANGUAGE C VOLATILE;
5358
5359/**
5360 * @brief Pass a token through iff it is a @c gate_mobius, else return NULL.
5361 *
5362 * The Möbius-precedence dispatch (see @c make_provenance_expression) wraps the
5363 * Möbius call in this and then @c COALESCE\ s it before the joint-width call:
5364 * a Möbius *success* always roots a @c gate_mobius (the compiler wraps even a
5365 * thin selector around the lineage), so it short-circuits and the joint-width
5366 * compiler never runs; a Möbius *decline* returns the literal lineage (never a
5367 * @c gate_mobius), so this yields NULL and @c COALESCE falls through to
5368 * joint-width. The lineage token is a plain plus/times/input, so the test is
5369 * unambiguous.
5370 */
5371CREATE OR REPLACE FUNCTION mobius_or_null(tok UUID)
5372RETURNS UUID AS $$
5373 SELECT CASE WHEN tok IS NOT NULL AND provsql.get_gate_type(tok) = 'mobius'
5374 THEN tok END
5375$$ LANGUAGE sql STABLE;
5376
5377/**
5378 * @brief Möbius-route provenance from a descriptor (the planner-substituted
5379 * entry point, and the manual one). Same descriptor and TID gather as
5380 * @c ucq_joint_provenance; on any decline (unsafe shape, cap, not TID)
5381 * returns @p fallback, so a recognised query never fails.
5382 */
5383CREATE OR REPLACE FUNCTION ucq_mobius_provenance(
5384 descriptor JSONB, fallback UUID DEFAULT NULL)
5385RETURNS UUID AS $$
5386DECLARE
5387 legs TEXT; sql TEXT; saved TEXT;
5388 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
5389 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
5390 avars INT[]:='{}'; aarity INT[]:='{}';
5391 d jsonb; a jsonb; v TEXT; didx INT:=0;
5392BEGIN
5393 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
5394 dnv := dnv || (d->>'n_vars')::INT;
5395 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5396 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
5397 aarity := aarity || jsonb_array_length(a->'vars');
5398 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5399 avars := avars || v::INT;
5400 END LOOP;
5401 END LOOP;
5402 didx := didx + 1;
5403 END LOOP;
5404
5405 SELECT string_agg(
5406 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
5407 rn - 1,
5408 (SELECT string_agg(format('(%I)::TEXT', c), ',')
5409 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
5410 rel,
5411 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
5412 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
5413 ELSE '' END),
5414 ' UNION ALL ')
5415 INTO legs
5416 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
5417
5418 sql := format($q$
5419 WITH facts(rel,elems,tok) AS (%s),
5420 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
5421 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
5422 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
5423 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
5424 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
5425 (SELECT array_agg(tok ORDER BY ord) FROM ord),
5426 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
5427 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
5428 JOIN dict dd ON dd.val = e.val)
5429 $q$, legs);
5430
5431 saved := current_setting('provsql.active', true);
5432 PERFORM set_config('provsql.active','off', true);
5433 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
5434 PERFORM set_config('provsql.active', saved, true);
5435
5436 -- Pass the normal-provenance fallback as the lineage: it is carried on the
5437 -- gate_mobius so the token still answers Shapley / semiring / PROV on the
5438 -- literal lineage (the Möbius combination is a probability-only shortcut).
5439 RETURN ucq_mobius_materialize_tracked(dnv,adisj,arel,avars,aarity,
5440 fact_rel,fact_elems,fact_arity,fact_tokens, fallback);
5441EXCEPTION WHEN OTHERS THEN
5442 RETURN fallback;
5443END;
5444$$ LANGUAGE plpgsql VOLATILE;
5445
5446/**
5447 * @brief Möbius lattice statistics + probability from a descriptor: the
5448 * demonstrability SRF. Gathers
5449 * the same TID facts as @c ucq_mobius_provenance, then runs the columnar
5450 * @c ucq_mobius_compile_stats.
5451 */
5452CREATE OR REPLACE FUNCTION mobius_compile_stats(
5453 IN descriptor JSONB,
5454 OUT probability DOUBLE PRECISION,
5455 OUT n_components INT,
5456 OUT n_cnf_conjuncts INT,
5457 OUT lattice_size INT,
5458 OUT n_nonzero INT,
5459 OUT n_cancelled INT,
5460 OUT cancelled_hard BOOLEAN,
5461 OUT dd_size BIGINT,
5462 OUT memo_hits BIGINT)
5463RETURNS RECORD AS $$
5464DECLARE
5465 legs TEXT; sql TEXT; saved TEXT;
5466 fact_rel INT[]; fact_elems INT[]; fact_arity INT[]; fact_tokens UUID[];
5467 dnv INT[]:='{}'; adisj INT[]:='{}'; arel INT[]:='{}';
5468 avars INT[]:='{}'; aarity INT[]:='{}';
5469 d jsonb; a jsonb; v TEXT; didx INT:=0;
5470BEGIN
5471 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
5472 dnv := dnv || (d->>'n_vars')::INT;
5473 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5474 adisj := adisj || didx; arel := arel || (a->>'rel')::INT;
5475 aarity := aarity || jsonb_array_length(a->'vars');
5476 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5477 avars := avars || v::INT;
5478 END LOOP;
5479 END LOOP;
5480 didx := didx + 1;
5481 END LOOP;
5482
5483 SELECT string_agg(
5484 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s',
5485 rn - 1,
5486 (SELECT string_agg(format('(%I)::TEXT', c), ',')
5487 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
5488 rel,
5489 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
5490 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
5491 ELSE '' END),
5492 ' UNION ALL ')
5493 INTO legs
5494 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
5495
5496 sql := format($q$
5497 WITH facts(rel,elems,tok) AS (%s),
5498 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
5499 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
5500 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
5501 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
5502 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
5503 (SELECT array_agg(tok ORDER BY ord) FROM ord),
5504 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
5505 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
5506 JOIN dict dd ON dd.val = e.val)
5507 $q$, legs);
5508
5509 saved := current_setting('provsql.active', true);
5510 PERFORM set_config('provsql.active','off', true);
5511 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems;
5512 PERFORM set_config('provsql.active', saved, true);
5513
5514 SELECT s.probability, s.n_components, s.n_cnf_conjuncts, s.lattice_size,
5515 s.n_nonzero, s.n_cancelled, s.cancelled_hard, s.dd_size, s.memo_hits
5516 INTO probability, n_components, n_cnf_conjuncts, lattice_size,
5517 n_nonzero, n_cancelled, cancelled_hard, dd_size, memo_hits
5518 FROM ucq_mobius_compile_stats(dnv,adisj,arel,avars,aarity,
5519 fact_rel,fact_elems,fact_arity,fact_tokens) s;
5520END;
5521$$ LANGUAGE plpgsql VOLATILE;
5522
5523/**
5524 * @brief Internal gather for the per-answer joint route: parse @p descriptor
5525 * into the columnar UCQ arrays and gather every fact (relation index,
5526 * dense element ids, provenance token) with the value dictionary.
5527 *
5528 * Used only by the planner-substituted @c ucq_joint_provenance_answer (the C
5529 * single-DP entry point), which calls it ONCE per query and then computes all
5530 * answers in one sweep. No head pinning: the single DP discovers the answers.
5531 * @c val_by_id maps a dense element id back to its TEXT value (so an answer's
5532 * head ids can be matched to the @c GROUP @c BY head TEXT).
5533 */
5534CREATE OR REPLACE FUNCTION ucq_joint_gather(
5535 descriptor JSONB,
5536 OUT disjunct_nvars INT[], OUT atom_disjunct INT[], OUT atom_rel INT[],
5537 OUT atom_vars INT[], OUT atom_arity INT[],
5538 OUT fact_rel INT[], OUT fact_elems INT[], OUT fact_arity INT[],
5539 OUT fact_tokens UUID[], OUT val_by_id TEXT[])
5540AS $$
5541DECLARE
5542 legs TEXT; sql TEXT; saved TEXT; d jsonb; a jsonb; v TEXT; didx INT := 0;
5543BEGIN
5544 disjunct_nvars:='{}'; atom_disjunct:='{}'; atom_rel:='{}';
5545 atom_vars:='{}'; atom_arity:='{}';
5546 FOR d IN SELECT * FROM jsonb_array_elements(descriptor->'disjuncts') LOOP
5547 disjunct_nvars := disjunct_nvars || (d->>'n_vars')::INT;
5548 FOR a IN SELECT * FROM jsonb_array_elements(d->'atoms') LOOP
5549 atom_disjunct := atom_disjunct || didx;
5550 atom_rel := atom_rel || (a->>'rel')::INT;
5551 atom_arity := atom_arity || jsonb_array_length(a->'vars');
5552 FOR v IN SELECT * FROM jsonb_array_elements_text(a->'vars') LOOP
5553 atom_vars := atom_vars || v::INT;
5554 END LOOP;
5555 END LOOP;
5556 didx := didx + 1;
5557 END LOOP;
5558
5559 SELECT string_agg(
5560 format('SELECT %s, ARRAY[%s]::TEXT[], provsql FROM %s%s', rn - 1,
5561 (SELECT string_agg(format('(%I)::TEXT', c), ',')
5562 FROM jsonb_array_elements_text(descriptor->'elem_cols'->(rn-1)::INT) c),
5563 rel,
5564 CASE WHEN coalesce(descriptor->'rel_where'->>(rn-1)::INT,'') <> ''
5565 THEN ' WHERE '||(descriptor->'rel_where'->>(rn-1)::INT)
5566 ELSE '' END),
5567 ' UNION ALL ')
5568 INTO legs
5569 FROM jsonb_array_elements_text(descriptor->'relations') WITH ORDINALITY t(rel, rn);
5570
5571 sql := format($q$
5572 WITH facts(rel,elems,tok) AS (%s),
5573 ord AS (SELECT row_number() OVER () AS ord, rel, elems, tok FROM facts),
5574 dict AS (SELECT val, (dense_rank() OVER (ORDER BY val))-1 AS id
5575 FROM (SELECT DISTINCT unnest(elems) AS val FROM facts) u)
5576 SELECT (SELECT array_agg(rel ORDER BY ord) FROM ord),
5577 (SELECT array_agg(cardinality(elems) ORDER BY ord) FROM ord),
5578 (SELECT array_agg(tok ORDER BY ord) FROM ord),
5579 (SELECT array_agg(dd.id ORDER BY o.ord, e.k)
5580 FROM ord o, LATERAL unnest(o.elems) WITH ORDINALITY e(val,k)
5581 JOIN dict dd ON dd.val = e.val),
5582 (SELECT array_agg(val ORDER BY id) FROM dict)
5583 $q$, legs);
5584
5585 saved := current_setting('provsql.active', true);
5586 PERFORM set_config('provsql.active','off', true);
5587 EXECUTE sql INTO fact_rel, fact_arity, fact_tokens, fact_elems, val_by_id;
5588 PERFORM set_config('provsql.active', saved, true);
5589END;
5590$$ LANGUAGE plpgsql VOLATILE;
5591
5592/**
5593 * @brief Per-answer joint-width provenance via the TOP-DOWN single DP
5594 * (planner-substituted, C).
5595 *
5596 * The transparent per-answer rewrite substitutes one call per output group.
5597 * On the FIRST call of a query the function gathers the facts once
5598 * (@c ucq_joint_gather), runs the single DP, and materialises EVERY answer's
5599 * certified d-D into the store, caching @c head_vals -> token in @c fn_extra;
5600 * each subsequent group call is an O(1) lookup -- so the whole GROUP BY costs
5601 * one gather + one decomposition + one sweep, not @p k of each. On any
5602 * decline (joint width too large) the @p fallback token (the normal
5603 * per-answer provenance) is returned, so the query never fails. The answer's
5604 * marginal / Shapley / expectation is then the standard evaluation on the
5605 * returned token -- one pipeline for the whole system.
5606 */
5607CREATE OR REPLACE FUNCTION ucq_joint_provenance_answer(
5608 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
5609RETURNS UUID AS 'provsql','ucq_joint_provenance_answer'
5610LANGUAGE C STABLE;
5611
5612/**
5613 * @brief Per-answer safe-UCQ Möbius provenance (planner-substituted): one
5614 * head-pinned Möbius circuit per output group. On the first call the
5615 * facts are gathered once (ucq_joint_gather) and cached; each group pins
5616 * @p head_vars to @p head_vals and compiles, caching head -> token. On
5617 * any decline returns @p fallback. STABLE: it caches per fn-call
5618 * context, so it is not re-evaluated within one scan.
5619 */
5620CREATE OR REPLACE FUNCTION ucq_mobius_provenance_answer(
5621 descriptor JSONB, head_vars INT[], head_vals TEXT[], fallback UUID DEFAULT NULL)
5622RETURNS UUID AS 'provsql','ucq_mobius_provenance_answer'
5623LANGUAGE C STABLE;
5624
5625
5626/**
5627 * @brief Compile and materialise the reachability provenance of every
5628 * vertex (columnar form, internal)
5629 *
5630 * All-targets variant of @c reachability_evaluate(): compiles, along a
5631 * tree decomposition of the data graph, one certified provenance
5632 * circuit per vertex reachable from some source in the all-edges-present
5633 * world, materialises the (shared, linear-size) circuits in the
5634 * provenance store -- @c plus / @c times gates carrying the d-DNNF
5635 * certificate, negated edges as @c monus(one, edge) -- and returns one
5636 * @c (vertex, token) row per such vertex. Sources form a possibly
5637 * *probabilistic source set*: each source arc is gated by the source
5638 * tuple's token, the nil UUID marking a certain (always present)
5639 * source. This is the engine behind the rewriter's
5640 * recursive-reachability route; the returned tokens are ordinary
5641 * provenance tokens usable with the whole evaluation surface, wrapped
5642 * in the 'absorptive' assumption marker (the compiled circuit is the
5643 * exact Boolean lineage but only the absorptive quotient of the
5644 * infinite recursive semiring provenance: probability and absorptive
5645 * semiring evaluations -- e.g. nonnegative min-plus -- are exact,
5646 * counting and why-provenance refuse).
5647 *
5648 * @param sources source vertex of each edge (dense INTEGER IDs)
5649 * @param destinations destination vertex of each edge
5650 * @param tokens provenance token of each edge tuple
5651 * @param probabilities probability of each edge tuple
5652 * @param block_keys per-edge BID key variable (nil UUID = independent
5653 * tuple; alternatives sharing a key are mutually exclusive, e.g.
5654 * from repair_key)
5655 * @param block_indices per-edge outcome index within its block
5656 * @param source_vertices the source vertices
5657 * @param source_tokens per-source provenance token (nil UUID = certain)
5658 * @param source_probabilities per-source probability
5659 * @param directed if false, each edge can be traversed both ways
5660 * @param[out] vertex a vertex reachable from some source
5661 * @param[out] token the materialised reachability provenance token of @c vertex
5662 */
5663CREATE OR REPLACE FUNCTION reachability_materialize(
5664 IN sources INT[],
5665 IN destinations INT[],
5666 IN tokens UUID[],
5667 IN probabilities DOUBLE PRECISION[],
5668 IN block_keys UUID[],
5669 IN block_indices INT[],
5670 IN source_vertices INT[],
5671 IN source_tokens UUID[],
5672 IN source_probabilities DOUBLE PRECISION[],
5673 IN directed BOOLEAN,
5674 OUT vertex INT,
5675 OUT token UUID)
5676 RETURNS SETOF RECORD AS
5677 'provsql','reachability_materialize' LANGUAGE C VOLATILE;
5678
5679
5680/**
5681 * @brief Bounded-hop variant of @c reachability_materialize() (internal)
5682 *
5683 * Compiles, along a tree decomposition of the data graph, one certified
5684 * provenance circuit per (vertex, walk length) pair achievable within
5685 * @p hop_bound edges -- the rows a hop-counting recursive CTE derives,
5686 * row @c (v,h) meaning "some *walk* of exactly @c h edges connects a
5687 * present source to @c v" -- and returns them as @c (vertex, hops,
5688 * token) with @p hop_seed added to the lengths (the CTE base arm's hop
5689 * constant). Also pre-creates, per vertex, the certified gate that a
5690 * hop-discarding query's deduplication will address, wired to the
5691 * compilation's native within-bound root, so the natural "within k
5692 * hops" probability evaluates through the linear certified route.
5693 *
5694 * @param sources source vertex of each edge (dense INTEGER IDs)
5695 * @param destinations destination vertex of each edge
5696 * @param tokens provenance token of each edge tuple
5697 * @param probabilities probability of each edge tuple
5698 * @param block_keys per-edge BID key variable (nil UUID = independent)
5699 * @param block_indices per-edge outcome index within its block
5700 * @param source_vertices the source vertices
5701 * @param source_tokens per-source provenance token (nil UUID = certain)
5702 * @param source_probabilities per-source probability
5703 * @param directed if false, each edge can be traversed both ways
5704 * @param hop_bound maximum walk length
5705 * @param hop_seed hop value of the base arm (added to reported lengths)
5706 * @param[out] vertex a reachable vertex
5707 * @param[out] hops the walk length at which @c vertex is reached
5708 * @param[out] token the materialised provenance token of the @c (vertex, hops) pair
5709 */
5710CREATE OR REPLACE FUNCTION reachability_materialize_hops(
5711 IN sources INT[],
5712 IN destinations INT[],
5713 IN tokens UUID[],
5714 IN probabilities DOUBLE PRECISION[],
5715 IN block_keys UUID[],
5716 IN block_indices INT[],
5717 IN source_vertices INT[],
5718 IN source_tokens UUID[],
5719 IN source_probabilities DOUBLE PRECISION[],
5720 IN directed BOOLEAN,
5721 IN hop_bound INT,
5722 IN hop_seed INT,
5723 OUT vertex INT,
5724 OUT hops INT,
5725 OUT token UUID)
5726 RETURNS SETOF RECORD AS
5727 'provsql','reachability_materialize_hops' LANGUAGE C VOLATILE;
5728
5729
5730/**
5731 * @brief Per-group "some member reachable" compilation (columnar form,
5732 * internal)
5733 *
5734 * For each distinct group in the parallel @p group_ids /
5735 * @p member_vertices arrays, compiles the certified circuit of "some
5736 * member vertex is reachable from a present source" along the data
5737 * decomposition -- the disjunction over the group's *correlated*
5738 * per-vertex reachability events, deterministic by construction
5739 * through the set-reachability state bit -- materialises it, and
5740 * returns one @c (group_id, token) row per group. Engine behind the
5741 * rewriter's cross-vertex aggregation planting.
5742 *
5743 * @param sources source vertex of each edge (dense INTEGER IDs)
5744 * @param destinations destination vertex of each edge
5745 * @param tokens provenance token of each edge tuple
5746 * @param probabilities probability of each edge tuple
5747 * @param block_keys per-edge BID key variable (nil UUID = independent)
5748 * @param block_indices per-edge outcome index within its block
5749 * @param source_vertices the source vertices
5750 * @param source_tokens per-source provenance token (nil UUID = certain)
5751 * @param source_probabilities per-source probability
5752 * @param directed if false, each edge can be traversed both ways
5753 * @param group_ids group identifier of each member row
5754 * @param member_vertices member vertex of each member row
5755 * @param[out] group_id a group whose every member is reachable
5756 * @param[out] token the materialised all-members-reachable provenance token of
5757 * @c group_id
5758 */
5759CREATE OR REPLACE FUNCTION reachability_materialize_any(
5760 IN sources INT[],
5761 IN destinations INT[],
5762 IN tokens UUID[],
5763 IN probabilities DOUBLE PRECISION[],
5764 IN block_keys UUID[],
5765 IN block_indices INT[],
5766 IN source_vertices INT[],
5767 IN source_tokens UUID[],
5768 IN source_probabilities DOUBLE PRECISION[],
5769 IN directed BOOLEAN,
5770 IN group_ids INT[],
5771 IN member_vertices INT[],
5772 OUT group_id INT,
5773 OUT token UUID)
5774 RETURNS SETOF RECORD AS
5775 'provsql','reachability_materialize_any' LANGUAGE C VOLATILE;
5776
5777/**
5778 * @brief Compile and materialise the "every member vertex reachable"
5779 * (k-terminal / coverage) circuit (columnar form, internal)
5780 *
5781 * Arguments as @c reachability_materialize_any() with a single member
5782 * set: compiles the certified circuit of "every member vertex is
5783 * reachable from a present source" -- the conjunction over the
5784 * members' *correlated* per-vertex events, deterministic by
5785 * construction through the pending rescuer-set congruence --
5786 * materialises it, and returns its token, wrapped in the
5787 * @c 'absorptive' assumption marker. Probability evaluation gives the
5788 * k-terminal reliability; nonnegative min-plus the cost of the
5789 * cheapest covering subgraph (directed Steiner cost), shared edges
5790 * paid once. A member vertex absent from the graph is unreachable:
5791 * the circuit is then constant false.
5792 *
5793 * @param sources source vertex of each edge (dense INTEGER IDs)
5794 * @param destinations destination vertex of each edge
5795 * @param tokens provenance token of each edge tuple
5796 * @param probabilities probability of each edge tuple
5797 * @param block_keys per-edge BID key variable (nil UUID = independent)
5798 * @param block_indices per-edge outcome index within its block
5799 * @param source_vertices the source vertices
5800 * @param source_tokens per-source provenance token (nil UUID = certain)
5801 * @param source_probabilities per-source probability
5802 * @param directed if false, each edge can be traversed both ways
5803 * @param member_vertices the member vertices (dense IDs)
5804 */
5805CREATE OR REPLACE FUNCTION reachability_materialize_cover(
5806 sources INT[],
5807 destinations INT[],
5808 tokens UUID[],
5809 probabilities DOUBLE PRECISION[],
5810 block_keys UUID[],
5811 block_indices INT[],
5812 source_vertices INT[],
5813 source_tokens UUID[],
5814 source_probabilities DOUBLE PRECISION[],
5815 directed BOOLEAN,
5816 member_vertices INT[])
5817 RETURNS UUID AS
5818 'provsql','reachability_materialize_cover' LANGUAGE C VOLATILE;
5819
5820/**
5821 * @brief Plant certified any-member-reachable gates for a grouped
5822 * reachability aggregation (internal)
5823 *
5824 * Called (at plan time, over SPI) by the recursive-CTE lowering when
5825 * the outer query aggregates a reachability working table by a column
5826 * of a joined, untracked member relation: @c GROUP @c BY collapses
5827 * each group's per-vertex reach tokens with @c provenance_plus, whose
5828 * disjuncts are correlated (they share edges) and would otherwise
5829 * leave the certified route. For each multi-member group this
5830 * pre-creates, at the canonical address of the group's token multiset,
5831 * a certified single-child plus over the group's native
5832 * any-member-reachable circuit (@c reachability_materialize_any), so
5833 * the natural aggregation stays on the linear evaluation route.
5834 * Best-effort: any failure leaves the generic path untouched (notice
5835 * under verbosity 10).
5836 *
5837 * @param work_name the lowered CTE's working table
5838 * @param node_attribute its vertex column
5839 * @param member_rel the joined member relation (must be untracked)
5840 * @param member_attribute the member relation's join column
5841 * @param group_attribute the member relation's grouping column
5842 * @param edge_rel the tracked edge relation (as for eval_reachability)
5843 * @param source_attribute name of the source-vertex column
5844 * @param destination_attribute name of the destination-vertex column
5845 * @param source_value the base arm's constant, as TEXT
5846 * @param directed if false, each edge can be traversed both ways
5847 * @param edge_quals optional deterministic filter over edge columns
5848 * @param source_rel source relation of a multi-source base arm
5849 * @param source_rel_attribute the source relation's vertex column
5850 * @param edge_sql deparsed edge subquery (join-defined edges)
5851 * @param member_quals optional deterministic filter over the member
5852 * relation's columns (table-qualified as @c t.column), restricting
5853 * which members participate in each group
5854 */
5855CREATE OR REPLACE FUNCTION plant_reach_any_groups(
5856 work_name TEXT,
5857 node_attribute TEXT,
5858 member_rel REGCLASS,
5859 member_attribute TEXT,
5860 group_attribute TEXT,
5861 edge_rel REGCLASS,
5862 source_attribute TEXT,
5863 destination_attribute TEXT,
5864 source_value TEXT,
5865 directed BOOLEAN,
5866 edge_quals TEXT DEFAULT NULL,
5867 source_rel REGCLASS DEFAULT NULL,
5868 source_rel_attribute TEXT DEFAULT NULL,
5869 edge_sql TEXT DEFAULT NULL,
5870 member_quals TEXT DEFAULT NULL)
5871 RETURNS VOID AS
5872$$
5873DECLARE
5874 e RECORD;
5875 grp RECORD;
5876 m RECORD;
5877 sv TEXT[];
5878 st UUID[];
5879 sp double precision[];
5880 gids INT[] := ARRAY[]::INT[];
5881 mids INT[] := ARRAY[]::INT[];
5882 vid INT;
5883 canonical UUID;
5884 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
5885BEGIN
5886 BEGIN
5887 -- A tracked member relation would make the aggregated tokens
5888 -- per-row products, not the bare reach tokens: nothing to plant.
5889 IF EXISTS (SELECT 1 FROM pg_attribute
5890 WHERE attrelid = member_rel AND attname = 'provsql'
5891 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped) THEN
5892 RETURN;
5893 END IF;
5894
5895 IF source_rel IS NOT NULL THEN
5896 SELECT g.source_values, g.source_tokens, g.source_probabilities
5897 INTO sv, st, sp
5898 FROM provsql.gather_reachability_sources(source_rel,
5899 source_rel_attribute) g;
5900 IF sv IS NULL THEN
5901 sv := ARRAY[]::TEXT[];
5902 st := ARRAY[]::UUID[];
5903 sp := ARRAY[]::float8[];
5904 END IF;
5905 ELSE
5906 sv := ARRAY[source_value];
5907 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
5908 sp := ARRAY[1.0::float8];
5909 END IF;
5910
5911 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
5912 destination_attribute,
5913 sv, edge_quals, edge_sql);
5914
5915 -- The groups, replicating the user's join semantics: per group, the
5916 -- member vertices and the multiset of their reach tokens (with the
5917 -- multiplicity the join produces). Single-member groups need no
5918 -- planting (provenance_plus passes a single token through).
5919 -- Two steps: materialise the joined rows with their per-row tokens
5920 -- (tracked CTAS, then strip the automatic provsql column), and only
5921 -- then aggregate the now-plain table -- aggregating provenance()
5922 -- inside a grouped tracked query would be rewritten as a
5923 -- provenance-aware aggregation, which is not what the planting
5924 -- needs.
5925 DROP TABLE IF EXISTS provsql_reach_any_flat_tmp;
5926 EXECUTE format(
5927 'CREATE TEMP TABLE provsql_reach_any_flat_tmp AS '
5928 || 'SELECT w.%1$I::TEXT AS node_val, provsql.provenance() AS tok, '
5929 || ' t.%5$I AS grp_key '
5930 || 'FROM %2$I w JOIN %3$s t ON w.%1$I = t.%4$I'
5931 -- The member-relation filter restricts which members participate
5932 -- (deparsed table-qualified as t.column); the working table side
5933 -- carries no provenance distinction here.
5934 || coalesce(' WHERE ' || member_quals, ''),
5935 node_attribute, work_name, member_rel::TEXT, member_attribute,
5936 group_attribute);
5937 PERFORM provsql.remove_provenance('provsql_reach_any_flat_tmp');
5938 DROP TABLE IF EXISTS provsql_reach_any_groups_tmp;
5939 CREATE TEMP TABLE provsql_reach_any_groups_tmp AS
5940 SELECT (row_number() OVER ())::INT AS gid, members, toks FROM (
5941 SELECT array_agg(node_val) AS members, array_agg(tok) AS toks
5942 FROM provsql_reach_any_flat_tmp
5943 GROUP BY grp_key HAVING count(*) >= 2) g;
5944 DROP TABLE provsql_reach_any_flat_tmp;
5945
5946 FOR grp IN SELECT gid, members FROM provsql_reach_any_groups_tmp LOOP
5947 FOR m IN SELECT DISTINCT unnest(grp.members) AS val LOOP
5948 vid := array_position(e.vertices, m.val);
5949 IF vid IS NOT NULL THEN
5950 gids := gids || grp.gid;
5951 mids := mids || vid;
5952 END IF;
5953 END LOOP;
5954 END LOOP;
5955 IF cardinality(gids) = 0 THEN
5956 DROP TABLE provsql_reach_any_groups_tmp;
5957 RETURN;
5958 END IF;
5959
5960 FOR grp IN
5961 SELECT a.group_id, a.token AS any_token, t.toks
5962 FROM provsql.reachability_materialize_any(
5963 e.sources, e.destinations, e.tokens, e.probabilities,
5964 e.block_keys, e.block_indices, e.extra_ids, st, sp,
5965 directed, gids, mids) a
5966 JOIN provsql_reach_any_groups_tmp t ON t.gid = a.group_id
5967 LOOP
5968 canonical := public.uuid_generate_v5(
5969 provsql.uuid_ns_provsql(),
5970 concat('plus-canonical',
5971 (SELECT array_agg(tok ORDER BY tok)
5972 FROM unnest(grp.toks) tok)));
5973 PERFORM provsql.create_gate(canonical, 'plus', ARRAY[grp.any_token]);
5974 PERFORM provsql.set_infos(canonical, 1);
5975 END LOOP;
5976 DROP TABLE provsql_reach_any_groups_tmp;
5977 IF verbosity >= 20 THEN
5978 -- Lift the function-level client_min_messages = warning for the
5979 -- one RAISE; the function-level SET restores the caller's value.
5980 PERFORM set_config('client_min_messages', 'notice', true);
5981 RAISE NOTICE 'ProvSQL: certified any-member gates planted for the aggregation of "%" by %.%',
5982 work_name, member_rel, group_attribute;
5983 PERFORM set_config('client_min_messages', 'warning', true);
5984 END IF;
5985 EXCEPTION WHEN OTHERS THEN
5986 IF verbosity >= 10 THEN
5987 PERFORM set_config('client_min_messages', 'notice', true);
5988 RAISE NOTICE 'ProvSQL: any-member planting for "%" skipped (%)',
5989 work_name, SQLERRM;
5990 PERFORM set_config('client_min_messages', 'warning', true);
5991 END IF;
5992 END;
5993END
5994-- No SET search_path: the deparsed edge subquery must resolve against
5995-- the caller's path; ProvSQL internals are schema-qualified.
5996$$ LANGUAGE plpgsql SET client_min_messages = warning;
5997
5998/**
5999 * @brief Plant the certified all-members-reachable gate for a
6000 * reachability self-join conjunction (internal)
6001 *
6002 * Called (at plan time, over SPI) by the recursive-CTE lowering when
6003 * the outer query self-joins a reachability working table with one
6004 * constant node binding per reference -- "are these k vertices all
6005 * reachable" -- whose row provenance @c provenance_times() computes as
6006 * the product of *correlated* per-vertex reach tokens (they share
6007 * edges). This pre-creates, at the times-canonical address of that
6008 * token multiset, a certified single-child times over the native
6009 * all-members-reachable circuit (@c reachability_materialize_cover),
6010 * so the natural conjunction stays on the linear certified route --
6011 * with the joint-worlds semantics: probability evaluation gives the
6012 * k-terminal reliability, and nonnegative min-plus the cost of the
6013 * cheapest covering subgraph (directed Steiner cost), shared edges
6014 * paid once where the raw product would pay them once per factor.
6015 * Best-effort: any failure leaves the generic path untouched (notice
6016 * under verbosity 10).
6017 *
6018 * @param work_name the lowered CTE's working table
6019 * @param node_attribute its vertex column
6020 * @param edge_rel the tracked edge relation (as for eval_reachability)
6021 * @param source_attribute name of the source-vertex column
6022 * @param destination_attribute name of the destination-vertex column
6023 * @param source_value the base arm's constant, as TEXT
6024 * @param directed if false, each edge can be traversed both ways
6025 * @param node_values the constant node bindings, as TEXT (multiset:
6026 * one per self-join reference)
6027 * @param edge_quals optional deterministic filter over edge columns
6028 * @param source_rel source relation of a multi-source base arm
6029 * @param source_rel_attribute the source relation's vertex column
6030 * @param edge_sql deparsed edge subquery (join-defined edges)
6031 */
6032CREATE OR REPLACE FUNCTION plant_reach_cover(
6033 work_name TEXT,
6034 node_attribute TEXT,
6035 edge_rel REGCLASS,
6036 source_attribute TEXT,
6037 destination_attribute TEXT,
6038 source_value TEXT,
6039 directed BOOLEAN,
6040 node_values TEXT[],
6041 edge_quals TEXT DEFAULT NULL,
6042 source_rel REGCLASS DEFAULT NULL,
6043 source_rel_attribute TEXT DEFAULT NULL,
6044 edge_sql TEXT DEFAULT NULL)
6045 RETURNS VOID AS
6046$$
6047DECLARE
6048 e RECORD;
6049 sv TEXT[];
6050 st UUID[];
6051 sp double precision[];
6052 val TEXT;
6053 vid INT;
6054 vids INT[] := ARRAY[]::INT[];
6055 tok UUID;
6056 toks UUID[] := ARRAY[]::UUID[];
6057 cover_token UUID;
6058 canonical UUID;
6059 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
6060BEGIN
6061 BEGIN
6062 IF source_rel IS NOT NULL THEN
6063 SELECT g.source_values, g.source_tokens, g.source_probabilities
6064 INTO sv, st, sp
6065 FROM provsql.gather_reachability_sources(source_rel,
6066 source_rel_attribute) g;
6067 IF sv IS NULL THEN
6068 sv := ARRAY[]::TEXT[];
6069 st := ARRAY[]::UUID[];
6070 sp := ARRAY[]::float8[];
6071 END IF;
6072 ELSE
6073 sv := ARRAY[source_value];
6074 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
6075 sp := ARRAY[1.0::float8];
6076 END IF;
6077
6078 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
6079 destination_attribute,
6080 sv, edge_quals, edge_sql);
6081
6082 -- The bound vertices and their per-row reach tokens, with the
6083 -- multiplicity the self-join produces. A vertex absent from the
6084 -- graph, or from the working table, means the join is empty: no
6085 -- row will exist, nothing to plant.
6086 FOREACH val IN ARRAY node_values LOOP
6087 vid := array_position(e.vertices, val);
6088 IF vid IS NULL THEN
6089 RETURN;
6090 END IF;
6091 vids := vids || vid;
6092 EXECUTE format('SELECT provsql FROM %I WHERE %I::TEXT = $1',
6093 work_name, node_attribute)
6094 INTO tok USING val;
6095 IF tok IS NULL THEN
6096 RETURN;
6097 END IF;
6098 toks := toks || tok;
6099 END LOOP;
6100
6101 cover_token := provsql.reachability_materialize_cover(
6102 e.sources, e.destinations, e.tokens, e.probabilities,
6103 e.block_keys, e.block_indices, e.extra_ids, st, sp,
6104 directed, vids);
6105
6106 SELECT public.uuid_generate_v5(
6107 provsql.uuid_ns_provsql(),
6108 concat('times-canonical', array_agg(t ORDER BY t)))
6109 FROM unnest(toks) t
6110 INTO canonical;
6111 PERFORM provsql.create_gate(canonical, 'times', ARRAY[cover_token]);
6112 PERFORM provsql.set_infos(canonical, 1);
6113 IF verbosity >= 20 THEN
6114 -- Lift the function-level client_min_messages = warning for the
6115 -- one RAISE; the function-level SET restores the caller's value.
6116 PERFORM set_config('client_min_messages', 'notice', true);
6117 RAISE NOTICE 'ProvSQL: certified all-members gate planted for the self-join of "%"',
6118 work_name;
6119 PERFORM set_config('client_min_messages', 'warning', true);
6120 END IF;
6121 EXCEPTION WHEN OTHERS THEN
6122 IF verbosity >= 10 THEN
6123 PERFORM set_config('client_min_messages', 'notice', true);
6124 RAISE NOTICE 'ProvSQL: all-members planting for "%" skipped (%)',
6125 work_name, SQLERRM;
6126 PERFORM set_config('client_min_messages', 'warning', true);
6127 END IF;
6128 END;
6129END
6130-- No SET search_path: the deparsed edge subquery must resolve against
6131-- the caller's path; ProvSQL internals are schema-qualified.
6132$$ LANGUAGE plpgsql SET client_min_messages = warning;
6133
6134/**
6135 * @brief Input leaves of a conjunction-shaped provenance token (internal)
6136 *
6137 * Descends a token's circuit through the conjunctive gate types
6138 * (@c times, and the pass-through @c project / @c eq where-provenance
6139 * wrappers) down to @c input leaves. Returns the distinct leaves, or
6140 * NULL when the circuit contains any other gate type (a disjunctive or
6141 * aggregate shape, which is not a conjunction of independent tuples).
6142 * Used by the reachability gathering to accept join-defined edges:
6143 * a derived edge whose token is a pure conjunction of base tuples.
6144 *
6145 * @param token the provenance token
6146 */
6147CREATE OR REPLACE FUNCTION token_conjunctive_leaves(token UUID)
6148 RETURNS UUID[] AS
6149$$
6150WITH RECURSIVE walk(g) AS (
6151 SELECT token
6152 UNION
6153 SELECT c FROM walk w, unnest(provsql.get_children(w.g)) AS c
6154 WHERE provsql.get_gate_type(w.g) IN ('times', 'project', 'eq')
6155)
6156SELECT CASE WHEN bool_and(provsql.get_gate_type(g)
6157 IN ('times', 'project', 'eq', 'input'))
6158 THEN array_agg(DISTINCT g)
6159 FILTER (WHERE provsql.get_gate_type(g) = 'input')
6160 ELSE NULL END
6161FROM walk;
6162$$ LANGUAGE sql STABLE;
6163
6164/**
6165 * @brief Gather the edges of a tracked relation in the columnar form
6166 * expected by reachability_evaluate (internal)
6167 *
6168 * Materializes the edge relation with its provenance tokens and
6169 * probabilities, maps arbitrary vertex values (compared as TEXT) onto
6170 * dense INTEGER IDs, and checks that every edge tuple carries a base
6171 * input token (independent tuples): reachability compilation along the
6172 * data is only correct when the edges are independent events, so views
6173 * or query results with derived provenance are rejected.
6174 *
6175 * @param rel the provenance-tracked edge relation
6176 * @param source_attribute name of the source-vertex column
6177 * @param destination_attribute name of the destination-vertex column
6178 * @param extra_vertices vertex values (as TEXT) that must be part of
6179 * the dense ID space even when they touch no edge -- the source
6180 * set in particular; their IDs come back in @c extra_ids
6181 * (aligned with the input)
6182 * @param edge_quals optional deterministic filter over the edge
6183 * relation's columns (SQL TEXT, deparsed by the rewriter from
6184 * the recursive arm's WHERE clause), restricting which edges
6185 * participate
6186 * @param rel_sql deparsed edge subquery to gather from instead of
6187 * @p rel (join-defined edges); the tokens are then conjunctions
6188 * of base tuples, validated for shape and disjoint supports
6189 *
6190 * The @c vertices output maps the dense IDs back to the original
6191 * vertex values (as TEXT, 1-indexed), for callers that need to label
6192 * per-vertex results.
6193 *
6194 * @param[out] sources source vertex (dense ID) of each gathered edge
6195 * @param[out] destinations destination vertex (dense ID) of each edge
6196 * @param[out] tokens provenance token of each edge tuple
6197 * @param[out] probabilities probability of each edge tuple
6198 * @param[out] block_keys per-edge BID key variable (nil UUID = independent)
6199 * @param[out] block_indices per-edge outcome index within its block
6200 * @param[out] extra_ids dense IDs assigned to the @p extra_vertices
6201 * @param[out] vertices dense-ID-to-original-value map (TEXT, 1-indexed)
6202 */
6203CREATE OR REPLACE FUNCTION gather_reachability_edges(
6204 IN rel REGCLASS,
6205 IN source_attribute TEXT,
6206 IN destination_attribute TEXT,
6207 IN extra_vertices TEXT[],
6208 IN edge_quals TEXT DEFAULT NULL,
6209 IN rel_sql TEXT DEFAULT NULL,
6210 OUT sources INT[],
6211 OUT destinations INT[],
6212 OUT tokens UUID[],
6213 OUT probabilities DOUBLE PRECISION[],
6214 OUT block_keys UUID[],
6215 OUT block_indices INT[],
6216 OUT extra_ids INT[],
6217 OUT vertices TEXT[])
6218AS
6219$$
6220DECLARE
6221 tkind TEXT;
6222 bkey_expr TEXT;
6223 sel_probs TEXT;
6224 sel_bkeys TEXT;
6225 sel_bidx TEXT;
6226 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
6227BEGIN
6228 -- Consult the per-table characterisation registry (TID / BID / OPAQUE,
6229 -- maintained by add_provenance / repair_key and the CTAS lineage hook):
6230 -- a TID relation is certified all-independent-inputs, a BID relation
6231 -- holds input or mulinput rows with the block structure given by the
6232 -- registry's key columns. Derived (OPAQUE), unregistered, or
6233 -- subquery-defined edges take the fully dynamic per-token path.
6234 IF rel IS NOT NULL AND rel_sql IS NULL THEN
6235 tkind := (provsql.get_table_info(rel::oid)).kind;
6236 END IF;
6237 IF tkind NOT IN ('tid', 'bid') THEN
6238 tkind := NULL;
6239 END IF;
6240 IF tkind = 'bid' THEN
6241 SELECT string_agg(quote_ident(a.attname) || '::TEXT', ' || '','' || '
6242 ORDER BY k.ord)
6243 INTO bkey_expr
6244 FROM unnest((provsql.get_table_info(rel::oid)).block_key)
6245 WITH ORDINALITY AS k(attnum, ord)
6246 JOIN pg_attribute a ON a.attrelid = rel AND a.attnum = k.attnum;
6247 -- An empty registry key means the whole table is one block.
6248 bkey_expr := coalesce(bkey_expr, quote_literal(''));
6249 END IF;
6250 IF tkind IS NOT NULL AND verbosity >= 20 THEN
6251 -- The function-level client_min_messages = warning (which silences
6252 -- the CTAS / DROP TABLE chatter) would also swallow this notice;
6253 -- lift it for the one RAISE. The function-level SET restores the
6254 -- caller's value at exit regardless.
6255 PERFORM set_config('client_min_messages', 'notice', true);
6256 RAISE NOTICE 'ProvSQL: catalog characterises % as %', rel, upper(tkind);
6257 PERFORM set_config('client_min_messages', 'warning', true);
6258 END IF;
6259
6260 -- Materialize the edges with their tokens; the planner hook resolves
6261 -- provenance() over the tracked relation, and remove_provenance strips
6262 -- the automatic provsql column so the later aggregation is plain SQL.
6263 -- For a BID relation the synthetic per-block key (a v5 UUID over the
6264 -- registry key columns' values) is computed here, while the columns
6265 -- are in scope.
6266 DROP TABLE IF EXISTS provsql_reachability_edges_tmp;
6267 EXECUTE format(
6268 'CREATE TEMP TABLE provsql_reachability_edges_tmp AS '
6269 || 'SELECT %1$I::TEXT AS u, %2$I::TEXT AS v, provsql.provenance() AS token%5$s '
6270 || 'FROM %3$s WHERE %1$I IS NOT NULL AND %2$I IS NOT NULL%4$s',
6271 source_attribute, destination_attribute,
6272 CASE WHEN rel_sql IS NULL THEN rel::TEXT
6273 ELSE '(' || rel_sql || ') AS provsql_edge_subquery' END,
6274 CASE WHEN edge_quals IS NULL THEN ''
6275 ELSE ' AND (' || edge_quals || ')' END,
6276 CASE WHEN tkind = 'bid'
6277 THEN ', public.uuid_generate_v5(provsql.uuid_ns_provsql(), '
6278 || quote_literal('bidblock' || rel::TEXT || ':')
6279 || ' || ' || bkey_expr || ') AS bkey'
6280 ELSE ', NULL::UUID AS bkey' END);
6281 PERFORM provsql.remove_provenance('provsql_reachability_edges_tmp');
6282
6283 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
6284 IF tkind IS NULL THEN
6285 -- Dynamic path: validate the token shapes and, for conjunction-shaped
6286 -- (join-defined) tokens, the pairwise disjointness of their supports.
6287 IF EXISTS (SELECT 1 FROM provsql_reachability_edges_tmp
6288 WHERE provsql.get_gate_type(token) NOT IN ('input', 'mulinput', 'times',
6289 'project', 'eq')) THEN
6290 DROP TABLE provsql_reachability_edges_tmp;
6291 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input, repair_key, or conjunctive join tokens', coalesce(rel::TEXT, 'the edge query');
6292 END IF;
6293 CREATE TEMP TABLE provsql_reachability_support_tmp AS
6294 SELECT t.token, l.leaf
6295 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp
6296 WHERE provsql.get_gate_type(token) IN ('times', 'project', 'eq')) t,
6297 LATERAL unnest(provsql.token_conjunctive_leaves(t.token)) AS l(leaf);
6298 IF EXISTS (SELECT 1
6299 FROM (SELECT DISTINCT token FROM provsql_reachability_edges_tmp) t
6300 WHERE provsql.get_gate_type(t.token) IN ('times', 'project', 'eq')
6301 AND provsql.token_conjunctive_leaves(t.token) IS NULL) THEN
6302 DROP TABLE provsql_reachability_support_tmp;
6303 DROP TABLE provsql_reachability_edges_tmp;
6304 RAISE EXCEPTION 'reachability: a join-defined edge token is not a pure conjunction of base tuples';
6305 END IF;
6306 IF EXISTS (SELECT 1 FROM (
6307 SELECT leaf FROM provsql_reachability_support_tmp
6308 UNION ALL
6309 SELECT DISTINCT token FROM provsql_reachability_edges_tmp
6310 WHERE provsql.get_gate_type(token) = 'input'
6311 ) all_leaves
6312 GROUP BY leaf HAVING count(*) > 1) THEN
6313 DROP TABLE provsql_reachability_support_tmp;
6314 DROP TABLE provsql_reachability_edges_tmp;
6315 RAISE EXCEPTION 'reachability: join-defined edges share base tuples (their supports overlap), so they are not independent';
6316 END IF;
6317 END IF;
6318
6319 -- Per-kind classification expressions for the final aggregation: a TID
6320 -- relation needs no per-row gate introspection at all; a BID relation
6321 -- one get_gate_type per row (the input/mulinput split), block keys from
6322 -- the precomputed column-derived key and indices by numbering within
6323 -- the block; the dynamic path reads the gates.
6324 IF tkind = 'tid' THEN
6325 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
6326 sel_bkeys := $sql$'00000000-0000-0000-0000-000000000000'::UUID$sql$;
6327 sel_bidx := '0';
6328 ELSIF tkind = 'bid' THEN
6329 sel_probs := 'coalesce(provsql.get_prob(e.token), 1.0)';
6330 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
6331 THEN e.bkey
6332 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
6333 sel_bidx := 'e.bidx';
6334 ELSE
6335 sel_probs := $sql$CASE WHEN provsql.get_gate_type(e.token) IN ('times','project','eq')
6336 THEN (SELECT CASE WHEN bool_or(coalesce(provsql.get_prob(s.leaf),1.0) = 0)
6337 THEN 0.0
6338 ELSE exp(sum(ln(coalesce(provsql.get_prob(s.leaf),1.0)))) END
6339 FROM provsql_reachability_support_tmp s
6340 WHERE s.token = e.token)
6341 ELSE coalesce(provsql.get_prob(e.token), 1.0) END$sql$;
6342 sel_bkeys := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
6343 THEN (provsql.get_children(e.token))[1]
6344 ELSE '00000000-0000-0000-0000-000000000000'::UUID END$sql$;
6345 sel_bidx := $sql$CASE WHEN provsql.get_gate_type(e.token) = 'mulinput'
6346 THEN (provsql.get_infos(e.token)).info1 ELSE 0 END$sql$;
6347 END IF;
6348
6349 EXECUTE format(
6350 $sql$
6351 WITH verts AS (
6352 SELECT u AS x FROM provsql_reachability_edges_tmp
6353 UNION SELECT v FROM provsql_reachability_edges_tmp
6354 UNION SELECT unnest($1)),
6355 ids AS (
6356 SELECT x, (row_number() OVER (ORDER BY x))::INT AS id FROM verts)
6357 SELECT array_agg(iu.id), array_agg(iv.id),
6358 array_agg(e.token),
6359 array_agg(%s),
6360 array_agg(%s),
6361 array_agg(%s),
6362 (SELECT array_agg(i.id ORDER BY ev.ord)
6363 FROM unnest($1) WITH ORDINALITY AS ev(x, ord)
6364 JOIN ids i ON i.x = ev.x),
6365 (SELECT array_agg(x ORDER BY id) FROM ids)
6366 FROM (SELECT t.*,
6367 (row_number() OVER (PARTITION BY t.bkey))::INT AS bidx
6368 FROM provsql_reachability_edges_tmp t) e
6369 JOIN ids iu ON iu.x = e.u
6370 JOIN ids iv ON iv.x = e.v
6371 $sql$, sel_probs, sel_bkeys, sel_bidx)
6372 INTO sources, destinations, tokens, probabilities, block_keys,
6373 block_indices, extra_ids, vertices
6374 USING extra_vertices;
6375
6376 DROP TABLE provsql_reachability_edges_tmp;
6377 DROP TABLE IF EXISTS provsql_reachability_support_tmp;
6378END
6379-- No SET search_path: the deparsed edge subquery (and the REGCLASS
6380-- rendering) must resolve against the caller's search_path; the ProvSQL
6381-- calls above are schema-qualified instead.
6382$$ LANGUAGE plpgsql SET client_min_messages = warning;
6383
6384
6385/**
6386 * @brief Gather a source relation's vertices, tokens and probabilities
6387 * (internal)
6388 *
6389 * For a provenance-tracked source relation, every tuple must carry a
6390 * base @c input token (a *probabilistic source set*); for an untracked
6391 * relation the sources are certain and the tokens come back as the nil
6392 * UUID. Vertex values are returned as TEXT, for the shared dense-ID
6393 * mapping of @c gather_reachability_edges().
6394 *
6395 * @param rel the source relation
6396 * @param source_attribute name of the vertex column
6397 * @param[out] source_values vertex value of each source tuple (as TEXT)
6398 * @param[out] source_tokens per-source base @c input token (nil UUID = certain)
6399 * @param[out] source_probabilities per-source probability
6400 */
6401CREATE OR REPLACE FUNCTION gather_reachability_sources(
6402 IN rel REGCLASS,
6403 IN source_attribute TEXT,
6404 OUT source_values TEXT[],
6405 OUT source_tokens UUID[],
6406 OUT source_probabilities DOUBLE PRECISION[])
6407AS
6408$$
6409DECLARE
6410 tracked BOOLEAN;
6411 tkind TEXT;
6412BEGIN
6413 SELECT EXISTS (
6414 SELECT 1 FROM pg_attribute
6415 WHERE attrelid = rel AND attname = 'provsql'
6416 AND atttypid = 'UUID'::REGTYPE AND NOT attisdropped)
6417 INTO tracked;
6418
6419 -- Registry consultation: a TID source relation is certified
6420 -- all-base-input, so the per-row gate check can be skipped; a BID one
6421 -- holds block-correlated tuples, which a probabilistic source set
6422 -- cannot model -- reject it before gathering anything.
6423 IF tracked THEN
6424 tkind := (get_table_info(rel::oid)).kind;
6425 IF tkind = 'bid' THEN
6426 RAISE EXCEPTION 'reachability: % is block-independent (repair_key); block-correlated source sets are not supported', rel;
6427 END IF;
6428 END IF;
6429
6430 DROP TABLE IF EXISTS provsql_reachability_sources_tmp;
6431 IF tracked THEN
6432 EXECUTE format(
6433 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
6434 || 'SELECT %1$I::TEXT AS x, provenance() AS token '
6435 || 'FROM %2$s WHERE %1$I IS NOT NULL',
6436 source_attribute, rel);
6437 PERFORM remove_provenance('provsql_reachability_sources_tmp');
6438 IF tkind IS DISTINCT FROM 'tid'
6439 AND EXISTS (SELECT 1 FROM provsql_reachability_sources_tmp
6440 WHERE get_gate_type(token) <> 'input') THEN
6441 DROP TABLE provsql_reachability_sources_tmp;
6442 RAISE EXCEPTION 'reachability: the provenance of % must consist of base input tokens (independent tuples); views or query results are not supported', rel;
6443 END IF;
6444 SELECT array_agg(x), array_agg(token),
6445 array_agg(coalesce(get_prob(token), 1.0))
6446 INTO source_values, source_tokens, source_probabilities
6447 FROM provsql_reachability_sources_tmp;
6448 DROP TABLE provsql_reachability_sources_tmp;
6449 ELSE
6450 EXECUTE format(
6451 'CREATE TEMP TABLE provsql_reachability_sources_tmp AS '
6452 || 'SELECT DISTINCT %1$I::TEXT AS x FROM %2$s WHERE %1$I IS NOT NULL',
6453 source_attribute, rel);
6454 SELECT array_agg(x),
6455 array_agg('00000000-0000-0000-0000-000000000000'::UUID),
6456 array_agg(1.0::float8)
6457 INTO source_values, source_tokens, source_probabilities
6458 FROM provsql_reachability_sources_tmp;
6459 DROP TABLE provsql_reachability_sources_tmp;
6460 END IF;
6461END
6462$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp,public SET client_min_messages = warning;
6463
6464/**
6465 * @brief Fixpoint driver for the recursive reachability shape:
6466 * decomposition-aligned compilation with fallback to eval_recursive
6467 *
6468 * Called (at plan time, over SPI) by the recursive-CTE lowering when
6469 * the provenance class is 'absorptive' or 'BOOLEAN'
6470 * (@c provsql.provenance) and the CTE matches the linear
6471 * reachability shape over a tracked base edge relation. Attempts the
6472 * decomposition-aligned route -- gather the edges, compile every
6473 * reachable vertex's certified provenance circuit along a tree
6474 * decomposition of the data graph, materialise them, and fill the
6475 * working table with one tokenised row per reachable vertex. On any
6476 * failure (data treewidth above the cap, per-node state bound, edges
6477 * that are not independent base tuples...), falls back to the generic
6478 * @c eval_recursive() fixpoint, preserving its behaviour exactly.
6479 *
6480 * @param edge_rel the provenance-tracked edge relation
6481 * @param source_attribute name of the source-vertex column
6482 * @param destination_attribute name of the destination-vertex column
6483 * @param source_value the base arm's constant, as TEXT
6484 * @param directed if false, each edge can be traversed both ways
6485 * @param work_name name of the working temp table (the CTE name)
6486 * @param colnames comma-separated user column names (for the fallback)
6487 * @param coldef column definitions of the working table
6488 * @param coltype type of the CTE's single column
6489 * @param body_sql deparsed CTE body (for the fallback)
6490 * @param edge_quals optional deterministic filter over edge columns
6491 * (deparsed from the recursive arm's WHERE clause)
6492 * @param source_rel source relation of a multi-source base arm
6493 * (@c SELECT col FROM sources), NULL for the constant form;
6494 * tracked sources form a probabilistic source set, untracked
6495 * ones are certain
6496 * @param source_rel_attribute the source relation's vertex column
6497 * @param edge_sql deparsed edge subquery when the recursive arm joins a
6498 * derived (join-defined) edge relation instead of a base one;
6499 * NULL for the REGCLASS form
6500 * @param hop_bound maximum number of recursive steps for the
6501 * hop-counting CTE shape (NULL for plain reachability)
6502 * @param hop_seed the base arm's hop constant (hop-counting shape)
6503 * @param hops_position 1-based position of the hop column among the
6504 * CTE's two columns (hop-counting shape)
6505 */
6506CREATE OR REPLACE FUNCTION eval_reachability(
6507 edge_rel REGCLASS,
6508 source_attribute TEXT,
6509 destination_attribute TEXT,
6510 source_value TEXT,
6511 directed BOOLEAN,
6512 work_name TEXT,
6513 colnames TEXT,
6514 coldef TEXT,
6515 coltype TEXT,
6516 body_sql TEXT,
6517 edge_quals TEXT DEFAULT NULL,
6518 source_rel REGCLASS DEFAULT NULL,
6519 source_rel_attribute TEXT DEFAULT NULL,
6520 edge_sql TEXT DEFAULT NULL,
6521 hop_bound INT DEFAULT NULL,
6522 hop_seed INT DEFAULT NULL,
6523 hops_position INT DEFAULT NULL)
6524 RETURNS VOID AS
6525$$
6526DECLARE
6527 e RECORD;
6528 sv TEXT[];
6529 st UUID[];
6530 sp double precision[];
6531 verbosity INT := coalesce(current_setting('provsql.verbose_level', true)::INT, 0);
6532BEGIN
6533 BEGIN
6534 IF source_rel IS NOT NULL THEN
6535 -- Multi-source: gather the source relation (probabilistic when
6536 -- tracked, certain otherwise).
6537 SELECT g.source_values, g.source_tokens, g.source_probabilities
6538 INTO sv, st, sp
6539 FROM provsql.gather_reachability_sources(source_rel,
6540 source_rel_attribute) g;
6541 IF sv IS NULL THEN
6542 sv := ARRAY[]::TEXT[];
6543 st := ARRAY[]::UUID[];
6544 sp := ARRAY[]::float8[];
6545 END IF;
6546 ELSE
6547 -- Constant base arm: one certain source.
6548 sv := ARRAY[source_value];
6549 st := ARRAY['00000000-0000-0000-0000-000000000000'::UUID];
6550 sp := ARRAY[1.0::float8];
6551 END IF;
6552
6553 e := provsql.gather_reachability_edges(edge_rel, source_attribute,
6554 destination_attribute,
6555 sv, edge_quals, edge_sql);
6556 IF to_regclass(work_name) IS NOT NULL THEN
6557 EXECUTE format('DROP TABLE %I', work_name);
6558 END IF;
6559 EXECUTE format('CREATE TEMP TABLE %I (%s, provsql UUID)', work_name, coldef);
6560 IF hop_bound IS NULL THEN
6561 EXECUTE format(
6562 'INSERT INTO %I SELECT ($1::TEXT[])[m.vertex]::%s, m.token '
6563 || 'FROM provsql.reachability_materialize($2, $3, $4, $5, $6, $7, $8, $9, $10, $11) m',
6564 work_name, coltype)
6565 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
6566 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed;
6567 ELSE
6568 -- Hop-counting shape: one row per (vertex, walk length), the hop
6569 -- column in its CTE position.
6570 EXECUTE format(
6571 'INSERT INTO %I SELECT %s, m.token '
6572 || 'FROM provsql.reachability_materialize_hops($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) m',
6573 work_name,
6574 CASE WHEN hops_position = 1
6575 THEN format('m.hops, ($1::TEXT[])[m.vertex]::%s', coltype)
6576 ELSE format('($1::TEXT[])[m.vertex]::%s, m.hops', coltype) END)
6577 USING e.vertices, e.sources, e.destinations, e.tokens, e.probabilities,
6578 e.block_keys, e.block_indices, e.extra_ids, st, sp, directed,
6579 hop_bound, hop_seed;
6580 END IF;
6581 IF verbosity >= 20 THEN
6582 RAISE NOTICE 'ProvSQL: recursive CTE "%" compiled along a tree decomposition of %',
6583 work_name, coalesce(edge_rel::TEXT, 'the join-defined edge query');
6584 END IF;
6585 EXCEPTION WHEN OTHERS THEN
6586 IF verbosity >= 10 THEN
6587 RAISE NOTICE 'ProvSQL: reachability route for "%" fell back to the generic fixpoint (%)',
6588 work_name, SQLERRM;
6589 END IF;
6590 PERFORM provsql.eval_recursive(body_sql, work_name, colnames, coldef);
6591 END;
6592END
6593$$ LANGUAGE plpgsql;
6594
6595
6596
6597/** @} */
6598
6599/** @defgroup provenance_output Provenance output
6600 * Functions for visualizing and exporting provenance circuits
6601 * in various formats.
6602 * @{
6603 */
6604
6605/**
6606 * @brief Return a DOT or TEXT visualization of the provenance circuit
6607 *
6608 * @param token root provenance token
6609 * @param token2desc mapping table for gate descriptions
6610 * @param dbg debug level (0 = normal)
6611 */
6612CREATE OR REPLACE FUNCTION view_circuit(
6613 token UUID,
6614 token2desc REGCLASS,
6615 dbg INT = 0)
6616 RETURNS TEXT AS
6617 'provsql','view_circuit' LANGUAGE C;
6618
6619/**
6620 * @brief Return a DOT visualisation of the d-DNNF compiled from the
6621 * provenance circuit
6622 *
6623 * Runs the requested external knowledge compiler and renders the
6624 * resulting d-DNNF as a GraphViz digraph.
6625 *
6626 * @param token root provenance token
6627 * @param compiler external compiler or in-process meta-route to invoke;
6628 * empty (the default) picks the highest-preference available compiler
6629 */
6630CREATE OR REPLACE FUNCTION compile_to_ddnnf_dot(
6631 token UUID,
6632 compiler TEXT = '')
6633 RETURNS TEXT AS
6634 'provsql','compile_to_ddnnf_dot' LANGUAGE C;
6635
6636/**
6637 * @brief Return the compiled d-DNNF of a provenance circuit in the
6638 * c2d / d4 ".nnf" TEXT interchange format.
6639 *
6640 * Companion to compile_to_ddnnf_dot (DOT, for viewing): this is the
6641 * machine-readable form, suitable for feeding to an external d-DNNF
6642 * reasoner / verifier or saving next to tseytin_cnf (same variable
6643 * numbering). Accepts the same compiler / meta-route names.
6644 *
6645 * @param token root provenance token
6646 * @param compiler compiler or in-process meta-route to use; empty (the
6647 * default) picks the highest-preference available compiler
6648 */
6649CREATE OR REPLACE FUNCTION compile_to_ddnnf(
6650 token UUID,
6651 compiler TEXT = '')
6652 RETURNS TEXT AS
6653 'provsql','compile_to_ddnnf' LANGUAGE C;
6654
6655/**
6656 * @brief Structural statistics of the d-DNNF a compiler produces for a
6657 * provenance circuit.
6658 *
6659 * Compiles the circuit with the given compiler / meta-route (same names
6660 * as compile_to_ddnnf_dot: d4, d4v2, c2d, minic2d, dsharp, panini-*,
6661 * tree-decomposition, interpret-as-dd, default) and returns a jsonb
6662 * object: nodes, edges, and / or / not / inputs counts, smooth, depth
6663 * (longest path), treewidth (null when not computable), and compile_ms.
6664 * Lets clients compare what each compiler produces on the same circuit.
6665 *
6666 * @param token root provenance token
6667 * @param compiler compiler or in-process meta-route to use; empty (the
6668 * default) picks the highest-preference available compiler
6669 */
6670CREATE OR REPLACE FUNCTION ddnnf_stats(
6671 token UUID,
6672 compiler TEXT = '')
6673 RETURNS jsonb AS
6674 'provsql','ddnnf_stats' LANGUAGE C;
6675
6676/**
6677 * @brief Return the DIMACS CNF (Tseytin transformation) of the provenance circuit
6678 *
6679 * Returns the same encoding the extension writes to a temp file before
6680 * invoking d4 / c2d / minic2d / dsharp. With @c weighted true (the
6681 * default), per-input probability weights are appended as @c w lines.
6682 *
6683 * @param token root provenance token
6684 * @param weighted include probability weights when true
6685 * @param mapping prepend "c input <var> <UUID> <prob>" comment lines
6686 * documenting which provenance input each variable stands for
6687 */
6688CREATE OR REPLACE FUNCTION tseytin_cnf(
6689 token UUID,
6690 weighted BOOLEAN = TRUE,
6691 mapping BOOLEAN = TRUE)
6692 RETURNS TEXT AS
6693 'provsql','tseytin_cnf' LANGUAGE C;
6694
6695/**
6696 * @brief Map each DIMACS variable of tseytin_cnf back to its
6697 * provenance input.
6698 *
6699 * Returns one row per input gate: the variable index (matching
6700 * tseytin_cnf and compile_to_ddnnf's NNF), the original-circuit UUID
6701 * of that input, and its probability. Lets a satisfying assignment or
6702 * weighted model count obtained from an external tool be read against
6703 * the provenance circuit.
6704 *
6705 * @param token root provenance token
6706 */
6707CREATE OR REPLACE FUNCTION tseytin_cnf_mapping_json(token UUID)
6708 RETURNS jsonb AS
6709 'provsql','tseytin_cnf_mapping_json' LANGUAGE C;
6710
6711CREATE OR REPLACE FUNCTION tseytin_cnf_mapping(token UUID)
6712 RETURNS TABLE(variable INT, gate UUID, probability FLOAT8) AS $$
6713 SELECT variable, gate, probability
6714 FROM jsonb_to_recordset(tseytin_cnf_mapping_json(token))
6715 AS x(variable INT, gate UUID, probability FLOAT8)
6716 ORDER BY variable
6717$$ LANGUAGE SQL STABLE;
6718
6719/**
6720 * @brief Return a DOT visualisation of the tree decomposition of the
6721 * provenance circuit
6722 *
6723 * Computes the min-fill decomposition used by the in-process
6724 * knowledge compiler. The first line of the output is a comment of
6725 * the form @c "// treewidth=<n>".
6726 *
6727 * @param token root provenance token
6728 */
6729CREATE OR REPLACE FUNCTION tree_decomposition_dot(
6730 token UUID)
6731 RETURNS TEXT AS
6732 'provsql','tree_decomposition_dot' LANGUAGE C;
6733
6734/**
6735 * @brief Report whether an external tool is on the backend's resolved PATH
6736 *
6737 * Uses the same @c find_external_tool() helper that the compilers
6738 * (d4 / c2d / minic2d / dsharp / panini), model counters (ganak /
6739 * sharpsat-td / dpmc via htb+dmc / weightmc), and visualisation
6740 * wrappers (graph-easy, dot) themselves consult, so the result
6741 * reflects exactly what a subsequent @c probability_evaluate or
6742 * @c view_circuit call would see, including the
6743 * @c provsql.tool_search_path GUC prepended to @c $PATH.
6744 *
6745 * Names with a slash are treated as paths and tested directly via
6746 * @c access(X_OK); bare names are resolved through @c /bin/sh's
6747 * @c command -v under the backend's PATH.
6748 *
6749 * @param name bare executable (e.g. @c 'd4') or an absolute path
6750 * @return true iff the tool resolves to an executable file
6751 */
6752CREATE OR REPLACE FUNCTION tool_available(name TEXT)
6753 RETURNS BOOLEAN AS
6754 'provsql','tool_available' LANGUAGE C STRICT;
6755
6756/* ----------------------------------------------------------------------
6757 * External-tool registry
6758 *
6759 * A catalog of the external tools ProvSQL can invoke (the knowledge
6760 * compilers, weighted model counters, and the graph-easy DOT renderer).
6761 * The default tools and their invocations are compiled in (seeded in C), so
6762 * out-of-the-box behaviour is unchanged with no configuration.
6763 *
6764 * Administrators may add / repoint / reorder / disable tools at run time;
6765 * those changes are persisted in the @c provsql.tool_overrides table below
6766 * and overlaid on the compiled seed, so they survive across sessions and
6767 * backends (and dump/restore). An empty overrides table means exactly the
6768 * compiled defaults. The mutators are superuser-only because a tool RECORD
6769 * names an executable run as the PostgreSQL OS user (the same trust level as
6770 * provsql.tool_search_path).
6771 * ---------------------------------------------------------------------- */
6772
6773/**
6774 * @brief Persistent overrides overlaid on the compiled-in tool seed.
6775 *
6776 * Each row is the complete desired RECORD for a tool (added or modified) keyed
6777 * by logical @c name, or a tombstone (@c removed = true) hiding a seeded
6778 * default. The effective registry is the compiled seed with tombstoned names
6779 * removed and the remaining rows upserted over it. Written only by the
6780 * superuser-only register_tool / unregister_tool / set_tool_* functions;
6781 * read back into each backend's in-memory registry on demand. Marked as a
6782 * configuration table so pg_dump carries an operator's registrations.
6783 */
6784CREATE TABLE IF NOT EXISTS tool_overrides(
6785 name TEXT PRIMARY KEY,
6786 removed BOOLEAN NOT NULL DEFAULT false,
6787 kind TEXT,
6788 executable TEXT,
6789 operations TEXT[],
6790 input_formats TEXT[],
6791 output_format TEXT,
6792 parser TEXT,
6793 preference INT,
6794 enabled BOOLEAN,
6795 dependencies TEXT[],
6796 argtpl TEXT,
6797 argtpl_circuit TEXT,
6798 endpoint TEXT
6799);
6800SELECT pg_catalog.pg_extension_config_dump('tool_overrides', '');
6801
6802/**
6803 * @brief Set-returning listing backing the @c provsql.tools view.
6804 *
6805 * @c operations / @c input_formats / @c output_format use the KCMCP
6806 * shared-registry names (see the KCMCP server protocol), so a CLI RECORD and
6807 * a future kcmcp-server RECORD are comparable; @c parser is the CLI-only tag
6808 * for how to decode the tool's raw output. @c argtpl is the command template
6809 * ({in}/{out}/... placeholders). @c available is true iff @c executable
6810 * (when set) and every dependency currently resolve on the backend's PATH.
6811 */
6812CREATE OR REPLACE FUNCTION tool_registry_list()
6813 RETURNS TABLE(name TEXT, kind TEXT, executable TEXT, operations TEXT[],
6814 input_formats TEXT[], output_format TEXT, parser TEXT,
6815 preference INT, enabled BOOLEAN, argtpl TEXT,
6816 argtpl_circuit TEXT, endpoint TEXT, available BOOLEAN) AS
6817 'provsql','tool_registry_list' LANGUAGE C STABLE;
6818
6819/**
6820 * @brief Read-only view of the registered tools.
6821 */
6822CREATE OR REPLACE VIEW tools AS
6823 SELECT name, kind, executable, operations, input_formats, output_format,
6824 parser, preference, enabled, argtpl, argtpl_circuit, endpoint,
6825 available
6826 FROM tool_registry_list();
6827
6828/**
6829 * @brief Register a tool, or replace the RECORD with the same logical name.
6830 *
6831 * @param name logical id (e.g. @c 'd4-jm62300'); also the value
6832 * @c provsql.fallback_compiler / the wmc tool selector use
6833 * @param executable executable to resolve on PATH (defaults to @c name)
6834 * @param kind @c 'cli' (spawn @c executable) or @c 'kcmcp' (talk to
6835 * the KCMCP server at @c endpoint)
6836 * @param operations capabilities (KCMCP names): @c 'compile' / @c 'wmc'
6837 * (and ProvSQL-local @c 'render')
6838 * @param input_formats accepted inputs (KCMCP names): @c 'dimacs-cnf',
6839 * @c 'circuit-bcs12' (listing @c 'circuit-bcs12' enables
6840 * the native-circuit fast path)
6841 * @param output_format result encoding (KCMCP names): @c 'ddnnf-nnf',
6842 * @c 'decimal', @c 'rational', ... (local @c 'panini-dd'
6843 * / @c 'ascii' where KCMCP has no code)
6844 * @param parser CLI-only decode tag: @c 'nnf' (the tolerant d4 / c2d
6845 * NNF reader), @c 'panini-dd', @c 'wmc-line',
6846 * @c 'weightmc', @c 'ascii'
6847 * @param argtpl command template; placeholders @c {in} / @c {out}
6848 * (and @c {binary} / @c {tmpdir} / @c {pivotAC}). When
6849 * it omits @c {binary}, the executable is prepended.
6850 * @param argtpl_circuit command used when the @c 'circuit-bcs12' input is
6851 * selected (a BC-S1.2 circuit rather than a CNF); only a
6852 * tool accepting that input needs it
6853 * @param preference ordering within an operation (higher first)
6854 * @param enabled whether the dispatchers may select it
6855 * @param endpoint for a @c 'kcmcp' RECORD, the server address:
6856 * @c 'unix:/path' or @c 'host:port'
6857 *
6858 * Superuser-only: a CLI RECORD runs an arbitrary command as the PostgreSQL
6859 * OS user, and a kcmcp RECORD names a socket the server connects to.
6860 */
6861CREATE OR REPLACE FUNCTION register_tool(
6862 name TEXT,
6863 executable TEXT DEFAULT NULL,
6864 kind TEXT DEFAULT 'cli',
6865 operations TEXT[] DEFAULT NULL,
6866 input_formats TEXT[] DEFAULT NULL,
6867 output_format TEXT DEFAULT NULL,
6868 parser TEXT DEFAULT NULL,
6869 argtpl TEXT DEFAULT NULL,
6870 argtpl_circuit TEXT DEFAULT NULL,
6871 preference INT DEFAULT 0,
6872 enabled BOOLEAN DEFAULT true,
6873 endpoint TEXT DEFAULT NULL)
6874 RETURNS VOID AS
6875 'provsql','tool_registry_register' LANGUAGE C;
6876
6877/** @brief Unregister a tool; errors on an unknown tool name. Superuser-only. */
6878CREATE OR REPLACE FUNCTION unregister_tool(name TEXT)
6879 RETURNS VOID AS
6880 'provsql','tool_registry_unregister' LANGUAGE C STRICT;
6881
6882/** @brief Enable/disable a tool; errors on an unknown tool name. Superuser-only. */
6883CREATE OR REPLACE FUNCTION set_tool_enabled(name TEXT, enabled BOOLEAN)
6884 RETURNS VOID AS
6885 'provsql','tool_registry_set_enabled' LANGUAGE C STRICT;
6886
6887/** @brief Set a tool's preference; errors on an unknown tool name. Superuser-only. */
6888CREATE OR REPLACE FUNCTION set_tool_preference(name TEXT, preference INT)
6889 RETURNS VOID AS
6890 'provsql','tool_registry_set_preference' LANGUAGE C STRICT;
6891
6892-- The mutators guard at the C level too, but revoke from PUBLIC so the
6893-- superuser requirement is visible in the catalog.
6894REVOKE ALL ON FUNCTION register_tool(TEXT, TEXT, TEXT, TEXT[], TEXT[], TEXT, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT) FROM PUBLIC;
6895REVOKE ALL ON FUNCTION unregister_tool(TEXT) FROM PUBLIC;
6896REVOKE ALL ON FUNCTION set_tool_enabled(TEXT, BOOLEAN) FROM PUBLIC;
6897REVOKE ALL ON FUNCTION set_tool_preference(TEXT, INT) FROM PUBLIC;
6898
6899/**
6900 * @brief Return an XML representation of the provenance circuit
6901 *
6902 * @param token root provenance token
6903 * @param token2desc optional mapping table for gate descriptions
6904 */
6905CREATE OR REPLACE FUNCTION to_provxml(
6906 token UUID,
6907 token2desc REGCLASS = NULL)
6908 RETURNS TEXT AS
6909 'provsql','to_provxml' LANGUAGE C;
6910
6911/** @brief Return the provenance token of the current query result tuple */
6912CREATE OR REPLACE FUNCTION provenance() RETURNS UUID AS
6913 'provsql', 'provenance' LANGUAGE C;
6914
6915/**
6916 * @brief Compute where-provenance for a result tuple
6917 *
6918 * Returns a TEXT representation showing which input columns
6919 * contributed to each output column.
6920 */
6921CREATE OR REPLACE FUNCTION where_provenance(token UUID)
6922 RETURNS TEXT AS
6923 'provsql','where_provenance' LANGUAGE C;
6924
6925/** @} */
6926
6927/** @defgroup circuit_init Circuit initialization
6928 * Functions and statements executed at extension load time to
6929 * reset internal caches and create the constant zero/one gates.
6930 * @{
6931 */
6932
6933/** @brief Reset the internal cache of OID constants used by the query rewriter */
6934CREATE OR REPLACE FUNCTION reset_constants_cache()
6935 RETURNS VOID AS
6936 'provsql', 'reset_constants_cache' LANGUAGE C;
6937
6938SELECT reset_constants_cache();
6939
6940SELECT create_gate(gate_zero(), 'zero');
6941SELECT create_gate(gate_one(), 'one');
6942
6943/** @} */
6944
6945/** @brief Types of update operations tracked for temporal provenance */
6946CREATE TYPE QUERY_TYPE_ENUM AS ENUM (
6947 'INSERT', -- Row was inserted
6948 'DELETE', -- Row was deleted
6949 'UPDATE', -- Row was updated
6950 'UNDO' -- Previous operation was undone
6951 );
6952
6953/** @defgroup compiled_semirings Compiled semirings
6954 * Definitions of compiled semirings
6955 * @{
6956 */
6957
6958/** @brief Evaluate provenance as a symbolic formula (e.g., "a ⊗ b ⊕ c") */
6959CREATE FUNCTION sr_formula(token ANYELEMENT, token2value REGCLASS)
6960 RETURNS VARCHAR AS
6961$$
6962BEGIN
6963 RETURN provsql.provenance_evaluate_compiled(
6964 token,
6965 token2value,
6966 'formula',
6967 '𝟙'::VARCHAR
6968 );
6969END
6970$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
6971
6972/** @brief Evaluate provenance over the counting semiring (ℕ) */
6973CREATE FUNCTION sr_counting(token ANYELEMENT, token2value REGCLASS)
6974 RETURNS INT AS
6975$$
6976BEGIN
6977 RETURN provsql.provenance_evaluate_compiled(
6978 token,
6979 token2value,
6980 'counting',
6981 1
6982 );
6983END
6984$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
6985
6986/** @brief Evaluate provenance as why-provenance (set of witness sets) */
6987CREATE FUNCTION sr_why(token ANYELEMENT, token2value REGCLASS)
6988 RETURNS VARCHAR AS
6989$$
6990BEGIN
6991 RETURN provsql.provenance_evaluate_compiled(
6992 token,
6993 token2value,
6994 'why',
6995 '{}'::VARCHAR
6996 );
6997END
6998$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
6999
7000/** @brief Evaluate provenance as how-provenance (canonical polynomial provenance ℕ[X], universal commutative-semiring provenance) */
7001CREATE FUNCTION sr_how(token ANYELEMENT, token2value REGCLASS)
7002 RETURNS VARCHAR AS
7003$$
7004BEGIN
7005 RETURN provsql.provenance_evaluate_compiled(
7006 token,
7007 token2value,
7008 'how',
7009 '{}'::VARCHAR
7010 );
7011END
7012$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7013
7014/** @brief Evaluate provenance as which-provenance (lineage: a single set of contributing labels) */
7015CREATE FUNCTION sr_which(token ANYELEMENT, token2value REGCLASS)
7016 RETURNS VARCHAR AS
7017$$
7018BEGIN
7019 RETURN provsql.provenance_evaluate_compiled(
7020 token,
7021 token2value,
7022 'which',
7023 '{}'::VARCHAR
7024 );
7025END
7026$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7027
7028/** @brief Evaluate provenance as a Boolean expression
7029 *
7030 * The optional @p token2value mapping labels the leaves of the
7031 * formula: when omitted, leaves are rendered as bare @c x@<id@>
7032 * placeholders.
7033 */
7034CREATE FUNCTION sr_boolexpr(token ANYELEMENT, token2value REGCLASS = NULL)
7035 RETURNS VARCHAR AS
7036$$
7037BEGIN
7038 IF token IS NULL THEN
7039 RETURN NULL;
7040 END IF;
7041 RETURN provsql.provenance_evaluate_compiled(
7042 token,
7043 token2value,
7044 'boolexpr',
7045 '⊤'::VARCHAR
7046 );
7047END
7048$$ LANGUAGE plpgsql PARALLEL SAFE STABLE;
7049
7050/** @brief Evaluate provenance over the Boolean semiring (true/false) */
7051CREATE FUNCTION sr_boolean(token ANYELEMENT, token2value REGCLASS)
7052 RETURNS BOOLEAN AS
7053$$
7054BEGIN
7055 RETURN provsql.provenance_evaluate_compiled(
7056 token,
7057 token2value,
7058 'BOOLEAN',
7059 TRUE
7060 );
7061END
7062$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7063
7064/** @brief Evaluate provenance over the tropical (min-plus) m-semiring
7065 *
7066 * Inputs are read as %float8 cost values; the additive identity
7067 * is <tt>'Infinity'::%float8</tt> and the multiplicative identity is 0.
7068 * Returns the cost of the cheapest derivation.
7069 *
7070 * With @p nonnegative, input costs are checked nonnegative and the
7071 * semiring is *absorptive*: evaluation then also accepts circuits
7072 * carrying the @c 'absorptive' assumption marker -- notably cyclic
7073 * recursive queries truncated at the absorptive value fixpoint, giving
7074 * exact min-cost reachability on cyclic data.
7075 */
7076CREATE FUNCTION sr_tropical(token ANYELEMENT, token2value REGCLASS,
7077 nonnegative BOOLEAN = false)
7078 RETURNS FLOAT AS
7079$$
7080BEGIN
7081 RETURN provsql.provenance_evaluate_compiled(
7082 token,
7083 token2value,
7084 CASE WHEN nonnegative THEN 'tropical_nonneg' ELSE 'tropical' END,
7085 0::FLOAT
7086 );
7087END
7088$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7089
7090/** @brief Evaluate provenance over the Viterbi (max-times) m-semiring
7091 *
7092 * Inputs are read as %float8 probability values in @f$[0,1]@f$.
7093 * Returns the probability of the most likely derivation.
7094 */
7095CREATE FUNCTION sr_viterbi(token ANYELEMENT, token2value REGCLASS)
7096 RETURNS FLOAT AS
7097$$
7098BEGIN
7099 RETURN provsql.provenance_evaluate_compiled(
7100 token,
7101 token2value,
7102 'viterbi',
7103 1::FLOAT
7104 );
7105END
7106$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7107
7108/** @brief Evaluate provenance over the Łukasiewicz fuzzy m-semiring
7109 *
7110 * Inputs are read as %float8 graded-truth values in @f$[0,1]@f$.
7111 * Addition is @f$\max@f$; multiplication is the Łukasiewicz t-norm
7112 * @f$\max(a + b - 1, 0)@f$, which preserves crisp truth and avoids
7113 * the near-zero collapse of long product chains.
7114 */
7115CREATE FUNCTION sr_lukasiewicz(token ANYELEMENT, token2value REGCLASS)
7116 RETURNS FLOAT AS
7117$$
7118BEGIN
7119 RETURN provsql.provenance_evaluate_compiled(
7120 token,
7121 token2value,
7122 'lukasiewicz',
7123 1::FLOAT
7124 );
7125END
7126$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7127
7128/** @brief Evaluate provenance over the min-max m-semiring on a user ENUM
7129 *
7130 * Inputs are read as values of a user-defined ENUM carrier; addition
7131 * is ENUM-min, multiplication is ENUM-max. Bottom and top of the ENUM
7132 * are derived from @c pg_enum.enumsortorder. The third argument is a
7133 * sample value of the carrier ENUM, used only for type inference; its
7134 * value is ignored.
7135 *
7136 * The security shape: alternative derivations combine to the least
7137 * sensitive label, joins combine to the most sensitive label.
7138 *
7139 * @param token Provenance token to evaluate.
7140 * @param token2value Mapping from input gates to ENUM values.
7141 * @param element_one Sample value of the carrier ENUM (any value works).
7142 */
7143CREATE FUNCTION sr_minmax(token UUID, token2value REGCLASS, element_one ANYENUM)
7144 RETURNS ANYENUM AS
7145$$
7146BEGIN
7147 RETURN provsql.provenance_evaluate_compiled(
7148 token,
7149 token2value,
7150 'minmax',
7151 element_one
7152 );
7153END
7154$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7155
7156/** @brief Evaluate provenance over the max-min m-semiring on a user ENUM
7157 *
7158 * Dual of :sqlfunc:`sr_minmax`: addition is ENUM-max, multiplication
7159 * is ENUM-min. The fuzzy / availability / trust shape: alternatives
7160 * combine to the most permissive label, joins combine to the strictest
7161 * label. The third argument is a sample value of the carrier ENUM,
7162 * used only for type inference; its value is ignored.
7163 *
7164 * @param token Provenance token to evaluate.
7165 * @param token2value Mapping from input gates to ENUM values.
7166 * @param element_one Sample value of the carrier ENUM (any value works).
7167 */
7168CREATE FUNCTION sr_maxmin(token UUID, token2value REGCLASS, element_one ANYENUM)
7169 RETURNS ANYENUM AS
7170$$
7171BEGIN
7172 RETURN provsql.provenance_evaluate_compiled(
7173 token,
7174 token2value,
7175 'maxmin',
7176 element_one
7177 );
7178END
7179$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7180
7181/** @} */
7182
7183/** @defgroup choose_aggregate choose aggregate
7184 * Choose one value among many, used in particular to code a mutually
7185 * exclusive choice as an aggregate.
7186 * @{
7187 */
7188
7189/** @brief Transition function for the choose aggregate (keeps first non-NULL value) */
7190CREATE FUNCTION choose_function(state ANYELEMENT, data ANYELEMENT)
7191 RETURNS ANYELEMENT AS
7192$$
7193BEGIN
7194 IF state IS NULL THEN
7195 RETURN data;
7196 ELSE
7197 RETURN state;
7198 END IF;
7199END
7200$$ LANGUAGE plpgsql PARALLEL SAFE IMMUTABLE;
7201
7202/** @brief Aggregate that returns an arbitrary non-NULL value from a group */
7203CREATE AGGREGATE choose(ANYELEMENT) (
7204 SFUNC = choose_function,
7205 STYPE = ANYELEMENT
7206);
7207
7208/** @brief Explodes a table column containing aggregated provenance into multiple rows.
7209 *
7210 * For each row in the input table, this function unnests the children of the
7211 * specified aggregate token column and produces one output row per child.
7212 * It reconstructs the corresponding value and provenance (`provsql`) for
7213 * each resulting row.
7214 *
7215 * The original table is replaced by the transformed table.
7216 *
7217 * @param _tbl Name of the table to transform.
7218 * @param AGG_TOKEN Name of the column containing the aggregate to explode.
7219 */
7220CREATE OR REPLACE FUNCTION explode_table(_tbl TEXT, AGG_TOKEN TEXT)
7221RETURNS VOID AS $$
7222DECLARE
7223 _nsp TEXT;
7224BEGIN
7225 -- Resolve the schema actually holding _tbl so the rebuilt table is
7226 -- recreated in place (the provsql helper functions are schema-qualified
7227 -- so this works whatever the caller's search_path is).
7228 SELECT n.nspname INTO _nsp
7229 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
7230 WHERE c.oid = _tbl::REGCLASS;
7231
7232 EXECUTE format('
7233 CREATE TABLE %1$I.temp_exploded AS
7234 SELECT
7235 %2$I.*,
7236 provsql.get_extra(children[2]) AS new_t,
7237 provsql.provenance_times(children[1], provsql) AS new_provsql
7238 FROM %1$I.%2$I,
7239 LATERAL (
7240 SELECT provsql.get_children(sm) AS children
7241 FROM UNNEST(provsql.get_children(%3$I)) AS sm
7242 ) AS sub', _nsp, _tbl, AGG_TOKEN);
7243 EXECUTE format('DROP TABLE %I.%I', _nsp, _tbl);
7244 EXECUTE format('ALTER TABLE %I.temp_exploded DROP COLUMN %I, DROP COLUMN provsql', _nsp, AGG_TOKEN);
7245 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_t TO %I', _nsp, AGG_TOKEN);
7246 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME COLUMN new_provsql TO provsql', _nsp);
7247 EXECUTE format('ALTER TABLE %I.temp_exploded RENAME TO %I', _nsp, _tbl);
7248END;
7249$$ LANGUAGE plpgsql;
7250
7251/** @} */
7252
7253/**
7254 * @brief Append @c provsql to this database's default search_path, if missing.
7255 *
7256 * ProvSQL's operators and functions live in the @c provsql schema and
7257 * are resolved through @c search_path. When @c provsql is absent from
7258 * the path some surfaces fail with a clear error (RV/AGG_TOKEN
7259 * arithmetic), but others can be silently misrouted by an implicit
7260 * cross-domain cast. This helper makes the common case painless: it
7261 * reads the current <em>database-level</em> search_path setting from
7262 * @c pg_db_role_setting, appends @c provsql if not already present
7263 * (never replacing or reordering the existing entries), and applies the
7264 * result with @c ALTER @c DATABASE. It is idempotent and emits a
7265 * @c NOTICE describing what it did.
7266 *
7267 * Only @b new sessions pick up the change; the calling session keeps its
7268 * current path. Role-level settings (if any) take precedence over the
7269 * database-level setting and are left untouched. The caller must be the
7270 * database owner or a superuser (the privilege model of @c ALTER
7271 * @c DATABASE). Returns the resulting search_path value.
7272 */
7273CREATE OR REPLACE FUNCTION setup_search_path()
7274 RETURNS TEXT
7275 LANGUAGE plpgsql AS $$
7276DECLARE
7277 db TEXT := current_database();
7278 cfg TEXT[];
7279 cur TEXT; -- existing database-level search_path value
7280 new_path TEXT;
7281BEGIN
7282 -- setrole = 0 selects the database-wide default, not a per-role override.
7283 SELECT s.setconfig INTO cfg
7284 FROM pg_db_role_setting s
7285 JOIN pg_database d ON d.oid = s.setdatabase
7286 WHERE d.datname = db AND s.setrole = 0;
7287
7288 IF cfg IS NOT NULL THEN
7289 SELECT substr(e, length('search_path=') + 1) INTO cur
7290 FROM unnest(cfg) AS e
7291 WHERE e LIKE 'search_path=%';
7292 END IF;
7293
7294 IF cur IS NULL THEN
7295 -- No database-level search_path at all: install the documented
7296 -- default with provsql appended.
7297 new_path := '"$user", public, provsql';
7298 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
7299 RAISE NOTICE 'ProvSQL: set search_path = % for database "%" (no previous database-level setting). Only new sessions are affected.',
7300 new_path, db;
7301 RETURN new_path;
7302 END IF;
7303
7304 -- Already contains provsql as a path element? Idempotent no-op.
7305 IF EXISTS (
7306 SELECT 1 FROM unnest(string_to[](cur, ',')) AS p
7307 WHERE btrim(btrim(p), '"') = 'provsql')
7308 THEN
7309 RAISE NOTICE 'ProvSQL: search_path for database "%" already contains provsql (= %); no change.',
7310 db, cur;
7311 RETURN cur;
7312 END IF;
7313
7314 new_path := cur || ', provsql';
7315 EXECUTE format('ALTER DATABASE %I SET search_path = %s', db, new_path);
7316 RAISE NOTICE 'ProvSQL: appended provsql to search_path for database "%" (now: %). Only new sessions are affected.',
7317 db, new_path;
7318 RETURN new_path;
7319END;
7320$$;
7321
7322GRANT USAGE ON SCHEMA provsql TO PUBLIC;
7323
7324SET search_path TO public;
7325
7326-- Installation-time advisory: if provsql is not in the database's default
7327-- search_path, point the user at setup_search_path(). reset_val reflects
7328-- the configured session default (postgresql.conf / ALTER DATABASE / ALTER
7329-- ROLE), unaffected by the SET search_path statements this script ran.
7330-- CREATE EXTENSION raises client_min_messages to WARNING for the duration
7331-- of the script, so we lower it around the RAISE NOTICE. SET LOCAL only:
7332-- it unwinds by itself when CREATE EXTENSION's transaction ends. An
7333-- explicit save/restore here would capture the WARNING clamp (already in
7334-- force when this block runs) and restore *that* at session level,
7335-- leaving the whole installing session with NOTICEs suppressed.
7336DO $$
7337DECLARE
7338 rp TEXT;
7339 has_provsql BOOLEAN;
7340BEGIN
7341 SELECT reset_val INTO rp FROM pg_settings WHERE name = 'search_path';
7342 SELECT bool_or(btrim(btrim(p), '"') = 'provsql')
7343 INTO has_provsql
7344 FROM unnest(string_to[](coalesce(rp, ''), ',')) AS p;
7345 IF NOT coalesce(has_provsql, false) THEN
7346 SET LOCAL client_min_messages = notice;
7347 RAISE NOTICE 'ProvSQL: schema "provsql" is not in your default search_path (currently: %).', rp;
7348 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());
7349 END IF;
7350END;
7351$$;
7352
7353-- Final constants-cache refresh. The planned SELECT statements earlier in
7354-- this script (reset_constants_cache itself, the zero/one create_gate calls)
7355-- make the installing session memoize the OID constants *mid-script*, while
7356-- objects defined later (notably the choose aggregate, used by the
7357-- scalar-subquery decorrelation) do not exist yet. Their optional lookups
7358-- then stay InvalidOid for the rest of the session, silently disabling the
7359-- corresponding rewrites (e.g. IN/NOT IN over a tracked relation would raise
7360-- "Subqueries ... not supported") until a new connection. Refreshing here,
7361-- after every object exists, repairs the installing session's cache.
7362SELECT provsql.reset_constants_cache();
7363SET search_path TO provsql;
7364
7365/** @defgroup update_provenance Update provenance (PostgreSQL 14+)
7366 * Extended provenance tracking for INSERT, UPDATE, DELETE, and UNDO
7367 * operations, including temporal validity ranges.
7368 * @{
7369 */
7370
7371/**
7372 * @brief Table recording the history of INSERT, UPDATE, DELETE, and UNDO operations
7373 *
7374 * Each row records one provenance-tracked modification, linking the
7375 * operation's provenance token to metadata (query TEXT, type, user,
7376 * TIMESTAMP) and the temporal validity range of the affected rows.
7377 */
7378CREATE TABLE update_provenance (
7379 provsql UUID,
7380 query TEXT,
7381 query_type QUERY_TYPE_ENUM,
7382 username TEXT,
7383 ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
7384 valid_time TSTZMULTIRANGE DEFAULT TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL))
7385);
7386
7387/** @cond INTERNAL */
7388/* Enable provenance tracking on an existing table (PostgreSQL 14+ version).
7389 * Overrides the common version; documented via add_provenance in provsql.common.sql. */
7390CREATE OR REPLACE FUNCTION add_provenance(_tbl REGCLASS)
7391 RETURNS VOID AS
7392$$
7393BEGIN
7394 -- Idempotence: a second add_provenance on an already-tracked table is
7395 -- a no-op with a NOTICE, so setup scripts and notebook cells can be
7396 -- re-run freely.
7397 IF EXISTS (
7398 SELECT 1 FROM pg_attribute
7399 WHERE attrelid = _tbl AND attname = 'provsql' AND NOT attisdropped
7400 ) THEN
7401 RAISE NOTICE 'table % already has provenance tracking', _tbl;
7402 RETURN;
7403 END IF;
7404 -- See the common-version body for the rationale of dropping the
7405 -- column DEFAULT and UNIQUE in favour of provenance_guard + a
7406 -- plain index.
7407 EXECUTE format('ALTER TABLE %s ADD COLUMN provsql UUID', _tbl);
7408 EXECUTE format(
7409 'UPDATE %s SET provsql = public.uuid_generate_v4() WHERE provsql IS NULL',
7410 _tbl);
7411 EXECUTE format('CREATE INDEX ON %s(provsql)', _tbl);
7412 EXECUTE format(
7413 'CREATE TRIGGER provenance_guard BEFORE INSERT OR UPDATE OF provsql '
7414 'ON %s FOR EACH ROW EXECUTE PROCEDURE provsql.provenance_guard()',
7415 _tbl);
7416
7417 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);
7418 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);
7419 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);
7420
7421 PERFORM provsql.set_table_info(_tbl::oid, 'tid');
7422 PERFORM provsql.set_ancestors(_tbl::oid, ARRAY[_tbl::oid]);
7423END
7424$$ LANGUAGE plpgsql SECURITY DEFINER;
7425/** @endcond */
7426
7427/** @cond INTERNAL */
7428/* Trigger function for DELETE statement provenance tracking (PostgreSQL 14+).
7429 * Overrides the common version; documented via delete_statement_trigger in provsql.common.sql. */
7430CREATE OR REPLACE FUNCTION delete_statement_trigger()
7431 RETURNS TRIGGER AS
7432$$
7433DECLARE
7434 query_text TEXT;
7435 delete_token UUID;
7436 old_token UUID;
7437 new_token UUID;
7438 r RECORD;
7439 enable_trigger BOOL;
7440BEGIN
7441 enable_trigger := current_setting('provsql.update_provenance', true);
7442 IF enable_trigger = 'f' THEN
7443 RETURN NULL;
7444 END IF;
7445 delete_token := public.uuid_generate_v4();
7446
7447 PERFORM create_gate(delete_token, 'update');
7448
7449 SELECT query
7450 INTO query_text
7451 FROM pg_stat_activity
7452 WHERE pid = pg_backend_pid();
7453
7454 INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time)
7455 VALUES (delete_token, query_text, 'DELETE', current_user, CURRENT_TIMESTAMP, TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)));
7456
7457 PERFORM set_config('provsql.update_provenance', 'off', false);
7458 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
7459 PERFORM set_config('provsql.update_provenance', 'on', false);
7460
7461 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
7462 old_token := r.provsql;
7463 new_token := provenance_monus(old_token, delete_token);
7464
7465 PERFORM set_config('provsql.update_provenance', 'off', false);
7466 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
7467 USING new_token, old_token;
7468 PERFORM set_config('provsql.update_provenance', 'on', false);
7469 END LOOP;
7470
7471 RETURN NULL;
7472END
7473$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
7474/** @endcond */
7475
7476/**
7477 * @brief Trigger function for INSERT statement provenance tracking
7478 *
7479 * Records the insertion in update_provenance and multiplies provenance
7480 * tokens of inserted rows with the insert token.
7481 */
7482CREATE OR REPLACE FUNCTION insert_statement_trigger()
7483 RETURNS TRIGGER AS
7484$$
7485DECLARE
7486 query_text TEXT;
7487 insert_token UUID;
7488 old_token UUID;
7489 new_token UUID;
7490 r RECORD;
7491 enable_trigger BOOL;
7492BEGIN
7493 enable_trigger := current_setting('provsql.update_provenance', true);
7494 IF enable_trigger = 'f' THEN
7495 RETURN NULL;
7496 END IF;
7497
7498 insert_token := public.uuid_generate_v4();
7499
7500 PERFORM create_gate(insert_token, 'update');
7501
7502 SELECT query
7503 INTO query_text
7504 FROM pg_stat_activity
7505 WHERE pid = pg_backend_pid();
7506
7507 INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time)
7508 VALUES (insert_token, query_text, 'INSERT', current_user, CURRENT_TIMESTAMP, TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)));
7509
7510 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
7511 old_token := r.provsql;
7512 new_token := provenance_times(old_token, insert_token);
7513 PERFORM set_config('provsql.update_provenance', 'off', false);
7514 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
7515 USING new_token, old_token;
7516 PERFORM set_config('provsql.update_provenance', 'on', false);
7517 END LOOP;
7518
7519 RETURN NULL;
7520END
7521$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
7522
7523/**
7524 * @brief Trigger function for UPDATE statement provenance tracking
7525 *
7526 * Records the update in update_provenance. Multiplies new-row tokens
7527 * with the update token and applies monus to old-row tokens.
7528 */
7529CREATE OR REPLACE FUNCTION update_statement_trigger()
7530 RETURNS TRIGGER AS
7531$$
7532DECLARE
7533 query_text TEXT;
7534 update_token UUID;
7535 old_token UUID;
7536 new_token UUID;
7537 r RECORD;
7538 enable_trigger BOOL;
7539BEGIN
7540 enable_trigger := current_setting('provsql.update_provenance', true);
7541 IF enable_trigger = 'f' THEN
7542 RETURN NULL;
7543 END IF;
7544 update_token := public.uuid_generate_v4();
7545
7546 PERFORM create_gate(update_token, 'update');
7547
7548 SELECT query
7549 INTO query_text
7550 FROM pg_stat_activity
7551 WHERE pid = pg_backend_pid();
7552
7553 INSERT INTO update_provenance (provsql, query, query_type, username, ts, valid_time)
7554 VALUES (update_token, query_text, 'UPDATE', current_user, CURRENT_TIMESTAMP, TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL)));
7555
7556 FOR r IN (SELECT * FROM NEW_TABLE) LOOP
7557 old_token := r.provsql;
7558 new_token := provenance_times(old_token, update_token);
7559
7560 PERFORM set_config('provsql.update_provenance', 'off', false);
7561 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
7562 USING new_token, old_token;
7563 PERFORM set_config('provsql.update_provenance', 'on', false);
7564 END LOOP;
7565
7566 PERFORM set_config('provsql.update_provenance', 'off', false);
7567 EXECUTE format('INSERT INTO %I.%I SELECT * FROM OLD_TABLE;', TG_TABLE_SCHEMA, TG_TABLE_NAME);
7568 PERFORM set_config('provsql.update_provenance', 'on', false);
7569
7570 FOR r IN (SELECT * FROM OLD_TABLE) LOOP
7571 old_token := r.provsql;
7572 new_token := provenance_monus(old_token, update_token);
7573
7574 PERFORM set_config('provsql.update_provenance', 'off', false);
7575 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2;', TG_TABLE_SCHEMA, TG_TABLE_NAME)
7576 USING new_token, old_token;
7577 PERFORM set_config('provsql.update_provenance', 'on', false);
7578 END LOOP;
7579
7580 RETURN NULL;
7581END
7582$$ LANGUAGE plpgsql SET search_path=provsql,pg_temp SECURITY DEFINER;
7583
7584
7585/** @} */
7586
7587/** @defgroup temporal_db Temporal DB (PostgreSQL 14+)
7588 * Functions for temporal database support. These use provenance
7589 * evaluation over the multirange semiring to track temporal validity
7590 * of tuples.
7591 * @{
7592 */
7593
7594SET search_path TO provsql;
7595
7596/**
7597 * @brief Evaluate provenance over the temporal (interval-union) m-semiring
7598 *
7599 * Inputs are read as %TSTZMULTIRANGE validity intervals; the additive
7600 * identity is <tt>'{}'::%TSTZMULTIRANGE</tt> (empty), the multiplicative
7601 * identity is <tt>'{(,)}'::%TSTZMULTIRANGE</tt> (universal). Returns the union
7602 * of intervals supporting the result, computed via the compiled circuit
7603 * traversal.
7604 *
7605 * @param token Provenance token to evaluate.
7606 * @param token2value Mapping from input gates to validity multiranges.
7607 */
7608CREATE FUNCTION sr_temporal(token ANYELEMENT, token2value REGCLASS)
7609 RETURNS TSTZMULTIRANGE AS
7610$$
7611BEGIN
7612 RETURN provsql.provenance_evaluate_compiled(
7613 token,
7614 token2value,
7615 'interval_union',
7616 '{(,)}'::TSTZMULTIRANGE
7617 );
7618END
7619$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7620
7621/**
7622 * @brief Evaluate provenance over the interval-union m-semiring
7623 * with a NUMERIC multirange carrier
7624 *
7625 * Inputs are read as %nummultirange validity ranges over a NUMERIC
7626 * domain (e.g. sensor measurement-validity ranges). Addition is
7627 * multirange union, multiplication is intersection, monus is set
7628 * difference; the additive identity is <tt>'{}'::%nummultirange</tt>
7629 * and the multiplicative identity is <tt>'{(,)}'::%nummultirange</tt>
7630 * (universal range).
7631 *
7632 * @param token Provenance token to evaluate.
7633 * @param token2value Mapping from input gates to NUMERIC multiranges.
7634 */
7635CREATE FUNCTION sr_interval_num(token ANYELEMENT, token2value REGCLASS)
7636 RETURNS nummultirange AS
7637$$
7638BEGIN
7639 RETURN provsql.provenance_evaluate_compiled(
7640 token,
7641 token2value,
7642 'interval_union',
7643 '{(,)}'::nummultirange
7644 );
7645END
7646$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7647
7648/**
7649 * @brief Evaluate provenance over the interval-union m-semiring
7650 * with an int4 multirange carrier
7651 *
7652 * Inputs are read as %int4multirange validity ranges over the
7653 * integers (e.g. page or line ranges of supporting documents).
7654 * Addition is multirange union, multiplication is intersection,
7655 * monus is set difference; the additive identity is
7656 * <tt>'{}'::%int4multirange</tt> and the multiplicative identity is
7657 * <tt>'{(,)}'::%int4multirange</tt>.
7658 *
7659 * @param token Provenance token to evaluate.
7660 * @param token2value Mapping from input gates to int4 multiranges.
7661 */
7662CREATE FUNCTION sr_interval_int(token ANYELEMENT, token2value REGCLASS)
7663 RETURNS int4multirange AS
7664$$
7665BEGIN
7666 RETURN provsql.provenance_evaluate_compiled(
7667 token,
7668 token2value,
7669 'interval_union',
7670 '{(,)}'::int4multirange
7671 );
7672END
7673$$ LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE;
7674
7675/**
7676 * @brief Evaluate temporal provenance as a TIMESTAMP multirange
7677 *
7678 * Thin wrapper around :sqlfunc:`sr_temporal` retained for backward
7679 * compatibility; both compute the same union of validity intervals.
7680 *
7681 * @param token provenance token to evaluate
7682 * @param token2value mapping table from tokens to temporal validity ranges
7683 */
7684CREATE OR REPLACE FUNCTION union_tstzintervals(
7685 token UUID,
7686 token2value REGCLASS
7687)
7688RETURNS TSTZMULTIRANGE AS
7689$$
7690 SELECT sr_temporal(token, token2value)
7691$$ LANGUAGE SQL PARALLEL SAFE STABLE;
7692
7693/**
7694 * @brief Query a table as it was at a specific point in time
7695 *
7696 * Returns all rows whose temporal validity includes the given TIMESTAMP.
7697 *
7698 * @param tablename name of the provenance-tracked table
7699 * @param at_time the point in time to query
7700 */
7701CREATE OR REPLACE FUNCTION timetravel(
7702 tablename TEXT,
7703 at_time TIMESTAMPTZ
7704)
7705RETURNS SETOF RECORD
7706LANGUAGE plpgsql
7707AS
7708$$
7709BEGIN
7710 RETURN QUERY EXECUTE format(
7711 '
7712 SELECT
7713 %1$I.*,
7714 sr_temporal(provenance(), %2$L)
7715 FROM
7716 %1$I
7717 WHERE
7718 sr_temporal(provenance(), %2$L) @> %3$L::TIMESTAMPTZ
7719 ',
7720 tablename,
7721 'provsql.time_validity_view',
7722 at_time::TEXT
7723 );
7724END;
7725$$;
7726
7727/**
7728 * @brief Query a table for rows valid during a time interval
7729 *
7730 * Returns all rows whose temporal validity overlaps the given range.
7731 *
7732 * @param tablename name of the provenance-tracked table
7733 * @param from_time start of the time interval
7734 * @param to_time end of the time interval
7735 */
7736CREATE OR REPLACE FUNCTION timeslice(
7737 tablename TEXT,
7738 from_time TIMESTAMPTZ,
7739 to_time TIMESTAMPTZ
7740)
7741RETURNS SETOF RECORD
7742LANGUAGE plpgsql
7743AS
7744$$
7745BEGIN
7746 RETURN QUERY EXECUTE format(
7747 '
7748 SELECT
7749 %1$I.*,
7750 sr_temporal(provenance(), %2$L)
7751 FROM
7752 %1$I
7753 WHERE
7754 sr_temporal(provenance(), %2$L)
7755 && tstzrange(%3$L::TIMESTAMPTZ, %4$L::TIMESTAMPTZ)
7756 ',
7757 tablename,
7758 'provsql.time_validity_view',
7759 from_time::TEXT,
7760 to_time::TEXT
7761 );
7762END;
7763$$;
7764
7765/**
7766 * @brief Query the full temporal history of specific rows
7767 *
7768 * Returns all versions of rows matching the given column values,
7769 * with their temporal validity ranges.
7770 *
7771 * @param tablename name of the provenance-tracked table
7772 * @param col_names array of column names to filter on
7773 * @param col_values array of corresponding values to match
7774 */
7775CREATE OR REPLACE FUNCTION history(
7776 tablename TEXT,
7777 col_names TEXT[],
7778 col_values TEXT[]
7779)
7780RETURNS SETOF RECORD
7781LANGUAGE plpgsql
7782AS
7783$$
7784DECLARE
7785 condition TEXT := '';
7786 i INT;
7787BEGIN
7788 IF array_length(col_names, 1) IS NULL
7789 OR array_length(col_values, 1) IS NULL
7790 OR array_length(col_names, 1) != array_length(col_values, 1)
7791 THEN
7792 RAISE EXCEPTION 'col_names and col_values must have the same (non-null) length';
7793 END IF;
7794
7795 FOR i IN 1..array_length(col_names, 1)
7796 LOOP
7797 IF i > 1 THEN
7798 condition := condition || ' AND ';
7799 END IF;
7800 condition := condition || format('%I = %L', col_names[i], col_values[i]);
7801 END LOOP;
7802
7803 RETURN QUERY EXECUTE format(
7804 '
7805 SELECT
7806 %I.*,
7807 sr_temporal(provenance(), %L)
7808 FROM
7809 %I
7810 WHERE
7811 %s
7812 ',
7813 tablename,
7814 'provsql.time_validity_view',
7815 tablename,
7816 condition
7817 );
7818END;
7819$$;
7820
7821/**
7822 * @brief Get the valid time range for a specific tuple
7823 *
7824 * @param token provenance token of the tuple
7825 * @param tablename name of the table containing the tuple
7826 */
7827CREATE OR REPLACE FUNCTION get_valid_time(
7828 token UUID,
7829 tablename TEXT
7830)
7831RETURNS TSTZMULTIRANGE
7832LANGUAGE plpgsql
7833AS $$
7834DECLARE
7835 result TSTZMULTIRANGE;
7836BEGIN
7837 EXECUTE format(
7838 '
7839 SELECT
7840 sr_temporal(provenance(), %L)
7841 FROM
7842 %I
7843 WHERE
7844 provsql = %L
7845 ',
7846 'provsql.time_validity_view',
7847 tablename,
7848 token
7849 )
7850 INTO result;
7851
7852 RETURN result;
7853END;
7854$$;
7855
7856/**
7857 * @brief Undo a previously recorded update operation
7858 *
7859 * Traverses all provenance-tracked tables and rewrites their circuits
7860 * to apply monus with respect to the given update token, effectively
7861 * undoing the operation.
7862 *
7863 * @param c UUID of the update operation to undo (from update_provenance)
7864 */
7865CREATE OR REPLACE FUNCTION undo(
7866 c UUID
7867)
7868RETURNS UUID
7869LANGUAGE plpgsql
7870AS $$
7871DECLARE
7872 undo_query TEXT;
7873 undone_query TEXT;
7874 undo_token UUID;
7875 schema_rec RECORD;
7876 table_rec RECORD;
7877 row_rec RECORD;
7878 new_x UUID;
7879BEGIN
7880 SELECT query INTO undone_query
7881 FROM update_provenance
7882 WHERE provsql = c
7883 LIMIT 1;
7884
7885 IF undone_query IS NULL THEN
7886 RAISE NOTICE 'Unable to find % in update_provenance', c;
7887 RETURN c;
7888 END IF;
7889
7890 SELECT query
7891 INTO undo_query
7892 FROM pg_stat_activity
7893 WHERE pid = pg_backend_pid();
7894
7895 undo_token := public.uuid_generate_v4();
7896 PERFORM create_gate(undo_token, 'update');
7897 INSERT INTO update_provenance(provsql, query, query_type, username, ts, valid_time)
7898 VALUES (
7899 undo_token,
7900 undo_query,
7901 'UNDO',
7902 current_user,
7903 CURRENT_TIMESTAMP,
7904 TSTZMULTIRANGE(tstzrange(CURRENT_TIMESTAMP, NULL))
7905 );
7906
7907 PERFORM set_config('provsql.update_provenance', 'off', false);
7908
7909 FOR schema_rec IN
7910 SELECT nspname
7911 FROM pg_namespace
7912 WHERE nspname NOT IN ('pg_catalog','information_schema','pg_toast','pg_temp_1','pg_toast_temp_1')
7913 LOOP
7914 FOR table_rec IN
7915 EXECUTE format('SELECT tablename AS tname FROM pg_tables WHERE schemaname = %L', schema_rec.nspname)
7916 LOOP
7917 IF EXISTS (
7918 SELECT 1
7919 FROM information_schema.columns
7920 WHERE table_schema = schema_rec.nspname
7921 AND table_name = table_rec.tname
7922 AND table_name <> 'update_provenance'
7923 AND column_name = 'provsql'
7924 ) THEN
7925 FOR row_rec IN
7926 EXECUTE format('SELECT provsql AS x FROM %I.%I', schema_rec.nspname, table_rec.tname)
7927 LOOP
7928 new_x := replace_the_circuit(row_rec.x, c, undo_token);
7929 EXECUTE format('UPDATE %I.%I SET provsql = $1 WHERE provsql = $2',
7930 schema_rec.nspname, table_rec.tname)
7931 USING new_x, row_rec.x;
7932 END LOOP;
7933 END IF;
7934 END LOOP;
7935 END LOOP;
7936
7937 PERFORM set_config('provsql.update_provenance', 'on', false);
7938
7939 RETURN undo_token;
7940END;
7941$$;
7942
7943/**
7944 * @brief Recursively rewrite a circuit to undo a specific operation
7945 *
7946 * Helper for undo(). Walks the circuit and replaces occurrences of
7947 * the target update gate with its monus.
7948 *
7949 * @param x provenance token to rewrite
7950 * @param c UUID of the update operation to undo
7951 * @param u UUID of the undo operation
7952 */
7953CREATE OR REPLACE FUNCTION replace_the_circuit(
7954 x UUID,
7955 c UUID,
7956 u UUID
7957)
7958RETURNS UUID
7959LANGUAGE plpgsql
7960AS $$
7961DECLARE
7962 nchildren UUID[];
7963 child UUID;
7964 ntoken UUID;
7965 ntype PROVENANCE_GATE;
7966BEGIN
7967 IF x = c THEN
7968 RETURN provenance_monus(c, u);
7969 -- update and input gates cannot have children
7970 ELSIF get_gate_type(x) = 'update' OR get_gate_type(x) = 'input' THEN
7971 RETURN x;
7972 ELSE
7973 nchildren := '{}';
7974 FOREACH child IN ARRAY get_children(x)
7975 LOOP
7976 nchildren := array_append(nchildren, replace_the_circuit(child, c, u));
7977 END LOOP;
7978
7979 ntoken := public.uuid_generate_v4();
7980 ntype := get_gate_type(x);
7981
7982 PERFORM create_gate(ntoken, ntype, nchildren);
7983 RETURN ntoken;
7984 END IF;
7985END;
7986$$;
7987
7988SELECT create_provenance_mapping_view('time_validity_view', 'update_provenance', 'valid_time');
7989
7990/** @} */
7991
7992SET search_path TO public;
7993
7994-- Final constants-cache refresh: same rationale as at the end of
7995-- provsql.common.sql. On PG14+ this file is appended after the common
7996-- script, so this is the last statement of the generated install script;
7997-- the refresh must come after every object has been created for the
7998-- installing session's memoized constants to be complete.
7999SELECT provsql.reset_constants_cache();