Re: Fold NOT IN / <> ALL expressions containing NULL to FALSE

From: Ilia Evdokimov <ilya(dot)evdokimov(at)tantorlabs(dot)com>
To: Rustam ALLAKOV <rustamallakov(at)gmail(dot)com>, pgsql-hackers(at)lists(dot)postgresql(dot)org
Cc: Yugo Nagata <nagata(at)sraoss(dot)co(dot)jp>
Subject: Re: Fold NOT IN / <> ALL expressions containing NULL to FALSE
Date: 2026-09-22 12:38:41
Message-ID: 38467c83-7f33-4d26-8da3-19d1ad62cbd2@tantorlabs.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On 9/13/26 21:12, Rustam ALLAKOV wrote:

> 1. Multidimensional arrays
>
> CREATE TEMP TABLE mda (a int[]);
> INSERT INTO mda VALUES (NULL::int[]);
> SELECT * FROM mda WHERE 1 <> ALL (ARRAY[NULL::int[], a]);
>
> master: 1 row
> v2: 0 rows

Nice catch. I didn't consider multidimensional arrays. The
non-const-array branch in clauses.c walked ArrayExpr->elements and
folded to false whenever it saw a NULL Const there, without checking
multidims. For a multidimensional ArrayExpr each elements entry is a
sub-array, not a scalar - a NULL sub-array doesn't imply a NULL scalar
element of the result.

So we should skip the fold when ((ArrayExpr *) arrayarg)->multidims is true.

> 2. ON CONFLICT with a partial index
>
> CREATE TEMP TABLE t (a int, b int);
> CREATE UNIQUE INDEX ti ON t (a) WHERE b <> ALL (ARRAY[1, NULL]);
> INSERT INTO t VALUES (1, 5)
> ON CONFLICT (a) WHERE b <> ALL (ARRAY[1, NULL]) DO NOTHING;
>
> master: succeeds
> v2: ERROR: there is no unique or exclusion constraint matching
> the ON CONFLICT specification

onConflict->arbiterWhere was preprocessed as an ordinary EXPRKIND_QUAL,
so the new folding introduced by this patch reduced it to constant
false. But arbiterWhere is never evaluated at runtime -
infer_arbiter_indexes() only uses it to check, at plan time, whether a
candidate index's predicate is implied by it via predicate_implied_by(),
which has no notion of a bare "false" clause vacuously implying
anything. So the folded arbiterWhere stopped matching the (correctly
unfolded) index predicate, even though the two are logically identical.

Fix: a new preprocess_expression() kind, EXPRKIND_ARBITER_WHERE, that
gets the same qual-shaped treatment as EXPRKIND_QUAL (AND/OR flattening,
canonicalize_qual, make_ands_implicit, etc.) but is routed through plain
eval_const_expressions() instead of eval_const_expressions_qual(), so
it's exempt from the new folding. An alternative would be teaching
predicate_implied_by() that a literal "false" clause vacuously implies
anything - didn't go that route since it's a general-purpose proof
routine used well beyond ON CONFLICT, but open to it if preferred.

I added this fix in v3-0002 patch. If anyone sees a better way to fix
this, happy to hear it.

> 3. No folding under AND/OR
>
> CREATE TEMP TABLE s (x int);
>
> -- Plans with v2:
> EXPLAIN (COSTS OFF) SELECT * FROM s
> WHERE x NOT IN (42, NULL); -- One-Time Filter: false
> EXPLAIN (COSTS OFF) SELECT * FROM s
> WHERE x NOT IN (42, NULL) AND x = 1; -- Seq Scan
> EXPLAIN (COSTS OFF) SELECT * FROM s
> WHERE x NOT IN (42, NULL) OR false; -- Seq Scan
>
> Perhaps this could be handled in canonicalize_qual().

eval_const_expressions_mutator() clears context->is_qual unconditionally
at entry, so it never reaches past the first node. When the qual is
itself an AND/OR, its arguments - processed via
simplify_and/or_arguments() - never see is_qual = true. Propagating it
into AND/OR arguments is safe, but must not propogate into NOT, CASE, or
non-qual contexts.

simplify_and/or_arguments() now take an is_qual parameter and set
context->is_qual before each of their own recursive calls.

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC,
https://tantorlabs.com/

Attachment Content-Type Size
v3-0001-Fold-NOT-IN-ALL-with-NULL-array-element-to-false-.patch text/x-patch 13.8 KB
v3-0002-Don-t-fold-ON-CONFLICT-s-arbiterWhere-to-constant.patch text/x-patch 3.4 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Previous Message Aleksander Alekseev 2026-09-22 12:30:43 Re: Write skew observed under serializable isolation