From 1b2c8bb9bf44534d50f565acc4ea0bec2786cfa2 Mon Sep 17 00:00:00 2001 From: Chengpeng Yan 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)