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 <vector>
10
11#include "Aggregation.h" // ComparisonOperator + getAggregationOperator
12#include "CmpEvaluatorCommon.h" // matchAggCmp, computeRefCounts, contributorProb
13
14namespace provsql {
15
16namespace {
17
18/* Partial Poisson-binomial PMF : compute @c dp[j] = Pr(exactly @c j
19 * successes among the @c N input Bernoullis) for @c j in @c [0, jmax]
20 * only. @c jmax is clamped to @c N. Cost : @c O(N x jmax). Rolling
21 * 1-D array, iterate j downward so each read references the
22 * not-yet-updated previous row. */
23static std::vector<double> partialPMF(const std::vector<double> &p,
24 std::size_t jmax)
25{
26 const std::size_t N = p.size();
27 jmax = std::min(jmax, N);
28 std::vector<double> dp(jmax + 1, 0.0);
29 dp[0] = 1.0;
30 for (std::size_t i = 0; i < N; ++i) {
31 const double pi = p[i];
32 const double qi = 1.0 - pi;
33 /* Cap inner loop at min(i+1, jmax) : entries beyond i are still
34 * zero and entries beyond jmax we never sum. */
35 const std::size_t upper = std::min(jmax, i + 1);
36 for (std::size_t j = upper; j >= 1; --j) {
37 dp[j] = dp[j] * qi + dp[j - 1] * pi;
38 }
39 dp[0] *= qi;
40 }
41 return dp;
42}
43
44/* Probability that the empty world occurs : @c prod_i (1 - p_i).
45 * Always needed for SQL HAVING semantics (the empty group never
46 * satisfies). */
47static double probZero(const std::vector<double> &p)
48{
49 double q = 1.0;
50 for (double pi : p) q *= (1.0 - pi);
51 return q;
52}
53
54/* Probability that at least @c T of the @c N Bernoullis succeed.
55 * Dispatches on which side of @c T is closer to the boundary to keep
56 * the partial DP at @c O(N x min(T, N - T + 1)).
57 * - If @c T-1 <= N-T (lower tail is smaller) : compute the lower
58 * partial PMF up to @c T-1 and return @c 1 - sum.
59 * - Otherwise (upper tail is smaller) : invert the Bernoullis,
60 * @c Y_i = 1 - X_i, and use @c Pr(B >= T) = Pr(sum Y <= N - T) ;
61 * the partial PMF on @c Y is computed up to @c N - T. */
62static double probAtLeast(const std::vector<double> &p, int T)
63{
64 const int N = static_cast<int>(p.size());
65 if (T <= 0) return 1.0;
66 if (T > N) return 0.0;
67
68 if (T - 1 <= N - T) {
69 auto dp = partialPMF(p, static_cast<std::size_t>(T - 1));
70 double sum = 0.0;
71 for (int j = 0; j <= T - 1; ++j) sum += dp[j];
72 return 1.0 - sum;
73 } else {
74 std::vector<double> q(N);
75 for (int i = 0; i < N; ++i) q[i] = 1.0 - p[i];
76 auto dp = partialPMF(q, static_cast<std::size_t>(N - T));
77 double sum = 0.0;
78 for (int j = 0; j <= N - T; ++j) sum += dp[j];
79 return sum;
80 }
81}
82
83/* Probability that at most @c T of the @c N Bernoullis succeed.
84 * Same smaller-side dispatch as @c probAtLeast : if @c T is closer
85 * to 0 compute the lower partial PMF and sum ; if @c T is closer to
86 * @c N invert and compute the upper tail's complement. */
87static double probAtMost(const std::vector<double> &p, int T)
88{
89 const int N = static_cast<int>(p.size());
90 if (T < 0) return 0.0;
91 if (T >= N) return 1.0;
92
93 if (T <= N - 1 - T) {
94 auto dp = partialPMF(p, static_cast<std::size_t>(T));
95 double sum = 0.0;
96 for (int j = 0; j <= T; ++j) sum += dp[j];
97 return sum;
98 } else {
99 std::vector<double> q(N);
100 for (int i = 0; i < N; ++i) q[i] = 1.0 - p[i];
101 auto dp = partialPMF(q, static_cast<std::size_t>(N - 1 - T));
102 double sum = 0.0;
103 for (int j = 0; j <= N - 1 - T; ++j) sum += dp[j];
104 return 1.0 - sum;
105 }
106}
107
108/* Probability that exactly @c T of the @c N Bernoullis succeed.
109 * Same smaller-side dispatch : @c Pr(B = T) = @c Pr(sum Y = N - T)
110 * with @c Y_i = 1 - X_i, computed at whichever side has the smaller
111 * partial PMF. */
112static double probEqual(const std::vector<double> &p, int T)
113{
114 const int N = static_cast<int>(p.size());
115 if (T < 0 || T > N) return 0.0;
116
117 if (T <= N - T) {
118 auto dp = partialPMF(p, static_cast<std::size_t>(T));
119 return dp[T];
120 } else {
121 std::vector<double> q(N);
122 for (int i = 0; i < N; ++i) q[i] = 1.0 - p[i];
123 auto dp = partialPMF(q, static_cast<std::size_t>(N - T));
124 return dp[N - T];
125 }
126}
127
128/* Does the empty-group count (0) satisfy "0 op C"? For a scalar
129 * aggregation (no GROUP BY) the empty input is a real possible world --
130 * one row whose COUNT is 0 -- so a true-on-empty predicate (= 0, < k,
131 * <= k, ...) selects it. The grouped cdfForOperator excludes the empty
132 * world unconditionally, so we add probZero back exactly when 0 op C
133 * holds. */
134static bool zeroSatisfies(ComparisonOperator op, int C)
135{
136 switch (op) {
137 case ComparisonOperator::GE: return 0 >= C;
138 case ComparisonOperator::GT: return 0 > C;
139 case ComparisonOperator::LE: return 0 <= C;
140 case ComparisonOperator::LT: return 0 < C;
141 case ComparisonOperator::EQ: return 0 == C;
142 case ComparisonOperator::NE: return 0 != C;
143 }
144 return false;
145}
146
147/* Map operator + threshold to @c Pr(B op C) under SQL HAVING
148 * semantics : the empty-group case (@c B = 0) is excluded regardless
149 * of operator, matching @c count_enum's @c if (m < 1) m = 1 clamp
150 * and its @c x >= 1 enumeration lower bound -- correct for a GROUPED
151 * aggregate (the empty group is no row). For a scalar aggregate
152 * (@p is_scalar) the empty world is real, so probZero is added back when
153 * @c zeroSatisfies.
154 *
155 * Each branch picks at most two of probAtLeast / probAtMost /
156 * probEqual / probZero, each O(N x min(C, N-C)) ; the whole
157 * dispatch is therefore O(N x min(C, N-C)) per cmp. */
158static double cdfForOperator(const std::vector<double> &p,
160 int C, bool is_scalar)
161{
162 const int N = static_cast<int>(p.size());
163 double r = 0.0;
164 switch (op) {
166 /* sizes >= max(C, 1) ; the clamp excludes the empty world for
167 * GE 0 / GE -K cases. No further pZero subtraction needed
168 * because the [eff_lo, N] range starts at 1 or above. */
169 r = probAtLeast(p, std::max(C, 1));
170 break;
171 }
173 r = probAtLeast(p, std::max(C + 1, 1));
174 break;
175 }
177 /* sizes [1, min(C, N)] = Pr(B <= min(C, N)) - Pr(B = 0). */
178 const int T = std::min(C, N);
179 r = (T < 1) ? 0.0 : probAtMost(p, T) - probZero(p);
180 break;
181 }
183 const int T = std::min(C - 1, N);
184 r = (T < 1) ? 0.0 : probAtMost(p, T) - probZero(p);
185 break;
186 }
188 r = (C < 1 || C > N) ? 0.0 : probEqual(p, C);
189 break;
190 }
192 /* sizes [1, N] \ {C} = (1 - Pr(B = 0)) - (Pr(B = C) if 1<=C<=N). */
193 const double nonempty = 1.0 - probZero(p);
194 const double eq = (C >= 1 && C <= N) ? probEqual(p, C) : 0.0;
195 r = nonempty - eq;
196 break;
197 }
198 }
199 if (is_scalar && zeroSatisfies(op, C))
200 r += probZero(p);
201 return r;
202}
203
204} // namespace
205
207{
208 unsigned resolved = 0;
209 const auto nb = gc.getNbGates();
210
211 /* Snapshot the cmp-gate ids so in-place rewrites don't affect the
212 * iteration : same pattern as runAnalyticEvaluator. */
213 std::vector<gate_t> cmps;
214 for (std::size_t i = 0; i < nb; ++i) {
215 auto g = static_cast<gate_t>(i);
216 if (gc.getGateType(g) == gate_cmp)
217 cmps.push_back(g);
218 }
219 if (cmps.empty()) return 0;
220
221 /* Reference counts are computed once and not updated as we resolve
222 * cmps : resolveCmpToBernoulli only clears the cmp's wires (it does
223 * not touch any other gate), so children's ref counts are unchanged
224 * with respect to the rest of the circuit. The snapshot reflects
225 * the pre-pass state, which is what we need to certify "no outside
226 * reachability" for each candidate's input leaves. */
227 auto ref = computeRefCounts(gc);
228
229 for (gate_t cmp : cmps) {
230 if (gc.getGateType(cmp) != gate_cmp) continue; /* defensive */
231
232 AggCmpMatch match;
233 if (!matchAggCmp(gc, cmp, match))
234 continue;
235
236 /* COUNT(*) over unit-weighted contributors only. matchAggCmp has
237 * already remapped SUM-of-1s to COUNT; a genuine COUNT with a
238 * non-unit weight, or a SUM / MIN / MAX / AVG aggregate, is out of
239 * this pre-pass's scope and is left for its own evaluator or for
240 * provsql_having. */
241 if (match.agg_kind != AggregationOperator::COUNT) continue;
242 {
243 bool all_one = true;
244 for (long m : match.ms) if (m != 1) { all_one = false; break; }
245 if (!all_one) continue;
246 }
247
248 const gate_t agg = match.agg;
249 const auto &semimods = match.semimods;
250 const auto &ks = match.ks;
251 const ComparisonOperator op = match.op;
252 const int C = static_cast<int>(match.C);
253
254 /* Independence certification. The contributors are independent
255 * Bernoulli trials -- the precondition for the Poisson-binomial --
256 * exactly when each contributor's sub-circuit
257 *
258 * K_i -> semimod_i -> gate_agg -> cmp
259 *
260 * is a private read-once tree, sharing no randomness with another
261 * contributor or with the rest of the circuit. We check:
262 *
263 * 1. ref_count[gate_agg] == 1 : the aggregate is consumed by this
264 * cmp alone (catches HAVING COUNT(*) >= a AND COUNT(*) <= b
265 * over a shared count, which would couple the cmps).
266 * 2. ref_count[semimod_i] == 1 : the wrapper is consumed by
267 * gate_agg alone.
268 * 3. Every randomness-bearing gate inside K_i has ref_count == 1
269 * (verified by @c contributorProb as it recurses). A single
270 * condition that simultaneously gives: leaf sets pairwise
271 * disjoint across contributors (a shared gate would have
272 * ref >= 2), no reuse outside the cmp (an external parent would
273 * push ref >= 2), and read-once-ness within a contributor (a
274 * leaf used twice would have ref >= 2) -- so the contributor's
275 * marginal is its read-once probability and the contributors
276 * are mutually independent. Generalises the previous
277 * "K_i is a single gate_input" rule to arbitrary products /
278 * sums of private leaves (e.g. the bid * expertise row of a
279 * join), and bails (leaving the cmp for provsql_having) on any
280 * unsupported gate type.
281 *
282 * Constants on the path (semimod's M = gate_value(1), the
283 * const_side gate_one + gate_value(C), and any gate_one / gate_zero
284 * inside a contributor) carry no randomness, so their ref counts
285 * are not constrained. */
286 if (ref[static_cast<std::size_t>(agg)] != 1) continue;
287 bool sound = true;
288 std::vector<double> p;
289 p.reserve(ks.size());
290 for (std::size_t i = 0; i < ks.size(); ++i) {
291 if (ref[static_cast<std::size_t>(semimods[i])] != 1) { sound = false; break; }
292 double pi = contributorProb(gc, ks[i], ref, sound);
293 if (!sound) break;
294 p.push_back(pi);
295 }
296 if (!sound) continue;
297
298 /* Scalar aggregation (no GROUP BY): the empty input is a real world,
299 * flagged in the agg gate's info2 high bit by provenance_aggregate. */
300 const bool is_scalar =
301 (gc.getInfos(agg).second & PROVSQL_AGG_SCALAR_FLAG) != 0;
302
303 /* Run the smaller-side dispatch over the contributor marginals. */
304 double pr = cdfForOperator(p, op, C, is_scalar);
305
306 /* Defensive clamp against floating-point roundoff. */
307 if (pr < 0.0) pr = 0.0;
308 if (pr > 1.0) pr = 1.0;
309
310 gc.resolveCmpToBernoulli(cmp, pr);
311 ++resolved;
312 }
313
314 return resolved;
315}
316
317} // 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.
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.
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< 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