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 * Self-joins and constants are handled by the two normalizations the JACM
35 * dichotomy runs before its lifted recursion -- **ranking** (Def. 4.1) and
36 * **shattering** (Prop. 2.10) -- implemented here as one shard split
37 * (@c normalizeShards): each relation whose atoms do not all carry the trivial
38 * all-distinct-variables pattern is partitioned into shard symbols, one per
39 * (equality pattern, pinned query constants) signature of its tuples, and every
40 * atom is expanded into the disjunction of its compatible shards. Because the
41 * shards partition the relation, disjoint shard symbols certify disjoint
42 * tuples, which is what the independence steps of the recursion rest on. A
43 * within-disjunct self-join that survives normalization (two components over
44 * one shard symbol, the JACM's @f$q_J@f$) is not independent and is not
45 * declined either: it falls through to the Möbius step, whose
46 * @f$P(c_1\land c_2) = P(c_1)+P(c_2)-P(c_1\lor c_2)@f$ is exactly the
47 * disjunctive detour the self-join forces (JACM Example 3.1).
48 *
49 * Restrictions: tuple-independent (TID) inputs, one probabilistic tuple per
50 * (relation, element tuple), and no two fact slots sharing a base tuple. A
51 * query shape the recursion cannot certify (an inversion, a \#P-hard CQ, a cap
52 * exceeded) declines (a C++ exception caught at the SQL boundary), so the query
53 * falls back to the normal provenance and never fails.
54 */
55extern "C" {
56#include "postgres.h"
57#include "fmgr.h"
58#include "funcapi.h"
59#include "miscadmin.h"
60#include "access/htup_details.h" /* heap_form_tuple (PG 10 declares it here) */
61#include "access/xact.h"
62#include "catalog/pg_type.h"
63#include "executor/spi.h"
64#include "utils/array.h"
65#include "utils/builtins.h"
66#include "utils/resowner.h"
67#include "utils/uuid.h"
68
69#include "compatibility.h" /* TYPALIGN_INT / TYPALIGN_CHAR fallback for PG < 13 */
70#include "provsql_utils.h"
71#include "provsql_mmap.h"
72
73/* Store-backed gate-type lookup (defined in provsql_mmap.c); used to enforce
74 * the TID restriction (G3) -- every fact token must be a bare gate_input. */
75extern Datum get_gate_type(PG_FUNCTION_ARGS);
76
77/* Data-cost cap (provsql.mobius_max_gates GUC, defined in provsql.c): the
78 * Möbius recursion is O(|D|^k), so a high-level safe query on large data is
79 * bounded by declining once it has built this many gates. */
81
82PG_FUNCTION_INFO_V1(ucq_mobius_materialize_tracked);
83PG_FUNCTION_INFO_V1(ucq_mobius_compile_stats);
84PG_FUNCTION_INFO_V1(ucq_mobius_provenance_answer);
85}
86
87#include "c_cpp_compatibility.h"
89#include "mobius_evaluate.h"
90#include "provsql_utils_cpp.h"
91
92#include <algorithm>
93#include <cmath>
94#include <cstdint>
95#include <functional>
96#include <limits>
97#include <map>
98#include <set>
99#include <stdexcept>
100#include <string>
101#include <unordered_map>
102#include <unordered_set>
103#include <vector>
104
105namespace {
106
107/** @brief Thrown when the Möbius route declines (unsafe shape, cap hit, ...). */
108class MobiusDecline : public std::runtime_error {
109public:
110 explicit MobiusDecline(const std::string &w) : std::runtime_error(w) {}
111};
112
113/** @brief One argument position of an atom: a variable or a bound constant. */
114struct Term {
115 bool isVar;
116 long v; ///< Variable id (isVar) or constant element-id (!isVar).
117 bool operator==(const Term &o) const { return isVar==o.isVar && v==o.v; }
118};
119
120/** @brief A relational atom: a relation symbol applied to terms. */
121struct MAtom {
122 unsigned rel;
123 std::vector<Term> args;
124};
125
126using Disjunct = std::vector<MAtom>; ///< A conjunction of atoms.
127using Sentence = std::vector<Disjunct>;///< A disjunction of conjunctions (UCQ).
128
129// ---------------------------------------------------------------------------
130// Statistics surfaced by the stats SRF (the demonstrability requirement).
131// ---------------------------------------------------------------------------
132struct MobiusStats {
133 int n_components = 0; ///< distinct connected-component literals at top
134 int n_cnf_conjuncts = 0; ///< M, the CNF conjunct count at the top level
135 int lattice_size = 0; ///< 2^M subsets enumerated at the top level
136 int lattice_collapsed = 0; ///< distinct elements after equivalence collapse
137 int n_nonzero = 0; ///< elements with coefficient != 0
138 int n_cancelled = 0; ///< distinct elements with coefficient == 0
139 bool cancelled_hard = false;///< some cancelled element is #P-hard (no separator)
140 long dd_size = 0; ///< gates materialised
141 long memo_hits = 0; ///< sentence-memo hits
142 double probability = 0.0; ///< P(Q)
143};
144
145// ===========================================================================
146// Homomorphisms (CQ containment) over single-component conjunctions.
147// ===========================================================================
148
149/// Backtracking homomorphism search: does a mapping of @p p's variables to
150/// @p q's terms exist sending every @p p-atom onto a @p q-atom (constants
151/// matched verbatim)? @p p, @p q are tiny (reduced-form components), so the
152/// naive search is cheap. Returns true iff @p p ⊑ @p q (i.e. @p q implies
153/// @p p as a query -- a hom p->q means every model of p is a model of q is the
154/// usual direction; here hom(p,q) maps p's atoms into q, witnessing q ⊑ p).
155bool homExists(const Disjunct &p, const Disjunct &q,
156 std::size_t ai, std::map<long,Term> &asg)
157{
158 if(ai == p.size())
159 return true;
160 const MAtom &pa = p[ai];
161 for(const MAtom &qa : q) {
162 if(qa.rel != pa.rel || qa.args.size() != pa.args.size())
163 continue;
164 std::vector<long> undo;
165 bool ok = true;
166 for(std::size_t k=0; k<pa.args.size(); ++k) {
167 const Term &pt = pa.args[k];
168 const Term &qt = qa.args[k];
169 if(!pt.isVar) {
170 if(qt.isVar || qt.v != pt.v) { ok=false; break; }
171 } else {
172 auto it = asg.find(pt.v);
173 if(it == asg.end()) { asg[pt.v]=qt; undo.push_back(pt.v); }
174 else if(!(it->second == qt)) { ok=false; break; }
175 }
176 }
177 if(ok && homExists(p, q, ai+1, asg))
178 return true;
179 for(long u : undo) asg.erase(u);
180 }
181 return false;
182}
183
184/// hom from @p p into @p q (maps p's atoms onto q's): witnesses q ⊑ p.
185bool hom(const Disjunct &p, const Disjunct &q)
186{
187 std::map<long,Term> asg;
188 return homExists(p, q, 0, asg);
189}
190
191/// Logical equivalence of two single-component conjunctions: mutual
192/// homomorphism, which is logical equivalence for conjunctive queries whether
193/// or not the conjunction is a core (on a self-join-free component, where the
194/// conjunction is its own core, it degenerates to isomorphism).
195bool equiv(const Disjunct &p, const Disjunct &q)
196{
197 return hom(p,q) && hom(q,p);
198}
199
200// ===========================================================================
201// Connected components.
202// ===========================================================================
203
204struct UF {
205 std::vector<int> p;
206 explicit UF(int n) : p(n) { for(int i=0;i<n;++i) p[i]=i; }
207 int find(int x){ while(p[x]!=x){ p[x]=p[p[x]]; x=p[x]; } return x; }
208 void uni(int a,int b){ p[find(a)]=find(b); }
209};
210
211/// Split a conjunction into its variable-connected components. Distinct
212/// components are probabilistically independent exactly when their fact
213/// footprints are disjoint (@c disjointFootprints); a shared relation symbol
214/// with no separating constant is a self-join, and goes to the Möbius step
215/// instead.
216std::vector<Disjunct> componentsOf(const Disjunct &d)
217{
218 const int n = static_cast<int>(d.size());
219 UF uf(n);
220 std::map<long, int> firstAtomOfVar;
221 for(int i=0;i<n;++i)
222 for(const auto &t : d[i].args)
223 if(t.isVar) {
224 auto it = firstAtomOfVar.find(t.v);
225 if(it==firstAtomOfVar.end()) firstAtomOfVar[t.v]=i;
226 else uf.uni(it->second, i);
227 }
228 std::map<int, Disjunct> byroot;
229 for(int i=0;i<n;++i)
230 byroot[uf.find(i)].push_back(d[i]);
231 std::vector<Disjunct> out;
232 for(auto &kv : byroot) out.push_back(std::move(kv.second));
233 return out;
234}
235
236/// Is an atom fully ground (no variables)?
237bool ground(const MAtom &a)
238{
239 for(const auto &t : a.args) if(t.isVar) return false;
240 return true;
241}
242
243// ===========================================================================
244// Data index (TID facts).
245// ===========================================================================
246
247struct FactIndex {
248 // (rel, element tuple) -> provenance token (UUID string). A certain
249 // (untracked) fact has an empty token.
250 std::map<std::pair<unsigned, std::vector<long>>, std::string> tok;
251 std::set<std::pair<unsigned, std::vector<long>>> present;
252 // (rel, position) -> active domain (distinct values seen there).
253 std::map<std::pair<unsigned,int>, std::set<long>> domain;
254
255 bool isPresent(unsigned rel, const std::vector<long> &el) const {
256 return present.count({rel, el}) > 0;
257 }
258 const std::string *token(unsigned rel, const std::vector<long> &el) const {
259 auto it = tok.find({rel, el});
260 return it==tok.end() ? nullptr : &it->second;
261 }
262};
263
264// ===========================================================================
265// Canonical structural keys (memoisation, and duplicate elimination).
266// ===========================================================================
267
268std::string canonDisjunct(const Disjunct &d)
269{
270 // Canonically rename variables by first occurrence in a sorted atom order.
271 // Build per-atom signatures with placeholder variable slots, then pick the
272 // lexicographically smallest atom ordering greedily (sufficient: keys only
273 // need same-structure -> same-key, not minimality).
274 std::vector<std::string> atomsigRaw;
275 for(const auto &a : d) {
276 std::string sg = "r" + std::to_string(a.rel) + "(";
277 for(const auto &t : a.args) {
278 if(t.isVar) sg += "v"; else sg += "c"+std::to_string(t.v);
279 sg += ",";
280 }
281 sg += ")";
282 atomsigRaw.push_back(sg);
283 }
284 // Sort atoms by raw signature, then assign canonical var ids in first
285 // occurrence order over that sorted sequence.
286 std::vector<int> order(d.size());
287 for(std::size_t i=0;i<d.size();++i) order[i]=static_cast<int>(i);
288 std::sort(order.begin(), order.end(),
289 [&](int a,int b){ return atomsigRaw[a]<atomsigRaw[b]; });
290 std::map<long,int> ren;
291 std::string out;
292 for(int idx : order) {
293 const MAtom &a = d[idx];
294 out += "r"+std::to_string(a.rel)+"(";
295 for(const auto &t : a.args) {
296 if(t.isVar) {
297 auto it = ren.find(t.v);
298 int id = (it==ren.end()) ? (ren[t.v]=static_cast<int>(ren.size())) : it->second;
299 out += "v"+std::to_string(id);
300 } else out += "c"+std::to_string(t.v);
301 out += ",";
302 }
303 out += ")";
304 }
305 return out;
306}
307
308std::string canonSentence(const Sentence &s)
309{
310 std::vector<std::string> parts;
311 for(const auto &d : s) parts.push_back(canonDisjunct(d));
312 std::sort(parts.begin(), parts.end());
313 parts.erase(std::unique(parts.begin(), parts.end()), parts.end());
314 std::string out;
315 for(const auto &p : parts){ out += p; out += "|"; }
316 return out;
317}
318
319// ===========================================================================
320// Independence certificates: fact footprints.
321//
322// Every independent gate the compiler builds (the disjoint-symbol product, the
323// independent union, the separator's independent project) is licensed by ONE
324// property: the sub-sentences it combines read disjoint sets of base tuples.
325// A footprint records that symbolically -- per atom, its relation and the
326// positions it pins to a constant -- and two footprints are certified disjoint
327// when every pair of atom patterns over one relation pins a common position to
328// two different constants (no tuple can satisfy both). Sound, and deliberately
329// not complete: an undetected disjointness costs a decline, never a wrong
330// answer.
331// ===========================================================================
332
333/// One atom's reach: its relation and its position -> constant pins.
334using FactPattern = std::pair<unsigned, std::map<int,long>>;
335using Footprint = std::set<FactPattern>;
336
337void addToFootprint(const Disjunct &d, Footprint &fp)
338{
339 for(const auto &a : d) {
340 std::map<int,long> c;
341 for(std::size_t i=0;i<a.args.size();++i)
342 if(!a.args[i].isVar) c[static_cast<int>(i)] = a.args[i].v;
343 fp.insert({a.rel, std::move(c)});
344 }
345}
346
347Footprint footprintOf(const Disjunct &d)
348{
349 Footprint fp; addToFootprint(d, fp); return fp;
350}
351
352/// Can no tuple satisfy both atom patterns?
353bool patternsDisjoint(const FactPattern &x, const FactPattern &y)
354{
355 if(x.first != y.first)
356 return true; // distinct relations: disjoint tuples
357 for(const auto &kv : x.second) {
358 auto it = y.second.find(kv.first);
359 if(it != y.second.end() && it->second != kv.second)
360 return true; // a position pinned to two constants
361 }
362 return false;
363}
364
365bool disjointFootprints(const Footprint &a, const Footprint &b)
366{
367 for(const auto &x : a)
368 for(const auto &y : b)
369 if(!patternsDisjoint(x,y)) return false;
370 return true;
371}
372
373// ===========================================================================
374// Shard normalization: ranking (Dalvi-Suciu Def. 4.1) and shattering
375// (Prop. 2.10).
376//
377// The lifted recursion certifies independence by relation symbol, which holds
378// only when every atom over a symbol reads the same, full relation. Two
379// rewrites restore that:
380//
381// - *shattering* separates an atom that pins a query constant (S(a,y), left
382// behind by a head pin or by a separator substitution) from one that does
383// not (S(x,z)): they overlap, and the shards S_{0=a} / S_{0≠a} do not;
384// - *ranking* separates an atom with a repeated variable (S(x,x)) from one
385// with distinct variables: the shards are the equality patterns of the
386// tuples (at most Bell(k) of them for arity k).
387//
388// Both are the same construction: partition each relation's tuples by their
389// signature -- the equality pattern of their positions, plus the query constant
390// each block is pinned to -- allocate one shard symbol per signature, and
391// expand every atom into the disjunction of the shards it can match, unifying
392// the variables and substituting the constants each shard forces. Since every
393// tuple lands in exactly one shard, the shards are disjoint (which is what the
394// independence certificates need) and each shard atom is back in reduced form:
395// distinct variables, no constants (pinned and duplicated positions are
396// projected away, so a shard has the arity of its free blocks).
397//
398// The pass is a no-op -- symbols, facts and cost untouched -- on a sentence
399// whose atoms all carry the trivial all-distinct-variables pattern, which is
400// every query the array decoder builds; it fires on head-pinned per-answer
401// queries and on manually written descriptors with repeated variables.
402// ===========================================================================
403
404/// Sentinel in @c ShardSig::cst for a block pinned to no query constant.
405const long SHARD_FREE = std::numeric_limits<long>::min();
406
407/// Signature of a tuple: for each position the representative position of its
408/// equality block, and per representative the query constant the block is
409/// pinned to (@c SHARD_FREE if its value is not a query constant).
410struct ShardSig {
411 std::vector<int> rep;
412 std::vector<long> cst;
413 bool operator<(const ShardSig &o) const {
414 if(rep != o.rep) return rep < o.rep;
415 return cst < o.cst;
416 }
417};
418
419/// One shard of a relation: its allocated symbol, its signature, and the
420/// representative positions that survive the projection (its arity).
421struct Shard {
422 unsigned sym = 0;
423 ShardSig sig;
424 std::vector<int> freepos;
425};
426
427/// Does this atom carry a non-trivial pattern (a constant or a repeated term),
428/// i.e. does its relation need to be split into shards?
429bool atomNeedsShards(const MAtom &a)
430{
431 for(std::size_t i=0;i<a.args.size();++i) {
432 if(!a.args[i].isVar) return true;
433 for(std::size_t j=0;j<i;++j)
434 if(a.args[j] == a.args[i]) return true;
435 }
436 return false;
437}
438
439ShardSig shardSigOf(const std::vector<long> &el, const std::set<long> &consts)
440{
441 const int k = static_cast<int>(el.size());
442 ShardSig sg;
443 sg.rep.assign(k, 0);
444 sg.cst.assign(k, SHARD_FREE);
445 for(int i=0;i<k;++i) {
446 int r = i;
447 for(int j=0;j<i;++j) if(el[j]==el[i]) { r = sg.rep[j]; break; }
448 sg.rep[i] = r;
449 }
450 for(int i=0;i<k;++i)
451 if(sg.rep[i]==i && consts.count(el[i])) sg.cst[i] = el[i];
452 return sg;
453}
454
455/// Can @p a match a tuple of shard @p sh? (A pre-filter: the equalities the
456/// shard forces are applied afterwards, and re-checked on the result.)
457bool shardCompatible(const MAtom &a, const Shard &sh)
458{
459 const int k = static_cast<int>(a.args.size());
460 if(static_cast<int>(sh.sig.rep.size()) != k)
461 return false; // arity mismatch with the data
462 for(int p=0;p<k;++p) {
463 const int r = sh.sig.rep[p];
464 const Term &tp = a.args[p], &tr = a.args[r];
465 if(!tp.isVar && !tr.isVar && tp.v != tr.v)
466 return false; // one block, two different constants
467 if(sh.sig.cst[r] != SHARD_FREE) {
468 if(!tp.isVar && tp.v != sh.sig.cst[r]) return false;
469 } else if(!tp.isVar) {
470 return false; // a free block holds no query constant
471 }
472 }
473 for(int p=0;p<k;++p)
474 for(int q=p+1;q<k;++q)
475 if(sh.sig.rep[p]!=sh.sig.rep[q] && a.args[p]==a.args[q])
476 return false; // distinct blocks, one term
477 return true;
478}
479
480/// Union-find over a disjunct's variables with per-class constant bindings:
481/// the substitution a choice of shards forces.
482class ShardSubst {
483public:
484 /// Unify two terms; false if that is inconsistent.
485 bool unify(const Term &x, const Term &y) {
486 if(!x.isVar && !y.isVar) return x.v == y.v;
487 if(!x.isVar) return bind(y.v, x.v);
488 if(!y.isVar) return bind(x.v, y.v);
489 const long rx = find(x.v), ry = find(y.v);
490 if(rx == ry) return true;
491 auto cx = bound.find(rx), cy = bound.find(ry);
492 if(cx!=bound.end() && cy!=bound.end() && cx->second != cy->second)
493 return false;
494 parent[ry] = rx;
495 if(cy != bound.end()) {
496 const long v = cy->second;
497 bound.erase(ry);
498 bound[rx] = v;
499 }
500 return true;
501 }
502 /// The term a variable / constant becomes under the substitution.
503 Term apply(const Term &t) {
504 if(!t.isVar) return t;
505 const long r = find(t.v);
506 auto it = bound.find(r);
507 Term o;
508 if(it != bound.end()) { o.isVar=false; o.v=it->second; }
509 else { o.isVar=true; o.v=r; }
510 return o;
511 }
512private:
513 std::map<long,long> parent; // variable -> parent (union-find)
514 std::map<long,long> bound; // class root -> the constant it is pinned to
515 long find(long v) {
516 auto it = parent.find(v);
517 if(it == parent.end()) { parent[v]=v; return v; }
518 if(it->second == v) return v;
519 const long r = find(it->second);
520 parent[v] = r;
521 return r;
522 }
523 bool bind(long var, long c) {
524 const long r = find(var);
525 auto it = bound.find(r);
526 if(it != bound.end()) return it->second == c;
527 bound[r] = c;
528 return true;
529 }
530};
531
532/// Cap on the disjuncts the shard expansion may produce (it is a product over
533/// the atoms of a disjunct); past it the route declines to joint-width. Well
534/// above what the rest of the compiler can consume anyway: the CNF conjunct cap
535/// and the transversal bound both bite long before a union this wide compiles.
536const std::size_t SHARD_MAX_DISJUNCTS = 256;
537
538/**
539 * @brief One rank + shatter pass of @p s over @p fi, splitting the relations
540 * in @p need.
541 *
542 * The expansion substitutes constants and unifies variables, so it can leave a
543 * non-trivial pattern on an atom of a relation that was NOT in @p need (an
544 * unshattered @c R(u) becomes @c R(a) in the branch where @c u is pinned).
545 * Such relations are reported through @p missing, and @c normalizeShards
546 * iterates to a fixpoint.
547 */
548bool normalizeShardsPass(const Sentence &s, const FactIndex &fi,
549 const std::set<unsigned> &need,
550 const std::set<long> &consts, unsigned maxRel,
551 Sentence &sOut, FactIndex &fiOut,
552 std::set<unsigned> &missing)
553{
554 missing.clear();
555 if(need.empty())
556 return false;
557
558 // Shard the facts: one shard per (relation, tuple signature), the tuple
559 // projected onto the shard's free blocks. A relation that needs no splitting
560 // is copied through unchanged.
561 std::map<unsigned, std::map<ShardSig, Shard>> shards;
562 unsigned next = maxRel + 1;
563 fiOut = FactIndex();
564 for(const auto &kv : fi.tok) {
565 const unsigned rel = kv.first.first;
566 const std::vector<long> &el = kv.first.second;
567 unsigned sym = rel;
568 std::vector<long> proj = el;
569 if(need.count(rel)) {
570 const ShardSig sg = shardSigOf(el, consts);
571 auto &byrel = shards[rel];
572 auto sit = byrel.find(sg);
573 if(sit == byrel.end()) {
574 Shard sh;
575 sh.sym = next++;
576 sh.sig = sg;
577 for(std::size_t i=0;i<el.size();++i)
578 if(sg.rep[i]==static_cast<int>(i) && sg.cst[i]==SHARD_FREE)
579 sh.freepos.push_back(static_cast<int>(i));
580 sit = byrel.emplace(sg, sh).first;
581 }
582 sym = sit->second.sym;
583 proj.clear();
584 for(int p : sit->second.freepos) proj.push_back(el[p]);
585 }
586 fiOut.tok[{sym, proj}] = kv.second;
587 fiOut.present.insert({sym, proj});
588 for(std::size_t i=0;i<proj.size();++i)
589 fiOut.domain[{sym, static_cast<int>(i)}].insert(proj[i]);
590 }
591
592 // Expand the sentence: per disjunct, every combination of one compatible
593 // shard per atom, under the substitution that combination forces.
594 sOut.clear();
595 std::set<std::string> emitted;
596 for(const Disjunct &d : s) {
597 std::vector<std::vector<const Shard *>> opts;
598 unsigned long prod = 1;
599 bool dead = false;
600 for(const MAtom &a : d) {
601 std::vector<const Shard *> o;
602 if(!need.count(a.rel)) {
603 o.push_back(nullptr); // kept as is
604 } else {
605 auto rit = shards.find(a.rel);
606 if(rit != shards.end())
607 for(const auto &kv : rit->second)
608 if(shardCompatible(a, kv.second)) o.push_back(&kv.second);
609 }
610 if(o.empty()) { dead = true; break; } // no tuple matches this atom
611 prod *= o.size();
612 if(prod > SHARD_MAX_DISJUNCTS)
613 throw MobiusDecline("Möbius: ranking/shattering expansion cap exceeded");
614 opts.push_back(std::move(o));
615 }
616 if(dead)
617 continue; // the conjunction is FALSE
618
619 std::vector<const Shard *> choice(opts.size(), nullptr);
620 std::function<void(std::size_t)> expand = [&](std::size_t ai) {
621 if(ai < opts.size()) {
622 for(const Shard *sh : opts[ai]) { choice[ai]=sh; expand(ai+1); }
623 return;
624 }
625 // The equalities and constants this combination forces.
626 ShardSubst sub;
627 for(std::size_t i=0;i<d.size();++i) {
628 const Shard *sh = choice[i];
629 if(sh == nullptr) continue;
630 for(std::size_t p=0;p<d[i].args.size();++p) {
631 const int r = sh->sig.rep[p];
632 if(r != static_cast<int>(p) && !sub.unify(d[i].args[p], d[i].args[r]))
633 return;
634 if(sh->sig.cst[r] != SHARD_FREE) {
635 Term c; c.isVar=false; c.v=sh->sig.cst[r];
636 if(!sub.unify(d[i].args[p], c)) return;
637 }
638 }
639 }
640 // Apply it, and re-check every atom against its shard: distinct blocks
641 // must not have collapsed onto one term, and a free block must not have
642 // been pinned to a query constant (its tuples carry none).
643 Disjunct nd;
644 for(std::size_t i=0;i<d.size();++i) {
645 const Shard *sh = choice[i];
646 std::vector<Term> ap;
647 ap.reserve(d[i].args.size());
648 for(const Term &t : d[i].args) ap.push_back(sub.apply(t));
649 MAtom na;
650 if(sh == nullptr) {
651 na.rel = d[i].rel;
652 na.args = std::move(ap);
653 if(atomNeedsShards(na))
654 missing.insert(na.rel); // the substitution left a pattern on it
655 } else {
656 for(std::size_t p=0;p<ap.size();++p)
657 for(std::size_t q=p+1;q<ap.size();++q)
658 if(sh->sig.rep[p]!=sh->sig.rep[q] && ap[p]==ap[q]) return;
659 for(int p : sh->freepos)
660 if(!ap[p].isVar) return;
661 na.rel = sh->sym;
662 for(int p : sh->freepos) na.args.push_back(ap[p]);
663 }
664 bool dup = false;
665 for(const MAtom &prev : nd)
666 if(prev.rel==na.rel && prev.args==na.args) { dup=true; break; }
667 if(!dup) nd.push_back(std::move(na));
668 }
669 if(nd.empty()) return;
670 if(sOut.size() >= SHARD_MAX_DISJUNCTS)
671 throw MobiusDecline("Möbius: ranking/shattering expansion cap exceeded");
672 sOut.push_back(std::move(nd));
673 };
674 expand(0);
675 }
676
677 // Drop duplicate disjuncts (idempotent in the union, and a smaller sentence
678 // keeps the CNF lattice small).
679 {
680 Sentence uniq;
681 for(const Disjunct &d : sOut)
682 if(emitted.insert(canonDisjunct(d)).second) uniq.push_back(d);
683 sOut = std::move(uniq);
684 }
685
686 // Re-globalise the variables: several expanded disjuncts descend from one
687 // original disjunct and would otherwise share its variable ids, while their
688 // existential quantifiers are separate. The recursion unifies variables
689 // ACROSS disjuncts by (relation, position), so a shared id would conflate
690 // two independent variables (and cost a separator).
691 {
692 long nextvar = 1;
693 for(const auto &d : s)
694 for(const auto &a : d)
695 for(const auto &t : a.args)
696 if(t.isVar) nextvar = std::max(nextvar, t.v + 1);
697 for(Disjunct &d : sOut) {
698 std::map<long,long> ren;
699 for(MAtom &a : d)
700 for(Term &t : a.args)
701 if(t.isVar) {
702 auto it = ren.find(t.v);
703 if(it == ren.end()) it = ren.emplace(t.v, nextvar++).first;
704 t.v = it->second;
705 }
706 }
707 }
708 return true;
709}
710
711/**
712 * @brief Rank + shatter @p s over @p fi into an equivalent reduced-form
713 * sentence over shard symbols.
714 *
715 * @return @c false when no relation needs splitting (then @p sOut / @p fiOut
716 * are untouched and the caller keeps the originals); @c true when the
717 * normalized pair has been written to @p sOut / @p fiOut.
718 */
719bool normalizeShards(const Sentence &s, const FactIndex &fi,
720 Sentence &sOut, FactIndex &fiOut)
721{
722 // Which relations need splitting, and the constants to shatter on (all
723 // constants of the sentence: a variable can be substituted into any of them
724 // by another atom's shard, so the closure is the whole set).
725 std::set<unsigned> need;
726 std::set<long> consts;
727 std::set<unsigned> rels;
728 unsigned maxRel = 0;
729 for(const auto &d : s)
730 for(const auto &a : d) {
731 maxRel = std::max(maxRel, a.rel);
732 rels.insert(a.rel);
733 if(atomNeedsShards(a)) need.insert(a.rel);
734 for(const auto &t : a.args) if(!t.isVar) consts.insert(t.v);
735 }
736 if(need.empty())
737 return false;
738 // Shard symbols are allocated above every EXISTING symbol -- including the
739 // relations the gather brought in but this sentence does not mention (a
740 // per-answer head pin compiles one disjunct of a wider descriptor), whose
741 // facts are copied through under their own symbol.
742 for(const auto &kv : fi.tok)
743 maxRel = std::max(maxRel, kv.first.first);
744
745 // Re-run until no unsharded relation is left carrying a pattern; each round
746 // adds at least one relation, so there are at most |relations| of them.
747 for(std::size_t round=0; round<=rels.size(); ++round) {
748 std::set<unsigned> missing;
749 const bool done = normalizeShardsPass(s, fi, need, consts, maxRel,
750 sOut, fiOut, missing);
751 if(missing.empty())
752 return done;
753 need.insert(missing.begin(), missing.end());
754 }
755 throw MobiusDecline("Möbius: ranking/shattering did not converge");
756}
757
758// ===========================================================================
759// The compiler.
760// ===========================================================================
761
762class MobiusCompiler {
763public:
764 MobiusCompiler(const FactIndex &fi, MobiusStats &st) : fi(fi), st(st) {
765 const constants_t c = get_constants(true);
766 times_oid = c.OID_FUNCTION_PROVENANCE_TIMES;
767 plus_oid = c.OID_FUNCTION_PROVENANCE_PLUS;
768 if(!OidIsValid(times_oid) || !OidIsValid(plus_oid))
769 provsql_error("ucq_mobius: provenance_times / provenance_plus unavailable");
770 }
771
772 /// Compile the top sentence, returning the root token. @p lineage is the
773 /// token of the literal Boolean provenance of the query (the normal lineage
774 /// the route falls back to); it is carried on the root gate_mobius as a
775 /// designated transparent child so every non-probability evaluator (semiring,
776 /// Shapley, Banzhaf, PROV export) sees the literal lineage and works, while
777 /// probability uses the signed Möbius combination. Empty = no lineage
778 /// (measure-only; the manual descriptor entry points without a fallback).
779 pg_uuid_t compileTop(const Sentence &s, const std::string &lineage = "") {
780 pending_lineage = lineage;
781 lineage_consumed = false;
782 pg_uuid_t root = compile(s, /*top=*/true);
783 if(!lineage_consumed && !(lineage.empty() && isMobiusGate(root)))
784 // The top did not go through a Möbius step (a safe query that reached
785 // this route, or one whose top rule was an independent union): wrap it in
786 // a thin gate_mobius selector carrying the lineage, if any, and the value
787 // with coefficient 1. The root must be a gate_mobius even so: it is what
788 // routes the token to the Möbius evaluator (the only one that reads the
789 // signed combinations of any nested gate_mobius) and what
790 // @c mobius_or_null tests to tell a success from a decline.
791 //
792 // Skipped when there is no lineage to attach AND the compiled root is
793 // already a gate_mobius: the wrapper would then be a pure identity (a
794 // one-element combination with coefficient 1) over a gate that already
795 // satisfies both requirements. That is the measure-only entry points'
796 // case (@c ucq_mobius_provenance and friends pass no lineage, so
797 // @c lineage_consumed never becomes true), where the top-level Möbius
798 // step's own combination is the root the caller should see. A root that
799 // is NOT a gate_mobius -- a gate_zero / gate_one from a combination that
800 // collapsed to a constant -- still gets wrapped, since only the wrapper
801 // makes such a token recognisable as a Möbius success.
802 root = mkMobius({root}, {1}, lineage);
803 return root;
804 }
805
806private:
807 const FactIndex &fi;
808 MobiusStats &st;
809 Oid times_oid = InvalidOid; // provsql.provenance_times
810 Oid plus_oid = InvalidOid; // provsql.provenance_plus
811 std::unordered_map<std::string, std::string> memo; // key -> uuid string
812 std::unordered_set<std::string> created;
813 std::string pending_lineage; // lineage to inline into the top Möbius step
814 bool lineage_consumed = false;
815
816 // -- materialisation -----------------------------------------------------
817
818 /// Delegate plus / times construction to the system's provenance_times /
819 /// provenance_plus (the single source of truth: they filter the semiring
820 /// identities, short-circuit 0 / 1 surviving children, and content-address
821 /// the gate in the store). The Möbius path must NOT mint its own plus / times
822 /// gates -- using these functions keeps one gate-construction code path and
823 /// lets the Möbius islands share sub-gates with the rest of the system.
824 pg_uuid_t callProvenance(bool isAnd, const std::vector<pg_uuid_t> &ch) {
825 std::vector<Datum> datums;
826 datums.reserve(ch.size());
827 for(const auto &u : ch)
828 datums.push_back(UUIDPGetDatum(const_cast<pg_uuid_t *>(&u)));
829 ArrayType *arr = construct_array(datums.empty()?nullptr:datums.data(),
830 static_cast<int>(datums.size()),
831 UUIDOID, 16, false, TYPALIGN_CHAR);
832 Datum res = OidFunctionCall1(isAnd ? times_oid : plus_oid,
833 PointerGetDatum(arr));
834 /* Data-cost cap: the lifted-inference recursion is O(|D|^k), so bound the
835 * total work and decline (-> joint-width / the ladder) past the cap rather
836 * than let a high-level safe query on large data out-cost the general
837 * pipeline. 0 disables the cap. */
838 ++st.dd_size;
840 static_cast<long>(st.dd_size) > static_cast<long>(provsql_mobius_max_gates))
841 throw MobiusDecline("Möbius: data-cost cap (provsql.mobius_max_gates) "
842 "exceeded -- the |D|^k recursion is too large");
843 return *DatumGetUUIDP(res);
844 }
845
846 /// gate_one (empty times) / gate_zero (empty plus).
847 pg_uuid_t mkConst(bool one) { return callProvenance(one, {}); }
848
849 /// True iff @p u is a @c gate_mobius this compilation minted. @c created
850 /// holds exactly the combination gates @c mkMobius produced (including one it
851 /// found already content-addressed in the store), so this needs no store
852 /// round-trip. Used by @c compileTop to skip an identity wrapper.
853 bool isMobiusGate(const pg_uuid_t &u) const {
854 return created.count(uuid2string(u)) != 0;
855 }
856
857 /// Independent OR / AND over child tokens. Children are deduplicated first:
858 /// the certified-independent evaluation is read-once, so a child must not be
859 /// double-counted (provenance_plus / provenance_times keep duplicates). The
860 /// 0 / 1-child and identity cases are handled by the provenance functions.
861 pg_uuid_t mkBool(bool isAnd, std::vector<pg_uuid_t> ch) {
862 std::vector<std::string> texts;
863 texts.reserve(ch.size());
864 for(const auto &c : ch) texts.push_back(uuid2string(c));
865 std::sort(texts.begin(), texts.end());
866 texts.erase(std::unique(texts.begin(), texts.end()), texts.end());
867 std::vector<pg_uuid_t> uniq;
868 uniq.reserve(texts.size());
869 for(const auto &t : texts) uniq.push_back(string2uuid(t));
870 return callProvenance(isAnd, uniq);
871 }
872
873 /// Signed Möbius combination gate over @p children with integer @p coeffs.
874 /// Coefficients are stored keyed by child UUID (@c "uuid:coeff" tokens in
875 /// @c extra), so evaluation is robust to any child reordering / dedup the
876 /// store may apply; duplicate children are merged (coefficients summed) and
877 /// zero-coefficient children dropped.
878 /// @p lineage (optional): the literal-lineage token, carried as a
879 /// designated transparent child marked @c "L:<uuid>" in @c extra (child 0).
880 pg_uuid_t mkMobius(const std::vector<pg_uuid_t> &children,
881 const std::vector<long> &coeffs,
882 const std::string &lineage = "") {
883 std::map<std::string,long> bycoeff;
884 std::vector<std::string> order;
885 for(std::size_t i=0;i<children.size();++i) {
886 const std::string u = uuid2string(children[i]);
887 if(!bycoeff.count(u)) order.push_back(u);
888 bycoeff[u] += coeffs[i];
889 }
890 std::vector<pg_uuid_t> ch;
891 std::string extra, name = "mobius[";
892 if(!lineage.empty()) {
893 ch.push_back(string2uuid(lineage)); // the literal lineage, child 0
894 extra += "L:" + lineage + " ";
895 name += "L:" + lineage + ",";
896 }
897 for(const std::string &u : order) {
898 if(bycoeff[u]==0) continue;
899 ch.push_back(string2uuid(u));
900 extra += u + ":" + std::to_string(bycoeff[u]) + " ";
901 name += u + ":" + std::to_string(bycoeff[u]) + ",";
902 }
903 name += "]";
904 if(ch.empty() || (!lineage.empty() && ch.size()==1))
905 return mkConst(false); // no surviving combination -> probability 0
906 pg_uuid_t u = provsqlUuidV5(name);
907 if(created.insert(uuid2string(u)).second) {
909 static_cast<unsigned>(ch.size()),
910 ch.data());
911 provsql_internal_set_extra(&u, extra.c_str());
912 ++st.dd_size;
913 }
914 return u;
915 }
916
917 // -- recursion -----------------------------------------------------------
918
919 pg_uuid_t compile(const Sentence &sentence, bool top=false);
920
921 /// Find a separator: a unification class of variable positions present in
922 /// every atom of every disjunct. Returns the (rel,pos) occurrences of the
923 /// class via @p occ and per-disjunct the variable ids of the class via
924 /// @p classVarsPerDisj; returns false if no separator exists.
925 bool findSeparator(const Sentence &s,
926 std::set<std::pair<unsigned,int>> &occ,
927 std::vector<std::set<long>> &classVarsPerDisj);
928
929 /// Möbius (inclusion-exclusion) step over the components of @p s.
930 pg_uuid_t mobiusStep(const Sentence &s, bool top);
931};
932
933// --- separator -------------------------------------------------------------
934
935bool MobiusCompiler::findSeparator(const Sentence &s,
936 std::set<std::pair<unsigned,int>> &occOut,
937 std::vector<std::set<long>> &classVarsPerDisj)
938{
939 // Collect all variable ids and union those sharing a (rel,pos).
940 std::map<long,int> id;
941 auto vid = [&](long v)->int{
942 auto it=id.find(v); if(it!=id.end()) return it->second;
943 int n=static_cast<int>(id.size()); id[v]=n; return n;
944 };
945 for(const auto &d : s) for(const auto &a : d) for(const auto &t : a.args)
946 if(t.isVar) vid(t.v);
947 if(id.empty()) return false;
948 UF uf(static_cast<int>(id.size()));
949 std::map<std::pair<unsigned,int>, long> firstAtPos;
950 for(const auto &d : s) for(const auto &a : d)
951 for(std::size_t k=0;k<a.args.size();++k) {
952 if(!a.args[k].isVar) continue;
953 auto key = std::make_pair(a.rel, static_cast<int>(k));
954 auto it = firstAtPos.find(key);
955 if(it==firstAtPos.end()) firstAtPos[key]=a.args[k].v;
956 else uf.uni(vid(it->second), vid(a.args[k].v));
957 }
958 // Candidate classes = class roots. A class is a separator iff in every
959 // disjunct, every atom contains a variable of that class.
960 std::vector<long> idToVar(id.size());
961 for(auto &kv : id) idToVar[kv.second]=kv.first;
962
963 std::set<int> roots;
964 for(int i=0;i<static_cast<int>(id.size());++i) roots.insert(uf.find(i));
965
966 for(int root : roots) {
967 bool covers = true;
968 std::vector<std::set<long>> cvars(s.size());
969 for(std::size_t di=0; di<s.size() && covers; ++di) {
970 const Disjunct &d = s[di];
971 for(const auto &a : d) {
972 bool atomHas=false;
973 for(const auto &t : a.args)
974 if(t.isVar && uf.find(vid(t.v))==root) {
975 atomHas=true; cvars[di].insert(t.v);
976 }
977 if(!atomHas){ covers=false; break; }
978 }
979 }
980 if(!covers) continue;
981 // A separator binds exactly ONE variable per disjunct: the disjunct's whole
982 // conjunction then sits under that single existential and Q ≡ ⋁_a Q[x:=a].
983 // With two class variables in one disjunct, substituting the same constant
984 // for both is a strictly stronger query (∃x φ ∧ ∃y ψ is not ⋁_a φ(a)∧ψ(a)),
985 // so this class is not a separator -- try the next one.
986 {
987 bool single = true;
988 for(const auto &cv : cvars) if(cv.size() != 1) { single = false; break; }
989 if(!single) continue;
990 }
991 // Record the (rel,pos) occurrences of this class (the substitution sites,
992 // and the active domain to project over).
993 std::set<std::pair<unsigned,int>> occ;
994 for(const auto &d : s) for(const auto &a : d)
995 for(std::size_t k=0;k<a.args.size();++k)
996 if(a.args[k].isVar && uf.find(vid(a.args[k].v))==root)
997 occ.insert({a.rel, static_cast<int>(k)});
998 // Each relation must carry the class at a single position: the piece for
999 // constant a then reads only tuples with a at that position, so pieces for
1000 // distinct constants are over disjoint tuples -- the certificate the
1001 // independent project rests on. (Two class positions of one relation would
1002 // let a tuple be read by one piece through one and by another through the
1003 // other.)
1004 {
1005 std::set<unsigned> rels;
1006 bool onePos = true;
1007 for(const auto &rp : occ)
1008 if(!rels.insert(rp.first).second) { onePos = false; break; }
1009 if(!onePos) continue;
1010 }
1011 occOut = std::move(occ);
1012 classVarsPerDisj = std::move(cvars);
1013 return true;
1014 }
1015 return false;
1016}
1017
1018// --- Möbius step -----------------------------------------------------------
1019
1020pg_uuid_t MobiusCompiler::mobiusStep(const Sentence &s, bool top)
1021{
1022 // 1. Components of each disjunct -> a global literal pool with equivalence
1023 // collapse. Each disjunct becomes the set of its component literal ids.
1024 std::vector<Disjunct> literals; // representative component per id
1025 auto litId = [&](const Disjunct &c)->int{
1026 for(std::size_t i=0;i<literals.size();++i)
1027 if(equiv(literals[i], c)) return static_cast<int>(i);
1028 literals.push_back(c);
1029 return static_cast<int>(literals.size()-1);
1030 };
1031 std::vector<std::set<int>> terms; // DNF terms (per disjunct)
1032 for(const auto &d : s) {
1033 std::set<int> t;
1034 for(const auto &c : componentsOf(d)) t.insert(litId(c));
1035 terms.push_back(std::move(t));
1036 }
1037 if(top) st.n_components = static_cast<int>(literals.size());
1038
1039 // Implication matrix on literals: imp[i][j] == (lit_i implies lit_j),
1040 // i.e. lit_i ⊑ lit_j, witnessed by hom(lit_j -> lit_i).
1041 const int L = static_cast<int>(literals.size());
1042 std::vector<std::vector<char>> imp(L, std::vector<char>(L, 0));
1043 for(int i=0;i<L;++i) for(int j=0;j<L;++j)
1044 imp[i][j] = hom(literals[j], literals[i]) ? 1 : 0;
1045
1046 // Minimise a disjunction (literal set): drop lit i if some other lit j in
1047 // the set is implied-BY i (i ⊑ j), since i ∨ j ≡ j. Keep the weakest.
1048 auto minimiseDisj = [&](std::set<int> in)->std::vector<int>{
1049 std::vector<int> v(in.begin(), in.end());
1050 std::vector<char> keep(v.size(),1);
1051 for(std::size_t a=0;a<v.size();++a) for(std::size_t b=0;b<v.size();++b)
1052 if(a!=b && keep[b] && imp[v[a]][v[b]]) { keep[a]=0; break; }
1053 std::vector<int> out;
1054 for(std::size_t a=0;a<v.size();++a) if(keep[a]) out.push_back(v[a]);
1055 std::sort(out.begin(), out.end());
1056 return out;
1057 };
1058
1059 // 2. DNF -> CNF: a clause picks one literal from each term and ORs them.
1060 // Cap the conjunct count.
1061 std::set<std::vector<int>> clauseSet;
1062 std::vector<std::set<int>> clauses; // working as literal sets
1063 // Enumerate transversals.
1064 std::vector<int> pick(terms.size(), 0);
1065 // Bound the product to avoid blow-up before the M cap is even computed.
1066 unsigned long prod = 1;
1067 for(const auto &t : terms) prod *= std::max<std::size_t>(1, t.size());
1068 if(prod > 100000UL)
1069 throw MobiusDecline("Möbius: DNF->CNF transversal count too large");
1070 std::function<void(std::size_t,std::set<int>&)> gen =
1071 [&](std::size_t ti, std::set<int> &acc){
1072 if(ti==terms.size()){
1073 std::vector<int> mn = minimiseDisj(acc);
1074 if(!mn.empty()) clauseSet.insert(mn);
1075 return;
1076 }
1077 for(int lit : terms[ti]) {
1078 bool fresh = acc.insert(lit).second;
1079 gen(ti+1, acc);
1080 if(fresh) acc.erase(lit);
1081 }
1082 };
1083 { std::set<int> acc; gen(0, acc); }
1084
1085 // Clause subsumption: drop clause B if some clause A ⊑ B (every literal of
1086 // A is implied by some literal of B... no: ⋁A implies ⋁B iff every a∈A has
1087 // a b∈B with a ⊑ b). Then B is redundant in the conjunction.
1088 std::vector<std::vector<int>> cl(clauseSet.begin(), clauseSet.end());
1089 auto disjImplies = [&](const std::vector<int>&A, const std::vector<int>&B){
1090 for(int a : A){ bool ok=false; for(int b : B) if(imp[a][b]){ ok=true; break; }
1091 if(!ok) return false; }
1092 return true;
1093 };
1094 std::vector<char> keepCl(cl.size(),1);
1095 for(std::size_t a=0;a<cl.size();++a) for(std::size_t b=0;b<cl.size();++b)
1096 if(a!=b && keepCl[a] && cl[a]!=cl[b] && disjImplies(cl[a],cl[b]))
1097 keepCl[b]=0;
1098 std::vector<std::vector<int>> M;
1099 for(std::size_t a=0;a<cl.size();++a) if(keepCl[a]) M.push_back(cl[a]);
1100
1101 const int m = static_cast<int>(M.size());
1102 if(top) { st.n_cnf_conjuncts = m; st.lattice_size = (m<=30)?(1<<m):0; }
1103 if(m == 1) {
1104 // The CNF collapsed to a single clause: the sentence is logically that
1105 // clause, a pure disjunction of components (its conjunctive structure was
1106 // redundant -- the shape a self-join with an implication between its
1107 // components leaves behind). There is no inclusion-exclusion to run, but
1108 // the simpler sentence may well compile; recurse on it unless it is the
1109 // sentence we already have (no progress -> decline).
1110 Sentence el;
1111 for(int lid : M[0]) el.push_back(literals[lid]);
1112 if(canonSentence(el) != canonSentence(s))
1113 return compile(el, top);
1114 }
1115 if(m < 2)
1116 throw MobiusDecline("Möbius: no inclusion-exclusion structure (M<2); "
1117 "sentence has no separator and does not decompose");
1119 throw MobiusDecline("Möbius: CNF conjunct count exceeds the cap "
1120 "(provsql.mobius_max_cnf)");
1121
1122 // 3. Enumerate non-empty subsets s of [m]; ψ_s = ⋁ of literals across the
1123 // clauses in s, minimised. Accumulate coefficient (-1)^{|s|+1} per
1124 // distinct ψ_s (keyed by its minimised literal-id set).
1125 std::map<std::vector<int>, long> coeff;
1126 for(unsigned mask=1; mask < (1u<<m); ++mask) {
1127 std::set<int> lits;
1128 for(int i=0;i<m;++i) if(mask & (1u<<i))
1129 lits.insert(M[i].begin(), M[i].end());
1130 std::vector<int> key = minimiseDisj(lits);
1131 const int pc = __builtin_popcount(mask);
1132 coeff[key] += (pc & 1) ? 1 : -1;
1133 }
1134 if(top) st.lattice_collapsed = static_cast<int>(coeff.size());
1135
1136 // 4. Build the gate_mobius over the surviving elements. Each element is a
1137 // pure disjunction of its literal components.
1138 std::vector<pg_uuid_t> children;
1139 std::vector<long> coeffs;
1140 int cancelled = 0;
1141 bool cancelledHard = false;
1142 for(const auto &kv : coeff) {
1143 if(kv.second == 0) {
1144 ++cancelled;
1145 // Is this cancelled element #P-hard (no separator, no decomposition)?
1146 // Probe cheaply: a pure disjunction of these literals with shared
1147 // symbols and no separator is the hard term whose cancellation makes
1148 // the query tractable.
1149 if(!cancelledHard) {
1150 Sentence el;
1151 for(int lid : kv.first) el.push_back(literals[lid]);
1152 std::set<std::pair<unsigned,int>> occ;
1153 std::vector<std::set<long>> cv;
1154 // Symbol-connected and no separator => hard.
1155 if(el.size() >= 2 && !findSeparator(el, occ, cv)) {
1156 // check symbol-connected
1157 UF uf(static_cast<int>(el.size()));
1158 std::map<unsigned,int> firstRelDisj;
1159 for(std::size_t i=0;i<el.size();++i)
1160 for(const auto &a : el[i])
1161 { auto it=firstRelDisj.find(a.rel);
1162 if(it==firstRelDisj.end()) firstRelDisj[a.rel]=static_cast<int>(i);
1163 else uf.uni(it->second, static_cast<int>(i)); }
1164 std::set<int> r; for(std::size_t i=0;i<el.size();++i) r.insert(uf.find(static_cast<int>(i)));
1165 if(r.size()==1) cancelledHard = true;
1166 }
1167 }
1168 continue;
1169 }
1170 Sentence el;
1171 for(int lid : kv.first) el.push_back(literals[lid]);
1172 children.push_back(compile(el, false));
1173 coeffs.push_back(kv.second);
1174 }
1175 if(top) { st.n_cancelled = cancelled; st.cancelled_hard = cancelledHard;
1176 st.n_nonzero = static_cast<int>(children.size()); }
1177
1178 if(children.empty())
1179 return mkConst(false);
1180 // The top-level Möbius step inlines the literal lineage onto its own gate so
1181 // the root gate_mobius carries it directly (single level); nested steps do
1182 // not (their values are discarded by the transparent-to-lineage passthrough).
1183 if(top && !pending_lineage.empty()) {
1184 lineage_consumed = true;
1185 return mkMobius(children, coeffs, pending_lineage);
1186 }
1187 return mkMobius(children, coeffs);
1188}
1189
1190// --- main recursion --------------------------------------------------------
1191
1192pg_uuid_t MobiusCompiler::compile(const Sentence &sentence0, bool top)
1193{
1194 CHECK_FOR_INTERRUPTS();
1195
1196 // 1. Drop disjuncts containing an ABSENT ground atom (the conjunction is
1197 // then FALSE). Present ground atoms are kept: under TID a tuple in the
1198 // relation is a probabilistic Bernoulli event (its input token), NOT a
1199 // certainty, so it stays an atom and bottoms out as a token leaf.
1200 Sentence s;
1201 for(const Disjunct &d0 : sentence0) {
1202 bool dead = false;
1203 for(const MAtom &a : d0) {
1204 if(ground(a)) {
1205 std::vector<long> el;
1206 for(const auto &t : a.args) el.push_back(t.v);
1207 if(!fi.isPresent(a.rel, el)) { dead=true; break; }
1208 }
1209 }
1210 if(!dead && !d0.empty()) s.push_back(d0);
1211 }
1212 if(s.empty())
1213 return mkConst(false);
1214
1215 // Base case: a single fully-ground disjunct is a conjunction of tuples ->
1216 // the independent AND of their input tokens (a certain / untracked tuple
1217 // contributes the identity gate_one).
1218 auto isGround = [&](const Disjunct &d){
1219 for(const auto &a : d) if(!ground(a)) return false;
1220 return true;
1221 };
1222 // A self-join can send two atoms onto the SAME tuple, so the token list is
1223 // deduplicated (by mkBool) before the AND is built: x ∧ x = x is what the
1224 // repetition means, whereas an *independent* AND over two copies of one
1225 // token would square its probability. Two DISTINCT tuples are independent
1226 // by the TID restriction (one token per (relation, element tuple), no token
1227 // shared between two fact slots -- both enforced at gather time).
1228 auto tokenAnd = [&](const Disjunct &d)->pg_uuid_t {
1229 std::vector<pg_uuid_t> toks;
1230 for(const auto &a : d) {
1231 std::vector<long> el; for(const auto &t : a.args) el.push_back(t.v);
1232 const std::string *tk = fi.token(a.rel, el);
1233 if(tk==nullptr) return mkConst(false); // absent (shouldn't reach here)
1234 if(tk->empty()) continue; // certain fact: identity for AND
1235 toks.push_back(string2uuid(*tk));
1236 }
1237 return mkBool(true, toks); // empty -> gate_one (all certain)
1238 };
1239
1240 // Memoisation.
1241 const std::string key = canonSentence(s);
1242 {
1243 auto it = memo.find(key);
1244 if(it!=memo.end()) { ++st.memo_hits; return string2uuid(it->second); }
1245 }
1246
1247 pg_uuid_t result;
1248
1249 // 2. Independent union: group the disjuncts by overlapping fact footprints
1250 // (disjoint footprints => disjoint tuples => independent). On a
1251 // reduced-form sentence this is the disjoint-vocabulary split; with
1252 // constants around it also separates, say, S(a,y) from S(b,z).
1253 {
1254 const int n = static_cast<int>(s.size());
1255 UF uf(n);
1256 std::vector<Footprint> fps;
1257 fps.reserve(n);
1258 for(const auto &d : s) fps.push_back(footprintOf(d));
1259 for(int i=0;i<n;++i)
1260 for(int j=i+1;j<n;++j)
1261 if(!disjointFootprints(fps[i], fps[j])) uf.uni(i, j);
1262 std::map<int, Sentence> groups;
1263 for(int i=0;i<n;++i) groups[uf.find(i)].push_back(s[i]);
1264 if(groups.size() > 1) {
1265 std::vector<pg_uuid_t> ch;
1266 for(auto &kv : groups) ch.push_back(compile(kv.second, false));
1267 result = mkBool(false, ch);
1268 memo[key] = uuid2string(result);
1269 return result;
1270 }
1271 }
1272
1273 // 3. A single group: every disjunct overlaps some other one.
1274 if(s.size()==1) {
1275 // Fully ground -> AND of tuple tokens (base case).
1276 if(isGround(s[0])) {
1277 result = tokenAnd(s[0]);
1278 memo[key] = uuid2string(result);
1279 return result;
1280 }
1281 // One disjunct (a CQ): split into variable-connected components and AND
1282 // them. Independence of the product needs the components to read disjoint
1283 // tuples, certified by their fact footprints (disjoint relation symbols, or
1284 // a position pinned to different constants). Components sharing a symbol
1285 // with no separating constant are a within-disjunct self-join: they are NOT
1286 // independent, so fall through -- the Möbius step below turns the
1287 // conjunction into the disjunctive detour P(c1)+P(c2)-P(c1∨c2).
1288 std::vector<Disjunct> comps = componentsOf(s[0]);
1289 if(comps.size() > 1) {
1290 std::vector<Footprint> fps;
1291 fps.reserve(comps.size());
1292 for(const auto &c : comps) fps.push_back(footprintOf(c));
1293 bool indep = true;
1294 for(std::size_t i=0;i<comps.size() && indep;++i)
1295 for(std::size_t j=i+1;j<comps.size();++j)
1296 if(!disjointFootprints(fps[i], fps[j])) { indep=false; break; }
1297 if(indep) {
1298 std::vector<pg_uuid_t> ch;
1299 for(auto &c : comps) { Sentence one{c}; ch.push_back(compile(one,false)); }
1300 result = mkBool(true, ch);
1301 memo[key] = uuid2string(result);
1302 return result;
1303 }
1304 }
1305 // A single connected CQ, or components that are not independent: the
1306 // separator branch below, then the Möbius step.
1307 }
1308
1309 // 4. Separator (independent project) if one exists.
1310 {
1311 std::set<std::pair<unsigned,int>> occ;
1312 std::vector<std::set<long>> classVars;
1313 if(findSeparator(s, occ, classVars)) {
1314 // Active domain of the separator class.
1315 std::set<long> dom;
1316 for(const auto &rp : occ) {
1317 auto it = fi.domain.find(rp);
1318 if(it!=fi.domain.end()) dom.insert(it->second.begin(), it->second.end());
1319 }
1320 std::vector<pg_uuid_t> ch;
1321 for(long a : dom) {
1322 // Substitute every class variable (per disjunct) by the constant a.
1323 Sentence sub;
1324 sub.reserve(s.size());
1325 for(std::size_t di=0; di<s.size(); ++di) {
1326 Disjunct nd;
1327 for(const MAtom &at : s[di]) {
1328 MAtom na = at;
1329 for(auto &t : na.args)
1330 if(t.isVar && classVars[di].count(t.v)) { t.isVar=false; t.v=a; }
1331 nd.push_back(std::move(na));
1332 }
1333 sub.push_back(std::move(nd));
1334 }
1335 ch.push_back(compile(sub, false)); // pieces for distinct a independent
1336 }
1337 result = mkBool(false, ch); // independent OR over the domain
1338 memo[key] = uuid2string(result);
1339 return result;
1340 }
1341 }
1342
1343 // 5. No separator. A single CONNECTED CQ here is genuinely #P-hard (no
1344 // separator, nothing to decompose). A single disjunct that did decompose
1345 // but whose components are not independent (a within-disjunct self-join)
1346 // goes to the Möbius step like a multi-disjunct sentence: its CNF is the
1347 // unit clauses of its components, and the signed enumeration computes
1348 // P(c1) + P(c2) - P(c1 ∨ c2) -- the disjunctive detour a self-join forces
1349 // (Dalvi & Suciu, Example 3.1).
1350 if(s.size()==1 && componentsOf(s[0]).size() < 2)
1351 throw MobiusDecline("Möbius: non-hierarchical single CQ (no separator); "
1352 "the query is #P-hard");
1353 result = mobiusStep(s, top);
1354 memo[key] = uuid2string(result);
1355 return result;
1356}
1357
1358/// Compile @p s over @p fi through the shard normalization, returning the root
1359/// token. Every entry point goes through here: ranking / shattering is a
1360/// no-op on an already reduced-form sentence (which is every query the array
1361/// decoder builds), so the common path pays only the scan that decides so.
1362pg_uuid_t compileNormalized(const Sentence &s, const FactIndex &fi,
1363 MobiusStats &st, const std::string &lineage = "")
1364{
1365 Sentence sn;
1366 FactIndex fin;
1367 const bool norm = normalizeShards(s, fi, sn, fin);
1368 if(norm && provsql_verbose >= 20)
1369 provsql_notice("ucq_mobius: ranking/shattering: %d disjuncts -> %d: %s",
1370 static_cast<int>(s.size()), static_cast<int>(sn.size()),
1371 canonSentence(sn).c_str());
1372 MobiusCompiler mc(norm ? fin : fi, st);
1373 return mc.compileTop(norm ? sn : s, lineage);
1374}
1375
1376// ===========================================================================
1377// Argument decoding (mirrors ucq_joint's columnar convention).
1378// ===========================================================================
1379
1380int checkedLen(ArrayType *a, const char *what) {
1381 if(a==NULL) return 0;
1382 if(ARR_NDIM(a)>1) provsql_error("ucq_mobius: %s must be 1-D", what);
1383 if(ARR_HASNULL(a)) provsql_error("ucq_mobius: %s must not contain NULLs", what);
1384 return ARR_NDIM(a)==0 ? 0 : ARR_DIMS(a)[0];
1385}
1386const int32 *intArr(FunctionCallInfo fcinfo, int n, const char *what, int &len){
1387 ArrayType *a = PG_ARGISNULL(n)?NULL:PG_GETARG_ARRAYTYPE_P(n);
1388 len = checkedLen(a, what);
1389 return (a==NULL||len==0)?NULL:(const int32*)ARR_DATA_PTR(a);
1390}
1391
1392/// Build the top sentence from the raw query arrays. Disjunct-local variables
1393/// are globalised by a per-disjunct offset (returned via @p base) so
1394/// unification across disjuncts is by (rel,pos), not by raw id, and so head
1395/// variables can be pinned per disjunct.
1396Sentence buildSentenceArrays(const int32 *d_nvars, int n_disj,
1397 const int32 *a_disj, const int32 *a_rel,
1398 const int32 *a_vars, int n_av,
1399 const int32 *a_arity, int n_ad,
1400 std::vector<long> &base)
1401{
1402 if(n_disj==0) provsql_error("ucq_mobius: the UCQ has no disjuncts");
1403 base.assign(n_disj, 0);
1404 long acc = 1; // start at 1 so id 0 is unused (avoids any sentinel clash)
1405 for(int d=0; d<n_disj; ++d){ base[d]=acc; acc += d_nvars[d] + 1; }
1406
1407 Sentence s(n_disj);
1408 int voff=0;
1409 for(int i=0;i<n_ad;++i){
1410 const int d=a_disj[i];
1411 if(d<0||d>=n_disj) provsql_error("ucq_mobius: atom disjunct out of range");
1412 const int ar=a_arity[i];
1413 if(ar<0||voff+ar>n_av)
1414 provsql_error("ucq_mobius: atom_vars shorter than arities");
1415 MAtom at; at.rel=static_cast<unsigned>(a_rel[i]);
1416 for(int k=0;k<ar;++k){
1417 Term t; t.isVar=true; t.v = base[d] + a_vars[voff+k];
1418 at.args.push_back(t);
1419 }
1420 voff+=ar;
1421 s[d].push_back(std::move(at));
1422 }
1423 return s;
1424}
1425
1426/// Build a FactIndex (and, optionally, the text->dense-id map) from the raw
1427/// fact arrays.
1428FactIndex buildFactIndexArrays(const int32 *f_rel, int n_fr,
1429 const int32 *f_elems, int n_fe,
1430 const int32 *f_arity,
1431 const pg_uuid_t *tok)
1432{
1433 // G3 (tuple independence): every present fact must be gated by a bare
1434 // gate_input. A fact whose token is an internal gate (a view-derived /
1435 // reachability / repair_key lineage) is correlated -- out of scope for the
1436 // lifted-inference recursion, which assumes independence -- so decline and
1437 // let the caller fall back (the more general joint-width route already had
1438 // its chance). The input-gate enum OID, fetched once.
1439 const Oid input_oid = get_constants(true).GATE_TYPE_TO_OID[gate_input];
1440
1441 FactIndex fi;
1442 // Self-join overlap guard: which (rel, element) key first carried each token.
1443 // The same base tuple feeding two DISTINCT fact slots (a self-join whose
1444 // constant prefilters are not disjoint, so the slots share rows) is a
1445 // correlation the independence-assuming recursion cannot represent.
1446 std::map<std::string, std::pair<unsigned, std::vector<long>>> token_owner;
1447 int eoff=0;
1448 for(int i=0;i<n_fr;++i){
1449 const int ar=f_arity[i];
1450 if(ar<0||eoff+ar>n_fe)
1451 provsql_error("ucq_mobius: fact_elems shorter than arities");
1452 unsigned rel=static_cast<unsigned>(f_rel[i]);
1453 std::vector<long> el;
1454 for(int k=0;k<ar;++k){
1455 long e=static_cast<long>(f_elems[eoff+k]);
1456 el.push_back(e);
1457 fi.domain[{rel,k}].insert(e);
1458 }
1459 eoff+=ar;
1460 fi.present.insert({rel, el});
1461 bool nil=true;
1462 for(int b=0;b<16;++b) if(tok[i].data[b]!=0) nil=false;
1463 if(!nil) {
1464 pg_uuid_t tk = tok[i];
1465 const Datum gt = DirectFunctionCall1(get_gate_type, UUIDPGetDatum(&tk));
1466 if(static_cast<Oid>(DatumGetInt32(gt)) != input_oid)
1467 throw MobiusDecline("non-TID input: a fact token is not a bare "
1468 "gate_input (correlated / derived lineage)");
1469 }
1470 // Bag multiplicity guard: the fact index is keyed by (rel, element tuple),
1471 // one token per key (the SJF-TID reduced form). Two DISTINCT probabilistic
1472 // tuples that project to the same element tuple -- e.g. a relation column
1473 // not bound by the query, or a duplicate-bearing input -- would silently
1474 // collapse to one event (lose the OR), under-counting the probability. The
1475 // lifted-inference recursion has no place to represent that disjunction, so
1476 // decline and let the joint-width route (which sees full multiplicity) take
1477 // it. (A repeated key with the SAME token is the same tuple gathered twice
1478 // -- harmless, kept.)
1479 const std::string newtok = nil ? std::string() : uuid2string(tok[i]);
1480 auto existing = fi.tok.find({rel, el});
1481 if(existing != fi.tok.end() && existing->second != newtok)
1482 throw MobiusDecline("bag multiplicity: two distinct tuples share an "
1483 "element tuple (non-reduced data) -- defer to "
1484 "joint-width, which keeps the multiplicity");
1485 // The same token under a DIFFERENT (rel, element) key means one base tuple
1486 // feeds two fact slots -- an overlapping (non-disjoint) self-join, a
1487 // correlation the lifted recursion would wrongly treat as independent.
1488 // Decline (joint-width tracks the shared token correctly). A self-join
1489 // WITHIN the query is not this case and passes: its atoms share one slot,
1490 // and ranking / shattering plus the Möbius step handle them. What this
1491 // catches is two relation legs of the descriptor (one table scanned twice
1492 // under overlapping prefilters), which no shard split can separate -- the
1493 // shards partition a leg, not the legs themselves.
1494 if(!newtok.empty()) {
1495 auto own = token_owner.find(newtok);
1496 if(own != token_owner.end() && own->second != std::make_pair(rel, el))
1497 throw MobiusDecline("self-join overlap: one tuple feeds two fact slots "
1498 "(non-disjoint self-join) -- defer to joint-width");
1499 token_owner[newtok] = {rel, el};
1500 }
1501 fi.tok[{rel, el}] = newtok;
1502 }
1503 return fi;
1504}
1505
1506/// Decode the query arrays (0..4) into the top sentence (Boolean path).
1507Sentence decodeQuery(FunctionCallInfo fcinfo)
1508{
1509 int n_disj,n_ad,n_ar,n_av,n_aa;
1510 const int32 *d_nvars = intArr(fcinfo,0,"disjunct_nvars",n_disj);
1511 const int32 *a_disj = intArr(fcinfo,1,"atom_disjunct",n_ad);
1512 const int32 *a_rel = intArr(fcinfo,2,"atom_rel",n_ar);
1513 const int32 *a_vars = intArr(fcinfo,3,"atom_vars",n_av);
1514 const int32 *a_arity = intArr(fcinfo,4,"atom_arity",n_aa);
1515 if(n_ad!=n_ar || n_ad!=n_aa)
1516 provsql_error("ucq_mobius: atom arrays must have the same length");
1517 std::vector<long> base;
1518 return buildSentenceArrays(d_nvars,n_disj,a_disj,a_rel,a_vars,n_av,
1519 a_arity,n_ad,base);
1520}
1521
1522/// Decode the fact arrays (5..8) into a FactIndex (Boolean path).
1523FactIndex decodeFacts(FunctionCallInfo fcinfo)
1524{
1525 int n_fr,n_fe,n_fa,n_ft;
1526 const int32 *f_rel = intArr(fcinfo,5,"fact_rel",n_fr);
1527 const int32 *f_elems = intArr(fcinfo,6,"fact_elems",n_fe);
1528 const int32 *f_arity = intArr(fcinfo,7,"fact_arity",n_fa);
1529 ArrayType *toks = PG_ARGISNULL(8)?NULL:PG_GETARG_ARRAYTYPE_P(8);
1530 n_ft = checkedLen(toks,"fact_tokens");
1531 if(n_fr!=n_fa || n_fr!=n_ft)
1532 provsql_error("ucq_mobius: fact arrays must have the same length");
1533 const pg_uuid_t *tok = (toks&&n_ft)?(const pg_uuid_t*)ARR_DATA_PTR(toks):NULL;
1534 return buildFactIndexArrays(f_rel,n_fr,f_elems,n_fe,f_arity,tok);
1535}
1536
1537// ===========================================================================
1538// Per-answer (free head variables): head-pin then compile, one circuit per
1539// output group. On the first call of a query the facts are gathered once
1540// (ucq_joint_gather) and the value dictionary cached; each group binds its
1541// head variables to their values, the head positions are substituted to
1542// constants across every disjunct, and the Möbius circuit is compiled and
1543// cached. Mirrors ucq_joint_provenance_answer (the per-group caching), but
1544// each answer is a separate compile rather than a single sweep.
1545// ===========================================================================
1546
1547struct MobAnswerCache {
1548 bool ready = false; ///< gather succeeded
1549 Sentence sentence; ///< the UCQ template (global vars)
1550 std::vector<long> base; ///< per-disjunct variable offset
1551 std::vector<int> d_nvars; ///< per-disjunct n_vars
1552 FactIndex fi; ///< the gathered facts
1553 std::map<std::string,long> val_to_id; ///< text value -> dense id
1554 std::map<std::string,std::string> tokcache; ///< head-key -> token uuid
1555};
1556
1557void mobAnswerCacheDelete(void *arg) { delete reinterpret_cast<MobAnswerCache*>(arg); }
1558
1559std::string mobHeadKey(const std::vector<std::string> &vals)
1560{
1561 std::string k;
1562 for(const auto &v : vals){ k += v; k.push_back('\x1f'); }
1563 return k;
1564}
1565
1566/// Gather the facts + value dictionary once (via ucq_joint_gather) into @p c.
1567/// Returns false on any failure (the caller then declines to the fallback).
1568bool mobGather(Datum descriptor, MobAnswerCache *c)
1569{
1570 SPI_connect();
1571 Oid argt[1] = { JSONBOID };
1572 Datum argv[1] = { descriptor };
1573 char argn[1] = { ' ' };
1574 const int rc = SPI_execute_with_args(
1575 "SELECT * FROM provsql.ucq_joint_gather($1)", 1, argt, argv, argn, true, 1);
1576 if(rc != SPI_OK_SELECT || SPI_processed != 1) { SPI_finish(); return false; }
1577
1578 bool ok = true;
1579 try {
1580 TupleDesc td = SPI_tuptable->tupdesc;
1581 HeapTuple row = SPI_tuptable->vals[0];
1582 bool isnull;
1583 auto ia = [&](int col, int &n)->const int32*{
1584 Datum d = SPI_getbinval(row, td, col, &isnull);
1585 if(isnull){ n=0; return nullptr; }
1586 ArrayType *a = DatumGetArrayTypeP(d);
1587 n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
1588 return (const int32*) ARR_DATA_PTR(a);
1589 };
1590 int n_dnv,n_adisj,n_arel,n_avars,n_aarity,n_frel,n_felems,n_farity;
1591 const int32 *dnv = ia(1,n_dnv);
1592 const int32 *adisj = ia(2,n_adisj);
1593 const int32 *arel = ia(3,n_arel);
1594 const int32 *avars = ia(4,n_avars);
1595 const int32 *aarity = ia(5,n_aarity);
1596 const int32 *frel = ia(6,n_frel);
1597 const int32 *felems = ia(7,n_felems);
1598 const int32 *farity = ia(8,n_farity);
1599 Datum dtok = SPI_getbinval(row, td, 9, &isnull);
1600 ArrayType *atok = isnull ? nullptr : DatumGetArrayTypeP(dtok);
1601 const int n_ftok = atok ? ArrayGetNItems(ARR_NDIM(atok), ARR_DIMS(atok)) : 0;
1602 const pg_uuid_t *ftok = atok ? (const pg_uuid_t*) ARR_DATA_PTR(atok) : nullptr;
1603 if(n_frel != n_farity || n_frel != n_ftok)
1604 throw MobiusDecline("ucq_mobius: fact arrays length mismatch");
1605
1606 c->sentence = buildSentenceArrays(dnv,n_dnv,adisj,arel,avars,n_avars,
1607 aarity,n_adisj,c->base);
1608 c->d_nvars.assign(dnv, dnv+n_dnv);
1609 c->fi = buildFactIndexArrays(frel,n_frel,felems,n_felems,farity,ftok);
1610
1611 // The value dictionary: dense id -> text, inverted to text -> id.
1612 Datum dval = SPI_getbinval(row, td, 10, &isnull);
1613 if(!isnull) {
1614 ArrayType *aval = DatumGetArrayTypeP(dval);
1615 Datum *elems; bool *nulls; int nval;
1616 deconstruct_array(aval, TEXTOID, -1, false, TYPALIGN_INT,
1617 &elems, &nulls, &nval);
1618 for(int i=0;i<nval;++i)
1619 if(!nulls[i])
1620 c->val_to_id[TextDatumGetCString(elems[i])] = i;
1621 }
1622 } catch(...) {
1623 ok = false;
1624 }
1625 SPI_finish();
1626 return ok;
1627}
1628
1629} // namespace
1630
1631/**
1632 * @brief Materialise the safe-UCQ Möbius circuit and return its root token.
1633 *
1634 * Columnar arguments (mirrors @c ucq_joint_materialize_tracked, TID inputs):
1635 * 0..4 disjunct_nvars, atom_disjunct, atom_rel, atom_vars, atom_arity
1636 * 5..8 fact_rel, fact_elems, fact_arity, fact_tokens
1637 * 9 lineage (uuid, optional): the literal Boolean provenance of the query,
1638 * carried on the root gate_mobius so the token still answers Shapley /
1639 * semiring / PROV on the normal lineage (the Möbius combination is a
1640 * probability-only shortcut layered over it).
1641 *
1642 * The root is a @c gate_mobius carrying the certified-independent Boolean
1643 * islands (the signed combination) and the lineage child; @c probability_evaluate
1644 * answers the \#P-hard UCQ in PTIME through the fast Möbius route, and every other
1645 * evaluator passes through to the lineage. Declines raise an error so the SQL
1646 * wrapper falls back.
1647 */
1648Datum ucq_mobius_materialize_tracked(PG_FUNCTION_ARGS)
1649{
1650 try {
1651 Sentence s = decodeQuery(fcinfo);
1652 FactIndex fi = decodeFacts(fcinfo);
1653 std::string lineage;
1654 if(!PG_ARGISNULL(9))
1655 lineage = uuid2string(*PG_GETARG_UUID_P(9));
1656 MobiusStats st;
1657 pg_uuid_t root = compileNormalized(s, fi, st, lineage);
1658 pg_uuid_t *u = (pg_uuid_t*) palloc(sizeof(pg_uuid_t));
1659 *u = root;
1660 PG_RETURN_UUID_P(u);
1661 } catch(const MobiusDecline &e) {
1662 provsql_error("ucq_mobius: %s", e.what());
1663 } catch(const std::exception &e) {
1664 provsql_error("ucq_mobius: %s", e.what());
1665 } catch(...) {
1666 provsql_error("ucq_mobius: unknown exception");
1667 }
1668 PG_RETURN_NULL();
1669}
1670
1671/**
1672 * @brief Compile the Möbius circuit and return the lattice statistics plus the
1673 * probability (the demonstrability surface). Same columnar arguments
1674 * as @c ucq_mobius_materialize_tracked.
1675 */
1676Datum ucq_mobius_compile_stats(PG_FUNCTION_ARGS)
1677{
1678 try {
1679 Sentence s = decodeQuery(fcinfo);
1680 FactIndex fi = decodeFacts(fcinfo);
1681 MobiusStats st;
1682 pg_uuid_t root = compileNormalized(s, fi, st);
1683 st.probability = mobius_probability_of(root);
1684
1685 TupleDesc tupdesc;
1686 if(get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
1687 provsql_error("ucq_mobius_compile_stats: expected composite return type");
1688 tupdesc = BlessTupleDesc(tupdesc);
1689 Datum values[9];
1690 bool nulls[9] = {false,false,false,false,false,false,false,false,false};
1691 values[0] = Float8GetDatum(st.probability);
1692 values[1] = Int32GetDatum(st.n_components);
1693 values[2] = Int32GetDatum(st.n_cnf_conjuncts);
1694 values[3] = Int32GetDatum(st.lattice_collapsed);
1695 values[4] = Int32GetDatum(st.n_nonzero);
1696 values[5] = Int32GetDatum(st.n_cancelled);
1697 values[6] = BoolGetDatum(st.cancelled_hard);
1698 values[7] = Int64GetDatum(static_cast<int64>(st.dd_size));
1699 values[8] = Int64GetDatum(static_cast<int64>(st.memo_hits));
1700 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
1701 } catch(const std::exception &e) {
1702 provsql_error("ucq_mobius_compile_stats: %s", e.what());
1703 } catch(...) {
1704 provsql_error("ucq_mobius_compile_stats: unknown exception");
1705 }
1706 PG_RETURN_NULL();
1707}
1708
1709/**
1710 * @brief Per-answer Möbius provenance (the planner-substituted entry point for
1711 * a non-Boolean UCQ with free head variables).
1712 *
1713 * Arguments: (descriptor jsonb, head_vars int[], head_vals text[],
1714 * fallback uuid). Called once per output group; on the first call the facts
1715 * are gathered once and the value dictionary cached, then each group pins its
1716 * head variables (the canonical head indices @p head_vars, in every disjunct)
1717 * to their values (@p head_vals, matched through the gather's text dictionary)
1718 * and compiles the head-pinned Möbius circuit, caching head-key -> token. On
1719 * any decline (unsafe shape, head value absent, ...) returns @p fallback.
1720 */
1721Datum ucq_mobius_provenance_answer(PG_FUNCTION_ARGS)
1722{
1723 MobAnswerCache *cache =
1724 reinterpret_cast<MobAnswerCache*>(fcinfo->flinfo->fn_extra);
1725
1726 if(cache == nullptr) {
1727 MemoryContext fnctx = fcinfo->flinfo->fn_mcxt;
1728 cache = new MobAnswerCache();
1729 MemoryContextCallback *cb = (MemoryContextCallback*)
1730 MemoryContextAllocZero(fnctx, sizeof(MemoryContextCallback));
1731 cb->func = mobAnswerCacheDelete;
1732 cb->arg = cache;
1733 MemoryContextRegisterResetCallback(fnctx, cb);
1734 fcinfo->flinfo->fn_extra = cache;
1735
1736 if(!PG_ARGISNULL(0)) {
1737 // Gather inside a subtransaction so a SQL error declines gracefully.
1738 MemoryContext oldcxt = CurrentMemoryContext;
1739 ResourceOwner oldowner = CurrentResourceOwner;
1740 BeginInternalSubTransaction(NULL);
1741 PG_TRY();
1742 {
1743 cache->ready = mobGather(PG_GETARG_DATUM(0), cache);
1744 ReleaseCurrentSubTransaction();
1745 MemoryContextSwitchTo(oldcxt);
1746 CurrentResourceOwner = oldowner;
1747 }
1748 PG_CATCH();
1749 {
1750 MemoryContextSwitchTo(oldcxt);
1751 RollbackAndReleaseCurrentSubTransaction();
1752 MemoryContextSwitchTo(oldcxt);
1753 CurrentResourceOwner = oldowner;
1754 FlushErrorState();
1755 cache->ready = false;
1756 }
1757 PG_END_TRY();
1758 }
1759 }
1760
1761 // The group's head values (text), and the head variable indices.
1762 if(cache->ready && !PG_ARGISNULL(1) && !PG_ARGISNULL(2)) {
1763 std::vector<int> head_vars;
1764 {
1765 ArrayType *a = PG_GETARG_ARRAYTYPE_P(1);
1766 const int32 *d = (const int32*) ARR_DATA_PTR(a);
1767 const int n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
1768 for(int i=0;i<n;++i) head_vars.push_back(d[i]);
1769 }
1770 std::vector<std::string> head_vals;
1771 {
1772 ArrayType *a = PG_GETARG_ARRAYTYPE_P(2);
1773 Datum *elems; bool *nulls; int n;
1774 deconstruct_array(a, TEXTOID, -1, false, TYPALIGN_INT, &elems, &nulls, &n);
1775 for(int i=0;i<n;++i)
1776 head_vals.push_back(nulls[i] ? std::string() : TextDatumGetCString(elems[i]));
1777 }
1778
1779 if(head_vars.size() == head_vals.size()) {
1780 const std::string key = mobHeadKey(head_vals);
1781 auto it = cache->tokcache.find(key);
1782 if(it != cache->tokcache.end()) {
1783 pg_uuid_t *u = (pg_uuid_t*) palloc(sizeof(pg_uuid_t));
1784 *u = string2uuid(it->second);
1785 PG_RETURN_UUID_P(u);
1786 }
1787 try {
1788 // Map each head value to its dense id; pin the head variable (canonical
1789 // index hv, hence global base[d]+hv in every disjunct) to that constant.
1790 Sentence s = cache->sentence;
1791 bool resolved = true;
1792 for(std::size_t h=0; h<head_vars.size() && resolved; ++h) {
1793 auto vit = cache->val_to_id.find(head_vals[h]);
1794 if(vit == cache->val_to_id.end()) { resolved = false; break; }
1795 const long val = vit->second;
1796 const int hv = head_vars[h];
1797 for(std::size_t d=0; d<s.size(); ++d) {
1798 const long gid = cache->base[d] + hv;
1799 for(MAtom &at : s[d])
1800 for(Term &t : at.args)
1801 if(t.isVar && t.v == gid) { t.isVar=false; t.v=val; }
1802 }
1803 }
1804 if(resolved) {
1805 // The per-group literal lineage (the normal per-answer provenance,
1806 // argument 3) is carried on the gate_mobius so this answer's token
1807 // still answers Shapley / semiring on its normal lineage.
1808 std::string lineage;
1809 if(!PG_ARGISNULL(3))
1810 lineage = uuid2string(*PG_GETARG_UUID_P(3));
1811 MobiusStats st;
1812 pg_uuid_t root = compileNormalized(s, cache->fi, st, lineage);
1813 cache->tokcache[key] = uuid2string(root);
1814 pg_uuid_t *u = (pg_uuid_t*) palloc(sizeof(pg_uuid_t));
1815 *u = root;
1816 PG_RETURN_UUID_P(u);
1817 }
1818 } catch(const std::exception &e) {
1819 // decline this group -> fallback (reported at verbose_level >= 5, the
1820 // only place a decline is otherwise invisible)
1821 if(provsql_verbose >= 5)
1822 provsql_notice("ucq_mobius: per-answer decline: %s", e.what());
1823 } catch(...) {
1824 // decline this group -> fallback
1825 }
1826 }
1827 }
1828
1829 if(PG_ARGISNULL(3))
1830 PG_RETURN_NULL();
1831 PG_RETURN_DATUM(PG_GETARG_DATUM(3));
1832}
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.
bool operator<(gate_t t, std::vector< gate_t >::size_type u)
Compare a gate_t against a std::vector size type.
Definition Circuit.h:240
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)
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:107
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)...
int provsql_verbose
Verbosity level; controlled by the provsql.verbose_level GUC.
Definition provsql.c:93
int provsql_mobius_max_cnf
Query-cost cap of the Möbius route: it declines when a sentence's CNF has more than this many conjunc...
Definition provsql.c:108
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
#define provsql_notice(fmt,...)
Emit a ProvSQL informational notice (execution continues).
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.