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 unsigned mulsb_counter = 0; ///< Fresh stick-breaking event ids.
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 a fresh independent IN (INPUT) slice node of mass @p p. Its
396 /// token is synthetic and unique so JointEncoding treats it as a brand
397 /// new event (a stick-breaking coin), never colliding with a real input.
398 int emitInput(double p) {
399 SliceGate s;
401 s.prob = p;
402 s.token = "\x01mulsb:" + std::to_string(mulsb_counter++);
403 slice.push_back(std::move(s));
404 return static_cast<int>(slice.size()) - 1;
405 }
406
407 /// Append a NOT slice node over the existing node @p child (>= 0).
408 int emitNot(int child) {
409 SliceGate s;
411 s.children = {static_cast<unsigned>(child)};
412 slice.push_back(std::move(s));
413 return static_cast<int>(slice.size()) - 1;
414 }
415
416 /// Stick-break the value range [@p start, @p end] of one block (balanced
417 /// bisection, mirroring BooleanCircuit::rewriteMultivaluedGatesRec): the
418 /// IN coin at each split is SHARED by both halves (so the values are
419 /// mutually exclusive), and each value's slice node is the AND of the
420 /// path of coins (positive on the left, negated on the right) that
421 /// selects it. @p prefix carries that path of already-chosen coins.
422 void expandRec(const std::vector<int> &idxs, const std::vector<double> &cum,
423 unsigned start, unsigned end, std::vector<int> &prefix) {
424 if (start == end) {
425 mul_resolved[idxs[start]] =
426 prefix.empty() ? emitInput(1.0) // sole value, mass 1
427 : binarize(SliceGateType::AND, prefix);
428 return;
429 }
430 const unsigned mid = (start + end) / 2;
431 const double prev_start = (start == 0) ? 0. : cum[start - 1];
432 const int g = emitInput((cum[mid] - prev_start) / (cum[end] - prev_start));
433 const int ng = emitNot(g);
434 prefix.push_back(g);
435 expandRec(idxs, cum, start, mid, prefix);
436 prefix.pop_back();
437 prefix.push_back(ng);
438 expandRec(idxs, cum, mid + 1, end, prefix);
439 prefix.pop_back();
440 }
441
442 /// Resolve every deferred mulinput leaf. Group the MulRefs by block,
443 /// order each block's values, stick-break it into shared IN/NOT/AND slice
444 /// nodes, then rewrite every deferred reference (in slice children) to the
445 /// resolved node. Call once, after every fact token has been walked.
446 void expandMulBlocks() {
447 if (mulrefs.empty())
448 return;
449 mul_resolved.assign(mulrefs.size(), -1);
450 std::map<std::string, std::vector<int>> by_block;
451 for (int i = 0; i < static_cast<int>(mulrefs.size()); ++i)
452 by_block[mulrefs[i].block].push_back(i);
453
454 for (auto &kv : by_block) {
455 std::vector<int> &idxs = kv.second;
456 std::sort(idxs.begin(), idxs.end(), [&](int a, int b) {
457 return mulrefs[a].value_index < mulrefs[b].value_index;
458 });
459 const unsigned n = static_cast<unsigned>(idxs.size());
460 std::vector<double> cum(n);
461 double c = 0.;
462 for (unsigned i = 0; i < n; ++i) {
463 c += mulrefs[idxs[i]].prob;
464 cum[i] = c;
465 }
466 std::vector<int> prefix;
467 // When the present values do not exhaust the block (their masses sum
468 // to less than 1, the rest being "key absent"), gate the whole block
469 // behind one block-active coin of that total mass.
470 constexpr double eps = std::numeric_limits<double>::epsilon() * 10;
471 if (cum[n - 1] < 1. - eps)
472 prefix.push_back(emitInput(cum[n - 1]));
473 expandRec(idxs, cum, 0, n - 1, prefix);
474 }
475
476 // Rewrite deferred references buried as children of AND/OR/NOT nodes.
477 for (SliceGate &sg : slice)
478 for (unsigned &ch : sg.children) {
479 const int ci = static_cast<int>(ch);
480 if (ci <= MULREF_BASE)
481 ch = static_cast<unsigned>(mul_resolved[MULREF_BASE - ci]);
482 }
483 }
484
485 /// Map a raw @c walk() code to a final slice node (resolving any deferred
486 /// mulinput reference). Returns -2 (true), -1 (false), or a slice index.
487 int resolveCode(int code) const {
488 if (code <= MULREF_BASE)
489 return mul_resolved[MULREF_BASE - code];
490 return code;
491 }
492};
493
494/**
495 * @brief Walk the fact tokens at arguments @p base..base+3 into a fact list
496 * and its circuit slice.
497 *
498 * @p base+0 fact_rel, @p base+1 fact_elems, @p base+2 fact_arity,
499 * @p base+3 fact_tokens (uuid[]). Each token is walked through the mmap
500 * circuit (shared slice nodes for shared gates -- the correlation the joint
501 * screen must see). On return @p facts holds the present facts (their gate
502 * indices into @p slice), @p n_elements is the element-id bound, and
503 * @p has_internal is true iff the slice has a non-INPUT gate (correlated).
504 * Shared by the Boolean tracked compile and the per-answer tracked sweep.
505 */
506void buildTrackedFactsArrays(const int32 *f_rel, int n_fr,
507 const int32 *f_elems, int n_fe,
508 const int32 *f_arity, int n_fa,
509 const pg_uuid_t *tok_data, int n_ft,
510 std::vector<Fact> &facts,
511 std::vector<SliceGate> &slice,
512 unsigned long &n_elements, bool &has_internal)
513{
514 if (n_fr != n_fa || n_fr != n_ft)
515 provsql_error("ucq_joint: fact arrays must have the same length");
516
517 SliceBuilder sb;
518 // Walk every fact's token first, keeping its raw slice code; mulinput
519 // (BID) leaves resolve only after the whole batch is seen (a value's
520 // block-mates live in other facts), so we finalise the facts in a second
521 // pass once expandMulBlocks() has stick-broken the blocks.
522 std::vector<std::pair<Fact, int>> pending;
523 pending.reserve(n_fr);
524 n_elements = 0;
525 int eoff = 0;
526 for (int i = 0; i < n_fr; ++i) {
527 const int ar = f_arity[i];
528 if (ar < 0 || eoff + ar > n_fe)
529 provsql_error("ucq_joint: fact_elems shorter than declared arities");
530 Fact f;
531 f.relation_id = static_cast<unsigned>(f_rel[i]);
532 for (int k = 0; k < ar; ++k) {
533 unsigned long e = static_cast<unsigned long>(f_elems[eoff + k]);
534 f.elements.push_back(e);
535 n_elements = std::max(n_elements, e + 1);
536 }
537 eoff += ar;
538 // Walk the token's slice.
539 GenericCircuit gc = getGenericCircuit(tok_data[i]);
540 const int node = sb.walk(gc, gc.getGate(uuid2string(tok_data[i])));
541 pending.emplace_back(std::move(f), node);
542 }
543
544 sb.expandMulBlocks();
545
546 facts.clear();
547 facts.reserve(pending.size());
548 for (auto &pf : pending) {
549 const int node = sb.resolveCode(pf.second);
550 if (node == -1)
551 continue; // constant-false token: the fact is never present
552 Fact &f = pf.first;
553 if (node == -2)
555 else {
557 f.gate = static_cast<std::size_t>(node);
558 }
559 facts.push_back(std::move(f));
560 }
561
562 has_internal = false;
563 for (const auto &sg : sb.slice)
564 if (sg.type != SliceGateType::INPUT) {
565 has_internal = true;
566 break;
567 }
568 slice = std::move(sb.slice);
569}
570
571/** @brief @c buildTrackedFactsArrays over the fact arrays at @p base..base+3. */
572void buildTrackedFacts(FunctionCallInfo fcinfo, int base,
573 std::vector<Fact> &facts, std::vector<SliceGate> &slice,
574 unsigned long &n_elements, bool &has_internal)
575{
576 int n_fr, n_fe, n_fa, n_ft;
577 const int32 *f_rel = intArray(fcinfo, base, "fact_rel", n_fr);
578 const int32 *f_elems = intArray(fcinfo, base + 1, "fact_elems", n_fe);
579 const int32 *f_arity = intArray(fcinfo, base + 2, "fact_arity", n_fa);
580 ArrayType *toks =
581 PG_ARGISNULL(base + 3) ? NULL : PG_GETARG_ARRAYTYPE_P(base + 3);
582 n_ft = checkedArrayLength(toks, "fact_tokens");
583 const pg_uuid_t *tok_data =
584 (toks && n_ft) ? (const pg_uuid_t *) ARR_DATA_PTR(toks) : NULL;
585 buildTrackedFactsArrays(f_rel, n_fr, f_elems, n_fe, f_arity, n_fa,
586 tok_data, n_ft, facts, slice, n_elements, has_internal);
587}
588
589/**
590 * @brief Single top-down DP materialisation: from the columnar fact arrays,
591 * walk the tokens once, run the single DP, and materialise every
592 * answer's certified d-D into the store.
593 *
594 * Returns a map from each answer's head (element-id tuple) to its root token.
595 * Throws @c TreeDecompositionException when the joint width is too large (the
596 * caller falls back). This is the engine behind the transparent per-answer
597 * route: one walk + one decomposition + one sweep for ALL answers.
598 */
599std::map<std::vector<unsigned long>, pg_uuid_t> materializeAnswersSingleDP(
600 const UCQ &ucq, const std::vector<unsigned> &head_vars,
601 const int32 *f_rel, int n_fr, const int32 *f_elems, int n_fe,
602 const int32 *f_arity, int n_fa, const pg_uuid_t *tok_data, int n_ft)
603{
604 std::vector<Fact> facts;
605 std::vector<SliceGate> slice;
606 unsigned long n_elements;
607 bool has_internal;
608 buildTrackedFactsArrays(f_rel, n_fr, f_elems, n_fe, f_arity, n_fa,
609 tok_data, n_ft, facts, slice, n_elements, has_internal);
610
611 const JointEncoding enc =
612 JointEncoding::fromCorrelated(std::move(facts), std::move(slice), n_elements);
613 const unsigned max_tw = static_cast<unsigned>(provsql_joint_max_treewidth);
614 const std::size_t max_states =
615 static_cast<std::size_t>(provsql_joint_max_states);
616
618 UCQJointCompiler::compileAnswersOneDP(enc, ucq, head_vars, max_tw, max_states);
619
620 std::vector<gate_t> roots;
621 roots.reserve(circ.answers.size());
622 for (const auto &a : circ.answers)
623 roots.push_back(a.root);
624 const auto uuid_of =
626
627 std::map<std::vector<unsigned long>, pg_uuid_t> out;
628 for (const auto &a : circ.answers)
629 out[a.head] = uuid_of.at(a.root);
630 return out;
631}
632
633/** @brief Run the correlated (tracked) compilation from the arguments. */
634UCQJointCompiler::Result runTrackedFromArgs(FunctionCallInfo fcinfo)
635{
636 UCQ ucq;
637 decodeQuery(fcinfo, ucq);
638
639 std::vector<Fact> facts;
640 std::vector<SliceGate> slice;
641 unsigned long n_elements;
642 bool has_internal;
643 buildTrackedFacts(fcinfo, 5, facts, slice, n_elements, has_internal);
644
645 const unsigned max_tw = static_cast<unsigned>(provsql_joint_max_treewidth);
646 const std::size_t max_states =
647 static_cast<std::size_t>(provsql_joint_max_states);
648
649 // Dispatch by input class: when the slice has no internal gates (every
650 // fact gated by a bare gate_input leaf -- the non-correlated / TID
651 // regime), use the data-graph fast path, which decomposes only the
652 // data (no gate vertices); the joint screen there is the data
653 // treewidth. Internal gates (correlated inputs) need the full joint
654 // data+circuit decomposition. Both give the same probability; the
655 // fast path is just cheaper. A shared leaf across *different* element
656 // tuples is a real correlation that fromFacts rejects -- fall back to
657 // the joint path then.
658 if (!has_internal) {
659 std::vector<FactRow> rows;
660 rows.reserve(facts.size());
661 for (const auto &f : facts) {
662 FactRow row;
663 row.relation_id = f.relation_id;
664 row.elements = f.elements;
665 if (f.kind == FactGateKind::GATE) {
666 row.token = slice[f.gate].token;
667 row.prob = slice[f.gate].prob;
668 } // CERTAIN: empty token, prob 1
669 rows.push_back(std::move(row));
670 }
671 try {
672 const JointEncoding enc = JointEncoding::fromFacts(rows);
673 try {
674 return UCQJointCompiler::compile(enc, ucq, max_tw, max_states);
675 } catch (TreeDecompositionException &) {
677 "ucq_joint: data treewidth exceeds the configured maximum (%d); "
678 "fall back to the standard probability ladder",
680 }
681 } catch (const JointCompilerException &) {
682 // A gate_input shared across different element tuples: genuinely
683 // correlated; fall through to the joint path below.
684 }
685 }
686
687 const JointEncoding enc =
688 JointEncoding::fromCorrelated(std::move(facts), std::move(slice),
689 n_elements);
690 try {
691 return UCQJointCompiler::compile(enc, ucq, max_tw, max_states);
692 } catch (TreeDecompositionException &) {
694 "ucq_joint: joint treewidth exceeds the configured maximum (%d); "
695 "fall back to the standard probability ladder",
697 throw;
698 }
699}
700
701} // namespace
702
703
704/**
705 * @brief PostgreSQL-callable entry point: UCQ probability plus
706 * compilation statistics (the three width columns and the
707 * structural stats).
708 */
709Datum ucq_joint_compile_stats(PG_FUNCTION_ARGS)
710{
711 try {
712 auto result = runFromArgs(fcinfo);
713
714 TupleDesc tupdesc;
715 if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
716 provsql_error("ucq_joint_compile_stats: expected composite return type");
717 tupdesc = BlessTupleDesc(tupdesc);
718
719 Datum values[8];
720 bool nulls[8] = {false, false, false, false, false, false, false, false};
721 values[0] = Float8GetDatum(result.dd.probabilityEvaluation());
722 values[1] = Int32GetDatum(static_cast<int32>(result.stats.joint_treewidth));
723 values[2] = Int32GetDatum(static_cast<int32>(result.stats.data_treewidth_lb));
724 values[3] = Int32GetDatum(static_cast<int32>(result.stats.circuit_treewidth_lb));
725 values[4] = Int64GetDatum(static_cast<int64>(result.stats.nb_bags));
726 values[5] = Int64GetDatum(static_cast<int64>(result.stats.max_states));
727 values[6] = Int64GetDatum(static_cast<int64>(result.stats.dd_size));
728 unsigned maxenum = 0;
729 for (unsigned ev : result.stats.n_enumerating)
730 if (ev > maxenum)
731 maxenum = ev;
732 values[7] = Int32GetDatum(static_cast<int32>(maxenum));
733
734 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
735 } catch (const std::exception &e) {
736 provsql_error("ucq_joint_compile_stats: %s", e.what());
737 } catch (...) {
738 provsql_error("ucq_joint_compile_stats: unknown exception");
739 }
740 PG_RETURN_NULL();
741}
742
743/**
744 * @brief PostgreSQL-callable entry point: compile the UCQ over correlated
745 * inputs and **materialise** its certified d-D into the store,
746 * returning the root provenance token.
747 *
748 * This is the architecturally-primary route: the joint-width compiler's
749 * job is to build the deterministic, decomposable circuit; the answer --
750 * probability, Shapley, expectation, any provenance-store evaluation --
751 * is then obtained through the single standard entry point on the
752 * returned token (e.g. @c probability_evaluate(token)), exploiting the
753 * d-D certificate for linear-time evaluation. Unlike the reachability
754 * route, the token is NOT wrapped in the @c 'absorptive' marker: the d-D
755 * is the exact Boolean provenance of the (non-recursive) UCQ.
756 */
757Datum ucq_joint_materialize_tracked(PG_FUNCTION_ARGS)
758{
759 try {
760 auto result = runTrackedFromArgs(fcinfo);
761 const auto uuid_of =
762 materializeCertifiedDD(result.dd, {result.dd.getRoot()},
764 pg_uuid_t *u = (pg_uuid_t *) palloc(sizeof(pg_uuid_t));
765 *u = uuid_of.at(result.dd.getRoot());
766 PG_RETURN_UUID_P(u);
767 } catch (const std::exception &e) {
768 provsql_error("ucq_joint_materialize: %s", e.what());
769 } catch (...) {
770 provsql_error("ucq_joint_materialize: unknown exception");
771 }
772 PG_RETURN_NULL();
773}
774
775
776/**
777 * @brief PostgreSQL-callable entry point: correlated UCQ probability plus
778 * compilation statistics (the three width columns substantiate
779 * Prop. 4.2.11: a correlated instance can have small data and
780 * circuit widths but large joint width).
781 */
782Datum ucq_joint_compile_stats_tracked(PG_FUNCTION_ARGS)
783{
784 try {
785 auto result = runTrackedFromArgs(fcinfo);
786
787 TupleDesc tupdesc;
788 if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
789 provsql_error("ucq_joint_compile_stats: expected composite return type");
790 tupdesc = BlessTupleDesc(tupdesc);
791
792 Datum values[8];
793 bool nulls[8] = {false, false, false, false, false, false, false, false};
794 values[0] = Float8GetDatum(result.dd.probabilityEvaluation());
795 values[1] = Int32GetDatum(static_cast<int32>(result.stats.joint_treewidth));
796 values[2] = Int32GetDatum(static_cast<int32>(result.stats.data_treewidth_lb));
797 values[3] = Int32GetDatum(static_cast<int32>(result.stats.circuit_treewidth_lb));
798 values[4] = Int64GetDatum(static_cast<int64>(result.stats.nb_bags));
799 values[5] = Int64GetDatum(static_cast<int64>(result.stats.max_states));
800 values[6] = Int64GetDatum(static_cast<int64>(result.stats.dd_size));
801 unsigned maxenum = 0;
802 for (unsigned ev : result.stats.n_enumerating)
803 if (ev > maxenum)
804 maxenum = ev;
805 values[7] = Int32GetDatum(static_cast<int32>(maxenum));
806
807 PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
808 } catch (const std::exception &e) {
809 provsql_error("ucq_joint_compile_stats: %s", e.what());
810 } catch (...) {
811 provsql_error("ucq_joint_compile_stats: unknown exception");
812 }
813 PG_RETURN_NULL();
814}
815
816
817
818
819
820// ---------------------------------------------------------------------
821// Transparent per-answer route (planner-substituted). One call per output
822// group; on the FIRST call we gather the facts once, run the single top-down
823// DP, materialise EVERY answer's certified d-D, and cache head -> token in
824// fn_extra. Each subsequent group is an O(1) lookup, so the whole GROUP BY
825// costs one gather + one decomposition + one sweep.
826// ---------------------------------------------------------------------
827
828namespace {
829
830/** @brief A query's cached answers: serialised head-text key -> token. */
831struct JwAnswerCache {
832 bool declined = false;
833 std::vector<std::string> keys;
834 std::vector<pg_uuid_t> tokens;
835};
836
837/** @brief Delete the cache when its memory context is reset (query end). */
838void jwAnswerCacheDelete(void *arg)
839{
840 delete reinterpret_cast<JwAnswerCache *>(arg);
841}
842
843/** @brief Serialise a head tuple (text values) to one lookup key. */
844std::string jwHeadKey(const std::vector<std::string> &vals)
845{
846 std::string k;
847 for (const auto &v : vals) { k += v; k.push_back('\x1f'); }
848 return k;
849}
850
851} // namespace
852
853/**
854 * @brief Gather + single-DP materialise all answers into @p cache.
855 *
856 * Runs inside a subtransaction (the caller wraps it): a SQL error from the
857 * gather (e.g. an unsupported token type for which the joint route declines)
858 * propagates as an @c ereport and is caught by the caller's @c PG_CATCH; a
859 * compiler decline (joint width too large) is a C++ exception caught here and
860 * signalled by returning @c false. Returns @c true and fills @p cache on
861 * success.
862 */
863static bool jwComputeCache(Datum descriptor,
864 const std::vector<unsigned> &head_vars,
865 JwAnswerCache *cache)
866{
867 SPI_connect();
868 Oid argt[1] = { JSONBOID };
869 Datum argv[1] = { descriptor };
870 char argn[1] = { ' ' };
871 const int rc = SPI_execute_with_args(
872 "SELECT * FROM provsql.ucq_joint_gather($1)", 1, argt, argv, argn, true, 1);
873 if (rc != SPI_OK_SELECT || SPI_processed != 1) {
874 SPI_finish();
875 return false;
876 }
877
878 std::vector<std::string> keys;
879 std::vector<pg_uuid_t> tokens;
880 bool ok = true;
881 try {
882 TupleDesc td = SPI_tuptable->tupdesc;
883 HeapTuple row = SPI_tuptable->vals[0];
884 bool isnull;
885 auto intArr = [&](int col, int &n) -> const int32 * {
886 Datum dd = SPI_getbinval(row, td, col, &isnull);
887 if (isnull) { n = 0; return nullptr; }
888 ArrayType *a = DatumGetArrayTypeP(dd);
889 n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
890 return (const int32 *) ARR_DATA_PTR(a);
891 };
892 int n_dnv, n_adisj, n_arel, n_avars, n_aarity, n_frel, n_felems, n_farity;
893 const int32 *dnv = intArr(1, n_dnv);
894 const int32 *adisj = intArr(2, n_adisj);
895 const int32 *arel = intArr(3, n_arel);
896 const int32 *avars = intArr(4, n_avars);
897 const int32 *aarity = intArr(5, n_aarity);
898 const int32 *frel = intArr(6, n_frel);
899 const int32 *felems = intArr(7, n_felems);
900 const int32 *farity = intArr(8, n_farity);
901 Datum dtok = SPI_getbinval(row, td, 9, &isnull);
902 ArrayType *atok = isnull ? nullptr : DatumGetArrayTypeP(dtok);
903 const int n_ftok = atok ? ArrayGetNItems(ARR_NDIM(atok), ARR_DIMS(atok)) : 0;
904 const pg_uuid_t *ftok =
905 atok ? (const pg_uuid_t *) ARR_DATA_PTR(atok) : nullptr;
906
907 std::vector<std::string> val_by_id;
908 Datum dval = SPI_getbinval(row, td, 10, &isnull);
909 if (!isnull) {
910 ArrayType *aval = DatumGetArrayTypeP(dval);
911 Datum *elems; bool *nulls; int nval;
912 deconstruct_array(aval, TEXTOID, -1, false, TYPALIGN_INT,
913 &elems, &nulls, &nval);
914 val_by_id.resize(nval);
915 for (int i = 0; i < nval; ++i)
916 val_by_id[i] = nulls[i] ? std::string() : TextDatumGetCString(elems[i]);
917 }
918
919 UCQ ucq;
920 ucq.disjuncts.resize(n_dnv);
921 for (int d = 0; d < n_dnv; ++d)
922 ucq.disjuncts[d].n_vars = static_cast<unsigned>(dnv[d]);
923 int voff = 0;
924 for (int i = 0; i < n_adisj; ++i) {
925 const int dd = adisj[i];
926 const int ar = aarity[i];
927 Atom atom;
928 atom.relation_id = static_cast<unsigned>(arel[i]);
929 for (int k = 0; k < ar; ++k)
930 atom.vars.push_back(static_cast<unsigned>(avars[voff + k]));
931 voff += ar;
932 ucq.disjuncts[dd].atoms.push_back(std::move(atom));
933 }
934
935 const auto answers = materializeAnswersSingleDP(
936 ucq, head_vars, frel, n_frel, felems, n_felems, farity, n_farity,
937 ftok, n_ftok);
938 keys.reserve(answers.size());
939 tokens.reserve(answers.size());
940 for (const auto &kv : answers) {
941 std::vector<std::string> txt;
942 txt.reserve(kv.first.size());
943 for (unsigned long id : kv.first)
944 txt.push_back(id < val_by_id.size() ? val_by_id[id] : std::string());
945 keys.push_back(jwHeadKey(txt));
946 tokens.push_back(kv.second);
947 }
948 } catch (...) {
949 ok = false; // compiler decline (joint width too large, ...)
950 }
951 SPI_finish();
952 if (ok) {
953 cache->keys = std::move(keys);
954 cache->tokens = std::move(tokens);
955 }
956 return ok;
957}
958
959/**
960 * @brief Per-answer joint-width provenance via the single top-down DP.
961 * See @c ucq_joint_provenance_answer in provsql.common.sql.
962 */
963Datum ucq_joint_provenance_answer(PG_FUNCTION_ARGS)
964{
965 JwAnswerCache *cache =
966 reinterpret_cast<JwAnswerCache *>(fcinfo->flinfo->fn_extra);
967
968 if (cache == nullptr) {
969 // First call: build and cache all answers in the (per-query) fn context.
970 MemoryContext fnctx = fcinfo->flinfo->fn_mcxt;
971 cache = new JwAnswerCache();
972 MemoryContextCallback *cb = (MemoryContextCallback *)
973 MemoryContextAllocZero(fnctx, sizeof(MemoryContextCallback));
974 cb->func = jwAnswerCacheDelete;
975 cb->arg = cache;
976 MemoryContextRegisterResetCallback(fnctx, cb);
977 fcinfo->flinfo->fn_extra = cache;
978
979 if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) {
980 cache->declined = true;
981 } else {
982 std::vector<unsigned> head_vars;
983 {
984 ArrayType *a = PG_GETARG_ARRAYTYPE_P(1);
985 const int32 *d = (const int32 *) ARR_DATA_PTR(a);
986 const int n = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
987 for (int i = 0; i < n; ++i)
988 head_vars.push_back(static_cast<unsigned>(d[i]));
989 }
990 const Datum desc = PG_GETARG_DATUM(0);
991
992 // Catch any error from the gather / compiler (an unsupported token, a
993 // width too large, ...) and decline gracefully to the @p fallback, so a
994 // recognised query never fails.
995 MemoryContext oldcxt = CurrentMemoryContext;
996 ResourceOwner oldowner = CurrentResourceOwner;
997 BeginInternalSubTransaction(NULL);
998 PG_TRY();
999 {
1000 if (!jwComputeCache(desc, head_vars, cache))
1001 cache->declined = true;
1002 ReleaseCurrentSubTransaction();
1003 MemoryContextSwitchTo(oldcxt);
1004 CurrentResourceOwner = oldowner;
1005 }
1006 PG_CATCH();
1007 {
1008 MemoryContextSwitchTo(oldcxt);
1009 RollbackAndReleaseCurrentSubTransaction();
1010 MemoryContextSwitchTo(oldcxt);
1011 CurrentResourceOwner = oldowner;
1012 FlushErrorState();
1013 cache->declined = true;
1014 cache->keys.clear();
1015 cache->tokens.clear();
1016 }
1017 PG_END_TRY();
1018 }
1019 }
1020
1021 // Every call: look the group's head up in the cache.
1022 if (!cache->declined && !PG_ARGISNULL(2)) {
1023 std::vector<std::string> hv;
1024 ArrayType *a = PG_GETARG_ARRAYTYPE_P(2);
1025 Datum *elems; bool *nulls; int n;
1026 deconstruct_array(a, TEXTOID, -1, false, TYPALIGN_INT, &elems, &nulls, &n);
1027 for (int i = 0; i < n; ++i)
1028 hv.push_back(nulls[i] ? std::string() : TextDatumGetCString(elems[i]));
1029 const std::string key = jwHeadKey(hv);
1030 for (std::size_t i = 0; i < cache->keys.size(); ++i)
1031 if (cache->keys[i] == key) {
1032 pg_uuid_t *u = (pg_uuid_t *) palloc(sizeof(pg_uuid_t));
1033 *u = cache->tokens[i];
1034 PG_RETURN_UUID_P(u);
1035 }
1036 }
1037
1038 // Declined, or the group is not an answer of the joint compiler: fall back.
1039 if (PG_ARGISNULL(3))
1040 PG_RETURN_NULL();
1041 PG_RETURN_DATUM(PG_GETARG_DATUM(3));
1042}
@ 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:104
int provsql_joint_max_treewidth
Maximum joint treewidth the joint-width UCQ compiler attempts before declining (caller falls back to ...
Definition provsql.c:103
#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).
UUID structure.
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.