ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
CountCmpEvaluator.cpp
Go to the documentation of this file.
1/**
2 * @file CountCmpEvaluator.cpp
3 * @brief Implementation of the Poisson-binomial pre-pass.
4 * See @c CountCmpEvaluator.h for the full docstring.
5 */
6#include "CountCmpEvaluator.h"
7
8#include <algorithm>
9#include <climits>
10#include <map>
11#include <vector>
12
13#include "Aggregation.h" // ComparisonOperator + getAggregationOperator
14#include "CmpEvaluatorCommon.h" // matchAggCmp, computeRefCounts, contributorProb
15
16namespace provsql {
17
18namespace {
19
20/* Partial Poisson-binomial PMF : compute @c dp[j] = Pr(exactly @c j
21 * successes among the @c N input Bernoullis) for @c j in @c [0, jmax]
22 * only. @c jmax is clamped to @c N. Cost : @c O(N x jmax). Rolling
23 * 1-D array, iterate j downward so each read references the
24 * not-yet-updated previous row. */
25static std::vector<double> partialPMF(const std::vector<double> &p,
26 std::size_t jmax)
27{
28 const std::size_t N = p.size();
29 jmax = std::min(jmax, N);
30 std::vector<double> dp(jmax + 1, 0.0);
31 dp[0] = 1.0;
32 for (std::size_t i = 0; i < N; ++i) {
33 const double pi = p[i];
34 const double qi = 1.0 - pi;
35 /* Cap inner loop at min(i+1, jmax) : entries beyond i are still
36 * zero and entries beyond jmax we never sum. */
37 const std::size_t upper = std::min(jmax, i + 1);
38 for (std::size_t j = upper; j >= 1; --j) {
39 dp[j] = dp[j] * qi + dp[j - 1] * pi;
40 }
41 dp[0] *= qi;
42 }
43 return dp;
44}
45
46/* Probability that the empty world occurs : @c prod_i (1 - p_i).
47 * Always needed for SQL HAVING semantics (the empty group never
48 * satisfies). */
49static double probZero(const std::vector<double> &p)
50{
51 double q = 1.0;
52 for (double pi : p) q *= (1.0 - pi);
53 return q;
54}
55
56/* Probability that at least @c T of the @c N Bernoullis succeed.
57 * Dispatches on which side of @c T is closer to the boundary to keep
58 * the partial DP at @c O(N x min(T, N - T + 1)).
59 * - If @c T-1 <= N-T (lower tail is smaller) : compute the lower
60 * partial PMF up to @c T-1 and return @c 1 - sum.
61 * - Otherwise (upper tail is smaller) : invert the Bernoullis,
62 * @c Y_i = 1 - X_i, and use @c Pr(B >= T) = Pr(sum Y <= N - T) ;
63 * the partial PMF on @c Y is computed up to @c N - T. */
64static double probAtLeast(const std::vector<double> &p, int T)
65{
66 const int N = static_cast<int>(p.size());
67 if (T <= 0) return 1.0;
68 if (T > N) return 0.0;
69
70 if (T - 1 <= N - T) {
71 auto dp = partialPMF(p, static_cast<std::size_t>(T - 1));
72 double sum = 0.0;
73 for (int j = 0; j <= T - 1; ++j) sum += dp[j];
74 return 1.0 - sum;
75 } else {
76 std::vector<double> q(N);
77 for (int i = 0; i < N; ++i) q[i] = 1.0 - p[i];
78 auto dp = partialPMF(q, static_cast<std::size_t>(N - T));
79 double sum = 0.0;
80 for (int j = 0; j <= N - T; ++j) sum += dp[j];
81 return sum;
82 }
83}
84
85/* Probability that at most @c T of the @c N Bernoullis succeed.
86 * Same smaller-side dispatch as @c probAtLeast : if @c T is closer
87 * to 0 compute the lower partial PMF and sum ; if @c T is closer to
88 * @c N invert and compute the upper tail's complement. */
89static double probAtMost(const std::vector<double> &p, int T)
90{
91 const int N = static_cast<int>(p.size());
92 if (T < 0) return 0.0;
93 if (T >= N) return 1.0;
94
95 if (T <= N - 1 - T) {
96 auto dp = partialPMF(p, static_cast<std::size_t>(T));
97 double sum = 0.0;
98 for (int j = 0; j <= T; ++j) sum += dp[j];
99 return sum;
100 } else {
101 std::vector<double> q(N);
102 for (int i = 0; i < N; ++i) q[i] = 1.0 - p[i];
103 auto dp = partialPMF(q, static_cast<std::size_t>(N - 1 - T));
104 double sum = 0.0;
105 for (int j = 0; j <= N - 1 - T; ++j) sum += dp[j];
106 return 1.0 - sum;
107 }
108}
109
110/* Probability that exactly @c T of the @c N Bernoullis succeed.
111 * Same smaller-side dispatch : @c Pr(B = T) = @c Pr(sum Y = N - T)
112 * with @c Y_i = 1 - X_i, computed at whichever side has the smaller
113 * partial PMF. */
114static double probEqual(const std::vector<double> &p, int T)
115{
116 const int N = static_cast<int>(p.size());
117 if (T < 0 || T > N) return 0.0;
118
119 if (T <= N - T) {
120 auto dp = partialPMF(p, static_cast<std::size_t>(T));
121 return dp[T];
122 } else {
123 std::vector<double> q(N);
124 for (int i = 0; i < N; ++i) q[i] = 1.0 - p[i];
125 auto dp = partialPMF(q, static_cast<std::size_t>(N - T));
126 return dp[N - T];
127 }
128}
129
130/* Does the empty-group count (0) satisfy "0 op C"? For a scalar
131 * aggregation (no GROUP BY) the empty input is a real possible world --
132 * one row whose COUNT is 0 -- so a true-on-empty predicate (= 0, < k,
133 * <= k, ...) selects it. The grouped cdfForOperator excludes the empty
134 * world unconditionally, so we add probZero back exactly when 0 op C
135 * holds. */
136static bool zeroSatisfies(ComparisonOperator op, int C)
137{
138 switch (op) {
139 case ComparisonOperator::GE: return 0 >= C;
140 case ComparisonOperator::GT: return 0 > C;
141 case ComparisonOperator::LE: return 0 <= C;
142 case ComparisonOperator::LT: return 0 < C;
143 case ComparisonOperator::EQ: return 0 == C;
144 case ComparisonOperator::NE: return 0 != C;
145 }
146 return false;
147}
148
149/* Map operator + threshold to @c Pr(B op C) under SQL HAVING
150 * semantics : the empty-group case (@c B = 0) is excluded regardless
151 * of operator, matching @c count_enum's @c if (m < 1) m = 1 clamp
152 * and its @c x >= 1 enumeration lower bound -- correct for a GROUPED
153 * aggregate (the empty group is no row). For a scalar aggregate
154 * (@p is_scalar) the empty world is real, so probZero is added back when
155 * @c zeroSatisfies.
156 *
157 * Each branch picks at most two of probAtLeast / probAtMost /
158 * probEqual / probZero, each O(N x min(C, N-C)) ; the whole
159 * dispatch is therefore O(N x min(C, N-C)) per cmp. */
160static double cdfForOperator(const std::vector<double> &p,
162 int C, bool is_scalar)
163{
164 const int N = static_cast<int>(p.size());
165 double r = 0.0;
166 switch (op) {
168 /* sizes >= max(C, 1) ; the clamp excludes the empty world for
169 * GE 0 / GE -K cases. No further pZero subtraction needed
170 * because the [eff_lo, N] range starts at 1 or above. */
171 r = probAtLeast(p, std::max(C, 1));
172 break;
173 }
175 r = probAtLeast(p, std::max(C + 1, 1));
176 break;
177 }
179 /* sizes [1, min(C, N)] = Pr(B <= min(C, N)) - Pr(B = 0). */
180 const int T = std::min(C, N);
181 r = (T < 1) ? 0.0 : probAtMost(p, T) - probZero(p);
182 break;
183 }
185 const int T = std::min(C - 1, N);
186 r = (T < 1) ? 0.0 : probAtMost(p, T) - probZero(p);
187 break;
188 }
190 r = (C < 1 || C > N) ? 0.0 : probEqual(p, C);
191 break;
192 }
194 /* sizes [1, N] \ {C} = (1 - Pr(B = 0)) - (Pr(B = C) if 1<=C<=N). */
195 const double nonempty = 1.0 - probZero(p);
196 const double eq = (C >= 1 && C <= N) ? probEqual(p, C) : 0.0;
197 r = nonempty - eq;
198 break;
199 }
200 }
201 if (is_scalar && zeroSatisfies(op, C))
202 r += probZero(p);
203 return r;
204}
205
206/* The values of a count satisfying "count op C", as the interval
207 * [lo, hi] ; false for NE, whose set is not an interval. */
208static bool countBounds(ComparisonOperator op, long C, long &lo, long &hi)
209{
210 lo = LONG_MIN;
211 hi = LONG_MAX;
212 switch (op) {
213 case ComparisonOperator::GE: lo = C; return true;
214 case ComparisonOperator::GT: lo = C + 1; return true;
215 case ComparisonOperator::LE: hi = C; return true;
216 case ComparisonOperator::LT: hi = C - 1; return true;
217 case ComparisonOperator::EQ: lo = hi = C; return true;
218 case ComparisonOperator::NE: return false;
219 }
220 return false;
221}
222
223/* Pr(lo <= B <= hi), the empty world (B = 0) counted only for a scalar
224 * aggregation, as in cdfForOperator. O(N x hi). */
225static double probInRange(const std::vector<double> &p, long lo, long hi,
226 bool is_scalar)
227{
228 const long N = static_cast<long>(p.size());
229 lo = std::max(lo, is_scalar ? 0L : 1L);
230 hi = std::min(hi, N);
231 if (lo > hi)
232 return 0.0;
233 const auto dp = partialPMF(p, static_cast<std::size_t>(hi));
234 double r = 0.0;
235 for (long j = lo; j <= hi; ++j)
236 r += dp[static_cast<std::size_t>(j)];
237 return r;
238}
239
240/* Two comparisons on one count, conjoined: "m < rank <= m + k" of an
241 * OFFSET m LIMIT k, or "count > 1 AND count <= 3" on the aggregate of a
242 * subquery. The count is shared, which the single-comparison pass above
243 * refuses (it would couple the two comparisons), but the two only meet in
244 * one times gate: their conjunction is the event that the count lies in the
245 * intersection of their intervals, a single Poisson-binomial range. The
246 * first comparison becomes a Bernoulli of that probability, the second
247 * 𝟙. Conditions: each comparison has that times gate as its only parent,
248 * both read the count through the same constant arithmetic, whose topmost
249 * gate (or the count itself) has these two parents only, the rest of the
250 * chain and the contributors being private as in the single case. */
251static unsigned resolveCountRangePairs(GenericCircuit &gc,
252 const std::vector<unsigned> &ref)
253{
254 struct Candidate {
255 gate_t cmp;
256 AggCmpMatch match;
257 };
258 const auto nb = gc.getNbGates();
259 std::map<gate_t, std::vector<gate_t>> parents;
260 std::map<gate_t, std::vector<Candidate>> by_top;
261 unsigned resolved = 0;
262
263 for (std::size_t i = 0; i < nb; ++i) {
264 auto g = static_cast<gate_t>(i);
265 for (gate_t w : gc.getWires(g))
266 if (gc.getGateType(w) == gate_cmp)
267 parents[w].push_back(g);
268 }
269 for (const auto &kv : parents) {
270 AggCmpMatch match;
271 if (kv.second.size() != 1 || !matchAggCmp(gc, kv.first, match) ||
272 match.agg_kind != AggregationOperator::COUNT)
273 continue;
274 if (!std::all_of(match.ms.begin(), match.ms.end(),
275 [](long m) { return m == 1; }))
276 continue;
277 const gate_t top = match.via.empty() ? match.agg : match.via.front();
278 by_top[top].push_back({kv.first, std::move(match)});
279 }
280
281 for (const auto &kv : by_top) {
282 const auto &pair = kv.second;
283 if (pair.size() != 2 || ref[static_cast<std::size_t>(kv.first)] != 2)
284 continue;
285 const AggCmpMatch &m0 = pair[0].match, &m1 = pair[1].match;
286 const gate_t parent = parents[pair[0].cmp].front();
287 if (parents[pair[1].cmp].front() != parent ||
288 gc.getGateType(parent) != gate_times ||
289 m0.agg != m1.agg || m0.via != m1.via)
290 continue;
291
292 bool sound = true;
293 for (std::size_t i = 1; i < m0.via.size(); ++i)
294 if (ref[static_cast<std::size_t>(m0.via[i])] != 1)
295 sound = false;
296 if (!m0.via.empty() && ref[static_cast<std::size_t>(m0.agg)] != 1)
297 sound = false;
298 std::vector<double> p;
299 p.reserve(m0.ks.size());
300 for (std::size_t i = 0; sound && i < m0.ks.size(); ++i) {
301 if (ref[static_cast<std::size_t>(m0.semimods[i])] != 1) {
302 sound = false;
303 break;
304 }
305 double pi = contributorProb(gc, m0.ks[i], ref, sound);
306 p.push_back(pi);
307 }
308 if (!sound)
309 continue;
310
311 long lo0, hi0, lo1, hi1;
312 if (!countBounds(m0.op, m0.C, lo0, hi0) ||
313 !countBounds(m1.op, m1.C, lo1, hi1))
314 continue;
315 const bool is_scalar =
316 (gc.getInfos(m0.agg).second & PROVSQL_AGG_SCALAR_FLAG) != 0;
317 double pr = probInRange(p, std::max(lo0, lo1), std::min(hi0, hi1),
318 is_scalar);
319 if (pr < 0.0) pr = 0.0;
320 if (pr > 1.0) pr = 1.0;
321
322 gc.resolveCmpToBernoulli(pair[0].cmp, pr);
323 gc.resolveCmpToBernoulli(pair[1].cmp, 1.0);
324 resolved += 2;
325 }
326 return resolved;
327}
328
329} // namespace
330
332{
333 unsigned resolved = 0;
334 const auto nb = gc.getNbGates();
335
336 /* Snapshot the cmp-gate ids so in-place rewrites don't affect the
337 * iteration : same pattern as runAnalyticEvaluator. */
338 std::vector<gate_t> cmps;
339 for (std::size_t i = 0; i < nb; ++i) {
340 auto g = static_cast<gate_t>(i);
341 if (gc.getGateType(g) == gate_cmp)
342 cmps.push_back(g);
343 }
344 if (cmps.empty()) return 0;
345
346 /* Reference counts are computed once and not updated as we resolve
347 * cmps : resolveCmpToBernoulli only clears the cmp's wires (it does
348 * not touch any other gate), so children's ref counts are unchanged
349 * with respect to the rest of the circuit. The snapshot reflects
350 * the pre-pass state, which is what we need to certify "no outside
351 * reachability" for each candidate's input leaves. */
352 auto ref = computeRefCounts(gc);
353
354 for (gate_t cmp : cmps) {
355 if (gc.getGateType(cmp) != gate_cmp) continue; /* defensive */
356
357 AggCmpMatch match;
358 if (!matchAggCmp(gc, cmp, match))
359 continue;
360
361 /* COUNT(*) over unit-weighted contributors only. matchAggCmp has
362 * already remapped SUM-of-1s to COUNT; a genuine COUNT with a
363 * non-unit weight, or a SUM / MIN / MAX / AVG aggregate, is out of
364 * this pre-pass's scope and is left for its own evaluator or for
365 * provsql_having. */
366 if (match.agg_kind != AggregationOperator::COUNT) continue;
367 {
368 bool all_one = true;
369 for (long m : match.ms) if (m != 1) { all_one = false; break; }
370 if (!all_one) continue;
371 }
372
373 const gate_t agg = match.agg;
374 const auto &semimods = match.semimods;
375 const auto &ks = match.ks;
376 const ComparisonOperator op = match.op;
377 const int C = static_cast<int>(match.C);
378
379 /* Independence certification. The contributors are independent
380 * Bernoulli trials -- the precondition for the Poisson-binomial --
381 * exactly when each contributor's sub-circuit
382 *
383 * K_i -> semimod_i -> gate_agg -> cmp
384 *
385 * is a private read-once tree, sharing no randomness with another
386 * contributor or with the rest of the circuit. We check:
387 *
388 * 1. ref_count[gate_agg] == 1 : the aggregate is consumed by this
389 * cmp alone (catches HAVING COUNT(*) >= a AND COUNT(*) <= b
390 * over a shared count, which would couple the cmps).
391 * 2. ref_count[semimod_i] == 1 : the wrapper is consumed by
392 * gate_agg alone.
393 * 3. Every randomness-bearing gate inside K_i has ref_count == 1
394 * (verified by @c contributorProb as it recurses). A single
395 * condition that simultaneously gives: leaf sets pairwise
396 * disjoint across contributors (a shared gate would have
397 * ref >= 2), no reuse outside the cmp (an external parent would
398 * push ref >= 2), and read-once-ness within a contributor (a
399 * leaf used twice would have ref >= 2) -- so the contributor's
400 * marginal is its read-once probability and the contributors
401 * are mutually independent. Generalises the previous
402 * "K_i is a single gate_input" rule to arbitrary products /
403 * sums of private leaves (e.g. the bid * expertise row of a
404 * join), and bails (leaving the cmp for provsql_having) on any
405 * unsupported gate type.
406 *
407 * Constants on the path (semimod's M = gate_value(1), the
408 * const_side gate_one + gate_value(C), and any gate_one / gate_zero
409 * inside a contributor) carry no randomness, so their ref counts
410 * are not constrained. */
411 if (!aggPrivateToCmp(match, ref)) continue;
412 bool sound = true;
413 std::vector<double> p;
414 p.reserve(ks.size());
415 for (std::size_t i = 0; i < ks.size(); ++i) {
416 if (ref[static_cast<std::size_t>(semimods[i])] != 1) { sound = false; break; }
417 double pi = contributorProb(gc, ks[i], ref, sound);
418 if (!sound) break;
419 p.push_back(pi);
420 }
421 if (!sound) continue;
422
423 /* Scalar aggregation (no GROUP BY): the empty input is a real world,
424 * flagged in the agg gate's info2 high bit by provenance_aggregate. */
425 const bool is_scalar =
426 (gc.getInfos(agg).second & PROVSQL_AGG_SCALAR_FLAG) != 0;
427
428 /* Run the smaller-side dispatch over the contributor marginals. */
429 double pr = cdfForOperator(p, op, C, is_scalar);
430
431 /* Defensive clamp against floating-point roundoff. */
432 if (pr < 0.0) pr = 0.0;
433 if (pr > 1.0) pr = 1.0;
434
435 gc.resolveCmpToBernoulli(cmp, pr);
436 ++resolved;
437 }
438
439 resolved += resolveCountRangePairs(gc, ref);
440 return resolved;
441}
442
443} // namespace provsql
Typed aggregation value, operator, and aggregator abstractions.
@ COUNT
COUNT(*) or COUNT(expr) → integer.
Definition Aggregation.h:52
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
Shared machinery for the closed-form HAVING gate_cmp probability evaluators (Poisson-binomial COUNT,...
Closed-form Poisson-binomial CDF resolution for HAVING COUNT(*) op C gate_cmps.
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
std::vector< gate_t >::size_type getNbGates() const
Return the total number of gates in the circuit.
Definition Circuit.h:103
In-memory provenance circuit with semiring-generic evaluation.
void resolveCmpToBernoulli(gate_t g, double p)
Replace a gate_cmp by a constant Boolean leaf (gate_one for p == 1, gate_zero for p == 0) or by a Ber...
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
unsigned runCountCmpEvaluator(GenericCircuit &gc)
Run the Poisson-binomial pre-pass over gc.
bool aggPrivateToCmp(const AggCmpMatch &match, const std::vector< unsigned > &ref)
Whether the aggregate of match is consumed by its comparison alone: agg and every gate of via has ref...
std::vector< unsigned > computeRefCounts(const GenericCircuit &gc)
Reference count of every gate as a wire-target across the whole circuit.
bool matchAggCmp(GenericCircuit &gc, gate_t cmp, AggCmpMatch &out)
Try to match cmp against gate_cmp(gate_agg(α, semimod_i(K_i, m_i)*), gate_value(C)).
double contributorProb(const GenericCircuit &gc, gate_t g, const std::vector< unsigned > &ref, bool &ok)
Read-once marginal probability of a count/aggregate contributor (the K side of a semimod).
#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...
Result of matching a gate_cmp against the canonical HAVING aggregate-comparison shape.
gate_t agg
the gate_agg operand of the cmp
long C
the constant threshold, on the same integer grid as ms
std::vector< gate_t > ks
the K side of each semimod (contributor root)
std::vector< gate_t > semimods
the per-child gate_semimod parents
std::vector< gate_t > via
the gate_arith gates of constant arithmetic between the cmp and agg, folded into op and C
std::vector< long > ms
the M side of each semimod (per-row value), scaled to a common integer grid (numeric / decimal-float ...
AggregationOperator agg_kind
effective aggregate (SUM-of-1s remapped to COUNT)
ComparisonOperator op
comparator, flipped if the agg sits on the right