v0-0002-Show-hashed-ScalarArrayOpExpr-decision-in-EXPLAIN.patch

text/plain

Filename: v0-0002-Show-hashed-ScalarArrayOpExpr-decision-in-EXPLAIN.patch
Type: text/plain
Part: 1
Message: Hashed SAOP on composite type with non-hashable column errors at runtime

Patch

Format: format-patch
Series: patch v0-0002
Subject: Show hashed ScalarArrayOpExpr decision in EXPLAIN
File+
src/backend/utils/adt/ruleutils.c 9 1
src/test/regress/expected/expressions.out 9 0
src/test/regress/sql/expressions.sql 3 0
From 241dee85f1094ce797861cd1d082691fd7ec41ab Mon Sep 17 00:00:00 2001
From: "Andrei V. Lepikhov" <lepihov@gmail.com>
Date: Fri, 5 Jun 2026 11:07:21 +0200
Subject: [PATCH v0 2/3] Show hashed ScalarArrayOpExpr decision in EXPLAIN

When the planner converts a ScalarArrayOpExpr to hash-table evaluation
(convert_saop_to_hashed_saop), the resulting node deparses identically
to a linear one, so EXPLAIN gives no indication of which strategy the
executor will use.  That made the hashed path invisible and awkward to
test.
---
 src/backend/utils/adt/ruleutils.c         | 10 +++++++++-
 src/test/regress/expected/expressions.out |  9 +++++++++
 src/test/regress/sql/expressions.sql      |  3 +++
 3 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..0567ba0886a 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9989,10 +9989,18 @@ get_rule_expr(Node *node, deparse_context *context,
 				if (!PRETTY_PAREN(context))
 					appendStringInfoChar(buf, '(');
 				get_rule_expr_paren(arg1, context, true, node);
-				appendStringInfo(buf, " %s %s (",
+
+				/*
+				 * Surface hashed decision in EXPLAIN.
+				 * hashfuncid is only ever set in a finished plan tree, so this
+				 * never appears in deparsed views, rules, or other stored
+				 * expressions.
+				 */
+				appendStringInfo(buf, " %s %s%s (",
 								 generate_operator_name(expr->opno,
 														exprType(arg1),
 														get_base_element_type(exprType(arg2))),
+								 OidIsValid(expr->hashfuncid) ? "hashed " : "",
 								 expr->useOr ? "ANY" : "ALL");
 				get_rule_expr_paren(arg2, context, true, node);
 
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index c35583cb2ea..9d4db9c3b2a 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -327,6 +327,15 @@ select return_text_input('a') not in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i
  f
 (1 row)
 
+-- Check tha explain marks the hashed decision.
+explain (verbose, costs off)
+select return_int_input(1) in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ Result
+   Output: (return_int_input(1) = hashed ANY ('{10,9,2,8,3,7,4,6,5,1}'::integer[]))
+(2 rows)
+
 rollback;
 -- Test with non-strict equality function.
 -- We need to create our own type for this.
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index 08691877902..fe9c330361b 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -131,6 +131,9 @@ select return_int_input(1) not in (null, null, null, null, null, null, null, nul
 select return_int_input(null::int) not in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
 select return_int_input(null::int) not in (10, 9, 2, 8, 3, 7, 4, 6, 5, null);
 select return_text_input('a') not in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j');
+-- Check tha explain marks the hashed decision.
+explain (verbose, costs off)
+select return_int_input(1) in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
 
 rollback;
 
-- 
2.54.0