BUG #19797: Wrong results: HAVING pushdown past GROUP BY on composite column with nondeterministic collation

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: 303677365(at)qq(dot)com
Subject: BUG #19797: Wrong results: HAVING pushdown past GROUP BY on composite column with nondeterministic collation
Date: 2026-10-26 09:30:01
Message-ID: 19797-7054aa017fa9d791@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19797
Logged by: chunling qin
Email address: 303677365(at)qq(dot)com
PostgreSQL version: 18.6
Operating system: Linux x86_64 (TencentOS 4.4), self-compiled, --wit
Description:

Hello,

When the grouped expression is a composite (record) column whose field
carries a nondeterministic
collation, a HAVING clause is pushed into WHERE and evaluated below the
aggregation. Grouping
equality is nondeterministic in that case, so the clause can remove members
of a group before
aggregation and change count(*).

The pushdown safety check only inspects the top-level varcollid of the
grouping Var. A composite
column has varcollid = InvalidOid (record is not sortable), so the check is
skipped -- yet record_eq
compares fields using each field's declared collation, which may be
nondeterministic.

Test case (self-contained):

CREATE COLLATION ci (provider = icu, locale = 'und-u-ks-level2',
deterministic = false);
CREATE TYPE t_pair AS (f text COLLATE ci);
CREATE TABLE t_rec2 (r t_pair, n int);
INSERT INTO t_rec2 VALUES (row('foo')::t_pair, 1), (row('FOO')::t_pair,
2);
ANALYZE t_rec2;

-- reference: the group {foo, FOO} has size 2
SELECT r, count(*) FROM t_rec2 GROUP BY r;

-- pushed down -> wrong
SELECT r, count(*) FROM t_rec2 GROUP BY r HAVING (r).f = 'foo' COLLATE
"C";

-- not pushable (contains an aggregate) -> correct
SELECT r, count(*) FROM t_rec2 GROUP BY r HAVING (r).f = 'foo' COLLATE
"C" OR count(*) = 99;

Actual output (18.6):

r | count
-------+-------
(foo) | 2 <- reference

r | count
-------+-------
(foo) | 1 <- pushed down

r | count
-------+-------
(foo) | 2 <- not pushable

Expected: the second query returns 2, or no row at all. The group is {foo,
FOO} under the field
collation, so its size is fixed at 2; evaluating the HAVING clause against
the group's
representative row can only yield (representative, 2) or nothing. 1 is not
reachable.

Plan of the second query on 18.6 (filter below the aggregate):

HashAggregate
Group Key: r
-> Seq Scan on t_rec2
Filter: ((r).f = 'foo'::text COLLATE "C")

Source: in 18.4 this is having_collation_conflict_walker() in planner.c
(~line 1517):
OidIsValid(var->varcollid) &&
!get_collation_isdeterministic(var->varcollid).
On current REL_18_STABLE that walker has been replaced by the shared
expression_has_grouping_conflict() (clauses.c), introduced by fe5d6295
(2026-07-06, "Fix qual
pushdown past grouping with mismatched equivalence", backpatched to 18), but
the new walker keeps
the same precondition (clauses.c:5606-5608):

if (OidIsValid(ctx->get_eqop(var, ctx->cb_context)) &&
OidIsValid(var->varcollid) &&
!get_collation_isdeterministic(var->varcollid))

so a composite grouping column is still not recognised as having
nondeterministic equality.

Notes:

- fe5d6295 does fix the same class of problem for ordinary columns. I
confirmed on 18.6 that the
related cases it targets now behave correctly: HAVING length(x) = 2 over a
ci grouping (returns
count = 3, matching the non-pushable form) and qual pushdown past window
PARTITION BY / DISTINCT ON
(window count 2 instead of 1; DISTINCT ON 0 rows instead of one wrong
row). Composite columns
appear to be the remaining gap.
- I have not checked branches 14-17, nor master.

Thanks,

chunling qin

Browse pgsql-bugs by date

  From Date Subject
Next Message Dilip Kumar 2026-10-26 10:38:46 Re: BUG #19779: Backend SIGSEGV in UPDATE with FOR UPDATE CTE joined to UNION ALL subquery
Previous Message Andrey Rachitskiy 2026-10-26 09:16:29 Re: BUG #19752: GROUP BY on a constant and a cast to a length-limited array type crashes the server while planning