ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
mobius_evaluate.cpp
Go to the documentation of this file.
1/**
2 * @file mobius_evaluate.cpp
3 * @brief Möbius-inversion exact route for safe UCQs (the last missing exact
4 * route of the Dalvi-Suciu dichotomy).
5 *
6 * Some unions of conjunctive queries are safe (PTIME data complexity) only
7 * because the \#P-hard terms of their inclusion-exclusion expansion carry a
8 * zero Möbius value on the CNF lattice and cancel. The canonical witness is
9 * QW / q9 (Dalvi-Suciu 2012; Monet & Olteanu 2018). No other ProvSQL route
10 * handles it in PTIME: the safe-query rewriter is per-CQ and hierarchical, the
11 * query is not inversion-free (that is the point), and on adversarial data the
12 * joint treewidth is unbounded.
13 *
14 * This file packages the **extensional** lattice-walking algorithm (Dalvi,
15 * Schnaitter & Suciu, "Computing query probability with incidence algebras",
16 * PODS 2010) the way the joint-width route already is: a compile-at-execution
17 * step producing a certified circuit over the gathered data, and a linear
18 * evaluation -- the only genuinely new evaluation primitive being a signed
19 * linear combination at @c gate_mobius nodes (see @c provsql_utils.h and the
20 * @c gate_mobius handling in @c probability_evaluate.cpp).
21 *
22 * The probability of a UCQ Q given in CNF as @f$\bigwedge_i d_i@f$ is, by
23 * inclusion-exclusion on the @f$\lnot d_i@f$,
24 * @f[ P(Q) = \sum_{\emptyset\neq s\subseteq[M]} (-1)^{|s|+1}
25 * P\Big(\bigvee_{i\in s} d_i\Big), @f]
26 * and grouping the @f$\bigvee_{i\in s} d_i@f$ up to logical equivalence
27 * collapses the hard term (its coefficient sums to zero -- the whole game).
28 * Each surviving @f$\bigvee_{i\in s} d_i@f$ is a safe disjunctive sentence,
29 * compiled recursively by the standard IndepStep / MobiusStep lifted-inference
30 * recursion (component split, disjoint-symbol product, separator
31 * independent-project, inner Möbius step) into certified-independent Boolean
32 * islands, combined at the root @c gate_mobius by the signed coefficients.
33 *
34 * Restrictions: reduced-form UCQs (no relation
35 * repeated within a disjunct, no repeated variable inside an atom, no
36 * constants), tuple-independent (TID) inputs. Anything outside that declines
37 * (a C++ exception caught at the SQL boundary), so the query falls back to the
38 * normal provenance and never fails.
39 */
40extern "C" {
41#include "postgres.h"
42#include "fmgr.h"
43#include "funcapi.h"
44#include "miscadmin.h"
45#include "access/htup_details.h" /* heap_form_tuple (PG 10 declares it here) */
46#include "access/xact.h"
47#include "catalog/pg_type.h"
48#include "executor/spi.h"
49#include "utils/array.h"
50#include "utils/builtins.h"
51#include "utils/resowner.h"
52#include "utils/uuid.h"
53
54#include "compatibility.h" /* TYPALIGN_INT / TYPALIGN_CHAR fallback for PG < 13 */
55#include "provsql_utils.h"
56#include "provsql_mmap.h"
57
58/* Store-backed gate-type lookup (defined in provsql_mmap.c); used to enforce
59 * the TID restriction (G3) -- every fact token must be a bare gate_input. */
60extern Datum get_gate_type(PG_FUNCTION_ARGS);
61
62/* Data-cost cap (provsql.mobius_max_gates GUC, defined in provsql.c): the
63 * Möbius recursion is O(|D|^k), so a high-level safe query on large data is
64 * bounded by declining once it has built this many gates. */
66
67PG_FUNCTION_INFO_V1(ucq_mobius_materialize_tracked);
68PG_FUNCTION_INFO_V1(ucq_mobius_compile_stats);
69PG_FUNCTION_INFO_V1(ucq_mobius_provenance_answer);
70}
71
72#include "c_cpp_compatibility.h"
74#include "mobius_evaluate.h"
75#include "provsql_utils_cpp.h"
76
77#include <algorithm>
78#include <cmath>
79#include <cstdint>
80#include <functional>
81#include <map>
82#include <set>
83#include <stdexcept>
84#include <string>
85#include <unordered_map>
86#include <unordered_set>
87#include <vector>
88
89namespace {
90
91/** @brief Thrown when the Möbius route declines (unsafe shape, cap hit, ...). */
92class MobiusDecline : public std::runtime_error {
93public:
94 explicit MobiusDecline(const std::string &w) : std::runtime_error(w) {}
95};
96
97/** @brief One argument position of an atom: a variable or a bound constant. */
98struct Term {
99 bool isVar;
100 long v; ///< Variable id (isVar) or constant element-id (!isVar).
101 bool operator==(const Term &o) const { return isVar==o.isVar && v==o.v; }
102};
103
104/** @brief A relational atom: a relation symbol applied to terms. */
105struct MAtom {
106 unsigned rel;
107 std::vector<Term> args;
108};
109
110using Disjunct = std::vector<MAtom>; ///< A conjunction of atoms.
111using Sentence = std::vector<Disjunct>;///< A disjunction of conjunctions (UCQ).
112
113// ---------------------------------------------------------------------------
114// Statistics surfaced by the stats SRF (the demonstrability requirement).
115// ---------------------------------------------------------------------------
116struct MobiusStats {
117 int n_components = 0; ///< distinct connected-component literals at top
118 int n_cnf_conjuncts = 0; ///< M, the CNF conjunct count at the top level
119 int lattice_size = 0; ///< 2^M subsets enumerated at the top level
120 int lattice_collapsed = 0; ///< distinct elements after equivalence collapse
121 int n_nonzero = 0; ///< elements with coefficient != 0
122 int n_cancelled = 0; ///< distinct elements with coefficient == 0
123 bool cancelled_hard = false;///< some cancelled element is #P-hard (no separator)
124 long dd_size = 0; ///< gates materialised
125 long memo_hits = 0; ///< sentence-memo hits
126 double probability = 0.0; ///< P(Q)
127};
128
129// ===========================================================================
130// Homomorphisms (CQ containment) over single-component conjunctions.
131// ===========================================================================
132
133/// Backtracking homomorphism search: does a mapping of @p p's variables to
134/// @p q's terms exist sending every @p p-atom onto a @p q-atom (constants
135/// matched verbatim)? @p p, @p q are tiny (reduced-form components), so the
136/// naive search is cheap. Returns true iff @p p ⊑ @p q (i.e. @p q implies
137/// @p p as a query -- a hom p->q means every model of p is a model of q is the
138/// usual direction; here hom(p,q) maps p's atoms into q, witnessing q ⊑ p).
139bool homExists(const Disjunct &p, const Disjunct &q,
140 std::size_t ai, std::map<long,Term> &asg)
141{
142 if(ai == p.size())
143 return true;
144 const MAtom &pa = p[ai];
145 for(const MAtom &qa : q) {
146 if(qa.rel != pa.rel || qa.args.size() != pa.args.size())
147 continue;
148 std::vector<long> undo;
149 bool ok = true;
150 for(std::size_t k=0; k<pa.args.size(); ++k) {
151 const Term &pt = pa.args[k];
152 const Term &qt = qa.args[k];
153 if(!pt.isVar) {
154 if(qt.isVar || qt.v != pt.v) { ok=false; break; }
155 } else {
156 auto it = asg.find(pt.v);
157 if(it == asg.end()) { asg[pt.v]=qt; undo.push_back(pt.v); }
158 else if(!(it->second == qt)) { ok=false; break; }
159 }
160 }
161 if(ok && homExists(p, q, ai+1, asg))
162 return true;
163 for(long u : undo) asg.erase(u);
164 }
165 return false;
166}
167
168/// hom from @p p into @p q (maps p's atoms onto q's): witnesses q ⊑ p.
169bool hom(const Disjunct &p, const Disjunct &q)
170{
171 std::map<long,Term> asg;
172 return homExists(p, q, 0, asg);
173}
174
175/// Logical equivalence of two single-component conjunctions (self-join-free
176/// components are cores, so this is isomorphism).
177bool equiv(const Disjunct &p, const Disjunct &q)
178{
179 return hom(p,q) && hom(q,p);
180}
181
182// ===========================================================================
183// Connected components.
184// ===========================================================================
185
186struct UF {
187 std::vector<int> p;
188 explicit UF(int n) : p(n) { for(int i=0;i<n;++i) p[i]=i; }
189 int find(int x){ while(p[x]!=x){ p[x]=p[p[x]]; x=p[x]; } return x; }
190 void uni(int a,int b){ p[find(a)]=find(b); }
191};
192
193/// Split a conjunction into its variable-connected components. (Reduced
194/// form: distinct components use disjoint relation symbols, so they are
195/// probabilistically independent.)
196std::vector<Disjunct> componentsOf(const Disjunct &d)
197{
198 const int n = static_cast<int>(d.size());
199 UF uf(n);
200 std::map<long, int> firstAtomOfVar;
201 for(int i=0;i<n;++i)
202 for(const auto &t : d[i].args)
203 if(t.isVar) {
204 auto it = firstAtomOfVar.find(t.v);
205 if(it==firstAtomOfVar.end()) firstAtomOfVar[t.v]=i;
206 else uf.uni(it->second, i);
207 }
208 std::map<int, Disjunct> byroot;
209 for(int i=0;i<n;++i)
210 byroot[uf.find(i)].push_back(d[i]);
211 std::vector<Disjunct> out;
212 for(auto &kv : byroot) out.push_back(std::move(kv.second));
213 return out;
214}
215
216/// Is an atom fully ground (no variables)?
217bool ground(const MAtom &a)
218{
219 for(const auto &t : a.args) if(t.isVar) return false;
220 return true;
221}
222
223// ===========================================================================
224// Data index (TID facts).
225// ===========================================================================
226
227struct FactIndex {
228 // (rel, element tuple) -> provenance token (UUID string). A certain
229 // (untracked) fact has an empty token.
230 std::map<std::pair<unsigned, std::vector<long>>, std::string> tok;
231 std::set<std::pair<unsigned, std::vector<long>>> present;
232 // (rel, position) -> active domain (distinct values seen there).
233 std::map<std::pair<unsigned,int>, std::set<long>> domain;
234
235 bool isPresent(unsigned rel, const std::vector<long> &el) const {
236 return present.count({rel, el}) > 0;
237 }
238 const std::string *token(unsigned rel, const std::vector<long> &el) const {
239 auto it = tok.find({rel, el});
240 return it==tok.end() ? nullptr : &it->second;
241 }
242};
243
244// ===========================================================================
245// The compiler.
246// ===========================================================================
247
248class MobiusCompiler {
249public:
250 MobiusCompiler(const FactIndex &fi, MobiusStats &st) : fi(fi), st(st) {
251 const constants_t c = get_constants(true);
252 times_oid = c.OID_FUNCTION_PROVENANCE_TIMES;
253 plus_oid = c.OID_FUNCTION_PROVENANCE_PLUS;
254 if(!OidIsValid(times_oid) || !OidIsValid(plus_oid))
255 provsql_error("ucq_mobius: provenance_times / provenance_plus unavailable");
256 }
257
258 /// Compile the top sentence, returning the root token. @p lineage is the
259 /// token of the literal Boolean provenance of the query (the normal lineage
260 /// the route falls back to); it is carried on the root gate_mobius as a
261 /// designated transparent child so every non-probability evaluator (semiring,
262 /// Shapley, Banzhaf, PROV export) sees the literal lineage and works, while
263 /// probability uses the signed Möbius combination. Empty = no lineage
264 /// (measure-only; the manual descriptor entry points without a fallback).
265 pg_uuid_t compileTop(const Sentence &s, const std::string &lineage = "") {
266 pending_lineage = lineage;
267 lineage_consumed = false;
268 pg_uuid_t root = compile(s, /*top=*/true);
269 if(!lineage.empty() && !lineage_consumed)
270 // The top did not go through a Möbius step (e.g. a safe query that
271 // reached this route): wrap it in a thin gate_mobius selector carrying
272 // the lineage and the value with coefficient 1.
273 root = mkMobius({root}, {1}, lineage);
274 return root;
275 }
276
277private:
278 const FactIndex &fi;
279 MobiusStats &st;
280 Oid times_oid = InvalidOid; // provsql.provenance_times
281 Oid plus_oid = InvalidOid; // provsql.provenance_plus
282 std::unordered_map<std::string, std::string> memo; // key -> uuid string
283 std::unordered_set<std::string> created;
284 std::string pending_lineage; // lineage to inline into the top Möbius step
285 bool lineage_consumed = false;
286
287 // -- materialisation -----------------------------------------------------
288
289 /// Delegate plus / times construction to the system's provenance_times /
290 /// provenance_plus (the single source of truth: they filter the semiring
291 /// identities, short-circuit 0 / 1 surviving children, and content-address
292 /// the gate in the store). The Möbius path must NOT mint its own plus / times
293 /// gates -- using these functions keeps one gate-construction code path and
294 /// lets the Möbius islands share sub-gates with the rest of the system.
295 pg_uuid_t callProvenance(bool isAnd, const std::vector<pg_uuid_t> &ch) {
296 std::vector<Datum> datums;
297 datums.reserve(ch.size());
298 for(const auto &u : ch)
299 datums.push_back(UUIDPGetDatum(const_cast<pg_uuid_t *>(&u)));
300 ArrayType *arr = construct_array(datums.empty()?nullptr:datums.data(),
301 static_cast<int>(datums.size()),
302 UUIDOID, 16, false, TYPALIGN_CHAR);
303 Datum res = OidFunctionCall1(isAnd ? times_oid : plus_oid,
304 PointerGetDatum(arr));
305 /* Data-cost cap: the lifted-inference recursion is O(|D|^k), so bound the
306 * total work and decline (-> joint-width / the ladder) past the cap rather
307 * than let a high-level safe query on large data out-cost the general
308 * pipeline. 0 disables the cap. */
309 ++st.dd_size;
311 static_cast<long>(st.dd_size) > static_cast<long>(provsql_mobius_max_gates))
312 throw MobiusDecline("Möbius: data-cost cap (provsql.mobius_max_gates) "
313 "exceeded -- the |D|^k recursion is too large");
314 return *DatumGetUUIDP(res);
315 }
316
317 /// gate_one (empty times) / gate_zero (empty plus).
318 pg_uuid_t mkConst(bool one) { return callProvenance(one, {}); }
319
320 /// Independent OR / AND over child tokens. Children are deduplicated first:
321 /// the certified-independent evaluation is read-once, so a child must not be
322 /// double-counted (provenance_plus / provenance_times keep duplicates). The
323 /// 0 / 1-child and identity cases are handled by the provenance functions.
324 pg_uuid_t mkBool(bool isAnd, std::vector<pg_uuid_t> ch) {
325 std::vector<std::string> texts;
326 texts.reserve(ch.size());
327 for(const auto &c : ch) texts.push_back(uuid2string(c));
328 std::sort(texts.begin(), texts.end());
329 texts.erase(std::unique(texts.begin(), texts.end()), texts.end());
330 std::vector<pg_uuid_t> uniq;
331 uniq.reserve(texts.size());
332 for(const auto &t : texts) uniq.push_back(string2uuid(t));
333 return callProvenance(isAnd, uniq);
334 }
335
336 /// Signed Möbius combination gate over @p children with integer @p coeffs.
337 /// Coefficients are stored keyed by child UUID (@c "uuid:coeff" tokens in
338 /// @c extra), so evaluation is robust to any child reordering / dedup the
339 /// store may apply; duplicate children are merged (coefficients summed) and
340 /// zero-coefficient children dropped.
341 /// @p lineage (optional): the literal-lineage token, carried as a
342 /// designated transparent child marked @c "L:<uuid>" in @c extra (child 0).
343 pg_uuid_t mkMobius(const std::vector<pg_uuid_t> &children,
344 const std::vector<long> &coeffs,
345 const std::string &lineage = "") {
346 std::map<std::string,long> bycoeff;
347 std::vector<std::string> order;
348 for(std::size_t i=0;i<children.size();++i) {
349 const std::string u = uuid2string(children[i]);
350 if(!bycoeff.count(u)) order.push_back(u);
351 bycoeff[u] += coeffs[i];
352 }
353 std::vector<pg_uuid_t> ch;
354 std::string extra, name = "mobius[";
355 if(!lineage.empty()) {
356 ch.push_back(string2uuid(lineage)); // the literal lineage, child 0
357 extra += "L:" + lineage + " ";
358 name += "L:" + lineage + ",";
359 }
360 for(const std::string &u : order) {
361 if(bycoeff[u]==0) continue;
362 ch.push_back(string2uuid(u));
363 extra += u + ":" + std::to_string(bycoeff[u]) + " ";
364 name += u + ":" + std::to_string(bycoeff[u]) + ",";
365 }
366 name += "]";
367 if(ch.empty() || (!lineage.empty() && ch.size()==1))
368 return mkConst(false); // no surviving combination -> probability 0
369 pg_uuid_t u = provsqlUuidV5(name);
370 if(created.insert(uuid2string(u)).second) {
372 static_cast<unsigned>(ch.size()),
373 ch.data());
374 provsql_internal_set_extra(&u, extra.c_str());
375 ++st.dd_size;
376 }
377 return u;
378 }
379
380 // -- canonical key (structural, for memoisation) -------------------------
381
382 std::string canonDisjunct(const Disjunct &d) const {
383 // Canonically rename variables by first occurrence in a sorted atom order.
384 // Build per-atom signatures with placeholder variable slots, then pick the
385 // lexicographically smallest atom ordering greedily (sufficient: keys only
386 // need same-structure -> same-key, not minimality).
387 std::vector<std::string> atomsigRaw;
388 for(const auto &a : d) {
389 std::string sg = "r" + std::to_string(a.rel) + "(";
390 for(const auto &t : a.args) {
391 if(t.isVar) sg += "v"; else sg += "c"+std::to_string(t.v);
392 sg += ",";
393 }
394 sg += ")";
395 atomsigRaw.push_back(sg);
396 }
397 // Sort atoms by raw signature, then assign canonical var ids in first
398 // occurrence order over that sorted sequence.
399 std::vector<int> order(d.size());
400 for(std::size_t i=0;i<d.size();++i) order[i]=static_cast<int>(i);
401 std::sort(order.begin(), order.end(),
402 [&](int a,int b){ return atomsigRaw[a]<atomsigRaw[b]; });
403 std::map<long,int> ren;
404 std::string out;
405 for(int idx : order) {
406 const MAtom &a = d[idx];
407 out += "r"+std::to_string(a.rel)+"(";
408 for(const auto &t : a.args) {
409 if(t.isVar) {
410 auto it = ren.find(t.v);
411 int id = (it==ren.end()) ? (ren[t.v]=static_cast<int>(ren.size())) : it->second;
412 out += "v"+std::to_string(id);
413 } else out += "c"+std::to_string(t.v);
414 out += ",";
415 }
416 out += ")";
417 }
418 return out;
419 }
420
421 std::string canonSentence(const Sentence &s) const {
422 std::vector<std::string> parts;
423 for(const auto &d : s) parts.push_back(canonDisjunct(d));
424 std::sort(parts.begin(), parts.end());
425 parts.erase(std::unique(parts.begin(), parts.end()), parts.end());
426 std::string out;
427 for(const auto &p : parts){ out += p; out += "|"; }
428 return out;
429 }
430
431 // -- recursion -----------------------------------------------------------
432
433 pg_uuid_t compile(const Sentence &sentence, bool top=false);
434
435 /// Find a separator: a unification class of variable positions present in
436 /// every atom of every disjunct. Returns the (rel,pos) occurrences of the
437 /// class via @p occ and per-disjunct the variable ids of the class via
438 /// @p classVarsPerDisj; returns false if no separator exists.
439 bool findSeparator(const Sentence &s,
440 std::set<std::pair<unsigned,int>> &occ,
441 std::vector<std::set<long>> &classVarsPerDisj);
442
443 /// Möbius (inclusion-exclusion) step over the components of @p s.
444 pg_uuid_t mobiusStep(const Sentence &s, bool top);
445};
446
447// --- separator -------------------------------------------------------------
448
449bool MobiusCompiler::findSeparator(const Sentence &s,
450 std::set<std::pair<unsigned,int>> &occOut,
451 std::vector<std::set<long>> &classVarsPerDisj)
452{
453 // Collect all variable ids and union those sharing a (rel,pos).
454 std::map<long,int> id;
455 auto vid = [&](long v)->int{
456 auto it=id.find(v); if(it!=id.end()) return it->second;
457 int n=static_cast<int>(id.size()); id[v]=n; return n;
458 };
459 for(const auto &d : s) for(const auto &a : d) for(const auto &t : a.args)
460 if(t.isVar) vid(t.v);
461 if(id.empty()) return false;
462 UF uf(static_cast<int>(id.size()));
463 std::map<std::pair<unsigned,int>, long> firstAtPos;
464 for(const auto &d : s) for(const auto &a : d)
465 for(std::size_t k=0;k<a.args.size();++k) {
466 if(!a.args[k].isVar) continue;
467 auto key = std::make_pair(a.rel, static_cast<int>(k));
468 auto it = firstAtPos.find(key);
469 if(it==firstAtPos.end()) firstAtPos[key]=a.args[k].v;
470 else uf.uni(vid(it->second), vid(a.args[k].v));
471 }
472 // Candidate classes = class roots. A class is a separator iff in every
473 // disjunct, every atom contains a variable of that class.
474 std::vector<long> idToVar(id.size());
475 for(auto &kv : id) idToVar[kv.second]=kv.first;
476
477 std::set<int> roots;
478 for(int i=0;i<static_cast<int>(id.size());++i) roots.insert(uf.find(i));
479
480 for(int root : roots) {
481 bool covers = true;
482 std::vector<std::set<long>> cvars(s.size());
483 for(std::size_t di=0; di<s.size() && covers; ++di) {
484 const Disjunct &d = s[di];
485 for(const auto &a : d) {
486 bool atomHas=false;
487 for(const auto &t : a.args)
488 if(t.isVar && uf.find(vid(t.v))==root) {
489 atomHas=true; cvars[di].insert(t.v);
490 }
491 if(!atomHas){ covers=false; break; }
492 }
493 }
494 if(!covers) continue;
495 // Found. Record the (rel,pos) occurrences of this class for the domain.
496 occOut.clear();
497 for(const auto &d : s) for(const auto &a : d)
498 for(std::size_t k=0;k<a.args.size();++k)
499 if(a.args[k].isVar && uf.find(vid(a.args[k].v))==root)
500 occOut.insert({a.rel, static_cast<int>(k)});
501 classVarsPerDisj = std::move(cvars);
502 return true;
503 }
504 return false;
505}
506
507// --- Möbius step -----------------------------------------------------------
508
509pg_uuid_t MobiusCompiler::mobiusStep(const Sentence &s, bool top)
510{
511 // 1. Components of each disjunct -> a global literal pool with equivalence
512 // collapse. Each disjunct becomes the set of its component literal ids.
513 std::vector<Disjunct> literals; // representative component per id
514 auto litId = [&](const Disjunct &c)->int{
515 for(std::size_t i=0;i<literals.size();++i)
516 if(equiv(literals[i], c)) return static_cast<int>(i);
517 literals.push_back(c);
518 return static_cast<int>(literals.size()-1);
519 };
520 std::vector<std::set<int>> terms; // DNF terms (per disjunct)
521 for(const auto &d : s) {
522 std::set<int> t;
523 for(const auto &c : componentsOf(d)) t.insert(litId(c));
524 terms.push_back(std::move(t));
525 }
526 if(top) st.n_components = static_cast<int>(literals.size());
527
528 // Implication matrix on literals: imp[i][j] == (lit_i implies lit_j),
529 // i.e. lit_i ⊑ lit_j, witnessed by hom(lit_j -> lit_i).
530 const int L = static_cast<int>(literals.size());
531 std::vector<std::vector<char>> imp(L, std::vector<char>(L, 0));
532 for(int i=0;i<L;++i) for(int j=0;j<L;++j)
533 imp[i][j] = hom(literals[j], literals[i]) ? 1 : 0;
534
535 // Minimise a disjunction (literal set): drop lit i if some other lit j in
536 // the set is implied-BY i (i ⊑ j), since i ∨ j ≡ j. Keep the weakest.
537 auto minimiseDisj = [&](std::set<int> in)->std::vector<int>{
538 std::vector<int> v(in.begin(), in.end());
539 std::vector<char> keep(v.size(),1);
540 for(std::size_t a=0;a<v.size();++a) for(std::size_t b=0;b<v.size();++b)
541 if(a!=b && keep[b] && imp[v[a]][v[b]]) { keep[a]=0; break; }
542 std::vector<int> out;
543 for(std::size_t a=0;a<v.size();++a) if(keep[a]) out.push_back(v[a]);
544 std::sort(out.begin(), out.end());
545 return out;
546 };
547
548 // 2. DNF -> CNF: a clause picks one literal from each term and ORs them.
549 // Cap the conjunct count.
550 std::set<std::vector<int>> clauseSet;
551 std::vector<std::set<int>> clauses; // working as literal sets
552 // Enumerate transversals.
553 std::vector<int> pick(terms.size(), 0);
554 // Bound the product to avoid blow-up before the M cap is even computed.
555 unsigned long prod = 1;
556 for(const auto &t : terms) prod *= std::max<std::size_t>(1, t.size());
557 if(prod > 100000UL)
558 throw MobiusDecline("Möbius: DNF->CNF transversal count too large");
559 std::function<void(std::size_t,std::set<int>&)> gen =
560 [&](std::size_t ti, std::set<int> &acc){
561 if(ti==terms.size()){
562 std::vector<int> mn = minimiseDisj(acc);
563 if(!mn.empty()) clauseSet.insert(mn);
564 return;
565 }
566 for(int lit : terms[ti]) {
567 bool fresh = acc.insert(lit).second;
568 gen(ti+1, acc);
569 if(fresh) acc.erase(lit);
570 }
571 };
572 { std::set<int> acc; gen(0, acc); }
573
574 // Clause subsumption: drop clause B if some clause A ⊑ B (every literal of
575 // A is implied by some literal of B... no: ⋁A implies ⋁B iff every a∈A has
576 // a b∈B with a ⊑ b). Then B is redundant in the conjunction.
577 std::vector<std::vector<int>> cl(clauseSet.begin(), clauseSet.end());
578 auto disjImplies = [&](const std::vector<int>&A, const std::vector<int>&B){
579 for(int a : A){ bool ok=false; for(int b : B) if(imp[a][b]){ ok=true; break; }
580 if(!ok) return false; }
581 return true;
582 };
583 std::vector<char> keepCl(cl.size(),1);
584 for(std::size_t a=0;a<cl.size();++a) for(std::size_t b=0;b<cl.size();++b)
585 if(a!=b && keepCl[a] && cl[a]!=cl[b] && disjImplies(cl[a],cl[b]))
586 keepCl[b]=0;
587 std::vector<std::vector<int>> M;
588 for(std::size_t a=0;a<cl.size();++a) if(keepCl[a]) M.push_back(cl[a]);
589
590 const int m = static_cast<int>(M.size());
591 if(top) { st.n_cnf_conjuncts = m; st.lattice_size = (m<=30)?(1<<m):0; }
592 if(m < 2)
593 throw MobiusDecline("Möbius: no inclusion-exclusion structure (M<2); "
594 "sentence has no separator and does not decompose");
595 if(m > 8)
596 throw MobiusDecline("Möbius: CNF conjunct count exceeds the cap (M>8)");
597
598 // 3. Enumerate non-empty subsets s of [m]; ψ_s = ⋁ of literals across the
599 // clauses in s, minimised. Accumulate coefficient (-1)^{|s|+1} per
600 // distinct ψ_s (keyed by its minimised literal-id set).
601 std::map<std::vector<int>, long> coeff;
602 for(unsigned mask=1; mask < (1u<<m); ++mask) {
603 std::set<int> lits;
604 for(int i=0;i<m;++i) if(mask & (1u<<i))
605 lits.insert(M[i].begin(), M[i].end());
606 std::vector<int> key = minimiseDisj(lits);
607 const int pc = __builtin_popcount(mask);
608 coeff[key] += (pc & 1) ? 1 : -1;
609 }
610 if(top) st.lattice_collapsed = static_cast<int>(coeff.size());
611
612 // 4. Build the gate_mobius over the surviving elements. Each element is a
613 // pure disjunction of its literal components.
614 std::vector<pg_uuid_t> children;
615 std::vector<long> coeffs;
616 int cancelled = 0;
617 bool cancelledHard = false;
618 for(const auto &kv : coeff) {
619 if(kv.second == 0) {
620 ++cancelled;
621 // Is this cancelled element #P-hard (no separator, no decomposition)?
622 // Probe cheaply: a pure disjunction of these literals with shared
623 // symbols and no separator is the hard term whose cancellation makes
624 // the query tractable.
625 if(!cancelledHard) {
626 Sentence el;
627 for(int lid : kv.first) el.push_back(literals[lid]);
628 std::set<std::pair<unsigned,int>> occ;
629 std::vector<std::set<long>> cv;
630 // Symbol-connected and no separator => hard.
631 if(el.size() >= 2 && !findSeparator(el, occ, cv)) {
632 // check symbol-connected
633 UF uf(static_cast<int>(el.size()));
634 std::map<unsigned,int> firstRelDisj;
635 for(std::size_t i=0;i<el.size();++i)
636 for(const auto &a : el[i])
637 { auto it=firstRelDisj.find(a.rel);
638 if(it==firstRelDisj.end()) firstRelDisj[a.rel]=static_cast<int>(i);
639 else uf.uni(it->second, static_cast<int>(i)); }
640 std::set<int> r; for(std::size_t i=0;i<el.size();++i) r.insert(uf.find(static_cast<int>(i)));
641 if(r.size()==1) cancelledHard = true;
642 }
643 }
644 continue;
645 }
646 Sentence el;
647 for(int lid : kv.first) el.push_back(literals[lid]);
648 children.push_back(compile(el, false));
649 coeffs.push_back(kv.second);
650 }
651 if(top) { st.n_cancelled = cancelled; st.cancelled_hard = cancelledHard;
652 st.n_nonzero = static_cast<int>(children.size()); }
653
654 if(children.empty())
655 return mkConst(false);
656 // The top-level Möbius step inlines the literal lineage onto its own gate so
657 // the root gate_mobius carries it directly (single level); nested steps do
658 // not (their values are discarded by the transparent-to-lineage passthrough).
659 if(top && !pending_lineage.empty()) {
660 lineage_consumed = true;
661 return mkMobius(children, coeffs, pending_lineage);
662 }
663 return mkMobius(children, coeffs);
664}
665
666// --- main recursion --------------------------------------------------------
667
668pg_uuid_t MobiusCompiler::compile(const Sentence &sentence0, bool top)
669{
670 CHECK_FOR_INTERRUPTS();
671
672 // 1. Drop disjuncts containing an ABSENT ground atom (the conjunction is
673 // then FALSE). Present ground atoms are kept: under TID a tuple in the
674 // relation is a probabilistic Bernoulli event (its input token), NOT a
675 // certainty, so it stays an atom and bottoms out as a token leaf.
676 Sentence s;
677 for(const Disjunct &d0 : sentence0) {
678 bool dead = false;
679 for(const MAtom &a : d0) {
680 if(ground(a)) {
681 std::vector<long> el;
682 for(const auto &t : a.args) el.push_back(t.v);
683 if(!fi.isPresent(a.rel, el)) { dead=true; break; }
684 }
685 }
686 if(!dead && !d0.empty()) s.push_back(d0);
687 }
688 if(s.empty())
689 return mkConst(false);
690
691 // Base case: a single fully-ground disjunct is a conjunction of tuples ->
692 // the independent AND of their input tokens (a certain / untracked tuple
693 // contributes the identity gate_one).
694 auto isGround = [&](const Disjunct &d){
695 for(const auto &a : d) if(!ground(a)) return false;
696 return true;
697 };
698 auto tokenAnd = [&](const Disjunct &d)->pg_uuid_t {
699 std::vector<pg_uuid_t> toks;
700 for(const auto &a : d) {
701 std::vector<long> el; for(const auto &t : a.args) el.push_back(t.v);
702 const std::string *tk = fi.token(a.rel, el);
703 if(tk==nullptr) return mkConst(false); // absent (shouldn't reach here)
704 if(tk->empty()) continue; // certain fact: identity for AND
705 toks.push_back(string2uuid(*tk));
706 }
707 return mkBool(true, toks); // empty -> gate_one (all certain)
708 };
709
710 // Memoisation.
711 const std::string key = canonSentence(s);
712 {
713 auto it = memo.find(key);
714 if(it!=memo.end()) { ++st.memo_hits; return string2uuid(it->second); }
715 }
716
717 pg_uuid_t result;
718
719 // 2. Independent union: split disjuncts into relation-symbol-connected
720 // groups (disjoint vocabulary => independent).
721 {
722 const int n = static_cast<int>(s.size());
723 UF uf(n);
724 std::map<unsigned,int> firstDisjWithRel;
725 for(int i=0;i<n;++i)
726 for(const auto &a : s[i]) {
727 auto it=firstDisjWithRel.find(a.rel);
728 if(it==firstDisjWithRel.end()) firstDisjWithRel[a.rel]=i;
729 else uf.uni(it->second, i);
730 }
731 std::map<int, Sentence> groups;
732 for(int i=0;i<n;++i) groups[uf.find(i)].push_back(s[i]);
733 if(groups.size() > 1) {
734 std::vector<pg_uuid_t> ch;
735 for(auto &kv : groups) ch.push_back(compile(kv.second, false));
736 result = mkBool(false, ch);
737 memo[key] = uuid2string(result);
738 return result;
739 }
740 }
741
742 // 3. Single symbol-connected group.
743 if(s.size()==1) {
744 // Fully ground -> AND of tuple tokens (base case).
745 if(isGround(s[0])) {
746 result = tokenAnd(s[0]);
747 memo[key] = uuid2string(result);
748 return result;
749 }
750 // One disjunct (a CQ): split into variable-connected components and AND
751 // them. Independence of the product needs the components to use disjoint
752 // relation symbols (reduced form); a shared symbol is a within-disjunct
753 // self-join (or a ground/var correlation on one relation) -- decline
754 // (handling it would need ranking/shattering).
755 std::vector<Disjunct> comps = componentsOf(s[0]);
756 if(comps.size() > 1) {
757 for(std::size_t i=0;i<comps.size();++i)
758 for(std::size_t j=i+1;j<comps.size();++j) {
759 std::set<unsigned> ri;
760 for(const auto &a : comps[i]) ri.insert(a.rel);
761 for(const auto &a : comps[j])
762 if(ri.count(a.rel))
763 throw MobiusDecline("Möbius: within-disjunct self-join (a "
764 "relation shared by two components) -- needs "
765 "ranking/shattering");
766 }
767 std::vector<pg_uuid_t> ch;
768 for(auto &c : comps) { Sentence one{c}; ch.push_back(compile(one,false)); }
769 result = mkBool(true, ch);
770 memo[key] = uuid2string(result);
771 return result;
772 }
773 // Single connected CQ: must have a separator (a var in all atoms) or it is
774 // #P-hard. Handled by the separator branch below.
775 }
776
777 // 4. Separator (independent project) if one exists.
778 {
779 std::set<std::pair<unsigned,int>> occ;
780 std::vector<std::set<long>> classVars;
781 if(findSeparator(s, occ, classVars)) {
782 // Active domain of the separator class.
783 std::set<long> dom;
784 for(const auto &rp : occ) {
785 auto it = fi.domain.find(rp);
786 if(it!=fi.domain.end()) dom.insert(it->second.begin(), it->second.end());
787 }
788 std::vector<pg_uuid_t> ch;
789 for(long a : dom) {
790 // Substitute every class variable (per disjunct) by the constant a.
791 Sentence sub;
792 sub.reserve(s.size());
793 for(std::size_t di=0; di<s.size(); ++di) {
794 Disjunct nd;
795 for(const MAtom &at : s[di]) {
796 MAtom na = at;
797 for(auto &t : na.args)
798 if(t.isVar && classVars[di].count(t.v)) { t.isVar=false; t.v=a; }
799 nd.push_back(std::move(na));
800 }
801 sub.push_back(std::move(nd));
802 }
803 ch.push_back(compile(sub, false)); // pieces for distinct a independent
804 }
805 result = mkBool(false, ch); // independent OR over the domain
806 memo[key] = uuid2string(result);
807 return result;
808 }
809 }
810
811 // 5. No separator: a single connected CQ here is genuinely #P-hard; a
812 // multi-disjunct sentence goes to the Möbius (inclusion-exclusion) step.
813 if(s.size()==1)
814 throw MobiusDecline("Möbius: non-hierarchical single CQ (no separator); "
815 "the query is #P-hard");
816 result = mobiusStep(s, top);
817 memo[key] = uuid2string(result);
818 return result;
819}
820
821// ===========================================================================
822// Argument decoding (mirrors ucq_joint's columnar convention).
823// ===========================================================================
824
825int checkedLen(ArrayType *a, const char *what) {
826 if(a==NULL) return 0;
827 if(ARR_NDIM(a)>1) provsql_error("ucq_mobius: %s must be 1-D", what);
828 if(ARR_HASNULL(a)) provsql_error("ucq_mobius: %s must not contain NULLs", what);
829 return ARR_NDIM(a)==0 ? 0 : ARR_DIMS(a)[0];
830}
831const int32 *intArr(FunctionCallInfo fcinfo, int n, const char *what, int &len){
832 ArrayType *a = PG_ARGISNULL(n)?NULL:PG_GETARG_ARRAYTYPE_P(n);
833 len = checkedLen(a, what);
834 return (a==NULL||len==0)?NULL:(const int32*)ARR_DATA_PTR(a);
835}
836
837/// Build the top sentence from the raw query arrays. Disjunct-local variables
838/// are globalised by a per-disjunct offset (returned via @p base) so
839/// unification across disjuncts is by (rel,pos), not by raw id, and so head
840/// variables can be pinned per disjunct.
841Sentence buildSentenceArrays(const int32 *d_nvars, int n_disj,
842 const int32 *a_disj, const int32 *a_rel,
843 const int32 *a_vars, int n_av,
844 const int32 *a_arity, int n_ad,
845 std::vector<long> &base)
846{
847 if(n_disj==0) provsql_error("ucq_mobius: the UCQ has no disjuncts");
848 base.assign(n_disj, 0);
849 long acc = 1; // start at 1 so id 0 is unused (avoids any sentinel clash)
850 for(int d=0; d<n_disj; ++d){ base[d]=acc; acc += d_nvars[d] + 1; }
851
852 Sentence s(n_disj);
853 int voff=0;
854 for(int i=0;i<n_ad;++i){
855 const int d=a_disj[i];
856 if(d<0||d>=n_disj) provsql_error("ucq_mobius: atom disjunct out of range");
857 const int ar=a_arity[i];
858 if(ar<0||voff+ar>n_av)
859 provsql_error("ucq_mobius: atom_vars shorter than arities");
860 MAtom at; at.rel=static_cast<unsigned>(a_rel[i]);
861 for(int k=0;k<ar;++k){
862 Term t; t.isVar=true; t.v = base[d] + a_vars[voff+k];
863 at.args.push_back(t);
864 }
865 voff+=ar;
866 s[d].push_back(std::move(at));
867 }
868 return s;
869}
870
871/// Build a FactIndex (and, optionally, the text->dense-id map) from the raw
872/// fact arrays.
873FactIndex buildFactIndexArrays(const int32 *f_rel, int n_fr,
874 const int32 *f_elems, int n_fe,
875 const int32 *f_arity,
876 const pg_uuid_t *tok)
877{
878 // G3 (tuple independence): every present fact must be gated by a bare
879 // gate_input. A fact whose token is an internal gate (a view-derived /
880 // reachability / repair_key lineage) is correlated -- out of scope for the
881 // lifted-inference recursion, which assumes independence -- so decline and
882 // let the caller fall back (the more general joint-width route already had
883 // its chance). The input-gate enum OID, fetched once.
884 const Oid input_oid = get_constants(true).GATE_TYPE_TO_OID[gate_input];
885
886 FactIndex fi;
887 // Self-join overlap guard: which (rel, element) key first carried each token.
888 // The same base tuple feeding two DISTINCT fact slots (a self-join whose
889 // constant prefilters are not disjoint, so the slots share rows) is a
890 // correlation the independence-assuming recursion cannot represent.
891 std::map<std::string, std::pair<unsigned, std::vector<long>>> token_owner;
892 int eoff=0;
893 for(int i=0;i<n_fr;++i){
894 const int ar=f_arity[i];
895 if(ar<0||eoff+ar>n_fe)
896 provsql_error("ucq_mobius: fact_elems shorter than arities");
897 unsigned rel=static_cast<unsigned>(f_rel[i]);
898 std::vector<long> el;
899 for(int k=0;k<ar;++k){
900 long e=static_cast<long>(f_elems[eoff+k]);
901 el.push_back(e);
902 fi.domain[{rel,k}].insert(e);
903 }
904 eoff+=ar;
905 fi.present.insert({rel, el});
906 bool nil=true;
907 for(int b=0;b<16;++b) if(tok[i].data[b]!=0) nil=false;
908 if(!nil) {
909 pg_uuid_t tk = tok[i];
910 const Datum gt = DirectFunctionCall1(get_gate_type, UUIDPGetDatum(&tk));
911 if(static_cast<Oid>(DatumGetInt32(gt)) != input_oid)
912 throw MobiusDecline("non-TID input: a fact token is not a bare "
913 "gate_input (correlated / derived lineage)");
914 }
915 // Bag multiplicity guard: the fact index is keyed by (rel, element tuple),
916 // one token per key (the SJF-TID reduced form). Two DISTINCT probabilistic
917 // tuples that project to the same element tuple -- e.g. a relation column
918 // not bound by the query, or a duplicate-bearing input -- would silently
919 // collapse to one event (lose the OR), under-counting the probability. The
920 // lifted-inference recursion has no place to represent that disjunction, so
921 // decline and let the joint-width route (which sees full multiplicity) take
922 // it. (A repeated key with the SAME token is the same tuple gathered twice
923 // -- harmless, kept.)
924 const std::string newtok = nil ? std::string() : uuid2string(tok[i]);
925 auto existing = fi.tok.find({rel, el});
926 if(existing != fi.tok.end() && existing->second != newtok)
927 throw MobiusDecline("bag multiplicity: two distinct tuples share an "
928 "element tuple (non-reduced data) -- defer to "
929 "joint-width, which keeps the multiplicity");
930 // The same token under a DIFFERENT (rel, element) key means one base tuple
931 // feeds two fact slots -- an overlapping (non-disjoint) self-join, a
932 // correlation the lifted recursion would wrongly treat as independent.
933 // Decline (joint-width tracks the shared token correctly). A disjoint
934 // self-join shares no token, so it passes.
935 if(!newtok.empty()) {
936 auto own = token_owner.find(newtok);
937 if(own != token_owner.end() && own->second != std::make_pair(rel, el))
938 throw MobiusDecline("self-join overlap: one tuple feeds two fact slots "
939 "(non-disjoint self-join) -- defer to joint-width");
940 token_owner[newtok] = {rel, el};
941 }
942 fi.tok[{rel, el}] = newtok;
943 }
944 return fi;
945}
946
947/// Decode the query arrays (0..4) into the top sentence (Boolean path).
948Sentence decodeQuery(FunctionCallInfo fcinfo)
949{
950 int n_disj,n_ad,n_ar,n_av,n_aa;
951 const int32 *d_nvars = intArr(fcinfo,0,"disjunct_nvars",n_disj);
952 const int32 *a_disj = intArr(fcinfo,1,"atom_disjunct",n_ad);
953 const int32 *a_rel = intArr(fcinfo,2,"atom_rel",n_ar);
954 const int32 *a_vars = intArr(fcinfo,3,"atom_vars",n_av);
955 const int32 *a_arity = intArr(fcinfo,4,"atom_arity",n_aa);
956 if(n_ad!=n_ar || n_ad!=n_aa)
957 provsql_error("ucq_mobius: atom arrays must have the same length");
958 std::vector<long> base;
959 return buildSentenceArrays(d_nvars,n_disj,a_disj,a_rel,a_vars,n_av,
960 a_arity,n_ad,base);
961}
962
963/// Decode the fact arrays (5..8) into a FactIndex (Boolean path).
964FactIndex decodeFacts(FunctionCallInfo fcinfo)
965{
966 int n_fr,n_fe,n_fa,n_ft;
967 const int32 *f_rel = intArr(fcinfo,5,"fact_rel",n_fr);
968 const int32 *f_elems = intArr(fcinfo,6,"fact_elems",n_fe);
969 const int32 *f_arity = intArr(fcinfo,7,"fact_arity",n_fa);
970 ArrayType *toks = PG_ARGISNULL(8)?NULL:PG_GETARG_ARRAYTYPE_P(8);
971 n_ft = checkedLen(toks,"fact_tokens");
972 if(n_fr!=n_fa || n_fr!=n_ft)
973 provsql_error("ucq_mobius: fact arrays must have the same length");
974 const pg_uuid_t *tok = (toks&&n_ft)?(const pg_uuid_t*)ARR_DATA_PTR(toks):NULL;
975 return buildFactIndexArrays(f_rel,n_fr,f_elems,n_fe,f_arity,tok);
976}
977
978// ===========================================================================
979// Per-answer (free head variables): head-pin then compile, one circuit per
980// output group. On the first call of a query the facts are gathered once
981// (ucq_joint_gather) and the value dictionary cached; each group binds its
982// head variables to their values, the head positions are substituted to
983// constants across every disjunct, and the Möbius circuit is compiled and
984// cached. Mirrors ucq_joint_provenance_answer (the per-group caching), but
985// each answer is a separate compile rather than a single sweep.
986// ===========================================================================
987
988struct MobAnswerCache {
989 bool ready = false; ///< gather succeeded
990 Sentence sentence; ///< the UCQ template (global vars)
991 std::vector<long> base; ///< per-disjunct variable offset
992 std::vector<int> d_nvars; ///< per-disjunct n_vars
993 FactIndex fi; ///< the gathered facts
994 std::map<std::string,long> val_to_id; ///< text value -> dense id
995 std::map<std::string,std::string> tokcache; ///< head-key -> token uuid
996};
997
998void mobAnswerCacheDelete(void *arg) { delete reinterpret_cast<MobAnswerCache*>(arg); }
999
1000std::string mobHeadKey(const std::vector<std::string> &vals)
1001{
1002 std::string k;
1003 for(const auto &v : vals){ k += v; k.push_back('\x1f'); }
1004 return k;
1005}
1006
1007/// Gather the facts + value dictionary once (via ucq_joint_gather) into @p c.
1008/// Returns false on any failure (the caller then declines to the fallback).
1009bool mobGather(Datum descriptor, MobAnswerCache *c)
1010{
1011 SPI_connect();
1012 Oid argt[1] = { JSONBOID };
1013 Datum argv[1] = { descriptor };
1014 char argn[1] = { ' ' };
1015 const int rc = SPI_execute_with_args(
1016 "SELECT * FROM provsql.ucq_joint_gather($1)", 1, argt, argv, argn, true, 1);
1017 if(rc != SPI_OK_SELECT || SPI_processed != 1) { SPI_finish(); return false; }
1018
1019 bool ok = true;
1020 try {
1021 TupleDesc td = SPI_tuptable->tupdesc;
1022 HeapTuple row = SPI_tuptable->vals[0];
1023 bool isnull;
1024 auto ia = [&](int col, int &n)->const int32*{
1025 Datum d = SPI_getbinval(row, td, col, &isnull);
1026 if(isnull){ n=0; return nullptr; }
1027 ArrayType *a = DatumGetArrayTypeP(d);
1028 n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
1029 return (const int32*) ARR_DATA_PTR(a);
1030 };
1031 int n_dnv,n_adisj,n_arel,n_avars,n_aarity,n_frel,n_felems,n_farity;
1032 const int32 *dnv = ia(1,n_dnv);
1033 const int32 *adisj = ia(2,n_adisj);
1034 const int32 *arel = ia(3,n_arel);
1035 const int32 *avars = ia(4,n_avars);
1036 const int32 *aarity = ia(5,n_aarity);
1037 const int32 *frel = ia(6,n_frel);
1038 const int32 *felems = ia(7,n_felems);
1039 const int32 *farity = ia(8,n_farity);
1040 Datum dtok = SPI_getbinval(row, td, 9, &isnull);
1041 ArrayType *atok = isnull ? nullptr : DatumGetArrayTypeP(dtok);
1042 const int n_ftok = atok ? ArrayGetNItems(ARR_NDIM(atok), ARR_DIMS(atok)) : 0;
1043 const pg_uuid_t *ftok = atok ? (const pg_uuid_t*) ARR_DATA_PTR(atok) : nullptr;
1044 if(n_frel != n_farity || n_frel != n_ftok)
1045 throw MobiusDecline("ucq_mobius: fact arrays length mismatch");
1046
1047 c->sentence = buildSentenceArrays(dnv,n_dnv,adisj,arel,avars,n_avars,
1048 aarity,n_adisj,c->base);
1049 c->d_nvars.assign(dnv, dnv+n_dnv);
1050 c->fi = buildFactIndexArrays(frel,n_frel,felems,n_felems,farity,ftok);
1051
1052 // The value dictionary: dense id -> text, inverted to text -> id.
1053 Datum dval = SPI_getbinval(row, td, 10, &isnull);
1054 if(!isnull) {
1055 ArrayType *aval = DatumGetArrayTypeP(dval);
1056 Datum *elems; bool *nulls; int nval;
1057 deconstruct_array(aval, TEXTOID, -1, false, TYPALIGN_INT,
1058 &elems, &nulls, &nval);
1059 for(int i=0;i<nval;++i)
1060 if(!nulls[i])
1061 c->val_to_id[TextDatumGetCString(elems[i])] = i;
1062 }
1063 } catch(...) {
1064 ok = false;
1065 }
1066 SPI_finish();
1067 return ok;
1068}
1069
1070} // namespace
1071
1072/**
1073 * @brief Materialise the safe-UCQ Möbius circuit and return its root token.
1074 *
1075 * Columnar arguments (mirrors @c ucq_joint_materialize_tracked, TID inputs):
1076 * 0..4 disjunct_nvars, atom_disjunct, atom_rel, atom_vars, atom_arity
1077 * 5..8 fact_rel, fact_elems, fact_arity, fact_tokens
1078 * 9 lineage (uuid, optional): the literal Boolean provenance of the query,
1079 * carried on the root gate_mobius so the token still answers Shapley /
1080 * semiring / PROV on the normal lineage (the Möbius combination is a
1081 * probability-only shortcut layered over it).
1082 *
1083 * The root is a @c gate_mobius carrying the certified-independent Boolean
1084 * islands (the signed combination) and the lineage child; @c probability_evaluate
1085 * answers the \#P-hard UCQ in PTIME through the fast Möbius route, and every other
1086 * evaluator passes through to the lineage. Declines raise an error so the SQL
1087 * wrapper falls back.
1088 */
1089Datum ucq_mobius_materialize_tracked(PG_FUNCTION_ARGS)
1090{
1091 try {
1092 Sentence s = decodeQuery(fcinfo);
1093 FactIndex fi = decodeFacts(fcinfo);
1094 std::string lineage;
1095 if(!PG_ARGISNULL(9))
1096 lineage = uuid2string(*PG_GETARG_UUID_P(9));
1097 MobiusStats st;
1098 MobiusCompiler mc(fi, st);
1099 pg_uuid_t root = mc.compileTop(s, lineage);
1100 pg_uuid_t *u = (pg_uuid_t*) palloc(sizeof(pg_uuid_t));
1101 *u = root;
1102 PG_RETURN_UUID_P(u);
1103 } catch(const MobiusDecline &e) {
1104 provsql_error("ucq_mobius: %s", e.what());
1105 } catch(const std::exception &e) {
1106 provsql_error("ucq_mobius: %s", e.what());
1107 } catch(...) {
1108 provsql_error("ucq_mobius: unknown exception");
1109 }
1110 PG_RETURN_NULL();
1111}
1112
1113/**
1114 * @brief Compile the Möbius circuit and return the lattice statistics plus the
1115 * probability (the demonstrability surface). Same columnar arguments
1116 * as @c ucq_mobius_materialize_tracked.
1117 */
1118Datum ucq_mobius_compile_stats(PG_FUNCTION_ARGS)
1119{
1120 try {
1121 Sentence s = decodeQuery(fcinfo);
1122 FactIndex fi = decodeFacts(fcinfo);
1123 MobiusStats st;
1124 MobiusCompiler mc(fi, st);
1125 pg_uuid_t root = mc.compileTop(s);
1126 st.probability = mobius_probability_of(root);
1127
1128 TupleDesc tupdesc;
1129 if(get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
1130 provsql_error("ucq_mobius_compile_stats: expected composite return type");
1131 tupdesc = BlessTupleDesc(tupdesc);
1132 Datum values[9];
1133 bool nulls[9] = {false,false,false,false,false,false,false,false,false};
1134 values[0] = Float8GetDatum(st.probability);
1135 values[1] = Int32GetDatum(st.n_components);
1136 values[2] = Int32GetDatum(st.n_cnf_conjuncts);
1137 values[3] = Int32GetDatum(st.lattice_collapsed);
1138 values[4] = Int32GetDatum(st.n_nonzero);
1139 values[5] = Int32GetDatum(st.n_cancelled);
1140 values[6] = BoolGetDatum(st.cancelled_hard);
1141 values[7] = Int64GetDatum(static_cast<int64>(st.dd_size));
1142 values[8] = Int64GetDatum(static_cast<int64>(st.memo_hits));
1143 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
1144 } catch(const std::exception &e) {
1145 provsql_error("ucq_mobius_compile_stats: %s", e.what());
1146 } catch(...) {
1147 provsql_error("ucq_mobius_compile_stats: unknown exception");
1148 }
1149 PG_RETURN_NULL();
1150}
1151
1152/**
1153 * @brief Per-answer Möbius provenance (the planner-substituted entry point for
1154 * a non-Boolean UCQ with free head variables).
1155 *
1156 * Arguments: (descriptor jsonb, head_vars int[], head_vals text[],
1157 * fallback uuid). Called once per output group; on the first call the facts
1158 * are gathered once and the value dictionary cached, then each group pins its
1159 * head variables (the canonical head indices @p head_vars, in every disjunct)
1160 * to their values (@p head_vals, matched through the gather's text dictionary)
1161 * and compiles the head-pinned Möbius circuit, caching head-key -> token. On
1162 * any decline (unsafe shape, head value absent, ...) returns @p fallback.
1163 */
1164Datum ucq_mobius_provenance_answer(PG_FUNCTION_ARGS)
1165{
1166 MobAnswerCache *cache =
1167 reinterpret_cast<MobAnswerCache*>(fcinfo->flinfo->fn_extra);
1168
1169 if(cache == nullptr) {
1170 MemoryContext fnctx = fcinfo->flinfo->fn_mcxt;
1171 cache = new MobAnswerCache();
1172 MemoryContextCallback *cb = (MemoryContextCallback*)
1173 MemoryContextAllocZero(fnctx, sizeof(MemoryContextCallback));
1174 cb->func = mobAnswerCacheDelete;
1175 cb->arg = cache;
1176 MemoryContextRegisterResetCallback(fnctx, cb);
1177 fcinfo->flinfo->fn_extra = cache;
1178
1179 if(!PG_ARGISNULL(0)) {
1180 // Gather inside a subtransaction so a SQL error declines gracefully.
1181 MemoryContext oldcxt = CurrentMemoryContext;
1182 ResourceOwner oldowner = CurrentResourceOwner;
1183 BeginInternalSubTransaction(NULL);
1184 PG_TRY();
1185 {
1186 cache->ready = mobGather(PG_GETARG_DATUM(0), cache);
1187 ReleaseCurrentSubTransaction();
1188 MemoryContextSwitchTo(oldcxt);
1189 CurrentResourceOwner = oldowner;
1190 }
1191 PG_CATCH();
1192 {
1193 MemoryContextSwitchTo(oldcxt);
1194 RollbackAndReleaseCurrentSubTransaction();
1195 MemoryContextSwitchTo(oldcxt);
1196 CurrentResourceOwner = oldowner;
1197 FlushErrorState();
1198 cache->ready = false;
1199 }
1200 PG_END_TRY();
1201 }
1202 }
1203
1204 // The group's head values (text), and the head variable indices.
1205 if(cache->ready && !PG_ARGISNULL(1) && !PG_ARGISNULL(2)) {
1206 std::vector<int> head_vars;
1207 {
1208 ArrayType *a = PG_GETARG_ARRAYTYPE_P(1);
1209 const int32 *d = (const int32*) ARR_DATA_PTR(a);
1210 const int n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
1211 for(int i=0;i<n;++i) head_vars.push_back(d[i]);
1212 }
1213 std::vector<std::string> head_vals;
1214 {
1215 ArrayType *a = PG_GETARG_ARRAYTYPE_P(2);
1216 Datum *elems; bool *nulls; int n;
1217 deconstruct_array(a, TEXTOID, -1, false, TYPALIGN_INT, &elems, &nulls, &n);
1218 for(int i=0;i<n;++i)
1219 head_vals.push_back(nulls[i] ? std::string() : TextDatumGetCString(elems[i]));
1220 }
1221
1222 if(head_vars.size() == head_vals.size()) {
1223 const std::string key = mobHeadKey(head_vals);
1224 auto it = cache->tokcache.find(key);
1225 if(it != cache->tokcache.end()) {
1226 pg_uuid_t *u = (pg_uuid_t*) palloc(sizeof(pg_uuid_t));
1227 *u = string2uuid(it->second);
1228 PG_RETURN_UUID_P(u);
1229 }
1230 try {
1231 // Map each head value to its dense id; pin the head variable (canonical
1232 // index hv, hence global base[d]+hv in every disjunct) to that constant.
1233 Sentence s = cache->sentence;
1234 bool resolved = true;
1235 for(std::size_t h=0; h<head_vars.size() && resolved; ++h) {
1236 auto vit = cache->val_to_id.find(head_vals[h]);
1237 if(vit == cache->val_to_id.end()) { resolved = false; break; }
1238 const long val = vit->second;
1239 const int hv = head_vars[h];
1240 for(std::size_t d=0; d<s.size(); ++d) {
1241 const long gid = cache->base[d] + hv;
1242 for(MAtom &at : s[d])
1243 for(Term &t : at.args)
1244 if(t.isVar && t.v == gid) { t.isVar=false; t.v=val; }
1245 }
1246 }
1247 if(resolved) {
1248 // The per-group literal lineage (the normal per-answer provenance,
1249 // argument 3) is carried on the gate_mobius so this answer's token
1250 // still answers Shapley / semiring on its normal lineage.
1251 std::string lineage;
1252 if(!PG_ARGISNULL(3))
1253 lineage = uuid2string(*PG_GETARG_UUID_P(3));
1254 MobiusStats st;
1255 MobiusCompiler mc(cache->fi, st);
1256 pg_uuid_t root = mc.compileTop(s, lineage);
1257 cache->tokcache[key] = uuid2string(root);
1258 pg_uuid_t *u = (pg_uuid_t*) palloc(sizeof(pg_uuid_t));
1259 *u = root;
1260 PG_RETURN_UUID_P(u);
1261 }
1262 } catch(...) {
1263 // decline this group -> fallback
1264 }
1265 }
1266 }
1267
1268 if(PG_ARGISNULL(3))
1269 PG_RETURN_NULL();
1270 PG_RETURN_DATUM(PG_GETARG_DATUM(3));
1271}
pg_uuid_t provsqlUuidV5(const std::string &name)
RFC 4122 version-5 UUID in the ProvSQL namespace.
Content-addressed materialisation of a certified d-D into the mmap provenance store.
static CircuitCache cache
Process-local singleton circuit gate cache.
Fix macro conflicts between PostgreSQL headers and the C++ STL/Boost.
PostgreSQL cross-version compatibility shims for ProvSQL.
#define TYPALIGN_CHAR
#define TYPALIGN_INT
Alignment codes for the array routines (construct_array / deconstruct_array).
Datum ucq_mobius_materialize_tracked(PG_FUNCTION_ARGS)
Materialise the safe-UCQ Möbius circuit and return its root token.
Datum ucq_mobius_provenance_answer(PG_FUNCTION_ARGS)
Per-answer Möbius provenance (the planner-substituted entry point for a non-Boolean UCQ with free hea...
Datum get_gate_type(PG_FUNCTION_ARGS)
PostgreSQL-callable wrapper for get_gate_type().
int provsql_mobius_max_gates
Data-cost cap of the Möbius route: it declines (falling through to joint-width / the ladder) once its...
Definition provsql.c:102
Datum ucq_mobius_compile_stats(PG_FUNCTION_ARGS)
Compile the Möbius circuit and return the lattice statistics plus the probability (the demonstrabilit...
Shared declaration for the Möbius-route probability sweep.
double mobius_probability_of(pg_uuid_t token)
Probability of a Möbius-route token (a gate_mobius-rooted circuit, or any Boolean island beneath one)...
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
void provsql_internal_set_extra(const pg_uuid_t *token, const char *str)
Internal entry point behind set_extra(): worker IPC only.
void provsql_internal_create_gate(const pg_uuid_t *token, gate_type type, unsigned nb_children, const pg_uuid_t *children_data)
Internal entry point behind create_gate(): cache + worker IPC.
Background worker and IPC primitives for mmap-backed circuit storage.
constants_t get_constants(bool failure_if_not_possible)
Retrieve the cached OID constants for the current database.
Core types, constants, and utilities shared across ProvSQL.
@ gate_mobius
Signed Möbius combination: a MEASURE-only gate carrying one integer coefficient per child (in extra,...
pg_uuid_t string2uuid(const string &source)
Parse a UUID string into a pg_uuid_t.
string uuid2string(pg_uuid_t uuid)
Format a pg_uuid_t as a std::string.
bool operator==(const pg_uuid_t &u, const pg_uuid_t &v)
Test two pg_uuid_t values for equality.
C++ utility functions for UUID manipulation.
Oid GATE_TYPE_TO_OID[nb_gate_types]
Array of the OID of each provenance_gate ENUM value.
Oid OID_FUNCTION_PROVENANCE_PLUS
OID of the provenance_plus FUNCTION.
Oid OID_FUNCTION_PROVENANCE_TIMES
OID of the provenance_times FUNCTION.
UUID structure.