Skip to content

Commit 1e98438

Browse files
HAVING: fix array_agg(bool) vs a constant bool array always failing, by reconciling PostgreSQL's scalar ('true'/'false') and array-element ('t'/'f') boolean text forms
1 parent ff24f27 commit 1e98438

4 files changed

Lines changed: 77 additions & 1 deletion

File tree

src/having_semantics.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ bool aggtype_is_boolean(unsigned oid) {
4949
return oid == BOOLOID;
5050
}
5151

52+
// array_agg's result type is the array type (e.g. boolean[]); its element type
53+
// is the comparison domain. PostgreSQL serialises a scalar bool as
54+
// 'true'/'false' but a bool array element as 't'/'f', so the array_agg
55+
// comparison must know when the elements are boolean to reconcile the two.
56+
bool aggtype_elem_is_boolean(unsigned oid) {
57+
if (oid == BOOLOID)
58+
return true;
59+
Oid elem = get_element_type(oid);
60+
return elem == BOOLOID;
61+
}
62+
5263
// Types handled by the numeric comparison domain (scaled to a common integer
5364
// grid). choose() over these is evaluated there -- including ordering
5465
// comparisons; choose() over any other type falls to the value-as-text domain.

src/having_semantics.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ bool semimod_extract_string_and_K(GenericCircuit &c, gate_t semimod_gate, std::s
3737
bool aggtype_is_text(unsigned oid);
3838
bool aggtype_is_integer(unsigned oid);
3939
bool 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').
43+
bool aggtype_elem_is_boolean(unsigned oid);
4044
bool aggtype_is_numeric(unsigned oid);
4145
bool parse_array_literal(const std::string &s, std::vector<std::string> &out);
4246
bool parse_decimal_scaled(const std::string &s, long &mantissa, int &scale);
@@ -437,6 +441,19 @@ void provsql_having(
437441
kvals.push_back(c.evaluate<SemiringT>(k_gate, mapping, S));
438442
}
439443

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+
440457
auto worlds = enumerate_array_agg_worlds(
441458
vals, target, effective_op == ComparisonOperator::EQ);
442459
pw_out = combine_exhaustive_worlds(worlds, kvals, /*upset=*/false,

test/expected/having_array_agg.out

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,27 @@ q|g|p
3030
text =[a,b]|A|0.2500
3131
text =[a,b]|B|0.0000
3232
(2 rows)
33+
add_provenance
34+
35+
(1 row)
36+
remove_provenance
37+
38+
(1 row)
39+
q|g|p
40+
bool =[t]|A|0.2500
41+
bool =[t]|B|0.5000
42+
(2 rows)
43+
remove_provenance
44+
45+
(1 row)
46+
q|g|p
47+
bool =[f,t]|A|0.2500
48+
bool =[f,t]|B|0.0000
49+
(2 rows)
50+
remove_provenance
51+
52+
(1 row)
53+
q|g|p
54+
bool <>[t]|A|0.5000
55+
bool <>[t]|B|0.0000
56+
(2 rows)

test/sql/having_array_agg.sql

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,29 @@ CREATE TABLE r4 AS SELECT g, round(probability_evaluate(provenance())::numeric,4
3131
FROM haa GROUP BY g HAVING array_agg(nm ORDER BY nm) = ARRAY['a','b'];
3232
SELECT remove_provenance('r4'); SELECT 'text =[a,b]' AS q, g, p FROM r4 ORDER BY g;
3333

34+
-- Boolean elements: PostgreSQL renders a scalar bool as 'true'/'false' but a
35+
-- bool array element as 't'/'f'; the array_agg comparison reconciles the two so
36+
-- the value-as-text match works. hb: A = {true, false}, B = {true}, p = 0.5.
37+
CREATE TABLE hb(g text, flag boolean);
38+
INSERT INTO hb VALUES ('A', true), ('A', false), ('B', true);
39+
SELECT add_provenance('hb');
40+
DO $$ BEGIN PERFORM set_prob(provenance(), 0.5) FROM hb; END $$;
41+
42+
-- = ARRAY[true]: only the true row present. A 0.25 B 0.5
43+
CREATE TABLE r5 AS SELECT g, round(probability_evaluate(provenance())::numeric,4) AS p
44+
FROM hb GROUP BY g HAVING array_agg(flag ORDER BY flag) = ARRAY[true];
45+
SELECT remove_provenance('r5'); SELECT 'bool =[t]' AS q, g, p FROM r5 ORDER BY g;
46+
47+
-- = ARRAY[false,true]: both A-rows present (ordered). A 0.25 B 0
48+
CREATE TABLE r6 AS SELECT g, round(probability_evaluate(provenance())::numeric,4) AS p
49+
FROM hb GROUP BY g HAVING array_agg(flag ORDER BY flag) = ARRAY[false,true];
50+
SELECT remove_provenance('r6'); SELECT 'bool =[f,t]' AS q, g, p FROM r6 ORDER BY g;
51+
52+
-- <> ARRAY[true]: non-empty and not exactly [true]. A 0.5 B 0
53+
CREATE TABLE r7 AS SELECT g, round(probability_evaluate(provenance())::numeric,4) AS p
54+
FROM hb GROUP BY g HAVING array_agg(flag ORDER BY flag) <> ARRAY[true];
55+
SELECT remove_provenance('r7'); SELECT 'bool <>[t]' AS q, g, p FROM r7 ORDER BY g;
56+
3457
DROP TABLE r1; DROP TABLE r2; DROP TABLE r3; DROP TABLE r4;
35-
DROP TABLE haa;
58+
DROP TABLE r5; DROP TABLE r6; DROP TABLE r7;
59+
DROP TABLE haa; DROP TABLE hb;

0 commit comments

Comments
 (0)