ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
GenericCircuit.hpp
Go to the documentation of this file.
1/**
2 * @file GenericCircuit.hpp
3 * @brief Template implementation of @c GenericCircuit::evaluate().
4 *
5 * Provides the out-of-line definition of the @c evaluate() template method
6 * declared in @c GenericCircuit.h. This file must be included (directly
7 * or transitively) by any translation unit that instantiates
8 * @c GenericCircuit::evaluate<S>() for a specific semiring type @c S.
9 *
10 * The @c evaluate() method performs a post-order traversal of the sub-circuit
11 * rooted at gate @p g, looking up input-gate values from @p provenance_mapping
12 * and combining them using the semiring operations:
13 *
14 * | Gate type | Semiring operation |
15 * |-------------|-------------------------------|
16 * | gate_input | lookup in @p provenance_mapping|
17 * | gate_plus | @c semiring.plus(children) |
18 * | gate_times | @c semiring.times(children) |
19 * | gate_monus | @c semiring.monus(left, right) |
20 * | gate_delta | @c semiring.delta(child) |
21 * | gate_cmp | @c semiring.cmp(left, op, right)|
22 * | gate_semimod| @c semiring.semimod(x, s) |
23 * | gate_agg | @c semiring.agg(op, children) |
24 * | gate_value | @c semiring.value(string) |
25 * | gate_one | @c semiring.one() |
26 * | gate_zero | @c semiring.zero() |
27 */
28#include "GenericCircuit.h"
29
30extern "C" {
31#include "utils/lsyscache.h"
32#include "miscadmin.h" // check_stack_depth
33}
34
35template<typename S, std::enable_if_t<std::is_base_of_v<semiring::Semiring<typename S::value_type>, S>, int> >
36typename S::value_type GenericCircuit::evaluate(gate_t g, std::unordered_map<gate_t, typename S::value_type> &provenance_mapping, S semiring) const
37{
38 /* Iterative post-order evaluation with @p provenance_mapping doubling as
39 * the memoisation table. Provenance circuits can be as deep as the data
40 * (a recursive fixpoint's times/plus chain, the decomposition-aligned
41 * reachability circuits of path-like graphs), so recursion on wires
42 * would overflow the C stack -- the previous implementation turned that
43 * into a "stack depth limit exceeded" error at a few thousand levels;
44 * the explicit stack removes the ceiling altogether. Every computed
45 * gate is memoised (a gate's semiring value is a pure function of the
46 * gate), so shared sub-DAGs are evaluated once and gate-creating
47 * semirings (BoolExpr, formula) preserve the sharing structurally. */
48 std::vector<gate_t> stack{g};
49
50 while(!stack.empty()) {
51 const gate_t u = stack.back();
52
53 /* The side-band assumption checks run BEFORE the memoisation
54 * lookup: input leaves are preloaded into @p provenance_mapping
55 * from the mapping table, and a fold collapse can redirect a
56 * marked gate onto such a leaf -- the marker must still refuse
57 * incompatible semirings there. */
58
59 /* In-memory Boolean-assumption marker (set by
60 * @c foldBooleanIdentities on gates whose wires were rewritten
61 * under a Boolean-only rule). Mirrors the @c gate_assumed
62 * structural-marker check below but applies to gates that keep
63 * their original type (the rule mutated their wires in place ;
64 * the persistent mmap was not touched). Same compatibility
65 * predicate, same failure mode. */
66 if(isBooleanAssumed(u) && !semiring.compatibleWithBooleanRewrite())
67 throw CircuitException(
68 "The requested semiring does not admit a homomorphism "
69 "from Boolean functions; this gate's wires were rewritten "
70 "under a Boolean-only rule (times-idempotence or "
71 "times-absorbs-plus, applied under the 'boolean' "
72 "provenance class) and the evaluation is unsound under "
73 "this semiring. Re-run under a more general provenance "
74 "class, or pick a Boolean-compatible semiring (boolean, "
75 "boolexpr, formula, ...).");
76
77 /* In-memory absorptive-assumption marker (set by the absorptive
78 * fold rules: plus-idempotence, plus-with-one absorber,
79 * plus-absorbs-times). Sound in every absorptive semiring; a
80 * semiring tolerating the stronger Boolean rewrite tolerates this
81 * weaker, Boolean-function-preserving one as well. */
82 if(isAbsorptiveAssumed(u) && !semiring.absorptive()
83 && !semiring.compatibleWithBooleanRewrite())
84 throw CircuitException(
85 "The requested semiring is not absorptive; this gate's "
86 "wires were rewritten under an absorptive rule "
87 "(plus-idempotence, plus-with-one absorber or "
88 "plus-absorbs-times, applied under the 'absorptive' or "
89 "'boolean' provenance class) and the evaluation is "
90 "unsound under this semiring. Re-run under the "
91 "'semiring' provenance class, or pick an absorptive "
92 "semiring (probability, boolean, nonnegative "
93 "tropical, ...).");
94
95 if(provenance_mapping.find(u) != provenance_mapping.end()) {
96 stack.pop_back();
97 continue;
98 }
99
100 const auto t = getGateType(u);
101
102 /* Leaves. */
103 switch(t) {
104 case gate_one:
105 case gate_input:
106 case gate_update:
107 case gate_mulinput:
108 // If not in provenance mapping, return no provenance (one of the semiring)
109 provenance_mapping.emplace(u, semiring.one());
110 stack.pop_back();
111 continue;
112 case gate_zero:
113 provenance_mapping.emplace(u, semiring.zero());
114 stack.pop_back();
115 continue;
116 case gate_value:
117 provenance_mapping.emplace(u, semiring.value(getExtra(u)));
118 stack.pop_back();
119 continue;
120 case gate_assumed:
121 /* Structural assumption marker: the wrapped sub-circuit was
122 * computed under the assumption named by the gate's label (the
123 * extra string; a gate stored without a label defaults to
124 * 'boolean'). Identity for semirings satisfying
125 * the assumption; fatal for the rest, since otherwise we would
126 * silently return a value the semiring's semantics does not
127 * justify.
128 *
129 * - 'boolean': the sub-circuit only preserves the Boolean
130 * function of the lineage (e.g. the safe-query rewrite
131 * collapses derivation multiplicities into a single witness);
132 * sound for semirings admitting a homomorphism from Boolean
133 * functions.
134 * - 'absorptive': the sub-circuit only represents the
135 * absorptive (Sorp) quotient of the recursive provenance --
136 * either truncated at the absorptive value fixpoint (cyclic
137 * recursion stopped once every minimal,
138 * tuple-repetition-free, derivation is covered) or compiled
139 * by the bounded-treewidth reachability route (whose world
140 * enumeration surfaces exactly the minimal derivation
141 * supports); longer derivations are absorbed in any
142 * absorptive semiring but genuinely missing for the rest
143 * (Deutch, Milo, Roy & Tannen, ICDT 2014). */
144 {
145 const std::string assumption = getExtra(u);
146 if(assumption.empty() || assumption == "boolean") {
147 if(!semiring.compatibleWithBooleanRewrite())
148 throw CircuitException(
149 "The requested semiring does not admit a homomorphism "
150 "from Boolean functions; the wrapped sub-circuit was "
151 "computed under a Boolean-provenance assumption "
152 "(typically by the safe-query rewrite, "
153 "provenance class 'boolean') and the evaluation is "
154 "unsound under this semiring. Re-run the query under "
155 "a more general provenance class, or pick a "
156 "Boolean-compatible semiring (boolean, boolexpr, "
157 "formula, ...).");
158 } else if(assumption == "absorptive") {
159 if(!semiring.absorptive())
160 throw CircuitException(
161 "The requested semiring is not absorptive; the "
162 "wrapped sub-circuit only represents the absorptive "
163 "quotient of a recursive query's provenance "
164 "(fixpoint truncation or compiled reachability "
165 "circuit), so its value is only defined for "
166 "absorptive semirings (probability, boolean, "
167 "formula-with-absorption, nonnegative tropical, "
168 "...). Counting and why-provenance of cyclic "
169 "recursion are genuinely infinite; on acyclic "
170 "data, re-run under the 'semiring' provenance "
171 "class.");
172 /* CAVEAT: absorptive() is a coarser gate than the compiled
173 * reachability route's actual soundness condition. That route
174 * materialises its world enumeration with genuine negation: each
175 * absent edge surfaces as monus(one, edge) (BooleanGate::NOT
176 * lowered to gate_monus; see ReachabilityCompiler.cpp and
177 * CertifiedDDMaterialize.cpp). The absorptive-quotient value
178 * comes out right only because, in every absorptive semiring we
179 * currently ship, (i) monus(one, x) is the times-neutral 'one' on
180 * a present-priced leaf, so the negative literals do not perturb
181 * the path-products, and (ii) any world a negative literal would
182 * kill is dominated by an edge-superset of equal value, hence
183 * absorbed. semiring.absorptive() checks NEITHER property. A
184 * future or user-defined absorptive m-semiring whose monus(one, .)
185 * is not the times-neutral, or whose monus is not
186 * "drop-if-dominated", would pass this gate yet read those
187 * monus(one, edge) gates with a value the path-sum argument does
188 * not justify -- a silently wrong result. If such a semiring is
189 * added, strengthen this guard (e.g. assert monus(one, x) == one
190 * for present-priced leaves, or add a dedicated capability flag)
191 * rather than relying on absorptive() alone. (Truncated cyclic
192 * recursion, the other 'absorptive' producer, ships only minimal
193 * derivations and carries no such negation, so it is unaffected.) */
194 } else
195 throw CircuitException(
196 "Unknown assumption marker '" + assumption + "'");
197 }
198 break;
199 case gate_cmp:
200 {
201 bool ok;
202 cmpOpFromOid(getInfos(u).first, ok);
203 if(!ok)
204 throw CircuitException(
205 "Comparison operator OID " +
206 std::to_string(getInfos(u).first) +
207 " not supported");
208 break;
209 }
210 default:
211 break;
212 }
213
214 /* Internal gate: make sure every child is computed first. */
215 {
216 bool ready = true;
217 for(const auto &c : getWires(u))
218 if(provenance_mapping.find(c) == provenance_mapping.end()) {
219 stack.push_back(c);
220 ready = false;
221 }
222 if(!ready)
223 continue;
224 }
225
226 const auto childValue = [&](int i) -> const typename S::value_type & {
227 return provenance_mapping.at(getWires(u)[i]);
228 };
229
230 switch(t) {
231 case gate_plus:
232 case gate_times:
233 case gate_monus: {
234 std::vector<typename S::value_type> childrenResult;
235 for(const auto &c : getWires(u))
236 childrenResult.push_back(provenance_mapping.at(c));
237 if(t==gate_plus) {
238 childrenResult.erase(std::remove(std::begin(childrenResult), std::end(childrenResult), semiring.zero()),
239 childrenResult.end());
240 provenance_mapping.emplace(u, semiring.plus(childrenResult));
241 } else if(t==gate_times) {
242 bool zero = false;
243 for(const auto &c: childrenResult) {
244 if(c==semiring.zero()) {
245 zero = true;
246 break;
247 }
248 }
249 if(zero)
250 provenance_mapping.emplace(u, semiring.zero());
251 else {
252 childrenResult.erase(std::remove(std::begin(childrenResult), std::end(childrenResult), semiring.one()),
253 childrenResult.end());
254 provenance_mapping.emplace(u, semiring.times(childrenResult));
255 }
256 } else {
257 if(childrenResult[0]==semiring.zero() || childrenResult[0]==childrenResult[1])
258 provenance_mapping.emplace(u, semiring.zero());
259 else
260 provenance_mapping.emplace(u, semiring.monus(childrenResult[0], childrenResult[1]));
261 }
262 break;
263 }
264
265 case gate_delta:
266 provenance_mapping.emplace(u, semiring.delta(childValue(0)));
267 break;
268
269 case gate_project:
270 case gate_eq:
271 case gate_annotation:
272 case gate_assumed:
273 // Where-provenance gates, the transparent annotation wrapper and the
274 // (compatibility-checked above) Boolean-assumption marker: identity
275 // for every admissible semiring. The annotation's extra string is
276 // inert metadata at evaluation time.
277 provenance_mapping.emplace(u, childValue(0));
278 break;
279
280 case gate_cmp:
281 {
282 bool ok;
283 ComparisonOperator op = cmpOpFromOid(getInfos(u).first, ok);
284 provenance_mapping.emplace(u, semiring.cmp(childValue(0), op, childValue(1)));
285 break;
286 }
287
288 case gate_semimod:
289 provenance_mapping.emplace(u, semiring.semimod(childValue(0), childValue(1)));
290 break;
291
292 case gate_agg:
293 {
294 auto infos = getInfos(u);
295
297
298 std::vector<typename S::value_type> vec;
299 for(const auto &c : getWires(u))
300 vec.push_back(provenance_mapping.at(c));
301 provenance_mapping.emplace(u, semiring.agg(op, vec));
302 break;
303 }
304
305 case gate_conditioned:
306 /* Conditioning marker: P(·|C) requires a normalising division that
307 * no general semiring provides (m-semirings have monus, not a
308 * multiplicative inverse). A conditioned token is evaluable only
309 * in the measure interpretation (probability_evaluate, special-
310 * cased at the root, or the random-variable / agg_token
311 * distribution evaluators), never under a generic sr_* semiring. */
312 throw CircuitException(
313 "The requested semiring does not support conditioning: "
314 "P(·|C) = P(·∧C)/P(C) needs a normalising division "
315 "no general semiring provides. A conditioned token is "
316 "evaluable only in the measure interpretation "
317 "(probability_evaluate, or the random-variable / agg_token "
318 "distribution evaluators).");
319
320 case gate_mobius: {
321 /* The signed Möbius combination is a probability-only shortcut layered
322 * over the normal provenance: the gate carries the literal lineage as a
323 * designated child marked "L:<uuid>" in extra. Every non-probability
324 * evaluator (this semiring path, hence Shapley / Banzhaf / PROV export)
325 * is TRANSPARENT to that lineage, so the token behaves like the ordinary
326 * provenance of the query. A nested gate_mobius (an inner
327 * inclusion-exclusion step) carries no lineage: its value is never used
328 * (the root passes through to the top lineage), but it must not throw, so
329 * it falls back to its first child. */
330 const std::string ex = getExtra(u);
331 gate_t lineage = u; // sentinel: not found
332 const std::string key = "L:";
333 std::size_t p = ex.find(key);
334 if(p != std::string::npos) {
335 std::size_t e = ex.find(' ', p);
336 const std::string luid =
337 ex.substr(p + key.size(),
338 e == std::string::npos ? std::string::npos : e - p - key.size());
339 for(const auto &c : getWires(u))
340 if(getUUID(c) == luid) { lineage = c; break; }
341 }
342 if(lineage != u)
343 provenance_mapping.emplace(u, provenance_mapping.at(lineage));
344 else
345 provenance_mapping.emplace(u, childValue(0));
346 break;
347 }
348
349 default:
350 throw CircuitException("Invalid gate type for semiring evaluation");
351 }
352
353 stack.pop_back();
354 }
355
356 return provenance_mapping.at(g);
357}
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
AggregationOperator getAggregationOperator(Oid oid)
Map a PostgreSQL aggregate function OID to an AggregationOperator.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
ComparisonOperator
SQL comparison operators used in gate_cmp circuit gates.
Definition Aggregation.h:39
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Semiring-agnostic in-memory provenance circuit.
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
std::vector< gate_t > & getWires(gate_t g)
Definition Circuit.h:140
gate_type getGateType(gate_t g) const
Definition Circuit.h:130
uuid getUUID(gate_t g) const
Definition Circuit.hpp:46
std::map< gate_t, std::pair< unsigned, unsigned > > infos
Per-gate (info1, info2) annotations.
S::value_type evaluate(gate_t g, std::unordered_map< gate_t, typename S::value_type > &provenance_mapping, S semiring) const
Evaluate the sub-circuit rooted at gate g over semiring semiring.
std::string getExtra(gate_t g) const
Return the string extra for gate g.
bool isAbsorptiveAssumed(gate_t g) const
Report whether g carries the absorptive-assumption flag.
bool isBooleanAssumed(gate_t g) const
Report whether g carries the Boolean-assumption flag.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
@ gate_annotation
Transparent single-child wrapper carrying a query-level annotation in extra (inversion-free certifica...
@ gate_mobius
Signed Möbius combination: a MEASURE-only gate carrying one integer coefficient per child (in extra,...
@ gate_conditioned
Conditioning marker with two children [target, evidence]: measure-only, probability_evaluate returns ...
@ gate_assumed
Structural marker over a single child whose sub-circuit was computed under a Boolean-provenance assum...