ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
ucq_joint_evaluate.cpp
Go to the documentation of this file.
1/**
2 * @file ucq_joint_evaluate.cpp
3 * @brief SQL entry points for the joint-width UCQ compiler.
4 *
5 * Exposes @c UCQJointCompiler (see @c UCQJointCompiler.h) to SQL. The
6 * compiler's job ends at the certified d-D circuit; probability / Shapley /
7 * expectation are the standard evaluation on the materialised token, so the
8 * SQL surface is materialisation, not probability:
9 *
10 * - @c ucq_joint_provenance_answer(): the planner-substituted per-answer
11 * entry point. On the first call of a query it gathers the facts once
12 * (@c ucq_joint_gather), runs the single top-down DP, and materialises every
13 * answer's d-D into the store, caching head -> token in @c fn_extra; each
14 * output group is then an O(1) lookup -- one gather + one decomposition +
15 * one sweep for the whole GROUP BY.
16 * - @c ucq_joint_materialize_tracked(): materialise the Boolean (existence)
17 * d-D of a UCQ over real provenance tokens, returning its root token.
18 * - @c ucq_joint_compile_stats(): the same compilation returning the
19 * probability together with the three width columns (joint width, data-only
20 * and circuit-only degeneracy lower bounds) that substantiate thesis
21 * Prop. 4.2.11 empirically, and the structural statistics; the columnar
22 * form takes the query and facts as flat parallel arrays.
23 *
24 * Element ids are dense integers assigned by the gather with a dictionary
25 * shared across relations (so join-compatible values match).
26 */
27extern "C" {
28#include "postgres.h"
29#include "fmgr.h"
30#include "funcapi.h"
31#include "miscadmin.h"
32#include "access/htup_details.h"
33#include "access/xact.h"
34#include "catalog/pg_type.h"
35#include "executor/spi.h"
36#include "utils/array.h"
37#include "utils/builtins.h"
38#include "utils/resowner.h"
39#include "utils/uuid.h"
40
41#include "compatibility.h" /* TYPALIGN_INT fallback for PG < 13 */
42#include "provsql_utils.h"
43
44PG_FUNCTION_INFO_V1(ucq_joint_compile_stats);
45PG_FUNCTION_INFO_V1(ucq_joint_compile_stats_tracked);
46PG_FUNCTION_INFO_V1(ucq_joint_materialize_tracked);
47PG_FUNCTION_INFO_V1(ucq_joint_provenance_answer);
48}
49
50#include "c_cpp_compatibility.h"
51#include "JointEncoding.h"
52#include "UCQJointCompiler.h"
53#include "CircuitFromMMap.h"
55#include "GenericCircuit.h"
56#include "provsql_utils_cpp.h"
57
58#include <algorithm>
59#include <cmath>
60#include <limits>
61#include <map>
62#include <string>
63#include <vector>
64
65namespace {
66
67/** @brief Validate a 1-D, NULL-free array argument and return its length. */
68int checkedArrayLength(ArrayType *arr, const char *what)
69{
70 if (arr == NULL)
71 return 0;
72 if (ARR_NDIM(arr) > 1)
73 provsql_error("ucq_joint: %s must be a one-dimensional array", what);
74 if (ARR_HASNULL(arr))
75 provsql_error("ucq_joint: %s must not contain NULLs", what);
76 return ARR_NDIM(arr) == 0 ? 0 : ARR_DIMS(arr)[0];
77}
78
79/** @brief Fetch an int[] argument's data pointer (NULL when absent). */
80const int32 *intArray(FunctionCallInfo fcinfo, int argno, const char *what,
81 int &len)
82{
83 ArrayType *a = PG_ARGISNULL(argno) ? NULL : PG_GETARG_ARRAYTYPE_P(argno);
84 len = checkedArrayLength(a, what);
85 return (a == NULL || len == 0) ? NULL : (const int32 *) ARR_DATA_PTR(a);
86}
87
88/**
89 * @brief Decode the ten columnar arguments into a @c UCQ and the facts.
90 *
91 * Argument layout (all NULL-free 1-D arrays):
92 * 0 disjunct_nvars int[] per disjunct: number of query variables
93 * 1 atom_disjunct int[] per atom: owning disjunct index
94 * 2 atom_rel int[] per atom: relation id
95 * 3 atom_vars int[] flattened atom variable lists
96 * 4 atom_arity int[] per atom: number of variables
97 * 5 fact_rel int[] per fact: relation id
98 * 6 fact_elems int[] flattened fact element lists
99 * 7 fact_arity int[] per fact: arity
100 * 8 fact_tokens uuid[] per fact: provenance token (nil = certain)
101 * 9 fact_probs float8[] per fact: probability
102 */
103void decodeArgs(FunctionCallInfo fcinfo, UCQ &ucq,
104 std::vector<FactRow> &rows)
105{
106 int n_disj, n_ad, n_ar, n_av, n_aa;
107 const int32 *d_nvars = intArray(fcinfo, 0, "disjunct_nvars", n_disj);
108 const int32 *a_disj = intArray(fcinfo, 1, "atom_disjunct", n_ad);
109 const int32 *a_rel = intArray(fcinfo, 2, "atom_rel", n_ar);
110 const int32 *a_vars = intArray(fcinfo, 3, "atom_vars", n_av);
111 const int32 *a_arity = intArray(fcinfo, 4, "atom_arity", n_aa);
112
113 if (n_disj == 0)
114 provsql_error("ucq_joint: the UCQ has no disjuncts");
115 if (n_ad != n_ar || n_ad != n_aa)
116 provsql_error("ucq_joint: atom arrays must have the same length");
117
118 ucq.disjuncts.resize(n_disj);
119 for (int d = 0; d < n_disj; ++d)
120 ucq.disjuncts[d].n_vars = static_cast<unsigned>(d_nvars[d]);
121
122 int voff = 0;
123 for (int i = 0; i < n_ad; ++i) {
124 const int d = a_disj[i];
125 if (d < 0 || d >= n_disj)
126 provsql_error("ucq_joint: atom disjunct index out of range");
127 const int ar = a_arity[i];
128 if (ar < 0 || voff + ar > n_av)
129 provsql_error("ucq_joint: atom_vars shorter than declared arities");
130 Atom atom;
131 atom.relation_id = static_cast<unsigned>(a_rel[i]);
132 atom.vars.reserve(ar);
133 for (int k = 0; k < ar; ++k)
134 atom.vars.push_back(static_cast<unsigned>(a_vars[voff + k]));
135 voff += ar;
136 ucq.disjuncts[d].atoms.push_back(std::move(atom));
137 }
138
139 int n_fr, n_fe, n_fa, n_fp, n_ft;
140 const int32 *f_rel = intArray(fcinfo, 5, "fact_rel", n_fr);
141 const int32 *f_elems = intArray(fcinfo, 6, "fact_elems", n_fe);
142 const int32 *f_arity = intArray(fcinfo, 7, "fact_arity", n_fa);
143 ArrayType *toks = PG_ARGISNULL(8) ? NULL : PG_GETARG_ARRAYTYPE_P(8);
144 ArrayType *prbs = PG_ARGISNULL(9) ? NULL : PG_GETARG_ARRAYTYPE_P(9);
145 n_ft = checkedArrayLength(toks, "fact_tokens");
146 n_fp = checkedArrayLength(prbs, "fact_probs");
147
148 if (n_fr != n_fa || n_fr != n_ft || n_fr != n_fp)
149 provsql_error("ucq_joint: fact arrays must have the same length");
150 const pg_uuid_t *tok_data =
151 (toks && n_ft) ? (const pg_uuid_t *) ARR_DATA_PTR(toks) : NULL;
152 const float8 *prob_data =
153 (prbs && n_fp) ? (const float8 *) ARR_DATA_PTR(prbs) : NULL;
154
155 int eoff = 0;
156 rows.reserve(n_fr);
157 for (int i = 0; i < n_fr; ++i) {
158 const int ar = f_arity[i];
159 if (ar < 0 || eoff + ar > n_fe)
160 provsql_error("ucq_joint: fact_elems shorter than declared arities");
161 FactRow row;
162 row.relation_id = static_cast<unsigned>(f_rel[i]);
163 row.elements.reserve(ar);
164 for (int k = 0; k < ar; ++k)
165 row.elements.push_back(static_cast<unsigned long>(f_elems[eoff + k]));
166 eoff += ar;
167 bool nil = true;
168 for (int b = 0; b < 16; ++b)
169 if (tok_data[i].data[b] != 0)
170 nil = false;
171 if (!nil)
172 row.token = uuid2string(tok_data[i]);
173 row.prob = prob_data[i];
174 rows.push_back(std::move(row));
175 }
176}
177
178/** @brief Run the compiler over the decoded arguments. */
179UCQJointCompiler::Result runFromArgs(FunctionCallInfo fcinfo)
180{
181 UCQ ucq;
182 std::vector<FactRow> rows;
183 decodeArgs(fcinfo, ucq, rows);
184
185 const JointEncoding enc = JointEncoding::fromFacts(rows);
186 const unsigned max_tw =
187 static_cast<unsigned>(provsql_joint_max_treewidth);
188 const std::size_t max_states =
189 static_cast<std::size_t>(provsql_joint_max_states);
190 try {
191 return UCQJointCompiler::compile(enc, ucq, max_tw, max_states);
192 } catch (TreeDecompositionException &) {
194 "ucq_joint: joint treewidth exceeds the configured maximum (%d); "
195 "fall back to the standard probability ladder",
197 throw; /* unreachable; placate the compiler */
198 }
199}
200
201// ---------------------------------------------------------------------
202// Tracked (correlated) path: walk the mmap circuit slice from the fact
203// tokens, then run the merged DP.
204// ---------------------------------------------------------------------
205
206/** @brief Decode the five query arrays (arguments 0..4) into a @c UCQ. */
207void decodeQuery(FunctionCallInfo fcinfo, UCQ &ucq)
208{
209 int n_disj, n_ad, n_ar, n_av, n_aa;
210 const int32 *d_nvars = intArray(fcinfo, 0, "disjunct_nvars", n_disj);
211 const int32 *a_disj = intArray(fcinfo, 1, "atom_disjunct", n_ad);
212 const int32 *a_rel = intArray(fcinfo, 2, "atom_rel", n_ar);
213 const int32 *a_vars = intArray(fcinfo, 3, "atom_vars", n_av);
214 const int32 *a_arity = intArray(fcinfo, 4, "atom_arity", n_aa);
215
216 if (n_disj == 0)
217 provsql_error("ucq_joint: the UCQ has no disjuncts");
218 if (n_ad != n_ar || n_ad != n_aa)
219 provsql_error("ucq_joint: atom arrays must have the same length");
220
221 ucq.disjuncts.resize(n_disj);
222 for (int d = 0; d < n_disj; ++d)
223 ucq.disjuncts[d].n_vars = static_cast<unsigned>(d_nvars[d]);
224
225 int voff = 0;
226 for (int i = 0; i < n_ad; ++i) {
227 const int d = a_disj[i];
228 if (d < 0 || d >= n_disj)
229 provsql_error("ucq_joint: atom disjunct index out of range");
230 const int ar = a_arity[i];
231 if (ar < 0 || voff + ar > n_av)
232 provsql_error("ucq_joint: atom_vars shorter than declared arities");
233 Atom atom;
234 atom.relation_id = static_cast<unsigned>(a_rel[i]);
235 atom.vars.reserve(ar);
236 for (int k = 0; k < ar; ++k)
237 atom.vars.push_back(static_cast<unsigned>(a_vars[voff + k]));
238 voff += ar;
239 ucq.disjuncts[d].atoms.push_back(std::move(atom));
240 }
241}
242
243/**
244 * @brief Build the circuit slice reachable from the fact tokens.
245 *
246 * Walks the mmap circuit (through @c getGenericCircuit) from each
247 * distinct token down to @c gate_input leaves, keying slice nodes by
248 * UUID so facts whose tokens share an internal gate or an event share
249 * the slice node (the correlation the joint screen must see). Stores
250 * are normalised to arity ≤ 2 (a fan-in-@e k @c gate_times / @c gate_plus
251 * becomes a left-deep binary tree). @c gate_monus(one, x) is the
252 * Boolean @c NOT x; constants fold; @c gate_mulinput / @c gate_mixture
253 * and other non-Boolean gate types are rejected (the caller falls back
254 * to the ladder).
255 *
256 * The returned per-node code is @c -2 for constant true, @c -1 for
257 * constant false, and otherwise the slice index.
258 */
259struct SliceBuilder {
260 std::vector<SliceGate> slice;
261 std::map<std::string, int> uuid2node; ///< UUID -> node code (cached).
262
263 /// A @c gate_mulinput leaf deferred until @c expandMulBlocks() can see
264 /// the whole categorical block it belongs to. @c block is the UUID of
265 /// the shared key gate (all values of one repair_key block share it);
266 /// @c value_index orders the values; @c prob is the value's mass.
267 struct MulRef { std::string block; unsigned value_index; double prob; };
268 std::vector<MulRef> mulrefs; ///< Deferred mulinput leaves.
269 std::vector<int> mul_resolved; ///< MulRef index -> slice node.
270 std::string mulsb_sig; ///< Identity of the block being stick-broken.
271
272 /// Node codes below this are deferred mulinput references; the actual
273 /// MulRef index is @c MULREF_BASE - code (so distinct from the -1/-2
274 /// constants and from any non-negative slice index).
275 static constexpr int MULREF_BASE = -1000000;
276
277 int binarize(SliceGateType t, std::vector<int> &children) {
278 if (children.empty())
279 return t == SliceGateType::AND ? -2 : -1; // empty AND=true, OR=false
280 int acc = children[0];
281 for (std::size_t i = 1; i < children.size(); ++i) {
282 SliceGate s;
283 s.type = t;
284 s.children = {static_cast<unsigned>(acc),
285 static_cast<unsigned>(children[i])};
286 slice.push_back(std::move(s));
287 acc = static_cast<int>(slice.size()) - 1;
288 }
289 return acc;
290 }
291
292 int walk(const GenericCircuit &gc, gate_t g) {
293 const std::string u = gc.getUUID(g);
294 if (!u.empty()) {
295 auto it = uuid2node.find(u);
296 if (it != uuid2node.end())
297 return it->second;
298 }
299 int res;
300 switch (gc.getGateType(g)) {
301 case gate_input: {
302 SliceGate s;
304 s.prob = gc.getProb(g);
305 s.token = u;
306 slice.push_back(std::move(s));
307 res = static_cast<int>(slice.size()) - 1;
308 break;
309 }
310 case gate_one:
311 res = -2;
312 break;
313 case gate_zero:
314 res = -1;
315 break;
316 case gate_times: {
317 std::vector<int> ch;
318 bool cfalse = false;
319 for (gate_t c : gc.getWires(g)) {
320 int r = walk(gc, c);
321 if (r == -1) { cfalse = true; break; }
322 if (r == -2) continue; // one: identity for AND
323 ch.push_back(r);
324 }
325 res = cfalse ? -1 : binarize(SliceGateType::AND, ch);
326 break;
327 }
328 case gate_plus: {
329 std::vector<int> ch;
330 bool ctrue = false;
331 for (gate_t c : gc.getWires(g)) {
332 int r = walk(gc, c);
333 if (r == -2) { ctrue = true; break; }
334 if (r == -1) continue; // zero: identity for OR
335 ch.push_back(r);
336 }
337 res = ctrue ? -2 : binarize(SliceGateType::OR, ch);
338 break;
339 }
340 case gate_monus: {
341 const auto &w = gc.getWires(g);
342 if (w.size() != 2)
343 throw JointCompilerException("malformed monus gate in circuit slice");
344 const int a = walk(gc, w[0]);
345 const int b = walk(gc, w[1]);
346 if (a != -2)
347 throw JointCompilerException(
348 "non-Boolean monus in circuit slice (only the negation "
349 "monus(one, x) is supported)");
350 if (b == -2) res = -1; // NOT true = false
351 else if (b == -1) res = -2; // NOT false = true
352 else {
353 SliceGate s;
355 s.children = {static_cast<unsigned>(b)};
356 slice.push_back(std::move(s));
357 res = static_cast<int>(slice.size()) - 1;
358 }
359 break;
360 }
361 case gate_mulinput: {
362 // A BID / repair_key categorical value. We cannot stick-break it
363 // here -- that needs the cumulative mass of every value in its block,
364 // which is scattered across the facts. Defer it: record the value
365 // and return a placeholder code; expandMulBlocks() resolves the whole
366 // block at once (after every fact has been walked) into shared
367 // independent IN/NOT/AND slice nodes that enforce the mutual
368 // exclusivity (the block's values share stick-breaking events -- the
369 // correlation the joint screen must see).
370 const auto &w = gc.getWires(g);
371 if (w.empty())
372 throw JointCompilerException("malformed mulinput gate (no key wire)");
373 const std::string key = gc.getUUID(w[0]);
374 if (key.empty())
375 throw JointCompilerException(
376 "mulinput key gate has no UUID (cannot group the block)");
377 MulRef m;
378 m.block = key;
379 m.value_index = gc.getInfos(g).first;
380 m.prob = gc.getProb(g);
381 mulrefs.push_back(std::move(m));
382 res = MULREF_BASE - static_cast<int>(mulrefs.size() - 1);
383 break;
384 }
385 default:
386 throw JointCompilerException(
387 "unsupported gate type in circuit slice (joint-width "
388 "compilation handles input/and/or/not and mulinput only)");
389 }
390 if (!u.empty())
391 uuid2node[u] = res;
392 return res;
393 }
394
395 /// Append an independent IN (INPUT) slice node of mass @p p: a
396 /// stick-breaking coin. The leading @c \x01 keeps its token out of the
397 /// UUID space of real inputs, so JointEncoding treats it as an event of
398 /// its own; @p tag names *which* coin it is.
399 ///
400 /// The tag must determine the coin, because @c CertifiedDDMaterialize
401 /// turns it into a store gate whose UUID is the hash of the token: two
402 /// compilations that emit the same tag then land on the same gate, and
403 /// that gate carries one probability. @c mulsb_sig pins the block --
404 /// its key gate plus every value's index and mass -- and the range that
405 /// follows pins the split within it, so identical coins collapse and
406 /// distinct ones never collide.
407 int emitInput(double p, const std::string &tag) {
408 SliceGate s;
410 s.prob = p;
411 s.token = "\x01mulsb:" + tag;
412 slice.push_back(std::move(s));
413 return static_cast<int>(slice.size()) - 1;
414 }
415
416 /// Append a NOT slice node over the existing node @p child (>= 0).
417 int emitNot(int child) {
418 SliceGate s;
420 s.children = {static_cast<unsigned>(child)};
421 slice.push_back(std::move(s));
422 return static_cast<int>(slice.size()) - 1;
423 }
424
425 /// Stick-break the value range [@p start, @p end] of one block (balanced
426 /// bisection, mirroring BooleanCircuit::rewriteMultivaluedGatesRec): the
427 /// IN coin at each split is SHARED by both halves (so the values are
428 /// mutually exclusive), and each value's slice node is the AND of the
429 /// path of coins (positive on the left, negated on the right) that
430 /// selects it. @p prefix carries that path of already-chosen coins.
431 void expandRec(const std::vector<int> &idxs, const std::vector<double> &cum,
432 unsigned start, unsigned end, std::vector<int> &prefix) {
433 if (start == end) {
434 mul_resolved[idxs[start]] =
435 prefix.empty() ? emitInput(1.0, mulsb_sig + ":sole") // sole value
436 : binarize(SliceGateType::AND, prefix);
437 return;
438 }
439 const unsigned mid = (start + end) / 2;
440 const double prev_start = (start == 0) ? 0. : cum[start - 1];
441 const int g = emitInput((cum[mid] - prev_start) / (cum[end] - prev_start),
442 mulsb_sig + ":" + std::to_string(start) + "-"
443 + std::to_string(end));
444 const int ng = emitNot(g);
445 prefix.push_back(g);
446 expandRec(idxs, cum, start, mid, prefix);
447 prefix.pop_back();
448 prefix.push_back(ng);
449 expandRec(idxs, cum, mid + 1, end, prefix);
450 prefix.pop_back();
451 }
452
453 /// Resolve every deferred mulinput leaf. Group the MulRefs by block,
454 /// order each block's values, stick-break it into shared IN/NOT/AND slice
455 /// nodes, then rewrite every deferred reference (in slice children) to the
456 /// resolved node. Call once, after every fact token has been walked.
457 void expandMulBlocks() {
458 if (mulrefs.empty())
459 return;
460 mul_resolved.assign(mulrefs.size(), -1);
461 std::map<std::string, std::vector<int>> by_block;
462 for (int i = 0; i < static_cast<int>(mulrefs.size()); ++i)
463 by_block[mulrefs[i].block].push_back(i);
464
465 for (auto &kv : by_block) {
466 std::vector<int> &idxs = kv.second;
467 std::sort(idxs.begin(), idxs.end(), [&](int a, int b) {
468 return mulrefs[a].value_index < mulrefs[b].value_index;
469 });
470 const unsigned n = static_cast<unsigned>(idxs.size());
471 std::vector<double> cum(n);
472 double c = 0.;
473 for (unsigned i = 0; i < n; ++i) {
474 c += mulrefs[idxs[i]].prob;
475 cum[i] = c;
476 }
477 // Identity of this block: its key gate, plus every present value's
478 // index and mass. Everything emitInput below stamps on a coin hangs
479 // off it (see emitInput).
480 mulsb_sig = kv.first;
481 for (unsigned i = 0; i < n; ++i)
482 mulsb_sig += ":" + std::to_string(mulrefs[idxs[i]].value_index) + "="
483 + std::to_string(mulrefs[idxs[i]].prob);
484
485 std::vector<int> prefix;
486 // When the present values do not exhaust the block (their masses sum
487 // to less than 1, the rest being "key absent"), gate the whole block
488 // behind one block-active coin of that total mass.
489 constexpr double eps = std::numeric_limits<double>::epsilon() * 10;
490 if (cum[n - 1] < 1. - eps)
491 prefix.push_back(emitInput(cum[n - 1], mulsb_sig + ":active"));
492 expandRec(idxs, cum, 0, n - 1, prefix);
493 }
494
495 // Rewrite deferred references buried as children of AND/OR/NOT nodes.
496 for (SliceGate &sg : slice)
497 for (unsigned &ch : sg.children) {
498 const int ci = static_cast<int>(ch);
499 if (ci <= MULREF_BASE)
500 ch = static_cast<unsigned>(mul_resolved[MULREF_BASE - ci]);
501 }
502 }
503
504 /// Map a raw @c walk() code to a final slice node (resolving any deferred
505 /// mulinput reference). Returns -2 (true), -1 (false), or a slice index.
506 int resolveCode(int code) const {
507 if (code <= MULREF_BASE)
508 return mul_resolved[MULREF_BASE - code];
509 return code;
510 }
511};
512
513/**
514 * @brief Walk the fact tokens at arguments @p base..base+3 into a fact list
515 * and its circuit slice.
516 *
517 * @p base+0 fact_rel, @p base+1 fact_elems, @p base+2 fact_arity,
518 * @p base+3 fact_tokens (uuid[]). Each token is walked through the mmap
519 * circuit (shared slice nodes for shared gates -- the correlation the joint
520 * screen must see). On return @p facts holds the present facts (their gate
521 * indices into @p slice), @p n_elements is the element-id bound, and
522 * @p has_internal is true iff the slice has a non-INPUT gate (correlated).
523 * Shared by the Boolean tracked compile and the per-answer tracked sweep.
524 */
525void buildTrackedFactsArrays(const int32 *f_rel, int n_fr,
526 const int32 *f_elems, int n_fe,
527 const int32 *f_arity, int n_fa,
528 const pg_uuid_t *tok_data, int n_ft,
529 std::vector<Fact> &facts,
530 std::vector<SliceGate> &slice,
531 unsigned long &n_elements, bool &has_internal)
532{
533 if (n_fr != n_fa || n_fr != n_ft)
534 provsql_error("ucq_joint: fact arrays must have the same length");
535
536 SliceBuilder sb;
537 // Walk every fact's token first, keeping its raw slice code; mulinput
538 // (BID) leaves resolve only after the whole batch is seen (a value's
539 // block-mates live in other facts), so we finalise the facts in a second
540 // pass once expandMulBlocks() has stick-broken the blocks.
541 std::vector<std::pair<Fact, int>> pending;
542 pending.reserve(n_fr);
543 n_elements = 0;
544 int eoff = 0;
545 for (int i = 0; i < n_fr; ++i) {
546 const int ar = f_arity[i];
547 if (ar < 0 || eoff + ar > n_fe)
548 provsql_error("ucq_joint: fact_elems shorter than declared arities");
549 Fact f;
550 f.relation_id = static_cast<unsigned>(f_rel[i]);
551 for (int k = 0; k < ar; ++k) {
552 unsigned long e = static_cast<unsigned long>(f_elems[eoff + k]);
553 f.elements.push_back(e);
554 n_elements = std::max(n_elements, e + 1);
555 }
556 eoff += ar;
557 // Walk the token's slice.
558 GenericCircuit gc = getGenericCircuit(tok_data[i]);
559 const int node = sb.walk(gc, gc.getGate(uuid2string(tok_data[i])));
560 pending.emplace_back(std::move(f), node);
561 }
562
563 sb.expandMulBlocks();
564
565 facts.clear();
566 facts.reserve(pending.size());
567 for (auto &pf : pending) {
568 const int node = sb.resolveCode(pf.second);
569 if (node == -1)
570 continue; // constant-false token: the fact is never present
571 Fact &f = pf.first;
572 if (node == -2)
574 else {
576 f.gate = static_cast<std::size_t>(node);
577 }
578 facts.push_back(std::move(f));
579 }
580
581 has_internal = false;
582 for (const auto &sg : sb.slice)
583 if (sg.type != SliceGateType::INPUT) {
584 has_internal = true;
585 break;
586 }
587 slice = std::move(sb.slice);
588}
589
590/** @brief @c buildTrackedFactsArrays over the fact arrays at @p base..base+3. */
591void buildTrackedFacts(FunctionCallInfo fcinfo, int base,
592 std::vector<Fact> &facts, std::vector<SliceGate> &slice,
593 unsigned long &n_elements, bool &has_internal)
594{
595 int n_fr, n_fe, n_fa, n_ft;
596 const int32 *f_rel = intArray(fcinfo, base, "fact_rel", n_fr);
597 const int32 *f_elems = intArray(fcinfo, base + 1, "fact_elems", n_fe);
598 const int32 *f_arity = intArray(fcinfo, base + 2, "fact_arity", n_fa);
599 ArrayType *toks =
600 PG_ARGISNULL(base + 3) ? NULL : PG_GETARG_ARRAYTYPE_P(base + 3);
601 n_ft = checkedArrayLength(toks, "fact_tokens");
602 const pg_uuid_t *tok_data =
603 (toks && n_ft) ? (const pg_uuid_t *) ARR_DATA_PTR(toks) : NULL;
604 buildTrackedFactsArrays(f_rel, n_fr, f_elems, n_fe, f_arity, n_fa,
605 tok_data, n_ft, facts, slice, n_elements, has_internal);
606}
607
608/**
609 * @brief Single top-down DP materialisation: from the columnar fact arrays,
610 * walk the tokens once, run the single DP, and materialise every
611 * answer's certified d-D into the store.
612 *
613 * Returns a map from each answer's head (element-id tuple) to its root token.
614 * Throws @c TreeDecompositionException when the joint width is too large (the
615 * caller falls back). This is the engine behind the transparent per-answer
616 * route: one walk + one decomposition + one sweep for ALL answers.
617 */
618std::map<std::vector<unsigned long>, pg_uuid_t> materializeAnswersSingleDP(
619 const UCQ &ucq, const std::vector<unsigned> &head_vars,
620 const int32 *f_rel, int n_fr, const int32 *f_elems, int n_fe,
621 const int32 *f_arity, int n_fa, const pg_uuid_t *tok_data, int n_ft)
622{
623 std::vector<Fact> facts;
624 std::vector<SliceGate> slice;
625 unsigned long n_elements;
626 bool has_internal;
627 buildTrackedFactsArrays(f_rel, n_fr, f_elems, n_fe, f_arity, n_fa,
628 tok_data, n_ft, facts, slice, n_elements, has_internal);
629
630 const JointEncoding enc =
631 JointEncoding::fromCorrelated(std::move(facts), std::move(slice), n_elements);
632 const unsigned max_tw = static_cast<unsigned>(provsql_joint_max_treewidth);
633 const std::size_t max_states =
634 static_cast<std::size_t>(provsql_joint_max_states);
635
637 UCQJointCompiler::compileAnswersOneDP(enc, ucq, head_vars, max_tw, max_states);
638
639 std::vector<gate_t> roots;
640 roots.reserve(circ.answers.size());
641 for (const auto &a : circ.answers)
642 roots.push_back(a.root);
643 const auto uuid_of =
645
646 std::map<std::vector<unsigned long>, pg_uuid_t> out;
647 for (const auto &a : circ.answers)
648 out[a.head] = uuid_of.at(a.root);
649 return out;
650}
651
652/** @brief Run the correlated (tracked) compilation from the arguments. */
653UCQJointCompiler::Result runTrackedFromArgs(FunctionCallInfo fcinfo)
654{
655 UCQ ucq;
656 decodeQuery(fcinfo, ucq);
657
658 std::vector<Fact> facts;
659 std::vector<SliceGate> slice;
660 unsigned long n_elements;
661 bool has_internal;
662 buildTrackedFacts(fcinfo, 5, facts, slice, n_elements, has_internal);
663
664 const unsigned max_tw = static_cast<unsigned>(provsql_joint_max_treewidth);
665 const std::size_t max_states =
666 static_cast<std::size_t>(provsql_joint_max_states);
667
668 // Dispatch by input class: when the slice has no internal gates (every
669 // fact gated by a bare gate_input leaf -- the non-correlated / TID
670 // regime), use the data-graph fast path, which decomposes only the
671 // data (no gate vertices); the joint screen there is the data
672 // treewidth. Internal gates (correlated inputs) need the full joint
673 // data+circuit decomposition. Both give the same probability; the
674 // fast path is just cheaper. A shared leaf across *different* element
675 // tuples is a real correlation that fromFacts rejects -- fall back to
676 // the joint path then.
677 if (!has_internal) {
678 std::vector<FactRow> rows;
679 rows.reserve(facts.size());
680 for (const auto &f : facts) {
681 FactRow row;
682 row.relation_id = f.relation_id;
683 row.elements = f.elements;
684 if (f.kind == FactGateKind::GATE) {
685 row.token = slice[f.gate].token;
686 row.prob = slice[f.gate].prob;
687 } // CERTAIN: empty token, prob 1
688 rows.push_back(std::move(row));
689 }
690 try {
691 const JointEncoding enc = JointEncoding::fromFacts(rows);
692 try {
693 return UCQJointCompiler::compile(enc, ucq, max_tw, max_states);
694 } catch (TreeDecompositionException &) {
696 "ucq_joint: data treewidth exceeds the configured maximum (%d); "
697 "fall back to the standard probability ladder",
699 }
700 } catch (const JointCompilerException &) {
701 // A gate_input shared across different element tuples: genuinely
702 // correlated; fall through to the joint path below.
703 }
704 }
705
706 const JointEncoding enc =
707 JointEncoding::fromCorrelated(std::move(facts), std::move(slice),
708 n_elements);
709 try {
710 return UCQJointCompiler::compile(enc, ucq, max_tw, max_states);
711 } catch (TreeDecompositionException &) {
713 "ucq_joint: joint treewidth exceeds the configured maximum (%d); "
714 "fall back to the standard probability ladder",
716 throw;
717 }
718}
719
720} // namespace
721
722
723/**
724 * @brief PostgreSQL-callable entry point: UCQ probability plus
725 * compilation statistics (the three width columns and the
726 * structural stats).
727 */
728Datum ucq_joint_compile_stats(PG_FUNCTION_ARGS)
729{
730 try {
731 auto result = runFromArgs(fcinfo);
732
733 TupleDesc tupdesc;
734 if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
735 provsql_error("ucq_joint_compile_stats: expected composite return type");
736 tupdesc = BlessTupleDesc(tupdesc);
737
738 Datum values[8];
739 bool nulls[8] = {false, false, false, false, false, false, false, false};
740 values[0] = Float8GetDatum(result.dd.probabilityEvaluation());
741 values[1] = Int32GetDatum(static_cast<int32>(result.stats.joint_treewidth));
742 values[2] = Int32GetDatum(static_cast<int32>(result.stats.data_treewidth_lb));
743 values[3] = Int32GetDatum(static_cast<int32>(result.stats.circuit_treewidth_lb));
744 values[4] = Int64GetDatum(static_cast<int64>(result.stats.nb_bags));
745 values[5] = Int64GetDatum(static_cast<int64>(result.stats.max_states));
746 values[6] = Int64GetDatum(static_cast<int64>(result.stats.dd_size));
747 unsigned maxenum = 0;
748 for (unsigned ev : result.stats.n_enumerating)
749 if (ev > maxenum)
750 maxenum = ev;
751 values[7] = Int32GetDatum(static_cast<int32>(maxenum));
752
753 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
754 } catch (const std::exception &e) {
755 provsql_error("ucq_joint_compile_stats: %s", e.what());
756 } catch (...) {
757 provsql_error("ucq_joint_compile_stats: unknown exception");
758 }
759 PG_RETURN_NULL();
760}
761
762/**
763 * @brief PostgreSQL-callable entry point: compile the UCQ over correlated
764 * inputs and **materialise** its certified d-D into the store,
765 * returning the root provenance token.
766 *
767 * This is the architecturally-primary route: the joint-width compiler's
768 * job is to build the deterministic, decomposable circuit; the answer --
769 * probability, Shapley, expectation, any provenance-store evaluation --
770 * is then obtained through the single standard entry point on the
771 * returned token (e.g. @c probability_evaluate(token)), exploiting the
772 * d-D certificate for linear-time evaluation. Unlike the reachability
773 * route, the token is NOT wrapped in the @c 'absorptive' marker: the d-D
774 * is the exact Boolean provenance of the (non-recursive) UCQ.
775 */
776Datum ucq_joint_materialize_tracked(PG_FUNCTION_ARGS)
777{
778 try {
779 auto result = runTrackedFromArgs(fcinfo);
780 const auto uuid_of =
781 materializeCertifiedDD(result.dd, {result.dd.getRoot()},
783 pg_uuid_t *u = (pg_uuid_t *) palloc(sizeof(pg_uuid_t));
784 *u = uuid_of.at(result.dd.getRoot());
785 PG_RETURN_UUID_P(u);
786 } catch (const std::exception &e) {
787 provsql_error("ucq_joint_materialize: %s", e.what());
788 } catch (...) {
789 provsql_error("ucq_joint_materialize: unknown exception");
790 }
791 PG_RETURN_NULL();
792}
793
794
795/**
796 * @brief PostgreSQL-callable entry point: correlated UCQ probability plus
797 * compilation statistics (the three width columns substantiate
798 * Prop. 4.2.11: a correlated instance can have small data and
799 * circuit widths but large joint width).
800 */
801Datum ucq_joint_compile_stats_tracked(PG_FUNCTION_ARGS)
802{
803 try {
804 auto result = runTrackedFromArgs(fcinfo);
805
806 TupleDesc tupdesc;
807 if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
808 provsql_error("ucq_joint_compile_stats: expected composite return type");
809 tupdesc = BlessTupleDesc(tupdesc);
810
811 Datum values[8];
812 bool nulls[8] = {false, false, false, false, false, false, false, false};
813 values[0] = Float8GetDatum(result.dd.probabilityEvaluation());
814 values[1] = Int32GetDatum(static_cast<int32>(result.stats.joint_treewidth));
815 values[2] = Int32GetDatum(static_cast<int32>(result.stats.data_treewidth_lb));
816 values[3] = Int32GetDatum(static_cast<int32>(result.stats.circuit_treewidth_lb));
817 values[4] = Int64GetDatum(static_cast<int64>(result.stats.nb_bags));
818 values[5] = Int64GetDatum(static_cast<int64>(result.stats.max_states));
819 values[6] = Int64GetDatum(static_cast<int64>(result.stats.dd_size));
820 unsigned maxenum = 0;
821 for (unsigned ev : result.stats.n_enumerating)
822 if (ev > maxenum)
823 maxenum = ev;
824 values[7] = Int32GetDatum(static_cast<int32>(maxenum));
825
826 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
827 } catch (const std::exception &e) {
828 provsql_error("ucq_joint_compile_stats: %s", e.what());
829 } catch (...) {
830 provsql_error("ucq_joint_compile_stats: unknown exception");
831 }
832 PG_RETURN_NULL();
833}
834
835
836
837
838
839// ---------------------------------------------------------------------
840// Transparent per-answer route (planner-substituted). One call per output
841// group; on the FIRST call we gather the facts once, run the single top-down
842// DP, materialise EVERY answer's certified d-D, and cache head -> token in
843// fn_extra. Each subsequent group is an O(1) lookup, so the whole GROUP BY
844// costs one gather + one decomposition + one sweep.
845// ---------------------------------------------------------------------
846
847namespace {
848
849/** @brief A query's cached answers: serialised head-text key -> token. */
850struct JwAnswerCache {
851 bool declined = false;
852 std::vector<std::string> keys;
853 std::vector<pg_uuid_t> tokens;
854};
855
856/** @brief Delete the cache when its memory context is reset (query end). */
857void jwAnswerCacheDelete(void *arg)
858{
859 delete reinterpret_cast<JwAnswerCache *>(arg);
860}
861
862/** @brief Serialise a head tuple (text values) to one lookup key. */
863std::string jwHeadKey(const std::vector<std::string> &vals)
864{
865 std::string k;
866 for (const auto &v : vals) { k += v; k.push_back('\x1f'); }
867 return k;
868}
869
870} // namespace
871
872/**
873 * @brief Gather + single-DP materialise all answers into @p cache.
874 *
875 * Runs inside a subtransaction (the caller wraps it): a SQL error from the
876 * gather (e.g. an unsupported token type for which the joint route declines)
877 * propagates as an @c ereport and is caught by the caller's @c PG_CATCH; a
878 * compiler decline (joint width too large) is a C++ exception caught here and
879 * signalled by returning @c false. Returns @c true and fills @p cache on
880 * success.
881 */
882static bool jwComputeCache(Datum descriptor,
883 const std::vector<unsigned> &head_vars,
884 JwAnswerCache *cache)
885{
886 SPI_connect();
887 Oid argt[1] = { JSONBOID };
888 Datum argv[1] = { descriptor };
889 char argn[1] = { ' ' };
890 const int rc = SPI_execute_with_args(
891 "SELECT * FROM provsql.ucq_joint_gather($1)", 1, argt, argv, argn, true, 1);
892 if (rc != SPI_OK_SELECT || SPI_processed != 1) {
893 SPI_finish();
894 return false;
895 }
896
897 std::vector<std::string> keys;
898 std::vector<pg_uuid_t> tokens;
899 bool ok = true;
900 try {
901 TupleDesc td = SPI_tuptable->tupdesc;
902 HeapTuple row = SPI_tuptable->vals[0];
903 bool isnull;
904 auto intArr = [&](int col, int &n) -> const int32 * {
905 Datum dd = SPI_getbinval(row, td, col, &isnull);
906 if (isnull) { n = 0; return nullptr; }
907 ArrayType *a = DatumGetArrayTypeP(dd);
908 n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
909 return (const int32 *) ARR_DATA_PTR(a);
910 };
911 int n_dnv, n_adisj, n_arel, n_avars, n_aarity, n_frel, n_felems, n_farity;
912 const int32 *dnv = intArr(1, n_dnv);
913 const int32 *adisj = intArr(2, n_adisj);
914 const int32 *arel = intArr(3, n_arel);
915 const int32 *avars = intArr(4, n_avars);
916 const int32 *aarity = intArr(5, n_aarity);
917 const int32 *frel = intArr(6, n_frel);
918 const int32 *felems = intArr(7, n_felems);
919 const int32 *farity = intArr(8, n_farity);
920 Datum dtok = SPI_getbinval(row, td, 9, &isnull);
921 ArrayType *atok = isnull ? nullptr : DatumGetArrayTypeP(dtok);
922 const int n_ftok = atok ? ArrayGetNItems(ARR_NDIM(atok), ARR_DIMS(atok)) : 0;
923 const pg_uuid_t *ftok =
924 atok ? (const pg_uuid_t *) ARR_DATA_PTR(atok) : nullptr;
925
926 std::vector<std::string> val_by_id;
927 Datum dval = SPI_getbinval(row, td, 10, &isnull);
928 if (!isnull) {
929 ArrayType *aval = DatumGetArrayTypeP(dval);
930 Datum *elems; bool *nulls; int nval;
931 deconstruct_array(aval, TEXTOID, -1, false, TYPALIGN_INT,
932 &elems, &nulls, &nval);
933 val_by_id.resize(nval);
934 for (int i = 0; i < nval; ++i)
935 val_by_id[i] = nulls[i] ? std::string() : TextDatumGetCString(elems[i]);
936 }
937
938 UCQ ucq;
939 ucq.disjuncts.resize(n_dnv);
940 for (int d = 0; d < n_dnv; ++d)
941 ucq.disjuncts[d].n_vars = static_cast<unsigned>(dnv[d]);
942 int voff = 0;
943 for (int i = 0; i < n_adisj; ++i) {
944 const int dd = adisj[i];
945 const int ar = aarity[i];
946 Atom atom;
947 atom.relation_id = static_cast<unsigned>(arel[i]);
948 for (int k = 0; k < ar; ++k)
949 atom.vars.push_back(static_cast<unsigned>(avars[voff + k]));
950 voff += ar;
951 ucq.disjuncts[dd].atoms.push_back(std::move(atom));
952 }
953
954 const auto answers = materializeAnswersSingleDP(
955 ucq, head_vars, frel, n_frel, felems, n_felems, farity, n_farity,
956 ftok, n_ftok);
957 keys.reserve(answers.size());
958 tokens.reserve(answers.size());
959 for (const auto &kv : answers) {
960 std::vector<std::string> txt;
961 txt.reserve(kv.first.size());
962 for (unsigned long id : kv.first)
963 txt.push_back(id < val_by_id.size() ? val_by_id[id] : std::string());
964 keys.push_back(jwHeadKey(txt));
965 tokens.push_back(kv.second);
966 }
967 } catch (...) {
968 ok = false; // compiler decline (joint width too large, ...)
969 }
970 SPI_finish();
971 if (ok) {
972 cache->keys = std::move(keys);
973 cache->tokens = std::move(tokens);
974 }
975 return ok;
976}
977
978/**
979 * @brief Per-answer joint-width provenance via the single top-down DP.
980 * See @c ucq_joint_provenance_answer in provsql.common.sql.
981 */
982Datum ucq_joint_provenance_answer(PG_FUNCTION_ARGS)
983{
984 JwAnswerCache *cache =
985 reinterpret_cast<JwAnswerCache *>(fcinfo->flinfo->fn_extra);
986
987 if (cache == nullptr) {
988 // First call: build and cache all answers in the (per-query) fn context.
989 MemoryContext fnctx = fcinfo->flinfo->fn_mcxt;
990 cache = new JwAnswerCache();
991 MemoryContextCallback *cb = (MemoryContextCallback *)
992 MemoryContextAllocZero(fnctx, sizeof(MemoryContextCallback));
993 cb->func = jwAnswerCacheDelete;
994 cb->arg = cache;
995 MemoryContextRegisterResetCallback(fnctx, cb);
996 fcinfo->flinfo->fn_extra = cache;
997
998 if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) {
999 cache->declined = true;
1000 } else {
1001 std::vector<unsigned> head_vars;
1002 {
1003 ArrayType *a = PG_GETARG_ARRAYTYPE_P(1);
1004 const int32 *d = (const int32 *) ARR_DATA_PTR(a);
1005 const int n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
1006 for (int i = 0; i < n; ++i)
1007 head_vars.push_back(static_cast<unsigned>(d[i]));
1008 }
1009 const Datum desc = PG_GETARG_DATUM(0);
1010
1011 // Catch any error from the gather / compiler (an unsupported token, a
1012 // width too large, ...) and decline gracefully to the @p fallback, so a
1013 // recognised query never fails.
1014 MemoryContext oldcxt = CurrentMemoryContext;
1015 ResourceOwner oldowner = CurrentResourceOwner;
1016 BeginInternalSubTransaction(NULL);
1017 PG_TRY();
1018 {
1019 if (!jwComputeCache(desc, head_vars, cache))
1020 cache->declined = true;
1021 ReleaseCurrentSubTransaction();
1022 MemoryContextSwitchTo(oldcxt);
1023 CurrentResourceOwner = oldowner;
1024 }
1025 PG_CATCH();
1026 {
1027 MemoryContextSwitchTo(oldcxt);
1028 RollbackAndReleaseCurrentSubTransaction();
1029 MemoryContextSwitchTo(oldcxt);
1030 CurrentResourceOwner = oldowner;
1031 FlushErrorState();
1032 cache->declined = true;
1033 cache->keys.clear();
1034 cache->tokens.clear();
1035 }
1036 PG_END_TRY();
1037 }
1038 }
1039
1040 // Every call: look the group's head up in the cache.
1041 if (!cache->declined && !PG_ARGISNULL(2)) {
1042 std::vector<std::string> hv;
1043 ArrayType *a = PG_GETARG_ARRAYTYPE_P(2);
1044 Datum *elems; bool *nulls; int n;
1045 deconstruct_array(a, TEXTOID, -1, false, TYPALIGN_INT, &elems, &nulls, &n);
1046 for (int i = 0; i < n; ++i)
1047 hv.push_back(nulls[i] ? std::string() : TextDatumGetCString(elems[i]));
1048 const std::string key = jwHeadKey(hv);
1049 for (std::size_t i = 0; i < cache->keys.size(); ++i)
1050 if (cache->keys[i] == key) {
1051 pg_uuid_t *u = (pg_uuid_t *) palloc(sizeof(pg_uuid_t));
1052 *u = cache->tokens[i];
1053 PG_RETURN_UUID_P(u);
1054 }
1055 }
1056
1057 // Declined, or the group is not an answer of the joint compiler: fall back.
1058 if (PG_ARGISNULL(3))
1059 PG_RETURN_NULL();
1060 PG_RETURN_DATUM(PG_GETARG_DATUM(3));
1061}
@ AND
Boolean AND aggregate.
Definition Aggregation.h:57
std::unordered_map< gate_t, pg_uuid_t, hash_gate_t > materializeCertifiedDD(const dDNNF &dd, const std::vector< gate_t > &roots, provsql_route route)
Materialise (the reachable part of) a certified d-D into the mmap store.
Content-addressed materialisation of a certified d-D into the mmap provenance store.
static CircuitCache cache
Process-local singleton circuit gate cache.
GenericCircuit getGenericCircuit(pg_uuid_t token)
Build a GenericCircuit from the mmap store rooted at token.
Build in-memory circuits from the mmap-backed persistent store.
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Semiring-agnostic in-memory provenance circuit.
Phase A of the joint-width UCQ compiler: assemble the joint graph of the data and its correlation str...
SliceGateType
Gate kind of a circuit-slice node (correlated regime).
@ GATE
Present iff its slice gate evaluates true (correlated regime).
@ CERTAIN
Always present: an untracked relation, constant-true token.
Phase C of the joint-width UCQ compiler: a UCQ-specialised homomorphism-type DP that runs directly ov...
Fix macro conflicts between PostgreSQL headers and the C++ STL/Boost.
std::vector< gate_t > & getWires(gate_t g)
Return a mutable reference to the child-wire list of gate g.
Definition Circuit.h:140
gateType getGateType(gate_t g) const
Return the type of gate g.
Definition Circuit.h:130
uuid getUUID(gate_t g) const
Return the UUID string associated with gate g.
Definition Circuit.hpp:46
gate_t getGate(const uuid &u)
Return (or create) the gate associated with UUID u.
Definition Circuit.hpp:33
In-memory provenance circuit with semiring-generic evaluation.
double getProb(gate_t g) const
Return the probability for gate g.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
Exception thrown when joint-width compilation cannot proceed.
The joint encoding of an instance: facts, world events, and the joint graph the screen and the DP run...
static JointEncoding fromCorrelated(std::vector< Fact > facts, std::vector< SliceGate > slice, unsigned long n_elements)
Construct the correlated-regime encoding from facts and an already-extracted circuit slice.
static JointEncoding fromFacts(const std::vector< FactRow > &rows)
Build the data-graph (§3.5 fast path) encoding from raw rows.
Exception thrown when a tree decomposition cannot be constructed.
static AnswerCircuit compileAnswersOneDP(const JointEncoding &enc, const UCQ &ucq, const std::vector< unsigned > &head_vars, unsigned max_treewidth=TreeDecomposition::MAX_TREEWIDTH, std::size_t max_states=DEFAULT_MAX_STATES)
static Result compile(const JointEncoding &enc, const UCQ &ucq, unsigned max_treewidth=TreeDecomposition::MAX_TREEWIDTH, std::size_t max_states=DEFAULT_MAX_STATES)
Compile a Boolean UCQ over enc into a certified d-D.
PostgreSQL cross-version compatibility shims for ProvSQL.
#define TYPALIGN_INT
Alignment codes for the array routines (construct_array / deconstruct_array).
int provsql_joint_max_states
Per-bag DP state-count cap of the joint-width UCQ compiler (the true safety net); provsql....
Definition provsql.c:129
int provsql_joint_max_treewidth
Maximum joint treewidth the joint-width UCQ compiler attempts before declining (caller falls back to ...
Definition provsql.c:127
#define provsql_error(fmt,...)
Report a fatal ProvSQL error and abort the current transaction.
Core types, constants, and utilities shared across ProvSQL.
@ PROVSQL_ROUTE_BOUNDED_JW
Joint-width UCQ compiler (src/UCQJointCompiler.h).
string uuid2string(pg_uuid_t uuid)
Format a pg_uuid_t as a std::string.
C++ utility functions for UUID manipulation.
One atom of a conjunctive query: a relation symbol applied to query variables.
std::vector< unsigned > vars
Query-variable indices, one per column.
unsigned relation_id
Dense id of the relation symbol.
One row of an atom's relation, as handed in by the SQL layer.
unsigned relation_id
Dense id of the relation symbol.
double prob
Tuple probability (for an independent gate_input token).
std::string token
Provenance gate (UUID); empty marks a certain (untracked) fact.
std::vector< unsigned long > elements
Dense ids of the row's domain elements.
A deduplicated fact participating in the DP.
std::size_t gate
Index into slice (when GATE).
std::vector< unsigned long > elements
Dense element ids (the fact's tuple).
FactGateKind kind
How the fact's presence is gated.
unsigned relation_id
Dense id of the relation symbol.
SliceGateType type
Node kind.
std::vector< unsigned > children
Child slice indices (≤ 2; empty for INPUT).
double prob
Marginal (INPUT only).
std::string token
Provenance token of the leaf (INPUT only; for the d-D IN gate UUID).
dDNNF dd
The shared certified d-D.
std::vector< AnswerRoot > answers
One root per discovered answer.
A compiled UCQ: the d-D and its statistics.
A union of conjunctive queries.
std::vector< CQ > disjuncts
The disjuncts (at least one).
Datum ucq_joint_compile_stats(PG_FUNCTION_ARGS)
PostgreSQL-callable entry point: UCQ probability plus compilation statistics (the three width columns...
static bool jwComputeCache(Datum descriptor, const std::vector< unsigned > &head_vars, JwAnswerCache *cache)
Gather + single-DP materialise all answers into cache.
Datum ucq_joint_compile_stats_tracked(PG_FUNCTION_ARGS)
PostgreSQL-callable entry point: correlated UCQ probability plus compilation statistics (the three wi...
Datum ucq_joint_materialize_tracked(PG_FUNCTION_ARGS)
PostgreSQL-callable entry point: compile the UCQ over correlated inputs and materialise its certified...
Datum ucq_joint_provenance_answer(PG_FUNCTION_ARGS)
Per-answer joint-width provenance via the single top-down DP.