ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
having_semantics.cpp
Go to the documentation of this file.
1/**
2 * @file having_semantics.cpp
3 * @brief Helper definitions for HAVING-clause provenance evaluation.
4 *
5 * Defines the small non-template helpers declared in
6 * @c provsql_having_detail in @c having_semantics.hpp. The actual
7 * possible-worlds logic (enumeration and the MIN / MAX closed form) is
8 * the @c provsql_having() template in the header.
9 */
10extern "C" {
11#include "postgres.h"
12#include "catalog/pg_type.h"
13#include "utils/lsyscache.h"
14#include "utils/typcache.h"
15#include "fmgr.h"
16}
17#include "c_cpp_compatibility.h"
18
19#include <algorithm>
20#include <climits>
21#include <strings.h>
22#include <string>
23#include <unordered_set>
24#include <vector>
25
26#include "having_semantics.hpp"
27
29
30// The comparison-domain type of a HAVING aggregate is the aggregate's
31// result type, stored in info2 of the gate_agg (set by
32// provenance_aggregate as set_infos(agg, aggfnoid, aggtype)). This
33// classifies it so the cmp can be evaluated in the right domain.
34bool aggtype_is_text(unsigned oid) {
35 switch (oid) {
36 case TEXTOID: case VARCHAROID: case BPCHAROID: case CHAROID: case NAMEOID:
37 return true;
38 default:
39 return false;
40 }
41}
42
43bool aggtype_is_integer(unsigned oid) {
44 switch (oid) {
45 case INT2OID: case INT4OID: case INT8OID:
46 return true;
47 default:
48 return false;
49 }
50}
51
52bool aggtype_is_boolean(unsigned oid) {
53 return oid == BOOLOID;
54}
55
56// array_agg's result type is the array type (e.g. boolean[]); its element type
57// is the comparison domain. PostgreSQL serialises a scalar bool as
58// 'true'/'false' but a bool array element as 't'/'f', so the array_agg
59// comparison must know when the elements are boolean to reconcile the two.
60bool aggtype_elem_is_boolean(unsigned oid) {
61 if (oid == BOOLOID)
62 return true;
63 Oid elem = get_element_type(oid);
64 return elem == BOOLOID;
65}
66
67// Types handled by the numeric comparison domain (scaled to a common integer
68// grid). choose() over these is evaluated there -- including ordering
69// comparisons; choose() over any other type falls to the value-as-text domain.
70bool aggtype_is_numeric(unsigned oid) {
71 switch (oid) {
72 case INT2OID: case INT4OID: case INT8OID:
73 case FLOAT4OID: case FLOAT8OID: case NUMERICOID:
74 return true;
75 default:
76 return false;
77 }
78}
79
80// Dense ranks of the values and of the threshold under the comparison
81// function of their type (a date, a timestamp...), read by the type's input
82// function from the text the gates hold.
83bool rank_values_by_type(unsigned typoid, const std::vector<std::string> &vals,
84 const std::string &threshold,
85 std::vector<long> &ranks, long &threshold_rank) {
86 TypeCacheEntry *tce = lookup_type_cache(typoid, TYPECACHE_CMP_PROC_FINFO);
87 if (tce == NULL || !OidIsValid(tce->cmp_proc))
88 return false;
89 Oid typinput, typioparam;
90 getTypeInputInfo(typoid, &typinput, &typioparam);
91 Oid collation = get_typcollation(typoid);
92
93 std::vector<Datum> datums;
94 datums.reserve(vals.size() + 1);
95 for (const auto &v : vals)
96 datums.push_back(OidInputFunctionCall(typinput, const_cast<char *>(v.c_str()),
97 typioparam, -1));
98 datums.push_back(OidInputFunctionCall(typinput,
99 const_cast<char *>(threshold.c_str()),
100 typioparam, -1));
101
102 auto cmp = [&](size_t a, size_t b) -> int {
103 return DatumGetInt32(FunctionCall2Coll(&tce->cmp_proc_finfo, collation,
104 datums[a], datums[b]));
105 };
106 std::vector<size_t> order(datums.size());
107 for (size_t i = 0; i < order.size(); ++i) order[i] = i;
108 std::sort(order.begin(), order.end(),
109 [&](size_t a, size_t b) { return cmp(a, b) < 0; });
110
111 std::vector<long> rank(datums.size());
112 long r = 0;
113 for (size_t k = 0; k < order.size(); ++k) {
114 if (k > 0 && cmp(order[k - 1], order[k]) != 0)
115 ++r;
116 rank[order[k]] = r;
117 }
118 ranks.assign(rank.begin(), rank.end() - 1);
119 threshold_rank = rank.back();
120 return true;
121}
122
123// Parse a PostgreSQL array output literal -- "{1,2}", "{a,\"b,c\"}" -- into its
124// top-level element texts (surrounding double quotes removed, backslash escapes
125// resolved; a NULL element becomes array_null_element()). Returns false on a malformed or nested-array literal. Sufficient
126// for one-dimensional arrays of scalar elements, which is what array_agg over a
127// provenance-tracked column produces.
128bool parse_array_literal(const std::string &s, std::vector<std::string> &out) {
129 out.clear();
130 size_t i = 0, n = s.size();
131 while (i < n && isspace((unsigned char) s[i])) i++;
132 if (i >= n || s[i] != '{') return false;
133 i++;
134 while (i < n && isspace((unsigned char) s[i])) i++;
135 if (i < n && s[i] == '}') return true; // empty array
136 while (i < n) {
137 std::string elem;
138 while (i < n && isspace((unsigned char) s[i])) i++;
139 if (i < n && s[i] == '"') {
140 i++;
141 while (i < n && s[i] != '"') {
142 if (s[i] == '\\' && i + 1 < n) { elem.push_back(s[i + 1]); i += 2; }
143 else { elem.push_back(s[i]); i++; }
144 }
145 if (i >= n) return false;
146 i++; // closing quote
147 } else {
148 while (i < n && s[i] != ',' && s[i] != '}') { elem.push_back(s[i]); i++; }
149 while (!elem.empty() && isspace((unsigned char) elem.back())) elem.pop_back();
150 // An unquoted NULL (any case) is the NULL element; the string 'NULL' is
151 // always output quoted.
152 if (elem.size() == 4 && strcasecmp(elem.c_str(), "NULL") == 0)
153 elem = array_null_element();
154 }
155 out.push_back(elem);
156 while (i < n && isspace((unsigned char) s[i])) i++;
157 if (i < n && s[i] == ',') { i++; continue; }
158 if (i < n && s[i] == '}') return true;
159 return false;
160 }
161 return false;
162}
163
164// Parse a plain decimal literal ("-12.340", "6", "6.5") into a scaled
165// integer: the value is @c mantissa * 10^(-scale). Returns false on
166// exponential notation, inf / nan, or anything that is not a plain
167// decimal -- those fall back to the existing (string / error) handling.
168bool parse_decimal_scaled(const std::string &s, long &mantissa, int &scale) {
169 if (s.empty()) return false;
170 std::size_t i = 0;
171 bool neg = false;
172 if (s[i] == '+' || s[i] == '-') { neg = (s[i] == '-'); ++i; }
173 std::string digits;
174 int sc = 0;
175 int exponent = 0;
176 bool seen_dot = false, seen_digit = false;
177 for (; i < s.size(); ++i) {
178 char ch = s[i];
179 if (ch == '.') {
180 if (seen_dot) return false;
181 seen_dot = true;
182 } else if (ch >= '0' && ch <= '9') {
183 digits.push_back(ch);
184 if (seen_dot) ++sc;
185 seen_digit = true;
186 } else if (ch == 'e' || ch == 'E') {
187 /* A float's own text uses an exponent for large and small magnitudes
188 * (1e+20, 2.5e-07), and that is the same decimal with its point moved:
189 * read it, and let the scale below carry it. Without this, a comparison
190 * against a value the arithmetic computed in double declined the exact
191 * grid and fell to the enumeration, which is sound but slower and
192 * sometimes refused. */
193 bool eneg = false;
194 std::string edigits;
195 if (!seen_digit) return false;
196 ++i;
197 if (i < s.size() && (s[i] == '+' || s[i] == '-')) {
198 eneg = (s[i] == '-');
199 ++i;
200 }
201 for (; i < s.size(); ++i) {
202 if (s[i] < '0' || s[i] > '9') return false;
203 edigits.push_back(s[i]);
204 if (edigits.size() > 4) return false; /* far beyond any grid */
205 }
206 if (edigits.empty()) return false;
207 exponent = std::stoi(edigits);
208 if (eneg) exponent = -exponent;
209 break;
210 } else {
211 return false; // inf, nan, separators, ...
212 }
213 }
214 if (!seen_digit) return false;
215 // Drop trailing zeros in the fractional part: they do not change the value
216 // (15.0000000000000000 == 15, 15.50 == 15.5) but inflate the scale, and a
217 // large scale forces every value to be rescaled to a huge integer grid that
218 // the value-aware sum DP cannot represent. Numeric division in particular
219 // yields such trailing-zero-padded thresholds. Only fractional zeros are
220 // dropped (scale > 0); trailing zeros of an integer (100) are significant.
221 while (sc > 0 && !digits.empty() && digits.back() == '0') {
222 digits.pop_back();
223 --sc;
224 }
225 /* The exponent moves the point: 1e+20 is the digit 1 with twenty zeros, and
226 * 2.5e-07 is 25 at scale 8. A negative scale is the first of those, and the
227 * zeros go into the digits, where a value too wide for the grid overflows
228 * below and is declined as any other wide value is. */
229 sc -= exponent;
230 while (sc < 0) {
231 digits.push_back('0');
232 ++sc;
233 if (digits.size() > 18) return false;
234 }
235 try {
236 std::size_t pos = 0;
237 long long val = std::stoll(digits, &pos);
238 if (pos != digits.size()) return false;
239 mantissa = neg ? -static_cast<long>(val) : static_cast<long>(val);
240 scale = sc;
241 return true;
242 } catch (...) { // out_of_range / invalid
243 return false;
244 }
245}
246
247// Rescale a (mantissa, scale) decimal to a common target scale, i.e.
248// mantissa * 10^(target_scale - scale). Returns false on overflow.
249bool rescale_to(long mantissa, int scale, int target_scale, long &out) {
250 long factor = 1;
251 for (int k = 0; k < target_scale - scale; ++k) {
252 if (factor > (LONG_MAX / 10)) return false;
253 factor *= 10;
254 }
255 if (mantissa != 0 &&
256 (mantissa > LONG_MAX / factor || mantissa < LONG_MIN / factor))
257 return false;
258 out = mantissa * factor;
259 return true;
260}
261
262// Map a cmp gate's Postgres operator to subset.cpp's ComparisonOperator
264 return cmpOpFromOid(c.getInfos(cmp_gate).first, ok);
265}
266
267// Flip operator for "C op agg" <=> "agg flip(op) C"
279
280// Extract a gate_semimod's gate_value extra as a raw string, along with its
281// K-gate operand. Used by the value-as-text HAVING comparison path.
285 std::string &m_out,
286 gate_t &k_gate_out)
287{
288 if (c.getGateType(semimod_gate) != gate_semimod) return false;
289
290 const auto &w = c.getWires(semimod_gate);
291 if (w.size() != 2) return false;
292
293 if (c.getGateType(w[1]) != gate_value) return false;
294 m_out = c.getExtra(w[1]);
295
296 k_gate_out = w[0];
297 return true;
298}
299
300// Extract a constant C encoded as gate_semimod(gate_one, gate_value("C")),
301// returning the gate_value's extra as a raw string. Used by the
302// value-as-text HAVING comparison path.
303bool extract_constant_string(GenericCircuit &c, gate_t x, std::string &C_out) {
304 if (c.getGateType(x) != gate_semimod)
305 return false;
306
307 const auto &w = c.getWires(x);
308 if (w.size() != 2)
309 return false;
310
311 if (c.getGateType(w[0]) != gate_one)
312 return false;
313
314 if (c.getGateType(w[1]) != gate_value)
315 return false;
316
317 C_out = c.getExtra(w[1]);
318 return true;
319}
320
321// Whether a comparison side is (or is arithmetic over) an aggregate: descend
322// through gate_arith until a gate_agg is found. Used to recognise the cmp
323// gates that the HAVING / WHERE-on-aggregate evaluator must resolve (as
324// opposed to RV comparisons, which carry gate_rv leaves instead).
326 gate_type t = c.getGateType(g);
327 if (t == gate_agg)
328 return true;
329 if (t == gate_arith) {
330 for (gate_t ch : c.getWires(g))
331 if (side_has_agg(c, ch))
332 return true;
333 }
334 return false;
335}
336
337// Collect cmp gates in the prov circuit, in post-order: a comparison comes
338// after every comparison below it, so that when it is resolved, the
339// comparisons its contributors read (a count over rows filtered by a
340// comparison on another aggregate) are already in the mapping.
341std::vector<gate_t> collect_sp_cmp_gates(GenericCircuit &c, gate_t start) {
342 std::vector<gate_t> out;
343 std::vector<std::pair<gate_t, bool>> stack; // (gate, children pushed)
344 stack.emplace_back(start, false);
345
346 std::unordered_set<gate_t> seen;
347
348 while (!stack.empty()) {
349 auto [cur, expanded] = stack.back();
350 stack.pop_back();
351
352 if (!expanded) {
353 if (!seen.insert(cur).second) continue;
354 stack.emplace_back(cur, true);
355 for (gate_t ch : c.getWires(cur))
356 if (!seen.count(ch))
357 stack.emplace_back(ch, false);
358 continue;
359 }
360
361 if (c.getGateType(cur) == gate_cmp) {
362 const auto &cw = c.getWires(cur);
363 if (cw.size() == 2) {
364 gate_t L = cw[0];
365 gate_t R = cw[1];
366
367 // Any comparison with an aggregate on either side (directly, or under
368 // arithmetic): the single-aggregate-vs-constant ones are handled by the
369 // fast path, the rest (agg-vs-agg, products of aggregates, c/agg, ...)
370 // by the general possible-worlds enumeration. RV comparisons carry
371 // gate_rv leaves instead and are left to the RV evaluator.
372 if (side_has_agg(c, L) || side_has_agg(c, R))
373 out.push_back(cur);
374 }
375 }
376 }
377 return out;
378}
379
380} // namespace provsql_having_detail
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
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
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
In-memory provenance circuit with semiring-generic evaluation.
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.
static Datum semimod_gate(const pg_uuid_t *value_token, const pg_uuid_t *token)
The semimod gate value_token ⊗ token.
Provenance evaluation helper for HAVING-clause circuits.
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 rank_values_by_type(unsigned typoid, const std::vector< std::string > &vals, const std::string &threshold, std::vector< long > &ranks, long &threshold_rank)
static bool side_has_agg(GenericCircuit &c, gate_t g)
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)
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)