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
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)