From c59dd93bc3510c9c20ddc4097ba42de20c113000 Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Fri, 21 Aug 2026 09:53:58 +0500
Subject: [PATCH] Fix semijoin RHS unique-ification to use join collation

When unique-ifying a semijoin RHS for JOIN_UNIQUE_*, pathkeys and Unique
take collation from the RHS expression (SortGroupClause does not carry
one).  That can be a different (deterministic) collation than the join's
inputcollid.  Unique then leaves values that are still equal under the
join, and the subsequent INNER join duplicates outer rows.

Force each semi_rhs_expr to expose the join operator's input collation
via canonicalize_ec_expression (RelabelType).

BUG #19633
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Suyang Zhong <syzhong16@gmail.com>
Discussion: https://www.postgresql.org/message-id/19633-647cd4c73a84b085@postgresql.org
---
 src/backend/optimizer/plan/initsplan.c        | 12 +++++++-
 .../regress/expected/collate.icu.utf8.out     | 29 +++++++++++++++++++
 src/test/regress/sql/collate.icu.utf8.sql     | 13 +++++++++
 3 files changed, 53 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index f08a918146c..20834d7fcc7 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -2628,7 +2628,17 @@ compute_semijoin_info(PlannerInfo *root, SpecialJoinInfo *sjinfo, List *clause)
 
 		/* so far so good, keep building lists */
 		semi_operators = lappend_oid(semi_operators, opno);
-		semi_rhs_exprs = lappend(semi_rhs_exprs, copyObject(right_expr));
+
+		/*
+		 * Ensure that the RHS expression exposes the join operator's input
+		 * collation.  The expression will later be used as a grouping key when
+		 * unique-ifying the RHS, so its collation must agree with the semijoin
+		 * equality semantics.
+		 */
+		semi_rhs_exprs = lappend(semi_rhs_exprs,
+								 canonicalize_ec_expression((Expr *) copyObject(right_expr),
+															exprType(right_expr),
+															op->inputcollid));
 	}
 
 	/* Punt if we didn't find at least one column to unique-ify */
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index fcfcc658bea..a7ca794a0a2 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -1988,6 +1988,35 @@ ORDER BY 1;
  ghi
 (4 rows)
 
+-- Unique-ification of an IN/semijoin RHS must use the join collation.
+CREATE TABLE t_semi_ci (c1 text COLLATE case_insensitive);
+CREATE TABLE t_semi_cs (c0 text);
+INSERT INTO t_semi_ci VALUES ('a'), ('x'), ('y');
+INSERT INTO t_semi_cs VALUES ('a'), ('a');
+ANALYZE t_semi_ci, t_semi_cs;
+INSERT INTO t_semi_cs VALUES ('A');
+SELECT count(*) FROM t_semi_ci WHERE c1 IN (SELECT c0 FROM t_semi_cs);
+ count 
+-------
+     1
+(1 row)
+
+SET enable_hashagg TO off;
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM t_semi_ci WHERE c1 IN (SELECT c0 FROM t_semi_cs);
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Aggregate
+   ->  Nested Loop
+         Join Filter: (t_semi_ci.c1 = ((t_semi_cs.c0)::text))
+         ->  Unique
+               ->  Sort
+                     Sort Key: t_semi_cs.c0 COLLATE case_insensitive
+                     ->  Seq Scan on t_semi_cs
+         ->  Seq Scan on t_semi_ci
+(8 rows)
+
+RESET enable_hashagg;
 CREATE TABLE test1ci (x text COLLATE case_insensitive);
 CREATE TABLE test2ci (x text COLLATE case_insensitive);
 CREATE TABLE test3ci (x text COLLATE case_insensitive);
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index ce4e2bb3ffd..b4994c64262 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -721,6 +721,19 @@ SELECT * FROM test3cs t1
                 WHERE t1.x = t2.x COLLATE case_insensitive)
 ORDER BY 1;
 
+-- Unique-ification of an IN/semijoin RHS must use the join collation.
+CREATE TABLE t_semi_ci (c1 text COLLATE case_insensitive);
+CREATE TABLE t_semi_cs (c0 text);
+INSERT INTO t_semi_ci VALUES ('a'), ('x'), ('y');
+INSERT INTO t_semi_cs VALUES ('a'), ('a');
+ANALYZE t_semi_ci, t_semi_cs;
+INSERT INTO t_semi_cs VALUES ('A');
+SELECT count(*) FROM t_semi_ci WHERE c1 IN (SELECT c0 FROM t_semi_cs);
+SET enable_hashagg TO off;
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM t_semi_ci WHERE c1 IN (SELECT c0 FROM t_semi_cs);
+RESET enable_hashagg;
+
 CREATE TABLE test1ci (x text COLLATE case_insensitive);
 CREATE TABLE test2ci (x text COLLATE case_insensitive);
 CREATE TABLE test3ci (x text COLLATE case_insensitive);
-- 
2.53.0

