diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 027361f20bb..b1b3255d616 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -464,9 +464,12 @@ returns bool
equalimage (equality implies image
equality
) support functions, registered under support
function number 4. These functions allow the core code to
- determine when it is safe to apply the btree deduplication
- optimization. Currently, equalimage
- functions are only called when building or rebuilding an index.
+ determine when two values that compare equal may be freely
+ substituted for one another. They are called when building or
+ rebuilding an index, to decide whether the btree deduplication
+ optimization is safe, and during query planning, to decide whether
+ an optimization that merges equal values may discard the
+ distinction between them.
An equalimage function must have the
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 014faa1622f..e525bdf1847 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -1183,21 +1183,12 @@ _bt_allequalimage(Relation rel, bool debugmessage)
for (int i = 0; i < IndexRelationGetNumberOfKeyAttributes(rel); i++)
{
- Oid opfamily = rel->rd_opfamily[i];
- Oid opcintype = rel->rd_opcintype[i];
- Oid collation = rel->rd_indcollation[i];
- Oid equalimageproc;
-
- equalimageproc = get_opfamily_proc(opfamily, opcintype, opcintype,
- BTEQUALIMAGE_PROC);
-
/*
- * If there is no BTEQUALIMAGE_PROC then deduplication is assumed to
- * be unsafe. Otherwise, actually call proc and see what it says.
+ * An opclass that lacks a BTEQUALIMAGE_PROC, or whose procedure
+ * returns false, makes deduplication unsafe for the whole index.
*/
- if (!OidIsValid(equalimageproc) ||
- !DatumGetBool(OidFunctionCall1Coll(equalimageproc, collation,
- ObjectIdGetDatum(opcintype))))
+ if (!opfamily_is_equalimage(rel->rd_opfamily[i], rel->rd_opcintype[i],
+ rel->rd_indcollation[i]))
{
allequalimage = false;
break;
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index fb6f81453ea..deaee399042 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -14,7 +14,6 @@
*/
#include "postgres.h"
-#include "access/nbtree.h"
#include "access/sysattr.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_type.h"
@@ -885,7 +884,6 @@ create_grouping_expr_infos(PlannerInfo *root)
SortGroupClause *sgc = lfirst_node(SortGroupClause, lc);
TargetEntry *tle = get_sortgroupclause_tle(sgc, root->processed_tlist);
TypeCacheEntry *tce;
- Oid equalimageproc;
Assert(tle->ressortgroupref > 0);
@@ -911,22 +909,13 @@ create_grouping_expr_infos(PlannerInfo *root)
!OidIsValid(tce->btree_opintype))
return;
- equalimageproc = get_opfamily_proc(tce->btree_opf,
- tce->btree_opintype,
- tce->btree_opintype,
- BTEQUALIMAGE_PROC);
-
/*
- * If there is no BTEQUALIMAGE_PROC, eager aggregation is assumed to
- * be unsafe. Otherwise, we call the procedure to check. We must be
- * careful to pass the expression's actual collation, rather than the
- * data type's default collation, to ensure that non-deterministic
- * collations are correctly handled.
+ * We must be careful to pass the expression's actual collation, rather
+ * than the data type's default collation, to ensure that
+ * non-deterministic collations are correctly handled.
*/
- if (!OidIsValid(equalimageproc) ||
- !DatumGetBool(OidFunctionCall1Coll(equalimageproc,
- exprCollation((Node *) tle->expr),
- ObjectIdGetDatum(tce->btree_opintype))))
+ if (!opfamily_is_equalimage(tce->btree_opf, tce->btree_opintype,
+ exprCollation((Node *) tle->expr)))
return;
exprs = lappend(exprs, tle->expr);
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 55cebe4a74b..07f25501da9 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -6414,18 +6414,22 @@ pull_paramids_walker(Node *node, Bitmapset **context)
* semantics compatible with the grouping eqop, or, for a nondeterministic
* collation, when the comparison applies a collation other than the column's.
*
- * For a nondeterministic collation, every other reference is rejected: a
- * comparison under a different collation, and any function or operator over
- * the column, because we cannot tell whether the function yields the same
- * result for values the grouping treats as equal, and many do not. A column
- * with a deterministic collation is not restricted this way.
+ * Every other reference -- a comparison under a different collation, or any
+ * function or operator over the column -- is opaque to us, so we accept it
+ * only when the grouping's equality is image equality. Then the values the
+ * grouping merges are interchangeable without loss of semantic information,
+ * and whatever wraps the column is bound to return the same answer for all of
+ * them. When it is not image equality, as for numeric (1 and 1.0), jsonb,
+ * float8 (0 and -0), record, or text under a nondeterministic collation, no
+ * such reasoning is available, and many wrappers do in fact distinguish the
+ * values: 1.0::text is not 1::text.
*
- * This leaves one case uncaught: with a deterministic collation, a function
- * over the column can still feed a finer comparison than the direct-operand
- * check sees, for example record_image_ops over a rebuilt record, or scale()
- * over numeric where two equal values differ in scale. Catching it would
- * require knowing that a type's equality is bitwise, which we do not test
- * here.
+ * Image equality is not quite bitwise equality for varlena types, because
+ * TOAST compression is not applied consistently on input. Expressions that
+ * expose physical representation rather than value, pg_column_size() for one,
+ * can therefore still tell apart values that the grouping merges. Those are
+ * outside the semantic contract that an equalimage procedure describes, and we
+ * make no attempt to detect them.
*
* Returns true if any such conflict exists.
*/
@@ -6477,18 +6481,23 @@ grouping_conflict_walker(Node *node, grouping_walker_ctx *ctx)
if (IsA(node, Var))
{
Var *var = (Var *) node;
+ Oid grouping_eqop = ctx->get_eqop(var, ctx->cb_context);
/*
* A grouping column reaches here when it was not handled as a direct
- * operand by a comparison node above (see the function header). That
- * is safe for a deterministic collation, but not for a
- * nondeterministic one, where the reference may distinguish values
- * the grouping considers equal. A bare boolean qual is safe too:
- * boolean is not collatable, so it takes the deterministic path here.
+ * operand by a comparison node above (see the function header), so we
+ * know nothing about the expression it is embedded in. Accept it only
+ * if the grouping's equality is image equality, which makes any two
+ * values the grouping merges interchangeable for every expression.
+ *
+ * This subsumes the nondeterministic-collation case: the equalimage
+ * procedure of a collatable type is handed the column's collation and
+ * answers false for a nondeterministic one. A bare boolean qual takes
+ * this path too, and stays safe, boolean equality being image
+ * equality.
*/
- if (OidIsValid(ctx->get_eqop(var, ctx->cb_context)) &&
- OidIsValid(var->varcollid) &&
- !get_collation_isdeterministic(var->varcollid))
+ if (OidIsValid(grouping_eqop) &&
+ !equality_op_is_equalimage(grouping_eqop, var->varcollid))
return true;
return false;
}
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index ee69f81945f..264a5575a04 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -16,7 +16,6 @@
#include
-#include "access/nbtree.h"
#include "catalog/pg_constraint.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
@@ -3038,7 +3037,6 @@ init_grouping_targets(PlannerInfo *root, RelOptInfo *rel,
*/
SortGroupClause *sgc;
TypeCacheEntry *tce;
- Oid equalimageproc;
/*
* But first, check if equality implies image equality for this
@@ -3051,22 +3049,13 @@ init_grouping_targets(PlannerInfo *root, RelOptInfo *rel,
!OidIsValid(tce->btree_opintype))
return false;
- equalimageproc = get_opfamily_proc(tce->btree_opf,
- tce->btree_opintype,
- tce->btree_opintype,
- BTEQUALIMAGE_PROC);
-
/*
- * If there is no BTEQUALIMAGE_PROC, eager aggregation is assumed
- * to be unsafe. Otherwise, we call the procedure to check. We
- * must be careful to pass the expression's actual collation,
+ * We must be careful to pass the expression's actual collation,
* rather than the data type's default collation, to ensure that
* non-deterministic collations are correctly handled.
*/
- if (!OidIsValid(equalimageproc) ||
- !DatumGetBool(OidFunctionCall1Coll(equalimageproc,
- exprCollation((Node *) expr),
- ObjectIdGetDatum(tce->btree_opintype))))
+ if (!opfamily_is_equalimage(tce->btree_opf, tce->btree_opintype,
+ exprCollation((Node *) expr)))
return false;
/* Create the SortGroupClause. */
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 9ef3922d17c..360ba470ef1 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -17,6 +17,7 @@
#include "access/hash.h"
#include "access/htup_details.h"
+#include "access/nbtree.h"
#include "bootstrap/bootstrap.h"
#include "catalog/namespace.h"
#include "catalog/pg_am.h"
@@ -1038,6 +1039,109 @@ get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 procnum)
return result;
}
+/*
+ * opfamily_is_equalimage
+ *
+ * Does opfamily promise "equality implies image equality" for the given
+ * input type and collation?
+ *
+ * A true result means that whenever the opfamily's ordering function reports
+ * two values equal, those values are interchangeable without any loss of
+ * semantic information; that is, no expression can tell them apart. Callers
+ * rely on this when they want to substitute one member of an equivalence
+ * class for another, as B-tree deduplication does.
+ *
+ * An opfamily that registers no BTEQUALIMAGE_PROC makes no such promise, so
+ * we must assume the property does not hold. Note that callers must pass the
+ * collation actually in use rather than the type's default collation: for a
+ * collatable type the answer depends on it, since a nondeterministic
+ * collation is not image equality.
+ */
+bool
+opfamily_is_equalimage(Oid opfamily, Oid opcintype, Oid collation)
+{
+ Oid equalimageproc;
+
+ equalimageproc = get_opfamily_proc(opfamily, opcintype, opcintype,
+ BTEQUALIMAGE_PROC);
+
+ if (!OidIsValid(equalimageproc))
+ return false;
+
+ return DatumGetBool(OidFunctionCall1Coll(equalimageproc, collation,
+ ObjectIdGetDatum(opcintype)));
+}
+
+/*
+ * equality_op_is_equalimage
+ *
+ * Does eqop define an equivalence under which equal values are
+ * interchangeable without any loss of semantic information?
+ *
+ * This is the operator-level counterpart of opfamily_is_equalimage(), for
+ * callers that know only the equality operator some mechanism uses to decide
+ * which values to merge -- a SortGroupClause's eqop, typically -- and not the
+ * opfamily it came from.
+ *
+ * Not knowing the opfamily is why we must demand a promise from every family
+ * in which eqop is the equality member, rather than accepting the first "yes"
+ * we find. Those families need not agree: texteq is the equality member of
+ * both text_ops and text_pattern_ops, and under a nondeterministic collation
+ * they describe different equivalences, the former case-folding where the
+ * latter is bytewise. text_pattern_ops registers btequalimage, which answers
+ * true whatever collation it is handed, so trusting it alone would let a
+ * caller substitute values that a case-insensitive grouping merged.
+ *
+ * A false result means "not proven", not "proven false", and callers must
+ * treat it as "not image equality". A type with no ordering opclass at all,
+ * such as xid, always lands there.
+ *
+ * 'collation' must be the collation actually applied to the values, not the
+ * type's default; see opfamily_is_equalimage().
+ */
+bool
+equality_op_is_equalimage(Oid eqop, Oid collation)
+{
+ Oid lefttype;
+ Oid righttype;
+ List *opfamilies;
+ bool result;
+ ListCell *lc;
+
+ op_input_types(eqop, &lefttype, &righttype);
+
+ /*
+ * An equalimage procedure describes a single type, so a cross-type
+ * operator gives us nothing to ask about. Grouping equality operators are
+ * never cross-type, so this costs no optimization in practice.
+ */
+ if (lefttype != righttype)
+ return false;
+
+ /*
+ * Collect the opfamilies before calling any of their procedures: the
+ * procedure is user-supplied code that can throw, and we would rather not
+ * be holding a syscache list reference when it does.
+ */
+ opfamilies = get_mergejoin_opfamilies(eqop);
+
+ /* No ordering opfamily at all means nothing promised anything. */
+ result = (opfamilies != NIL);
+
+ foreach(lc, opfamilies)
+ {
+ if (!opfamily_is_equalimage(lfirst_oid(lc), lefttype, collation))
+ {
+ result = false;
+ break;
+ }
+ }
+
+ list_free(opfamilies);
+
+ return result;
+}
+
/* ---------- ATTRIBUTE CACHES ---------- */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 865980cb0f1..09887ddf093 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -95,6 +95,8 @@ extern bool collations_agree_on_equality(Oid coll1, Oid coll2);
extern bool op_is_safe_index_member(Oid opno);
extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype,
int16 procnum);
+extern bool opfamily_is_equalimage(Oid opfamily, Oid opcintype, Oid collation);
+extern bool equality_op_is_equalimage(Oid eqop, Oid collation);
extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok);
extern AttrNumber get_attnum(Oid relid, const char *attname);
extern char get_attgenerated(Oid relid, AttrNumber attnum);
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 7d07619956f..bae145a3b88 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1809,6 +1809,130 @@ select a, count(*) from t_having group by a having a = row(1.0)::avg_rec;
drop table t_having;
drop type avg_rec;
+-- A HAVING clause that reaches the grouping column through a wrapper, rather
+-- than as a direct operand of a comparison, must NOT be pushed down to WHERE
+-- unless the grouping's equality is image equality: the wrapper can tell apart
+-- values that GROUP BY merged into one group.
+create temp table t_eqimg (n numeric, f float8, j jsonb, i int);
+insert into t_eqimg values (1, '0', '1', 1), (1.0, '-0', '1.0', 1);
+-- baselines: each of these is a single group of two rows
+select n, count(*) from t_eqimg group by n;
+ n | count
+---+-------
+ 1 | 2
+(1 row)
+
+select f, count(*) from t_eqimg group by f;
+ f | count
+---+-------
+ 0 | 2
+(1 row)
+
+select j, count(*) from t_eqimg group by j;
+ j | count
+---+-------
+ 1 | 2
+(1 row)
+
+-- numeric equality ignores scale, so the clause must stay in HAVING
+explain (costs off)
+select n, count(*) from t_eqimg group by n having n::text = '1';
+ QUERY PLAN
+-----------------------------------
+ HashAggregate
+ Group Key: n
+ Filter: ((n)::text = '1'::text)
+ -> Seq Scan on t_eqimg
+(4 rows)
+
+select n, count(*) from t_eqimg group by n having n::text = '1';
+ n | count
+---+-------
+ 1 | 2
+(1 row)
+
+-- float8 equality merges 0 and -0
+explain (costs off)
+select f, count(*) from t_eqimg group by f having f::text = '0';
+ QUERY PLAN
+-----------------------------------
+ HashAggregate
+ Group Key: f
+ Filter: ((f)::text = '0'::text)
+ -> Seq Scan on t_eqimg
+(4 rows)
+
+select f, count(*) from t_eqimg group by f having f::text = '0';
+ f | count
+---+-------
+ 0 | 2
+(1 row)
+
+-- jsonb numbers compare as numeric but print their trailing zeroes
+explain (costs off)
+select j, count(*) from t_eqimg group by j having j::text = '1';
+ QUERY PLAN
+-----------------------------------
+ HashAggregate
+ Group Key: j
+ Filter: ((j)::text = '1'::text)
+ -> Seq Scan on t_eqimg
+(4 rows)
+
+select j, count(*) from t_eqimg group by j having j::text = '1';
+ j | count
+---+-------
+ 1 | 2
+(1 row)
+
+-- The same conflict reached through an outer WHERE over a GROUP BY subquery,
+-- which subquery_push_qual turns into a HAVING clause before we get to it.
+-- A WHERE clause may only select the subquery's output rows, never alter
+-- them, so both the count and the group key must be unaffected here.
+explain (costs off)
+select n, c from (select n, count(*) c from t_eqimg group by n) s
+where n::text = '1';
+ QUERY PLAN
+-------------------------------------------
+ HashAggregate
+ Group Key: t_eqimg.n
+ Filter: ((t_eqimg.n)::text = '1'::text)
+ -> Seq Scan on t_eqimg
+(4 rows)
+
+select n, c from (select n, count(*) c from t_eqimg group by n) s
+where n::text = '1';
+ n | c
+---+---
+ 1 | 2
+(1 row)
+
+select n, c from (select n, count(*) c from t_eqimg group by n) s
+where n::text = '1.0';
+ n | c
+---+---
+(0 rows)
+
+-- int equality is image equality, so a wrapped reference is still pushable
+explain (costs off)
+select i, count(*) from t_eqimg group by i having i + 1 = 2;
+ QUERY PLAN
+-------------------------------------
+ GroupAggregate
+ Group Key: i
+ -> Sort
+ Sort Key: i
+ -> Seq Scan on t_eqimg
+ Filter: ((i + 1) = 2)
+(6 rows)
+
+select i, count(*) from t_eqimg group by i having i + 1 = 2;
+ i | count
+---+-------
+ 1 | 2
+(1 row)
+
+drop table t_eqimg;
--
-- Test GROUP BY matching of join columns that are type-coerced due to USING
--
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index cf295d56507..1dad491f525 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -2270,6 +2270,81 @@ WHERE a *= ROW(1.0)::t_rec;
(1.0)
(2 rows)
+ROLLBACK;
+--
+-- A qual that reaches a grouping column of the subquery through a wrapper,
+-- rather than as a direct operand of a comparison, is only pushable when the
+-- grouping's equality is image equality. numeric equality is not: 1 and 1.0
+-- are equal but do not print alike, so a pushed-down qual could both drop a
+-- row the grouping would have kept and change which row represents the group.
+--
+BEGIN;
+CREATE TEMP TABLE eqimg_num (n numeric);
+INSERT INTO eqimg_num VALUES (1), (1.0);
+-- the subquery emits a single row, so the outer WHERE can only keep or drop it
+SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s;
+ n
+---
+ 1
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0';
+ QUERY PLAN
+---------------------------------------
+ Subquery Scan on s
+ Filter: ((s.n)::text = '1.0'::text)
+ -> HashAggregate
+ Group Key: eqimg_num.n
+ -> Seq Scan on eqimg_num
+(5 rows)
+
+SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0';
+ n
+---
+(0 rows)
+
+-- UNION groups by the same equality
+EXPLAIN (COSTS OFF)
+SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s
+WHERE n::text = '1.0';
+ QUERY PLAN
+-----------------------------------------------------
+ Subquery Scan on s
+ Filter: ((s.n)::text = '1.0'::text)
+ -> HashAggregate
+ Group Key: eqimg_num.n
+ -> Append
+ -> Seq Scan on eqimg_num
+ -> Seq Scan on eqimg_num eqimg_num_1
+(7 rows)
+
+SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s
+WHERE n::text = '1.0';
+ n
+---
+(0 rows)
+
+-- int equality is image equality, so the same shape of qual is pushable
+CREATE TEMP TABLE eqimg_int (i int);
+INSERT INTO eqimg_int VALUES (1), (1);
+EXPLAIN (COSTS OFF)
+SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2;
+ QUERY PLAN
+-------------------------------------
+ Unique
+ -> Sort
+ Sort Key: eqimg_int.i
+ -> Seq Scan on eqimg_int
+ Filter: ((i + 1) = 2)
+(5 rows)
+
+SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2;
+ i
+---
+ 1
+(1 row)
+
ROLLBACK;
--
-- Test that LIMIT can be pushed to SORT through a subquery that just projects
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index 91f8342166f..c9893ef17e6 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -652,6 +652,52 @@ select a, count(*) from t_having group by a having a = row(1.0)::avg_rec;
drop table t_having;
drop type avg_rec;
+-- A HAVING clause that reaches the grouping column through a wrapper, rather
+-- than as a direct operand of a comparison, must NOT be pushed down to WHERE
+-- unless the grouping's equality is image equality: the wrapper can tell apart
+-- values that GROUP BY merged into one group.
+create temp table t_eqimg (n numeric, f float8, j jsonb, i int);
+insert into t_eqimg values (1, '0', '1', 1), (1.0, '-0', '1.0', 1);
+
+-- baselines: each of these is a single group of two rows
+select n, count(*) from t_eqimg group by n;
+select f, count(*) from t_eqimg group by f;
+select j, count(*) from t_eqimg group by j;
+
+-- numeric equality ignores scale, so the clause must stay in HAVING
+explain (costs off)
+select n, count(*) from t_eqimg group by n having n::text = '1';
+select n, count(*) from t_eqimg group by n having n::text = '1';
+
+-- float8 equality merges 0 and -0
+explain (costs off)
+select f, count(*) from t_eqimg group by f having f::text = '0';
+select f, count(*) from t_eqimg group by f having f::text = '0';
+
+-- jsonb numbers compare as numeric but print their trailing zeroes
+explain (costs off)
+select j, count(*) from t_eqimg group by j having j::text = '1';
+select j, count(*) from t_eqimg group by j having j::text = '1';
+
+-- The same conflict reached through an outer WHERE over a GROUP BY subquery,
+-- which subquery_push_qual turns into a HAVING clause before we get to it.
+-- A WHERE clause may only select the subquery's output rows, never alter
+-- them, so both the count and the group key must be unaffected here.
+explain (costs off)
+select n, c from (select n, count(*) c from t_eqimg group by n) s
+where n::text = '1';
+select n, c from (select n, count(*) c from t_eqimg group by n) s
+where n::text = '1';
+select n, c from (select n, count(*) c from t_eqimg group by n) s
+where n::text = '1.0';
+
+-- int equality is image equality, so a wrapped reference is still pushable
+explain (costs off)
+select i, count(*) from t_eqimg group by i having i + 1 = 2;
+select i, count(*) from t_eqimg group by i having i + 1 = 2;
+
+drop table t_eqimg;
+
--
-- Test GROUP BY matching of join columns that are type-coerced due to USING
--
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index 07438694f6e..d6ef3badaa2 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -1110,6 +1110,45 @@ WHERE a *= ROW(1.0)::t_rec;
ROLLBACK;
+--
+-- A qual that reaches a grouping column of the subquery through a wrapper,
+-- rather than as a direct operand of a comparison, is only pushable when the
+-- grouping's equality is image equality. numeric equality is not: 1 and 1.0
+-- are equal but do not print alike, so a pushed-down qual could both drop a
+-- row the grouping would have kept and change which row represents the group.
+--
+BEGIN;
+
+CREATE TEMP TABLE eqimg_num (n numeric);
+INSERT INTO eqimg_num VALUES (1), (1.0);
+
+-- the subquery emits a single row, so the outer WHERE can only keep or drop it
+SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s;
+
+EXPLAIN (COSTS OFF)
+SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0';
+
+SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0';
+
+-- UNION groups by the same equality
+EXPLAIN (COSTS OFF)
+SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s
+WHERE n::text = '1.0';
+
+SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s
+WHERE n::text = '1.0';
+
+-- int equality is image equality, so the same shape of qual is pushable
+CREATE TEMP TABLE eqimg_int (i int);
+INSERT INTO eqimg_int VALUES (1), (1);
+
+EXPLAIN (COSTS OFF)
+SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2;
+
+SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2;
+
+ROLLBACK;
+
--
-- Test that LIMIT can be pushed to SORT through a subquery that just projects
-- columns. We check for that having happened by looking to see if EXPLAIN