ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
subset.cpp
Go to the documentation of this file.
1/**
2 * @file subset.cpp
3 * @brief Valid-world enumeration for aggregate HAVING predicates.
4 *
5 * Implements @c enumerate_valid_worlds() declared in @c subset.hpp.
6 *
7 * For a list of @f$n@f$ tuples with individual values, the function
8 * iterates over all @f$2^n@f$ possible worlds (bitmasks), computes the
9 * aggregate of the present tuples' values using the requested
10 * @c AggregationOperator, and tests the comparison predicate. All
11 * valid worlds are collected and returned.
12 *
13 * The @c upset output flag is set to @c true when the set of valid
14 * worlds is upward-closed (every superset of a valid world is also
15 * valid), which is the case for monotone aggregation predicates (e.g.
16 * SUM ≥ k). This information is used to optimise the evaluation of
17 * monotone HAVING clauses.
18 *
19 * Internal helpers in an anonymous namespace:
20 * - @c increment(): advance a bitmask to the next possible world.
21 * - @c compute_agg(): compute the aggregate value for one bitmask.
22 */
23#include "subset.hpp"
24#include <algorithm>
25#include <cstddef>
26#include <cstdint>
27#include <limits>
28#include <vector>
29#include <stdexcept>
30#include <cassert>
31
32namespace {
33static bool increment(mask_t &v)
34{
35 for(size_t i=0; i<v.size(); ++i)
36 {
37 v[i]=!v[i];
38 if(v[i])
39 return true;
40 }
41 return false;
42}
43
44static std::vector<mask_t> all_worlds(const std::vector<long> &values)
45{
46 std::vector<mask_t> worlds;
47 mask_t mask(values.size());
48 // Skip empty world
49 while(increment(mask))
50 worlds.push_back(mask);
51 return worlds;
52}
53
54static void append_range(std::vector<mask_t> &out,
55 const std::vector<std::vector<mask_t> > &dp,
56 long long lo,
57 long long hi)
58{
59 if (dp.empty()) return;
60 const long long J = static_cast<long long>(dp.size())-1;
61 lo = std::max(lo, 0LL);
62 hi = std::min(hi, J);
63 if (lo>hi) return;
64
65 for (long long j = lo; j <= hi; ++j) {
66 out.insert(out.end(), dp[j].begin(), dp[j].end());
67 }
68}
69
70class DPException : public std::exception {};
71
72/** @brief Return the minimum of two values. */
73#define MIN(x,y) ((x)<(y)?(x):(y))
74
75static std::vector<mask_t> sum_dp(const std::vector<long> &values, long C, ComparisonOperator op, bool absorptive, bool &upset, bool keep_empty=false)
76{
77 const std::size_t n = values.size();
78
79 std::vector<mask_t> R;
80
81 // We first deal with NEQ by combining LT and GT
82 if(op == ComparisonOperator::NE) {
83 std::vector<mask_t> lt= sum_dp(values, C, ComparisonOperator::LT, absorptive, upset, keep_empty);
84 std::vector<mask_t> gt= sum_dp(values, C, ComparisonOperator::GT, absorptive, upset, keep_empty);
85 R.reserve(lt.size()+gt.size());
86 R.insert(R.end(),lt.begin(),lt.end());
87 R.insert(R.end(),gt.begin(),gt.end());
88 return R;
89 }
90
91 long long T=0;
92 for (long w: values) {
93 if (w < 0)
94 throw DPException();
95 T+=w;
96 }
97
98 //no valid worlds case
99 if (op == ComparisonOperator::GT && C>=T) return {};
100 if (op == ComparisonOperator::GE && C>T) return {};
101 if (op == ComparisonOperator::LT && C<=0) return {};
102 if (op == ComparisonOperator::LE && C<0) return {};
103 if (op == ComparisonOperator::EQ && (C>T || C<0)) return {};
104
105 //tautology cases
106 if (op == ComparisonOperator::GT && C<0) return all_worlds(values);
107 if (op == ComparisonOperator::GE && C<=0) return all_worlds(values);
108 if (op == ComparisonOperator::LT && C>T) return all_worlds(values);
109 if (op == ComparisonOperator::LE && C>=T) return all_worlds(values);
110
111 long long J=0;
113 J=T;
114 else if (op==ComparisonOperator::LT)
115 J=MIN(C-1,T);
116 else
117 J=MIN(C,T);
118
119 assert(J>=0);
120
121 // The DP is pseudo-polynomial: it allocates one bucket per integer in
122 // [0, J]. A large J (huge aggregate values, or a high-scale decimal grid)
123 // would make that array impractical -- bail out so the caller falls back to
124 // exact subset enumeration, which is magnitude-independent.
125 static const long long SUM_DP_MAX_J = 10000000LL;
126 if (J > SUM_DP_MAX_J)
127 throw DPException();
128
129 std::vector<std::vector<mask_t> > dp(static_cast<std::size_t>(J) + 1);
130 dp[0].push_back(mask_t(n)); // dp[0] <- {emptyset}
131
132 long long pref_sum=0;
133
134 for (std::size_t i=0; i<n; ++i)
135 {
136 const long w=values[i];
137 pref_sum+=w;
138 const long long j_max=MIN(J,pref_sum);
139
140 for (long long j = j_max; j >= w; --j) {
141 const long long p = j - w;
142 if(absorptive && ((op==ComparisonOperator::GT && p>C) ||
143 (op==ComparisonOperator::GE && p>=C))) {
144 upset=true;
145 continue;
146 }
147 size_t s=dp[p].size();
148 for(size_t k=0; k<s; ++k) {
149 mask_t m = dp[p][k];
150 m[i] = true;
151 dp[j].push_back(m);
152 }
153 }
154 }
155
156 switch(op){
158 append_range(R,dp,C,C);
159 break;
160
162 append_range(R,dp,C+1,J);
163 break;
164
165
167 append_range(R,dp,0,C-1);
168 break;
169
171 append_range(R,dp,C,J);
172 break;
173
175 append_range(R,dp,0,C);
176 break;
177
178 case ComparisonOperator::NE: // case already processed
179 assert(false);
180 }
181
182 // dp[0] holds the empty world together with every non-empty world whose
183 // present tuples all sum to 0 (value 0 being SUM's additive identity). A
184 // HAVING predicate is never satisfied by the empty group, but those value-0
185 // worlds are legitimate non-empty worlds and must be kept -- cf. the ValidSum
186 // proof, which removes only the single world {emptyset} from the selected
187 // range. So the <, <= (and =, when C=0) ranges above include dp[0], and we
188 // drop only the all-absent mask here. Skipping all of dp[0] instead (the old
189 // `lo=1`) silently dropped, e.g., a BID-block choice of a value-0 tuple under
190 // `sum < k`.
191 R.erase(std::remove_if(R.begin(), R.end(),
192 [](const mask_t &m) {
193 for(size_t i=0; i<m.size(); ++i) if(m[i]) return false;
194 return true;
195 }),
196 R.end());
197
198 // keep_empty marks a SCALAR COUNT enumerated through this value-aware DP
199 // (count(col) with NULL contributors, whose 0/1 values make COUNT a SUM of
200 // indicators). Unlike a genuine SUM -- empty group SQL NULL, so the empty
201 // world never satisfies -- a COUNT's empty group has the real value 0, so the
202 // all-absent world is a legitimate possible world: re-add it exactly when 0
203 // satisfies the predicate (a true-on-empty bound: = 0, < k, <= k, <> k!=0).
204 if (keep_empty) {
205 bool zero_sat = false;
206 switch(op) {
207 case ComparisonOperator::EQ: zero_sat = (C == 0); break;
208 case ComparisonOperator::NE: zero_sat = (C != 0); break;
209 case ComparisonOperator::LT: zero_sat = (0 < C); break;
210 case ComparisonOperator::LE: zero_sat = (0 <= C); break;
211 case ComparisonOperator::GT: zero_sat = (0 > C); break;
212 case ComparisonOperator::GE: zero_sat = (0 >= C); break;
213 }
214 if (zero_sat)
215 R.push_back(mask_t(n));
216 }
217
218 return R;
219}
220
221//generate k-subsets form an n-set
222static void combinations(std::size_t start,
223 int k_left,
224 mask_t mask,
225 std::vector<mask_t> &out)
226{
227 const size_t n = mask.size();
228
229 if (k_left == 0) {
230 out.push_back(mask);
231 return;
232 }
233
234 if (start >= n) return;
235
236 const std::size_t remaining = n - start;
237 if (remaining < static_cast<std::size_t>(k_left)) return;
238
239 combinations(start + 1, k_left, mask, out);
240
241 mask[start]=true;
242 combinations(start + 1, k_left - 1, mask, out);
243}
244
245static std::vector<mask_t> count_enum(const std::vector<long> &values, long m, ComparisonOperator op, bool absorptive, bool &upset, bool is_scalar)
246{
247 const int n = static_cast<int>(values.size());
248 std::vector<mask_t> out;
249
250 auto add_exact_k = [&](long k) {
251 if (k < 0 || k > n) return;
252 combinations(0, static_cast<int>(k), mask_t(n), out);
253 };
254
255 /* The lowest count a group can have: 1 for a grouped aggregate (the empty
256 * group is no row, so a HAVING predicate is never evaluated on it -- the
257 * count >= 0 / count > -K family collapses to "non-empty" rather than to a
258 * tautology), but 0 for a SCALAR aggregate (no GROUP BY), whose single result
259 * row always exists with the empty input contributing count 0. Folding the
260 * empty world into each branch's bound (rather than appending it afterwards)
261 * keeps it consistent with the upset / minimal-witness structure: for the GE
262 * upset, count >= 0 then has minimal witness {} and is correctly a tautology;
263 * the empty subset is annotated by the caller as one ⊗ (𝟙 ⊖ ⊕(tuples)). */
264 const long lo = is_scalar ? 0 : 1;
265
266 switch (op)
267 {
269 if (m >= lo) add_exact_k(m);
270 break;
271
273 ++m;
274 [[fallthrough]];
276 /* count >= m : minimal present count is max(m, lo). */
277 const long mink = std::max(m, lo);
278 if (absorptive) {
279 upset = true;
280 add_exact_k(mink);
281 } else
282 for (long k = mink; k <= n; ++k) add_exact_k(k);
283 break;
284 }
285
287 --m;
288 [[fallthrough]];
290 /* count <= m : worlds k in [lo, m] (empty world k=0 included for a scalar
291 * aggregate iff m >= 0). */
292 for (long k = lo; k <= m; ++k) add_exact_k(k);
293 break;
294
296 /* count != m : every world in [lo, n] except k = m (the scalar empty world
297 * k=0 is included iff m != 0). */
298 for (long k = lo; k <= n; ++k)
299 if (k != m) add_exact_k(k);
300 break;
301 }
302
303 return out;
304}
305
306}
307
308/**
309 * @brief Apply a comparison operator to two values.
310 * @tparam I Type of the left operand.
311 * @tparam J Type of the right operand.
312 * @param a Left operand.
313 * @param op Comparison operator.
314 * @param b Right operand.
315 * @return Result of the comparison.
316 */
317template<typename I, typename J>
318static bool compare(I a, ComparisonOperator op, J b) {
319 switch (op) {
320 case ComparisonOperator::EQ: return a == b;
321 case ComparisonOperator::NE: return a != b;
322 case ComparisonOperator::GT: return a > b;
323 case ComparisonOperator::LT: return a < b;
324 case ComparisonOperator::GE: return a >= b;
325 case ComparisonOperator::LE: return a <= b;
326 }
327 return false;
328}
329
330/**
331 * @brief Evaluate whether the aggregation of @p values masked by @p mask satisfies @p op @p constant.
332 * @param values Input values to aggregate.
333 * @param mask Boolean mask selecting which values to include.
334 * @param constant Right-hand side constant of the comparison.
335 * @param op Comparison operator.
336 * @param aggregator Aggregator to apply to the selected values.
337 * @return @c true if the aggregate result satisfies the comparison.
338 */
339bool evaluate(const std::vector<long>& values,
340 const std::vector<bool>& mask,
341 long constant, ComparisonOperator op,
342 std::unique_ptr<Aggregator> aggregator)
343{
344 for (std::size_t i = 0; i < values.size(); ++i) {
345 if (mask[i]) aggregator->add(AggValue {values[i]});
346 }
347 auto res = aggregator->finalize();
348 switch(aggregator->resultType()) {
349 case ValueType::INT:
350 return compare(std::get<long>(res.v), op, constant);
352 return compare(std::get<bool>(res.v), op, constant);
353 case ValueType::FLOAT:
354 return compare(std::get<double>(res.v), op, constant);
355 default:
356 throw std::runtime_error("Cannot compare this kind of value");
357 }
358}
359
361 const std::vector<long> &values,
362 const mask_t &present,
363 long constant,
365 AggregationOperator agg_kind)
366{
367 // Empty-group exclusion: an all-absent group does not exist, so HAVING is
368 // never satisfied on it (the one edge convention a sampler must reproduce to
369 // match the expansion).
370 bool any = false;
371 for (bool b : present)
372 if (b) { any = true; break; }
373 if (!any)
374 return false;
375
376 return evaluate(values, present, constant, op,
377 makeAggregator(agg_kind, ValueType::INT));
378}
379
380/**
381 * @brief Enumerate all subsets satisfying a HAVING predicate by exhaustive search.
382 * @param values Input values.
383 * @param constant Constant for the comparison.
384 * @param op Comparison operator.
385 * @param agg_kind Aggregation function to apply.
386 * @param absorptive Whether the semiring is absorptive.
387 * @param upset Set to @c true if the result set forms an upset (monotone).
388 * @return Vector of satisfying subset masks.
389 */
390std::vector<mask_t> enumerate_exhaustive(
391 const std::vector<long> &values,
392 long constant,
394 AggregationOperator agg_kind,
395 bool absorptive,
396 bool &upset)
397{
398 const size_t n = values.size();
399
400 std::vector<mask_t> worlds;
401 mask_t mask(n);
402
403 bool all_worlds = true;
404
405 while(increment(mask)) { // Skipping empty world (handled by agg_cmp_holds_in_world too)
406 if(agg_cmp_holds_in_world(values, mask, constant, op, agg_kind))
407 worlds.push_back(mask);
408 else
409 all_worlds=false;
410 }
411
412 if(all_worlds && absorptive)
413 {
414 worlds.clear();
415
416 // In that case, the result is equivalent to the upset generated by
417 // the single-tuple possible worlds
418 combinations(0, 1, mask_t(n), worlds);
419 upset=true;
420 }
421
422 return worlds;
423}
424
425std::vector<mask_t> enumerate_valid_worlds(
426 const std::vector<long> &values,
427 long constant,
429 AggregationOperator agg_kind,
430 bool absorptive,
431 bool &upset,
432 bool is_scalar
433 )
434{
435 if (agg_kind == AggregationOperator::COUNT) {
436 /* count(*) (every contributor's value is the unit 1) is pure cardinality:
437 * count_enum enumerates by subset size and folds the scalar empty world via
438 * its lo bound. count(col) keeps the COUNT identity but carries per-row 0/1
439 * values (0 for a NULL-valued / null-padded row that still keeps the group
440 * alive); cardinality would wrongly count the 0-valued rows, so route it to
441 * the value-aware sum_dp -- with keep_empty=is_scalar, since a scalar
442 * count(col)'s empty group is the real value 0 (not SQL NULL like a sum). */
443 bool all_one = true;
444 for (long v : values) if (v != 1) { all_one = false; break; }
445 if (all_one)
446 return count_enum(values,constant,op, absorptive, upset, is_scalar);
447 try {
448 return sum_dp(values, constant, op, absorptive, upset, /*keep_empty=*/is_scalar);
449 } catch(DPException &e) {
450 // 0/1 values are non-negative, so this never throws; fall back defensively.
451 return enumerate_exhaustive(values, constant, op, agg_kind, absorptive, upset);
452 }
453 }
454
455 if(agg_kind == AggregationOperator::SUM)
456 try {
457 return sum_dp(values, constant, op, absorptive, upset);
458 } catch(DPException &e) {
459 // We will use the default implementation of the enumeration
460 }
461
462 return enumerate_exhaustive(values, constant, op, agg_kind, absorptive, upset);
463}
464
465std::vector<mask_t> enumerate_array_agg_worlds(
466 const std::vector<std::string> &vals,
467 const std::vector<std::string> &target,
468 bool want_equal)
469{
470 const size_t n = vals.size();
471 std::vector<mask_t> worlds;
472 mask_t mask(n);
473
474 // increment() starts from the all-absent mask and never yields it, so the
475 // empty group (which never satisfies a HAVING clause) is excluded for free.
476 while(increment(mask)) {
477 std::vector<std::string> present;
478 present.reserve(n);
479 for(size_t i = 0; i < n; ++i)
480 if(mask[i]) present.push_back(vals[i]);
481 if((present == target) == want_equal)
482 worlds.push_back(mask);
483 }
484 return worlds;
485}
std::unique_ptr< Aggregator > makeAggregator(AggregationOperator op, ValueType t)
Create a concrete Aggregator for the given operator and value type.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
@ COUNT
COUNT(*) or COUNT(expr) → integer.
Definition Aggregation.h:52
@ SUM
SUM → integer or float.
Definition Aggregation.h:53
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
@ INT
Signed 64-bit integer.
Definition Aggregation.h:68
@ BOOLEAN
Boolean.
Definition Aggregation.h:70
@ FLOAT
Double-precision float.
Definition Aggregation.h:69
A dynamically-typed aggregate value.
Definition Aggregation.h:86
bool evaluate(const std::vector< long > &values, const std::vector< bool > &mask, long constant, ComparisonOperator op, std::unique_ptr< Aggregator > aggregator)
Evaluate whether the aggregation of values masked by mask satisfies op constant.
Definition subset.cpp:339
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
#define MIN(x, y)
Return the minimum of two values.
Definition subset.cpp:73
static bool compare(I a, ComparisonOperator op, J b)
Apply a comparison operator to two values.
Definition subset.cpp:318
bool agg_cmp_holds_in_world(const std::vector< long > &values, const mask_t &present, long constant, ComparisonOperator op, AggregationOperator agg_kind)
Canonical per-world decision: does agg(present) op constant hold in the world where exactly the prese...
Definition subset.cpp:360
std::vector< mask_t > enumerate_exhaustive(const std::vector< long > &values, long constant, ComparisonOperator op, AggregationOperator agg_kind, bool absorptive, bool &upset)
Enumerate all subsets satisfying a HAVING predicate by exhaustive search.
Definition subset.cpp:390
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.
std::vector< bool > mask_t
A bitmask over tuples representing one possible world.
Definition subset.hpp:29