From 8bce7e3bc75b9c3576f589fad6c6294042ca5ed5 Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Mon, 27 Jul 2026 17:35:49 +0530 Subject: [PATCH] Fix SAOP nullability analysis below top level find_nonnullable_rels() and find_nonnullable_vars() use is_strict_saop() to determine whether a ScalarArrayOpExpr rejects NULL input. At top level, it is enough for NULL input to make the clause false or NULL. After descending below NOT, a BooleanTest, or a strict function, however, the ScalarArrayOpExpr itself must return NULL. Both walkers passed falseOK = true unconditionally. Hence an ANY expression over an empty array was treated as strict even in the latter context, although NULL = ANY(empty_array) is false. This could incorrectly prove a subquery output non-null and produce wrong answers after converting NOT IN to an anti-join. It could also incorrectly reduce an outer join. Pass the walkers' current top_level state as falseOK, and add regression tests for both cases. --- src/backend/optimizer/util/clauses.c | 4 ++-- src/test/regress/expected/subselect.out | 24 ++++++++++++++++++++++++ src/test/regress/sql/subselect.sql | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index aa8886ec210..ef1c5e4102b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -1546,7 +1546,7 @@ find_nonnullable_rels_walker(Node *node, bool top_level) { ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; - if (is_strict_saop(expr, true)) + if (is_strict_saop(expr, top_level)) result = find_nonnullable_rels_walker((Node *) expr->args, false); } else if (IsA(node, BoolExpr)) @@ -1799,7 +1799,7 @@ find_nonnullable_vars_walker(Node *node, bool top_level) { ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; - if (is_strict_saop(expr, true)) + if (is_strict_saop(expr, top_level)) result = find_nonnullable_vars_walker((Node *) expr->args, false); } else if (IsA(node, BoolExpr)) diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index e7ff7191082..7c647326c62 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -3998,4 +3998,28 @@ WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); ----+----- (0 rows) +-- A ScalarArrayOpExpr below NOT must return NULL, not just false, to prove +-- that its input is nonnullable +CREATE TEMP TABLE saop_outer (x int NOT NULL); +CREATE TEMP TABLE saop_inner (y int); +INSERT INTO saop_outer VALUES (1); +INSERT INTO saop_inner VALUES (NULL); +SELECT * FROM saop_outer +WHERE x NOT IN ( + SELECT y FROM saop_inner + WHERE NOT (y = ANY ('{}'::int[])) +); + x +--- +(0 rows) + +-- Likewise, do not reduce this LEFT JOIN to an inner join +SELECT saop_outer.* +FROM saop_outer LEFT JOIN saop_inner ON x = y +WHERE (y = ANY ('{}'::int[])) IS FALSE; + x +--- + 1 +(1 row) + ROLLBACK; diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 76bce5cdba5..5c35cba4372 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -1751,4 +1751,22 @@ WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); SELECT * FROM not_null_tab WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); +-- A ScalarArrayOpExpr below NOT must return NULL, not just false, to prove +-- that its input is nonnullable +CREATE TEMP TABLE saop_outer (x int NOT NULL); +CREATE TEMP TABLE saop_inner (y int); +INSERT INTO saop_outer VALUES (1); +INSERT INTO saop_inner VALUES (NULL); + +SELECT * FROM saop_outer +WHERE x NOT IN ( + SELECT y FROM saop_inner + WHERE NOT (y = ANY ('{}'::int[])) +); + +-- Likewise, do not reduce this LEFT JOIN to an inner join +SELECT saop_outer.* +FROM saop_outer LEFT JOIN saop_inner ON x = y +WHERE (y = ANY ('{}'::int[])) IS FALSE; + ROLLBACK; -- 2.34.1