v1-0001-Fix-hashed-ScalarArrayOp-NULL-handling-for-non-st.patch

application/octet-stream

Filename: v1-0001-Fix-hashed-ScalarArrayOp-NULL-handling-for-non-st.patch
Type: application/octet-stream
Part: 1
Message: [PATCH] Fix hashed ScalarArrayOp semantics for NULL LHS with non-strict comparators

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v1-0001
Subject: Fix hashed ScalarArrayOp NULL handling for non-strict operators
File+
src/backend/executor/execExprInterp.c 91 4
src/test/regress/expected/expressions.out 30 0
src/test/regress/sql/expressions.sql 11 0
From 1b2c8bb9bf44534d50f565acc4ea0bec2786cfa2 Mon Sep 17 00:00:00 2001
From: Chengpeng Yan <chengpeng_yan@outlook.com>
Date: Fri, 13 Feb 2026 13:11:20 +0800
Subject: [PATCH v1 1/2] Fix hashed ScalarArrayOp NULL handling for non-strict
 operators

ExecEvalHashedScalarArrayOp() only short-circuits NULL lhs values for
strict operators.  For non-strict operators, a NULL lhs could still reach
the hash probe path, diverging from ExecEvalScalarArrayOp() and returning
a non-NULL result where SQL's three-valued logic requires NULL.  The hash
probe would also depend on a NULL lhs Datum payload, whose value is not
meaningful.

Fix this by bypassing the hash lookup for NULL lhs values with non-strict
operators and falling back to the same per-element reduction used by the
linear ScalarArrayOp path.  Preserve the existing strict fast path and add
regression coverage for hashed and non-hashed IN / NOT IN cases.
---
 src/backend/executor/execExprInterp.c     | 95 ++++++++++++++++++++++-
 src/test/regress/expected/expressions.out | 30 +++++++
 src/test/regress/sql/expressions.sql      | 11 +++
 3 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 3c4843cde86..5437a8cd4b7 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4243,12 +4243,99 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco
 	Assert(!*op->resnull);
 
 	/*
-	 * If the scalar is NULL, and the function is strict, return NULL; no
-	 * point in executing the search.
+	 * If the scalar is NULL, we can only use the hash table with strict
+	 * functions.  For non-strict functions we must evaluate each element to
+	 * preserve SQL's three-valued logic; probing the hash table would also
+	 * depend on the NULL lhs Datum payload, whose value is not meaningful.
 	 */
-	if (fcinfo->args[0].isnull && strictfunc)
+	if (scalar_isnull)
 	{
-		*op->resnull = true;
+		ArrayType  *arr;
+		int16		typlen;
+		bool		typbyval;
+		char		typalign;
+		uint8		typalignby;
+		int			nitems;
+		char	   *s;
+		uint8	   *bitmap;
+		int			bitmask;
+
+		if (strictfunc)
+		{
+			*op->resnull = true;
+			return;
+		}
+
+		arr = DatumGetArrayTypeP(*op->resvalue);
+
+		get_typlenbyvalalign(ARR_ELEMTYPE(arr),
+							 &typlen,
+							 &typbyval,
+							 &typalign);
+		typalignby = typalign_to_alignby(typalign);
+
+		nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr));
+
+		/* Compute IN as an OR reduction of equality results. */
+		result = BoolGetDatum(false);
+		resultnull = false;
+
+		fcinfo->args[0].value = (Datum) 0;
+		fcinfo->args[0].isnull = true;
+
+		s = (char *) ARR_DATA_PTR(arr);
+		bitmap = ARR_NULLBITMAP(arr);
+		bitmask = 1;
+		for (int i = 0; i < nitems; i++)
+		{
+			Datum		elt;
+			Datum		thisresult;
+
+			/* Get array element, checking for NULL. */
+			if (bitmap && (*bitmap & bitmask) == 0)
+			{
+				fcinfo->args[1].value = (Datum) 0;
+				fcinfo->args[1].isnull = true;
+			}
+			else
+			{
+				elt = fetch_att(s, typbyval, typlen);
+				s = att_addlength_pointer(s, typlen, s);
+				s = (char *) att_nominal_alignby(s, typalignby);
+				fcinfo->args[1].value = elt;
+				fcinfo->args[1].isnull = false;
+			}
+
+			fcinfo->isnull = false;
+			thisresult = op->d.hashedscalararrayop.finfo->fn_addr(fcinfo);
+
+			if (fcinfo->isnull)
+				resultnull = true;
+			else if (DatumGetBool(thisresult))
+			{
+				result = BoolGetDatum(true);
+				resultnull = false;
+				break;
+			}
+
+			/* Advance bitmap pointer if any. */
+			if (bitmap)
+			{
+				bitmask <<= 1;
+				if (bitmask == 0x100)
+				{
+					bitmap++;
+					bitmask = 1;
+				}
+			}
+		}
+
+		/* Reverse for NOT IN; NULL stays NULL. */
+		if (!inclause && !resultnull)
+			result = BoolGetDatum(!DatumGetBool(result));
+
+		*op->resvalue = result;
+		*op->resnull = resultnull;
 		return;
 	}
 
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index 9a3c97b15a3..5ce0c0d11de 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -388,6 +388,36 @@ default for type myint using hash as
   function    1   myinthash(myint);
 create table inttest (a myint);
 insert into inttest values(1::myint),(null);
+-- scalar NULL with a non-strict operator still requires per-element checks.
+-- With no NULLs on the right side, IN and NOT IN should both yield NULL.
+select (a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint)) is null
+from inttest where a is null;
+ ?column? 
+----------
+ t
+(1 row)
+
+select (a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint)) is null
+from inttest where a is null;
+ ?column? 
+----------
+ t
+(1 row)
+
+select (a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint)) is null
+from inttest where a is null;
+ ?column? 
+----------
+ t
+(1 row)
+
+select (a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint)) is null
+from inttest where a is null;
+ ?column? 
+----------
+ t
+(1 row)
+
 -- try an array with enough elements to cause hashing
 select * from inttest where a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null);
  a 
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index e02c21f3368..33af3a17128 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -198,6 +198,17 @@ default for type myint using hash as
 create table inttest (a myint);
 insert into inttest values(1::myint),(null);
 
+-- scalar NULL with a non-strict operator still requires per-element checks.
+-- With no NULLs on the right side, IN and NOT IN should both yield NULL.
+select (a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint)) is null
+from inttest where a is null;
+select (a in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint)) is null
+from inttest where a is null;
+select (a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint)) is null
+from inttest where a is null;
+select (a not in (0::myint,1::myint,2::myint,3::myint,4::myint,5::myint)) is null
+from inttest where a is null;
+
 -- try an array with enough elements to cause hashing
 select * from inttest where a in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null);
 select * from inttest where a not in (1::myint,2::myint,3::myint,4::myint,5::myint,6::myint,7::myint,8::myint,9::myint, null);
-- 
2.50.1 (Apple Git-155)