ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
having_semantics.hpp
Go to the documentation of this file.
1/**
2 * @file having_semantics.hpp
3 * @brief Provenance evaluation helper for HAVING-clause circuits.
4 *
5 * When a query includes a HAVING clause, ProvSQL creates a special
6 * sub-circuit that encodes the aggregate predicate. Before the main
7 * provenance circuit can be evaluated over a semiring, these HAVING
8 * sub-circuits must first be evaluated to determine which groups
9 * pass the filter.
10 *
11 * The single public entry point @c provsql_having() rewrites HAVING
12 * comparison gates in the circuit by enumerating possible worlds, for
13 * any compiled semiring. If the HAVING gate type is incompatible with
14 * the requested semiring, the function is a no-op.
15 */
16#ifndef PROVSQL_HAVING_SEMANTICS_HPP
17#define PROVSQL_HAVING_SEMANTICS_HPP
18
19#include <algorithm>
20#include <cstdint>
21#include <functional>
22#include <map>
23#include <stdexcept>
24#include <string>
25#include <vector>
26
27#include "GenericCircuit.hpp"
28#include "provsql_utils.h"
29#include "subset.hpp"
30#include "Aggregation.h"
31
32/** @cond INTERNAL */
33namespace provsql_having_detail {
34std::vector<gate_t> collect_sp_cmp_gates(GenericCircuit &c, gate_t start);
35bool extract_constant_string(GenericCircuit &c, gate_t x, std::string &C_out);
36bool semimod_extract_string_and_K(GenericCircuit &c, gate_t semimod_gate, std::string &m_out, gate_t &k_gate_out);
37bool aggtype_is_text(unsigned oid);
38bool aggtype_is_integer(unsigned oid);
39bool aggtype_is_boolean(unsigned oid);
40// True if @p oid is boolean, or an array whose element type is boolean. Used
41// by the array_agg comparison to reconcile PostgreSQL's two boolean text forms
42// (scalar 'true'/'false' vs array-element 't'/'f').
43bool aggtype_elem_is_boolean(unsigned oid);
44bool aggtype_is_numeric(unsigned oid);
45bool parse_array_literal(const std::string &s, std::vector<std::string> &out);
46bool parse_decimal_scaled(const std::string &s, long &mantissa, int &scale);
47bool rescale_to(long mantissa, int scale, int target_scale, long &out);
48ComparisonOperator map_cmp_op(GenericCircuit &c, gate_t cmp_gate, bool &ok);
50}
51/** @endcond */
52
53/**
54 * @brief Rewrite HAVING comparison gates in the circuit by enumerating possible worlds.
55 *
56 * @tparam SemiringT The semiring type used for evaluation.
57 * @tparam MapT The provenance mapping type (gate_t → semiring value).
58 * @param c Circuit to rewrite.
59 * @param g Root gate of the sub-circuit to inspect.
60 * @param mapping Provenance mapping updated in place.
61 * @param S Semiring instance (default-constructed for stateless semirings).
62 */
63template <typename SemiringT, typename MapT>
66 gate_t g,
67 MapT &mapping,
68 SemiringT S = SemiringT{})
69{
70 using namespace provsql_having_detail;
71
72 std::vector<gate_t> cmp_gates = collect_sp_cmp_gates(c, g);
73
74 if (cmp_gates.empty())
75 return;
76
77 // Whether the world enumeration over these contributor annotations can
78 // be built *certified*: the semiring persists d-DNNF certificates
79 // (circuit-building BoolExpr), every contributor is an independent
80 // literal (base Bernoulli or constant) so world terms are decomposable,
81 // no literal repeats (a repeat would couple two contributors), and the
82 // contributor count is small enough that the complete-worlds
83 // enumeration the certificate requires stays affordable.
84 auto certifiable_contributors =
85 [&](const std::vector<typename SemiringT::value_type> &kv) -> bool {
86 if (!S.certifying())
87 return false;
88 if (kv.size() > 16)
89 return false;
90 for (size_t i = 0; i < kv.size(); ++i) {
91 if (!S.independent_literal(kv[i]))
92 return false;
93 // Distinctness among non-constant literals (constants are
94 // variable-free, so repeats of one() / zero() are harmless).
95 if (kv[i] == S.one() || kv[i] == S.zero())
96 continue;
97 for (size_t j = 0; j < i; ++j)
98 if (kv[j] == kv[i])
99 return false;
100 }
101 return true;
102 };
103
104 // Combine a set of valid-world bitmasks (from an exhaustive enumeration) into
105 // the m-semiring predicate-provenance. Shared by every HAVING domain that
106 // enumerates exact worlds (the numeric comparison and array_agg): a certifying
107 // semiring gets one certified world term per world under a deterministic OR;
108 // otherwise each world contributes ∏present ⊗ (1 ⊖ ⊕missing), with the upset
109 // and monotone MIN/MAX shortcuts dropping the absent contributors that the
110 // enumeration left unconstrained. @p op / @p agg_kind drive only those
111 // shortcuts (no-ops for array_agg).
112 auto combine_exhaustive_worlds =
113 [&](const std::vector<mask_t> &worlds,
114 const std::vector<typename SemiringT::value_type> &kvals,
115 bool upset, ComparisonOperator op,
116 AggregationOperator agg_kind) -> typename SemiringT::value_type {
117 if (worlds.empty())
118 return S.zero();
119
120 if (certifiable_contributors(kvals)) {
121 std::vector<typename SemiringT::value_type> disjuncts;
122 disjuncts.reserve(worlds.size());
123 for (const auto &mask : worlds) {
124 std::vector<typename SemiringT::value_type> present, missing;
125 for (size_t i = 0; i < kvals.size(); ++i)
126 (mask[i] ? present : missing).push_back(kvals[i]);
127 disjuncts.push_back(S.certified_world_term(present, missing));
128 }
129 return S.certified_exclusive_plus(disjuncts);
130 }
131
132 std::vector<typename SemiringT::value_type> disjuncts;
133 disjuncts.reserve(worlds.size());
134 const size_t n = kvals.size();
135
136 for (const auto &mask : worlds) {
137 std::vector<typename SemiringT::value_type> present, missing;
138 present.reserve(n);
139 missing.reserve(n);
140
141 for (size_t i = 0; i < n; ++i) {
142 if (mask[i]) {
143 if (kvals[i] != S.one())
144 present.push_back(kvals[i]);
145 } else if (upset) {
146 // The world enumeration produced an upset generated by a subset
147 } else if ((op == ComparisonOperator::GE || op == ComparisonOperator::GT) &&
148 S.absorptive() && agg_kind == AggregationOperator::MAX) {
149 // Monotonously increasing behavior: do not add anything to missing
150 } else if ((op == ComparisonOperator::LE || op == ComparisonOperator::LT) &&
151 S.absorptive() && agg_kind == AggregationOperator::MIN) {
152 // Monotonously decreasing behavior: do not add anything to missing
153 } else {
154 if (kvals[i] != S.zero())
155 missing.push_back(kvals[i]);
156 }
157 }
158
159 auto present_prod = S.times(present);
160
161 if (missing.empty()) {
162 disjuncts.push_back(std::move(present_prod));
163 } else {
164 auto monus_factor = S.monus(S.one(), S.plus(missing));
165 auto term = monus_factor;
166 if (present_prod != S.one())
167 term = S.times(std::vector<typename SemiringT::value_type>{
168 present_prod, monus_factor});
169 disjuncts.push_back(std::move(term));
170 }
171 }
172
173 return S.plus(disjuncts);
174 };
175
176 auto pw_from_cmp_gate = [&](gate_t cmp_gate, typename SemiringT::value_type &pw_out) -> bool {
177 const auto &cw = c.getWires(cmp_gate);
178 if (cw.size() != 2) return false;
179
180 gate_t L = cw[0];
181 gate_t R = cw[1];
182
183 bool okop = false;
184 ComparisonOperator op = map_cmp_op(c, cmp_gate, okop);
185 if (!okop) return false;
186
187 auto build_from = [&](gate_t agg_side, gate_t const_side, ComparisonOperator effective_op) -> bool {
188 if (c.getGateType(agg_side) != gate_agg) return false;
189
190 // info2 of the gate_agg is the aggregate's result type -- the
191 // comparison domain (int / numeric / float / text) -- in its low 31 bits;
192 // the high bit is the scalar-aggregation flag, masked off here.
193 const unsigned aggtype = c.getInfos(agg_side).second & PROVSQL_AGG_TYPE_MASK;
194 const bool is_scalar =
195 (c.getInfos(agg_side).second & PROVSQL_AGG_SCALAR_FLAG) != 0;
196 AggregationOperator agg_kind = getAggregationOperator(c.getInfos(agg_side).first);
197 const auto &children = c.getWires(agg_side);
198
199 // ---- Value-as-text domain: choose() over a non-numeric scalar constant.
200 // Covers text and any other type whose values round-trip through
201 // their text representation (bool, date, uuid, enum, ...); numeric
202 // choose() goes to the numeric domain below (which also supports the
203 // ordering comparisons). Only = / <> are exposed here. ----
204 if (aggtype_is_text(aggtype) ||
205 (agg_kind == AggregationOperator::CHOOSE &&
206 !aggtype_is_numeric(aggtype))) {
207 std::string C_str;
208 if (!extract_constant_string(c, const_side, C_str)) return false;
209
210 std::vector<std::string> mvals_str;
211 std::vector<typename SemiringT::value_type> kvals;
212 mvals_str.reserve(children.size());
213 kvals.reserve(children.size());
214 for (gate_t ch : children) {
215 if (c.getGateType(ch) != gate_semimod) return false;
216 std::string m_str;
217 gate_t k_gate{};
218 if (!semimod_extract_string_and_K(c, ch, m_str, k_gate)) return false;
219 mvals_str.push_back(m_str);
220 kvals.push_back(c.evaluate<SemiringT>(k_gate, mapping, S));
221 }
222
223 // Only choose() is supported (the only text-valued aggregate whose
224 // possible-world value is decided occurrence-by-occurrence), and
225 // only = / <> are exposed for text.
226 if (agg_kind != AggregationOperator::CHOOSE)
227 throw std::runtime_error(
228 "comparing an aggregate with a text constant in HAVING "
229 "is only implemented for choose()");
230 if (effective_op != ComparisonOperator::EQ &&
231 effective_op != ComparisonOperator::NE)
232 throw std::runtime_error(
233 "only = and <> are supported when comparing choose() "
234 "with a text constant");
235
236 // choose() is PICKFIRST: in a world W its value is that of the
237 // lowest-index present element. So choose(W) op C holds iff the
238 // first present element matches; summing the annotations of all such
239 // worlds telescopes (free suffix sums to one):
240 // pw = ⊕_{i : vᵢ matches} kᵢ ⊗ (⊗_{j<i} (1 ⊖ kⱼ))
241 //
242 // The disjuncts are mutually exclusive by construction (they
243 // differ on the first present index): when the semiring
244 // certifies enumerations, build them as certified world terms
245 // under a certified deterministic OR instead.
246 if (certifiable_contributors(kvals)) {
247 std::vector<typename SemiringT::value_type> disjuncts;
248 std::vector<typename SemiringT::value_type> before;
249 for (size_t i = 0; i < kvals.size(); ++i) {
250 bool match = (effective_op == ComparisonOperator::EQ)
251 ? (mvals_str[i] == C_str)
252 : (mvals_str[i] != C_str);
253 if (match)
254 disjuncts.push_back(S.certified_world_term(
255 std::vector<typename SemiringT::value_type>{kvals[i]},
256 before));
257 before.push_back(kvals[i]);
258 }
259 pw_out = disjuncts.empty() ? S.zero()
260 : S.certified_exclusive_plus(disjuncts);
261 return true;
262 }
263 const auto one = S.one();
264 std::vector<typename SemiringT::value_type> disjuncts;
265 auto prefix = one;
266 for (size_t i = 0; i < kvals.size(); ++i) {
267 bool match = (effective_op == ComparisonOperator::EQ)
268 ? (mvals_str[i] == C_str)
269 : (mvals_str[i] != C_str);
270 if (match) {
271 if (prefix == one)
272 disjuncts.push_back(kvals[i]);
273 else if (kvals[i] == one)
274 disjuncts.push_back(prefix);
275 else
276 disjuncts.push_back(S.times(
277 std::vector<typename SemiringT::value_type>{kvals[i], prefix}));
278 }
279 auto absent = S.monus(one, kvals[i]);
280 prefix = (prefix == one)
281 ? absent
282 : S.times(std::vector<typename SemiringT::value_type>{
283 prefix, absent});
284 }
285 pw_out = disjuncts.empty() ? S.zero() : S.plus(disjuncts);
286 return true;
287 }
288
289 // ---- Boolean comparison domain: bool_or / bool_and (and the every
290 // alias for bool_and) compared with a boolean constant. A boolean
291 // aggregate has only two possible values, so the worlds yielding the
292 // wanted value are characterised directly in the m-semiring rather
293 // than by a 2^n enumeration. The rows partition by their value into
294 // a class that must have at least one present member ("someE") and a
295 // class that must be wholly absent ("noneF"):
296 // bool_or = true : someE = true-rows (false free)
297 // bool_or = false : someE = false-rows, noneF = true-rows
298 // bool_and = true : someE = true-rows, noneF = false-rows
299 // bool_and = false : someE = false-rows (true free)
300 // "at least one of someE present" telescopes by the first present
301 // index (the choose pattern); "none of noneF present" is a product
302 // of complements. Non-empty groups are enforced by someE being
303 // non-empty. ----
304 if (aggtype_is_boolean(aggtype)) {
305 if (agg_kind != AggregationOperator::OR &&
306 agg_kind != AggregationOperator::AND)
307 return false;
308 if (effective_op != ComparisonOperator::EQ &&
309 effective_op != ComparisonOperator::NE)
310 throw std::runtime_error(
311 "only = and <> are supported when comparing a boolean aggregate "
312 "(bool_or / bool_and / every) with a constant in HAVING");
313
314 auto parse_bool = [](const std::string &s, bool &ok) -> bool {
315 ok = true;
316 if (s == "t" || s == "true" || s == "1") return true;
317 if (s == "f" || s == "false" || s == "0") return false;
318 ok = false; return false;
319 };
320
321 std::string C_str;
322 if (!extract_constant_string(c, const_side, C_str)) return false;
323 bool okc = false;
324 const bool C = parse_bool(C_str, okc);
325 if (!okc) return false;
326 // The aggregate value the satisfying worlds must produce.
327 const bool target = (effective_op == ComparisonOperator::EQ) ? C : !C;
328
329 std::vector<bool> vals;
330 std::vector<typename SemiringT::value_type> kvals;
331 vals.reserve(children.size());
332 kvals.reserve(children.size());
333 for (gate_t ch : children) {
334 if (c.getGateType(ch) != gate_semimod) return false;
335 std::string m_str;
336 gate_t k_gate{};
337 if (!semimod_extract_string_and_K(c, ch, m_str, k_gate)) return false;
338 bool okv = false;
339 const bool b = parse_bool(m_str, okv);
340 if (!okv) return false;
341 vals.push_back(b);
342 kvals.push_back(c.evaluate<SemiringT>(k_gate, mapping, S));
343 }
344
345 const bool want_or = (agg_kind == AggregationOperator::OR);
346 std::vector<size_t> someE; // at least one of these must be present
347 std::vector<size_t> noneF; // all of these must be absent
348 if (want_or == target) {
349 // bool_or=true / bool_and=false: one trigger row suffices, the other
350 // class is free.
351 for (size_t i = 0; i < vals.size(); ++i)
352 if (vals[i] == want_or) someE.push_back(i);
353 } else {
354 // bool_or=false / bool_and=true: the trigger class must be wholly
355 // absent and the opposite class must have at least one present row.
356 for (size_t i = 0; i < vals.size(); ++i)
357 (vals[i] == want_or ? noneF : someE).push_back(i);
358 }
359
360 if (someE.empty()) { pw_out = S.zero(); return true; }
361
362 if (certifiable_contributors(kvals)) {
363 std::vector<typename SemiringT::value_type> disjuncts;
364 std::vector<typename SemiringT::value_type> before; // someE rows before i
365 for (size_t e : someE) {
366 std::vector<typename SemiringT::value_type> missing = before;
367 for (size_t f : noneF) missing.push_back(kvals[f]);
368 disjuncts.push_back(S.certified_world_term(
369 std::vector<typename SemiringT::value_type>{kvals[e]}, missing));
370 before.push_back(kvals[e]);
371 }
372 pw_out = S.certified_exclusive_plus(disjuncts);
373 return true;
374 }
375
376 const auto one = S.one();
377 // "none of noneF present": product of complements.
378 auto none_factor = one;
379 for (size_t f : noneF) {
380 auto absent = S.monus(one, kvals[f]);
381 none_factor = (none_factor == one)
382 ? absent
383 : S.times(std::vector<typename SemiringT::value_type>{none_factor,
384 absent});
385 }
386 // "at least one of someE present": telescope by first present index.
387 std::vector<typename SemiringT::value_type> disjuncts;
388 auto prefix = one;
389 for (size_t i : someE) {
390 if (prefix == one)
391 disjuncts.push_back(kvals[i]);
392 else if (kvals[i] == one)
393 disjuncts.push_back(prefix);
394 else
395 disjuncts.push_back(S.times(
396 std::vector<typename SemiringT::value_type>{kvals[i], prefix}));
397 auto absent = S.monus(one, kvals[i]);
398 prefix = (prefix == one)
399 ? absent
400 : S.times(std::vector<typename SemiringT::value_type>{prefix, absent});
401 }
402 auto some_value = disjuncts.empty() ? S.zero() : S.plus(disjuncts);
403 if (none_factor == one)
404 pw_out = some_value;
405 else if (some_value == one)
406 pw_out = none_factor;
407 else
408 pw_out = S.times(
409 std::vector<typename SemiringT::value_type>{none_factor, some_value});
410 return true;
411 }
412
413 // ---- Array comparison domain: array_agg(x) against a constant array.
414 // No aggregate-specific optimization (the general pipeline): scan the
415 // non-empty worlds whose ordered present elements equal (=) or differ
416 // (<>) from the constant array, then combine those worlds in the
417 // m-semiring. Elements are compared as their text representations,
418 // so any element type works. ----
419 if (agg_kind == AggregationOperator::ARRAY_AGG) {
420 if (effective_op != ComparisonOperator::EQ &&
421 effective_op != ComparisonOperator::NE)
422 throw std::runtime_error(
423 "only = and <> are supported when comparing array_agg() with a "
424 "constant array in HAVING");
425
426 std::string C_str;
427 if (!extract_constant_string(c, const_side, C_str)) return false;
428 std::vector<std::string> target;
429 if (!parse_array_literal(C_str, target)) return false;
430
431 std::vector<std::string> vals;
432 std::vector<typename SemiringT::value_type> kvals;
433 vals.reserve(children.size());
434 kvals.reserve(children.size());
435 for (gate_t ch : children) {
436 if (c.getGateType(ch) != gate_semimod) return false;
437 std::string m_str;
438 gate_t k_gate{};
439 if (!semimod_extract_string_and_K(c, ch, m_str, k_gate)) return false;
440 vals.push_back(m_str);
441 kvals.push_back(c.evaluate<SemiringT>(k_gate, mapping, S));
442 }
443
444 // Boolean elements: the row values carry the scalar bool text
445 // ('true'/'false') while the constant array's elements come back in
446 // PostgreSQL's array form ('t'/'f'). Canonicalise both sides so the
447 // text comparison below sees the same representation.
448 if (aggtype_elem_is_boolean(aggtype)) {
449 auto canon_bool = [](std::string &s) {
450 if (s == "t" || s == "true" || s == "1") s = "true";
451 else if (s == "f" || s == "false" || s == "0") s = "false";
452 };
453 for (auto &e : target) canon_bool(e);
454 for (auto &v : vals) canon_bool(v);
455 }
456
457 auto worlds = enumerate_array_agg_worlds(
458 vals, target, effective_op == ComparisonOperator::EQ);
459 pw_out = combine_exhaustive_worlds(worlds, kvals, /*upset=*/false,
460 effective_op, agg_kind);
461 return true;
462 }
463
464 // ---- Numeric comparison domain (int / numeric / float): unify by
465 // scaling every value and the threshold to a common integer
466 // grid by their decimal text, so a numeric(p,d) / finite-decimal
467 // float column is evaluated exactly and fractional thresholds
468 // work. Integer is the scale-0 case. ----
469 std::string C_str;
470 if (!extract_constant_string(c, const_side, C_str)) return false;
471 long C_mant = 0; int C_scale = 0;
472 if (!parse_decimal_scaled(C_str, C_mant, C_scale)) return false;
473
474 std::vector<long> m_mant;
475 std::vector<int> m_scale;
476 std::vector<typename SemiringT::value_type> kvals;
477 m_mant.reserve(children.size());
478 m_scale.reserve(children.size());
479 kvals.reserve(children.size());
480 for (gate_t ch : children) {
481 if (c.getGateType(ch) != gate_semimod) return false;
482 std::string m_str;
483 gate_t k_gate{};
484 if (!semimod_extract_string_and_K(c, ch, m_str, k_gate)) return false;
485 long mm = 0; int ms = 0;
486 if (!parse_decimal_scaled(m_str, mm, ms)) return false;
487 m_mant.push_back(mm);
488 m_scale.push_back(ms);
489 kvals.push_back(c.evaluate<SemiringT>(k_gate, mapping, S));
490 }
491
492 // Common scale: rescale every value and the threshold to integers.
493 int target_scale = C_scale;
494 for (int ms : m_scale) target_scale = std::max(target_scale, ms);
495 long C = 0;
496 if (!rescale_to(C_mant, C_scale, target_scale, C)) return false;
497 std::vector<long> mvals(m_mant.size());
498 for (size_t i = 0; i < m_mant.size(); ++i)
499 if (!rescale_to(m_mant[i], m_scale[i], target_scale, mvals[i])) return false;
500
501 // A certified enumeration needs the *complete* valid worlds (every
502 // contributor present or explicitly negated): the upset shortcut
503 // and the monotone MIN/MAX skips below produce overlapping,
504 // non-exclusive disjuncts, sound for absorptive evaluation but
505 // unmarkable. When certifying, request the full enumeration --
506 // the same one non-absorptive semirings already use.
507 const bool certify = certifiable_contributors(kvals);
508
509 bool upset = false;
510 auto worlds = enumerate_valid_worlds(mvals, C, effective_op, agg_kind,
511 certify ? false : S.absorptive(),
512 upset, is_scalar);
513
514 pw_out = combine_exhaustive_worlds(worlds, kvals, upset, op, agg_kind);
515 return true;
516 };
517
518 // General possible-worlds evaluation for a comparison over an *arithmetic
519 // expression of one or more aggregates* (sum(x)*sum(y) > k, sum(x) > sum(y),
520 // 100/sum(x) > 5, ...), which the single-aggregate path above cannot fold.
521 // We enumerate the joint possible worlds over the union of the aggregates'
522 // contributors, evaluate the arithmetic numerically in each world, and
523 // combine the worlds where the comparison holds in the semiring exactly as
524 // the single-aggregate path does (present_prod ⊗ (1 ⊖ missing_sum)). This
525 // is exponential in the number of distinct contributors, so it bails out
526 // (returning false, leaving the gate unresolved) beyond a small bound.
527 auto build_general = [&](gate_t Lx, gate_t Rx,
528 ComparisonOperator opx) -> bool {
529 struct AggInfo {
531 bool is_int; // integer-typed result?
532 bool is_scalar; // aggregation without GROUP BY?
533 std::vector<std::pair<int, double> > contribs; // (distinct-K index, value)
534 };
535 // A decimal text denotes an integer iff it has no fractional/exponent part.
536 auto text_is_int = [](const std::string &s) -> bool {
537 if (s.empty()) return false;
538 for (char ch : s)
539 if (!((ch >= '0' && ch <= '9') || ch == '+' || ch == '-'))
540 return false;
541 return true;
542 };
543 std::map<gate_t, AggInfo> aggs;
544 std::map<gate_t, int> kindex;
545 std::vector<gate_t> kgates;
546
547 std::function<bool(gate_t)> collect = [&](gate_t gx) -> bool {
548 gate_type gt = c.getGateType(gx);
549 if (gt == gate_agg) {
550 if (aggs.count(gx))
551 return true;
552 AggInfo ai;
553 ai.kind = getAggregationOperator(c.getInfos(gx).first);
554 ai.is_int = aggtype_is_integer(c.getInfos(gx).second & PROVSQL_AGG_TYPE_MASK);
555 ai.is_scalar =
556 (c.getInfos(gx).second & PROVSQL_AGG_SCALAR_FLAG) != 0;
557 for (gate_t ch : c.getWires(gx)) {
558 if (c.getGateType(ch) != gate_semimod)
559 return false;
560 std::string ms;
561 gate_t kg{};
562 if (!semimod_extract_string_and_K(c, ch, ms, kg))
563 return false;
564 int idx;
565 auto it = kindex.find(kg);
566 if (it == kindex.end()) {
567 idx = static_cast<int>(kgates.size());
568 kindex[kg] = idx;
569 kgates.push_back(kg);
570 } else
571 idx = it->second;
572 double m;
573 try { m = std::stod(ms); } catch (...) { return false; }
574 ai.contribs.emplace_back(idx, m);
575 }
576 aggs.emplace(gx, std::move(ai));
577 return true;
578 }
579 if (gt == gate_arith) {
580 for (gate_t ch : c.getWires(gx))
581 if (!collect(ch))
582 return false;
583 return true;
584 }
585 if (gt == gate_value)
586 return true;
587 if (gt == gate_semimod) { // a constant threshold: semimod(1, value)
588 std::string ms;
589 gate_t kg{};
590 if (!semimod_extract_string_and_K(c, gx, ms, kg))
591 return false;
592 return c.getGateType(kg) == gate_one;
593 }
594 return false;
595 };
596
597 if (!collect(Lx) || !collect(Rx))
598 return false;
599 if (aggs.empty())
600 return false;
601 const size_t n = kgates.size();
602 if (n == 0 || n > 24) // 2^n enumeration: keep it bounded
603 return false;
604
605 // Numeric value of a subexpression in a given world, tracking whether it
606 // is integer-valued so that division floors as SQL does (NULL -> false).
607 std::function<bool(gate_t, uint64_t, double &, bool &)> eval =
608 [&](gate_t gx, uint64_t world, double &out, bool &is_int) -> bool {
609 gate_type gt = c.getGateType(gx);
610 if (gt == gate_value) {
611 std::string s = c.getExtra(gx);
612 try { out = std::stod(s); } catch (...) { return false; }
613 is_int = text_is_int(s);
614 return true;
615 }
616 if (gt == gate_semimod) { // constant threshold
617 std::string ms; gate_t kg{};
618 if (!semimod_extract_string_and_K(c, gx, ms, kg)) return false;
619 try { out = std::stod(ms); } catch (...) { return false; }
620 is_int = text_is_int(ms);
621 return true;
622 }
623 if (gt == gate_agg) {
624 const AggInfo &ai = aggs.at(gx);
625 double acc = 0, mn = 0, mx = 0;
626 long cnt = 0;
627 bool first = true;
628 for (const auto &pr : ai.contribs)
629 if (world & (uint64_t(1) << pr.first)) {
630 double m = pr.second;
631 acc += m; ++cnt;
632 if (first) { mn = mx = m; first = false; }
633 else { mn = std::min(mn, m); mx = std::max(mx, m); }
634 }
635 is_int = ai.is_int;
636 // An aggregate with no surviving contributor in this world declines
637 // the world (returning false), which the caller reads as "the
638 // comparison does not hold here". Two situations produce cnt == 0
639 // and both want that answer: the group has no row present, so the
640 // group's row does not exist; or the group is non-empty but every
641 // contributed value was NULL, so the aggregate is SQL NULL and the
642 // comparison is NULL, hence false. (A NULL-valued row never reaches
643 // contribs -- the aggregation rewriting drops it -- which is why the
644 // two are indistinguishable here, and why they need not be.)
645 //
646 // COUNT is the exception: an empty set genuinely counts 0, so the
647 // world has a value whenever a row exists to carry it -- which is
648 // the case for a scalar aggregation, and not for a grouped one whose
649 // empty group is no row at all. The gate records COUNT for both
650 // count(*) and count(expr) (see make_aggregation_expression), so no
651 // guessing from the values is needed here.
652 switch (ai.kind) {
653 case AggregationOperator::SUM: if (cnt == 0) return false; out = acc; return true;
654 case AggregationOperator::COUNT: // values 1 or 0/1
655 if (cnt == 0 && !ai.is_scalar) return false;
656 out = acc; return true;
657 case AggregationOperator::AVG: if (cnt == 0) return false; out = acc / cnt; return true;
658 case AggregationOperator::MIN: if (cnt == 0) return false; out = mn; return true;
659 case AggregationOperator::MAX: if (cnt == 0) return false; out = mx; return true;
660 default: return false;
661 }
662 }
663 if (gt == gate_arith) {
664 const auto &w = c.getWires(gx);
665 unsigned aop = static_cast<unsigned>(c.getInfos(gx).first);
666 if (aop == PROVSQL_ARITH_PLUS || aop == PROVSQL_ARITH_TIMES) {
667 double r = (aop == PROVSQL_ARITH_PLUS) ? 0 : 1;
668 bool all_int = true;
669 for (gate_t ch : w) {
670 double v; bool vi;
671 if (!eval(ch, world, v, vi)) return false;
672 if (aop == PROVSQL_ARITH_PLUS) r += v; else r *= v;
673 all_int = all_int && vi;
674 }
675 out = r; is_int = all_int; return true;
676 }
677 if (aop == PROVSQL_ARITH_MINUS) {
678 if (w.size() != 2) return false;
679 double a, b; bool ai, bi;
680 if (!eval(w[0], world, a, ai) || !eval(w[1], world, b, bi)) return false;
681 out = a - b; is_int = ai && bi; return true;
682 }
683 if (aop == PROVSQL_ARITH_DIV) {
684 if (w.size() != 2) return false;
685 double a, b; bool ai, bi;
686 if (!eval(w[0], world, a, ai) || !eval(w[1], world, b, bi)) return false;
687 if (b == 0) return false;
688 if (ai && bi) { // SQL integer division truncates toward zero
689 out = static_cast<double>(static_cast<long long>(a) /
690 static_cast<long long>(b));
691 is_int = true;
692 } else {
693 out = a / b; is_int = false;
694 }
695 return true;
696 }
697 if (aop == PROVSQL_ARITH_NEG) {
698 if (w.size() != 1) return false;
699 double a; bool ai;
700 if (!eval(w[0], world, a, ai)) return false;
701 out = -a; is_int = ai; return true;
702 }
703 if (aop == PROVSQL_ARITH_MAX || aop == PROVSQL_ARITH_MIN) {
704 if (w.empty()) return false;
705 double r = 0; bool all_int = true, first = true;
706 for (gate_t ch : w) {
707 double v; bool vi;
708 if (!eval(ch, world, v, vi)) return false;
709 if (first) { r = v; first = false; }
710 else r = (aop == PROVSQL_ARITH_MAX) ? std::max(r, v)
711 : std::min(r, v);
712 all_int = all_int && vi;
713 }
714 out = r; is_int = all_int; return true;
715 }
716 return false;
717 }
718 return false;
719 };
720
721 std::vector<typename SemiringT::value_type> kval(n);
722 for (size_t i = 0; i < n; ++i)
723 kval[i] = c.evaluate<SemiringT>(kgates[i], mapping, S);
724
725 // The joint enumeration below is over complete worlds already:
726 // certify the disjuncts when the semiring and contributors allow.
727 const bool certify = certifiable_contributors(kval);
728
729 // Whether the world in which no contributor is present is itself a
730 // valid world. For a grouped aggregation it is not: an empty group is
731 // no row, so there is nothing for the comparison to hold of. A scalar
732 // aggregation always yields its single row, empty input included, so
733 // that world is real and a predicate true on it (count(*) = 0, and
734 // anything the empty-input values satisfy) must pick it up. Mixed
735 // shapes follow the grouped reading: whichever side is grouped has no
736 // row there, so the joined row does not exist either.
737 const bool empty_world_valid =
738 !aggs.empty() &&
739 std::all_of(aggs.begin(), aggs.end(),
740 [](const std::pair<const gate_t, AggInfo> &e) {
741 return e.second.is_scalar;
742 });
743
744 std::vector<typename SemiringT::value_type> disjuncts;
745 const uint64_t total = uint64_t(1) << n;
746 for (uint64_t world = empty_world_valid ? 0 : 1; world < total; ++world) {
747 double lv, rv;
748 bool lint, rint;
749 if (!eval(Lx, world, lv, lint) || !eval(Rx, world, rv, rint))
750 continue; // NULL comparison: false
751 bool holds = false;
752 switch (opx) {
753 case ComparisonOperator::EQ: holds = (lv == rv); break;
754 case ComparisonOperator::NE: holds = (lv != rv); break;
755 case ComparisonOperator::LT: holds = (lv < rv); break;
756 case ComparisonOperator::LE: holds = (lv <= rv); break;
757 case ComparisonOperator::GT: holds = (lv > rv); break;
758 case ComparisonOperator::GE: holds = (lv >= rv); break;
759 }
760 if (!holds)
761 continue;
762
763 std::vector<typename SemiringT::value_type> present, missing;
764 for (size_t i = 0; i < n; ++i) {
765 if (world & (uint64_t(1) << i)) {
766 if (certify || kval[i] != S.one()) present.push_back(kval[i]);
767 } else {
768 if (certify || kval[i] != S.zero()) missing.push_back(kval[i]);
769 }
770 }
771 if (certify) {
772 disjuncts.push_back(S.certified_world_term(present, missing));
773 continue;
774 }
775 auto present_prod = S.times(present);
776 if (missing.empty())
777 disjuncts.push_back(std::move(present_prod));
778 else {
779 auto monus_factor = S.monus(S.one(), S.plus(missing));
780 disjuncts.push_back(
781 present_prod == S.one()
782 ? monus_factor
783 : S.times(std::vector<typename SemiringT::value_type>{
784 present_prod, monus_factor}));
785 }
786 }
787
788 pw_out = disjuncts.empty() ? S.zero()
789 : certify ? S.certified_exclusive_plus(disjuncts)
790 : S.plus(disjuncts);
791 return true;
792 };
793
794 if (c.getGateType(L) == gate_agg && build_from(L, R, op))
795 return true;
796 if (c.getGateType(R) == gate_agg && build_from(R, L, flip_op(op)))
797 return true;
798
799 return build_general(L, R, op);
800 };
801
802 // Each comparison gate is resolved on its own: its possible-world
803 // enumeration reads the circuit and writes only its own mapping entry, so
804 // one gate the enumeration cannot handle -- an agg-vs-agg shape past the
805 // contributor cap, an unsupported aggregate -- says nothing about the
806 // others. Skip it and carry on rather than abandoning the gates that
807 // follow: those are left for the semiring's own cmp() to deal with, which
808 // renders them (Formula) or raises, and a gate this loop could have
809 // resolved should not be demoted to that just because of its position in
810 // collect_sp_cmp_gates' traversal.
811 for (gate_t cmp_gate : cmp_gates) {
812 typename SemiringT::value_type pw;
813 if (!pw_from_cmp_gate(cmp_gate, pw))
814 continue;
815
816 mapping[cmp_gate] = std::move(pw);
817 }
818}
819
820#endif
AggregationOperator getAggregationOperator(Oid oid)
Map a PostgreSQL aggregate function OID to an AggregationOperator.
Typed aggregation value, operator, and aggregator abstractions.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
@ OR
Boolean OR aggregate.
Definition Aggregation.h:58
@ MAX
MAX → input type.
Definition Aggregation.h:55
@ COUNT
COUNT(*) or COUNT(expr) → integer.
Definition Aggregation.h:52
@ AND
Boolean AND aggregate.
Definition Aggregation.h:57
@ SUM
SUM → integer or float.
Definition Aggregation.h:53
@ ARRAY_AGG
Array aggregation.
Definition Aggregation.h:60
@ MIN
MIN → input type.
Definition Aggregation.h:54
@ CHOOSE
Arbitrary selection (pick one element).
Definition Aggregation.h:59
@ AVG
AVG → float.
Definition Aggregation.h:56
ComparisonOperator
SQL comparison operators used in gate_cmp circuit gates.
Definition Aggregation.h:39
@ LT
Less than (<).
Definition Aggregation.h:43
@ GT
Greater than (>).
Definition Aggregation.h:45
@ LE
Less than or equal (<=).
Definition Aggregation.h:42
@ NE
Not equal (<>).
Definition Aggregation.h:41
@ GE
Greater than or equal (>=).
Definition Aggregation.h:44
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Template implementation of GenericCircuit::evaluate().
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
In-memory provenance circuit with semiring-generic evaluation.
S::value_type evaluate(gate_t g, std::unordered_map< gate_t, typename S::value_type > &provenance_mapping, S semiring) const
Evaluate the sub-circuit rooted at gate g over semiring semiring.
std::string getExtra(gate_t g) const
Return the string extra for gate g.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
void provsql_having(GenericCircuit &c, gate_t g, MapT &mapping, SemiringT S=SemiringT{})
Rewrite HAVING comparison gates in the circuit by enumerating possible worlds.
bool rescale_to(long mantissa, int scale, int target_scale, long &out)
bool aggtype_is_text(unsigned oid)
bool aggtype_is_numeric(unsigned oid)
bool aggtype_is_integer(unsigned oid)
bool parse_array_literal(const std::string &s, std::vector< std::string > &out)
ComparisonOperator flip_op(ComparisonOperator op)
bool aggtype_elem_is_boolean(unsigned oid)
std::vector< gate_t > collect_sp_cmp_gates(GenericCircuit &c, gate_t start)
bool parse_decimal_scaled(const std::string &s, long &mantissa, int &scale)
ComparisonOperator map_cmp_op(GenericCircuit &c, gate_t cmp_gate, bool &ok)
bool aggtype_is_boolean(unsigned oid)
bool semimod_extract_string_and_K(GenericCircuit &c, gate_t semimod_gate, std::string &m_out, gate_t &k_gate_out)
bool extract_constant_string(GenericCircuit &c, gate_t x, std::string &C_out)
Core types, constants, and utilities shared across ProvSQL.
@ PROVSQL_ARITH_DIV
binary, child0 / child1
@ PROVSQL_ARITH_PLUS
n-ary, sum of children
@ PROVSQL_ARITH_NEG
unary, -child0
@ PROVSQL_ARITH_MINUS
binary, child0 - child1
@ PROVSQL_ARITH_TIMES
n-ary, product of children
@ PROVSQL_ARITH_MIN
n-ary, min of children (order statistic; least / min aggregate)
@ PROVSQL_ARITH_MAX
n-ary, max of children (order statistic; greatest / max aggregate)
#define PROVSQL_AGG_TYPE_MASK
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
#define PROVSQL_AGG_SCALAR_FLAG
Scalar-aggregation flag, stored in the upper bit of a gate_agg's info2 (whose low 31 bits hold the ag...
std::vector< mask_t > enumerate_valid_worlds(const std::vector< long > &values, long constant, ComparisonOperator op, AggregationOperator agg_kind, bool absorptive, bool &upset, bool is_scalar)
Enumerate all subsets of n tuples satisfying an aggregate predicate.
Definition subset.cpp:425
std::vector< mask_t > enumerate_array_agg_worlds(const std::vector< std::string > &vals, const std::vector< std::string > &target, bool want_equal)
Enumerate the non-empty worlds whose ordered present elements equal (want_equal true) or differ from ...
Definition subset.cpp:465
Enumerate tuple subsets satisfying an aggregate HAVING predicate.