From 52ece8d5f10a50bf4ce54d3b71b62411302bfba0 Mon Sep 17 00:00:00 2001 From: Evdokimov Ilia Date: Fri, 25 Sep 2026 18:05:55 +0500 Subject: [PATCH v4] Fold "x op ALL (array)" with a NULL element to false in qual context --- src/backend/commands/copy.c | 2 +- src/backend/optimizer/plan/planner.c | 32 +- src/backend/optimizer/plan/subselect.c | 2 +- src/backend/optimizer/util/clauses.c | 165 +++++++++ src/backend/optimizer/util/inherit.c | 2 +- src/include/optimizer/optimizer.h | 1 + src/test/regress/expected/insert_conflict.out | 18 + src/test/regress/expected/planner_est.out | 24 +- src/test/regress/expected/predicate.out | 336 ++++++++++++++++++ src/test/regress/sql/insert_conflict.sql | 13 + src/test/regress/sql/predicate.sql | 111 ++++++ 11 files changed, 685 insertions(+), 21 deletions(-) diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 003b70852bb..68cb535f73f 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -205,7 +205,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, } /* Reduce WHERE clause to standard list-of-AND-terms form */ - whereClause = eval_const_expressions(NULL, whereClause); + whereClause = eval_const_expressions_qual(NULL, whereClause); whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false); whereClause = (Node *) make_ands_implicit((Expr *) whereClause); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 55a35aa3397..884d6d61954 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -98,6 +98,7 @@ create_upper_paths_hook_type create_upper_paths_hook = NULL; #define EXPRKIND_TABLEFUNC 11 #define EXPRKIND_TABLEFUNC_LATERAL 12 #define EXPRKIND_GROUPEXPR 13 +#define EXPRKIND_ARBITER_WHERE 14 /* * Data specific to grouping sets @@ -1057,10 +1058,21 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name, preprocess_expression(root, (Node *) parse->onConflict->arbiterElems, EXPRKIND_ARBITER_ELEM); + /* + * arbiterWhere is never evaluated as a runtime qual: it is only + * used to match a partial index's predicate via + * predicate_implied_by() in infer_arbiter_indexes(). Use + * EXPRKIND_ARBITER_WHERE (not EXPRKIND_QUAL) so it still gets the + * usual qual-shaped preprocessing (AND/OR flattening, + * canonicalize_qual, implicit-AND format) but is not subject to the + * qual-only constant folding in eval_const_expressions_qual(), + * which could replace it with a bare Const and break the + * structural predicate match. + */ parse->onConflict->arbiterWhere = preprocess_expression(root, parse->onConflict->arbiterWhere, - EXPRKIND_QUAL); + EXPRKIND_ARBITER_WHERE); parse->onConflict->onConflictSet = (List *) preprocess_expression(root, (Node *) parse->onConflict->onConflictSet, @@ -1438,12 +1450,17 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind) * with AND directly under AND, nor OR directly under OR. */ if (kind != EXPRKIND_RTFUNC) - expr = eval_const_expressions(root, expr); + { + if (kind == EXPRKIND_QUAL) + expr = eval_const_expressions_qual(root, expr); + else + expr = eval_const_expressions(root, expr); + } /* * If it's a qual or havingQual, canonicalize it. */ - if (kind == EXPRKIND_QUAL) + if (kind == EXPRKIND_QUAL || kind == EXPRKIND_ARBITER_WHERE) { expr = (Node *) canonicalize_qual((Expr *) expr, false); @@ -1458,7 +1475,8 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind) * hashfuncid of any that might execute more quickly by using hash lookups * instead of a linear search. */ - if (kind == EXPRKIND_QUAL || kind == EXPRKIND_TARGET) + if (kind == EXPRKIND_QUAL || kind == EXPRKIND_TARGET || + kind == EXPRKIND_ARBITER_WHERE) { convert_saop_to_hashed_saop(expr); } @@ -1474,7 +1492,9 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind) /* Expand SubLinks to SubPlans */ if (root->parse->hasSubLinks) - expr = SS_process_sublinks(root, expr, (kind == EXPRKIND_QUAL)); + expr = SS_process_sublinks(root, expr, + (kind == EXPRKIND_QUAL || + kind == EXPRKIND_ARBITER_WHERE)); /* * XXX do not insert anything here unless you have grokked the comments in @@ -1491,7 +1511,7 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind) * would be unable to simplify a top-level AND correctly. Also, * SS_process_sublinks expects explicit-AND format.) */ - if (kind == EXPRKIND_QUAL) + if (kind == EXPRKIND_QUAL || kind == EXPRKIND_ARBITER_WHERE) expr = (Node *) make_ands_implicit((Expr *) expr); return expr; diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 5760b616813..f1b95041cee 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -1984,7 +1984,7 @@ convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, subroot.type = T_PlannerInfo; subroot.glob = root->glob; subroot.parse = subselect; - whereClause = eval_const_expressions(&subroot, whereClause); + whereClause = eval_const_expressions_qual(&subroot, whereClause); whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false); whereClause = (Node *) make_ands_implicit((Expr *) whereClause); diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 3e1f210652d..046335a2dc3 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -142,6 +142,8 @@ static Node *eval_const_expressions_mutator(Node *node, static bool contain_non_const_walker(Node *node, void *context); static bool ece_function_is_safe(Oid funcid, eval_const_expressions_context *context); +static bool saop_never_true(ScalarArrayOpExpr *saop); +static Node *simplify_qual_null_saops(Node *node); static List *simplify_or_arguments(List *args, eval_const_expressions_context *context, bool *haveNull, bool *forceTrue); @@ -2650,6 +2652,161 @@ eval_const_expressions(PlannerInfo *root, Node *node) return eval_const_expressions_mutator(node, &context); } +/*-------------------- + * eval_const_expressions_qual + * + * Same as eval_const_expressions, but for an expression that is used as a + * qual, i.e. in a context where a NULL result has the same effect as FALSE. + * This allows additional simplifications; see simplify_qual_null_saops. + * + * The input should be in explicit-AND format: the extra simplification does + * not look into the elements of an implicit-AND List. + *-------------------- + */ +Node * +eval_const_expressions_qual(PlannerInfo *root, Node *node) +{ + node = eval_const_expressions(root, node); + return simplify_qual_null_saops(node); +} + +/* + * saop_never_true + * Is this ScalarArrayOpExpr "x op ALL (array)" with a strict operator + * and an array known to contain a NULL element? + * + * Such an expression can never yield TRUE: each comparison against the NULL + * element yields NULL, so the result is either FALSE or NULL. + */ +static bool +saop_never_true(ScalarArrayOpExpr *saop) +{ + Node *arrayarg; + + if (saop->useOr) + return false; + + set_sa_opfuncid(saop); + if (!func_strict(saop->opfuncid)) + return false; + + arrayarg = lsecond(saop->args); + + if (IsA(arrayarg, Const)) + { + Const *arrayconst = (Const *) arrayarg; + + /* A NULL array is handled by ordinary constant folding */ + if (arrayconst->constisnull) + return false; + + return array_contains_nulls(DatumGetArrayTypeP(arrayconst->constvalue)); + } + else if (IsA(arrayarg, ArrayExpr)) + { + ArrayExpr *arrayexpr = (ArrayExpr *) arrayarg; + ListCell *lc; + + /* + * In a multidimensional ARRAY[] the elements are sub-arrays, and a + * NULL sub-array doesn't produce a NULL element: e.g. + * ARRAY[NULL::int[], NULL::int[]] is an empty array, over which ALL + * is TRUE. + */ + if (arrayexpr->multidims) + return false; + + foreach(lc, arrayexpr->elements) + { + Node *elem = (Node *) lfirst(lc); + + if (IsA(elem, Const) && ((Const *) elem)->constisnull) + return true; + } + } + + return false; +} + +/* + * simplify_qual_null_saops + * Simplify an already const-simplified boolean expression that is used + * in a context where NULL has the same effect as FALSE, such as a WHERE + * or JOIN qual, a CASE WHEN condition, or an aggregate FILTER clause. + * + * In such a context "x op ALL (array)" is replaced by constant FALSE if + * saop_never_true() says it can never yield TRUE. We also look through + * AND and OR, since replacing a NULL argument by FALSE cannot change whether + * an AND or OR yields TRUE. An AND with a FALSE (or NULL) argument then + * reduces to FALSE, and such arguments are dropped from an OR. We must not + * look through NOT, since NOT NULL is NULL but NOT FALSE is TRUE, nor into + * the arguments of any other kind of expression. + * + * This is done as a separate pass over the output of + * eval_const_expressions_mutator, rather than while simplifying the + * ScalarArrayOpExpr node itself, because NOTs pushed down by negate_clause(), + * simplified boolean equalities such as "(expr) = true", and inlined SQL + * functions can all produce such a node only after its own simplification + * is complete. + */ +static Node * +simplify_qual_null_saops(Node *node) +{ + if (node == NULL) + return NULL; + + if (IsA(node, ScalarArrayOpExpr)) + { + if (saop_never_true((ScalarArrayOpExpr *) node)) + return makeBoolConst(false, false); + } + else if (is_andclause(node) || is_orclause(node)) + { + BoolExpr *expr = (BoolExpr *) node; + bool isand = (expr->boolop == AND_EXPR); + List *newargs = NIL; + bool changed = false; + ListCell *lc; + + foreach(lc, expr->args) + { + Node *arg = (Node *) lfirst(lc); + Node *newarg = simplify_qual_null_saops(arg); + + if (newarg != arg) + changed = true; + + /* FALSE and NULL are equivalent here */ + if (IsA(newarg, Const) && + (((Const *) newarg)->constisnull || + !DatumGetBool(((Const *) newarg)->constvalue))) + { + if (isand) + return makeBoolConst(false, false); + changed = true; + continue; /* drop it from the OR */ + } + + /* Keep the result flat, as eval_const_expressions does */ + if ((isand && is_andclause(newarg)) || + (!isand && is_orclause(newarg))) + newargs = list_concat(newargs, ((BoolExpr *) newarg)->args); + else + newargs = lappend(newargs, newarg); + } + + if (!changed) + return node; + if (newargs == NIL) + return makeBoolConst(false, false); + if (list_length(newargs) == 1) + return (Node *) linitial(newargs); + return (Node *) makeBoolExpr(expr->boolop, newargs, expr->location); + } + + return node; +} + #define MIN_ARRAY_SIZE_FOR_HASHED_SAOP 9 /*-------------------- * convert_saop_to_hashed_saop @@ -2944,6 +3101,9 @@ eval_const_expressions_mutator(Node *node, aggfilter = (Expr *) eval_const_expressions_mutator((Node *) expr->aggfilter, context); + /* NULL and FALSE are equivalent in a FILTER clause */ + aggfilter = (Expr *) + simplify_qual_null_saops((Node *) aggfilter); /* And build the replacement WindowFunc node */ newexpr = makeNode(WindowFunc); @@ -3009,6 +3169,9 @@ eval_const_expressions_mutator(Node *node, } case T_Aggref: node = ece_generic_processing(node); + /* NULL and FALSE are equivalent in a FILTER clause */ + ((Aggref *) node)->aggfilter = (Expr *) + simplify_qual_null_saops((Node *) ((Aggref *) node)->aggfilter); if (context->root != NULL) return simplify_aggref((Aggref *) node, context); return node; @@ -3699,6 +3862,8 @@ eval_const_expressions_mutator(Node *node, /* Simplify this alternative's test condition */ casecond = eval_const_expressions_mutator((Node *) oldcasewhen->expr, context); + /* NULL and FALSE are equivalent in a WHEN condition */ + casecond = simplify_qual_null_saops(casecond); /* * If the test condition is constant FALSE (or NULL), then diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c index 6e1d2b14bc4..38074365a5c 100644 --- a/src/backend/optimizer/util/inherit.c +++ b/src/backend/optimizer/util/inherit.c @@ -863,7 +863,7 @@ apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel, childqual = adjust_appendrel_attrs(root, (Node *) rinfo->clause, 1, &appinfo); - childqual = eval_const_expressions(root, childqual); + childqual = eval_const_expressions_qual(root, childqual); /* check for flat-out constant */ if (childqual && IsA(childqual, Const)) { diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h index cb6241e2bdd..5be86a38a2f 100644 --- a/src/include/optimizer/optimizer.h +++ b/src/include/optimizer/optimizer.h @@ -145,6 +145,7 @@ extern bool contain_volatile_functions_after_planning(Expr *expr); extern bool contain_volatile_functions_not_nextval(Node *clause); extern Node *eval_const_expressions(PlannerInfo *root, Node *node); +extern Node *eval_const_expressions_qual(PlannerInfo *root, Node *node); extern void convert_saop_to_hashed_saop(Node *node); diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out index b4c9bdb0e55..0258e9465e3 100644 --- a/src/test/regress/expected/insert_conflict.out +++ b/src/test/regress/expected/insert_conflict.out @@ -476,6 +476,24 @@ ERROR: there is no unique or exclusion constraint matching the ON CONFLICT spec insert into insertconflicttest values (23, 'Blackberry') on conflict (fruit) where fruit like '%berry' do update set fruit = excluded.fruit; ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification drop index partial_key_index; +-- The inference WHERE clause must not be reduced to constant-FALSE even +-- though it can never be true, else it no longer matches the index predicate +create unique index null_saop_key_index on insertconflicttest(key) where fruit <> all (array['Apple', null]); +-- Succeeds +explain (costs off) +insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing; + QUERY PLAN +------------------------------------------------- + Insert on insertconflicttest + Conflict Resolution: NOTHING + Conflict Arbiter Indexes: null_saop_key_index + -> Result +(4 rows) + +insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing; +insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) and fruit like '%berry' do nothing; +drop index null_saop_key_index; +delete from insertconflicttest where key = 1001; -- -- Test that wholerow references to ON CONFLICT's EXCLUDED work -- diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out index 236cb274a78..806970eebac 100644 --- a/src/test/regress/expected/planner_est.out +++ b/src/test/regress/expected/planner_est.out @@ -192,23 +192,23 @@ false, true, false, true); SELECT explain_mask_costs($$ SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 99, NULL]);$$, false, true, false, true); - explain_mask_costs ---------------------------------------------------------- - Seq Scan on tenk1 (cost=N..N rows=1 width=N) - Filter: (unique1 <> ALL ('{1,2,99,NULL}'::integer[])) -(2 rows) + explain_mask_costs +------------------------------------ + Result (cost=N..N rows=0 width=N) + Replaces: Scan on tenk1 + One-Time Filter: false +(3 rows) -- Try a non-const array containing a NULL SELECT explain_mask_costs($$ SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 98, (SELECT 99), NULL]);$$, false, true, false, true); - explain_mask_costs -------------------------------------------------------------------------------------- - Seq Scan on tenk1 (cost=N..N rows=1 width=N) - Filter: (unique1 <> ALL (ARRAY[1, 2, 98, (InitPlan expr_1).col1, NULL::integer])) - InitPlan expr_1 - -> Result (cost=N..N rows=1 width=N) -(4 rows) + explain_mask_costs +------------------------------------ + Result (cost=N..N rows=0 width=N) + Replaces: Scan on tenk1 + One-Time Filter: false +(3 rows) -- Verify that scalarineqsel() works on "char" columns CREATE TEMP TABLE char_table_1 AS diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out index feae77cb840..b7b3b2af4e0 100644 --- a/src/test/regress/expected/predicate.out +++ b/src/test/regress/expected/predicate.out @@ -954,3 +954,339 @@ SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; (1 row) DROP TABLE bool_tab; +-- +-- Test that "x op ALL (array)" with a strict operator and a NULL array +-- element is reduced to constant-FALSE where NULL and FALSE are equivalent +-- +CREATE TABLE null_saop_tab (a int, b int, arr int[]); +INSERT INTO null_saop_tab VALUES (1, 1, NULL), (42, 2, '{}'), (NULL, 3, NULL); +ANALYZE null_saop_tab; +-- Ensure NOT IN and <> ALL are reduced to constant-FALSE +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL); + a | b | arr +---+---+----- +(0 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]); + a | b | arr +---+---+----- +(0 rows) + +-- Ensure equivalent forms are reduced to constant-FALSE as well +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL)); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL)); + a | b | arr +---+---+----- +(0 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL])); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL])); + a | b | arr +---+---+----- +(0 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL)); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL)); + a | b | arr +---+---+----- +(0 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true; + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true; + a | b | arr +---+---+----- +(0 rows) + +CREATE FUNCTION null_saop_func(int) RETURNS bool LANGUAGE sql IMMUTABLE +AS $$ SELECT $1 NOT IN (42, NULL) $$; +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE null_saop_func(a); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE null_saop_func(a); + a | b | arr +---+---+----- +(0 rows) + +DROP FUNCTION null_saop_func(int); +-- Ensure it is reduced under AND and OR +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1; + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1; + a | b | arr +---+---+----- +(0 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1; + QUERY PLAN +--------------------------- + Seq Scan on null_saop_tab + Filter: (b = 1) +(2 rows) + +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1; + a | b | arr +---+---+----- + 1 | 1 | +(1 row) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab +WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL))); + QUERY PLAN +----------------------------------- + Result + Replaces: Scan on null_saop_tab + One-Time Filter: false +(3 rows) + +SELECT * FROM null_saop_tab +WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL))); + a | b | arr +---+---+----- +(0 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0; + QUERY PLAN +--------------------------------- + Seq Scan on null_saop_tab + Filter: ((a > 0) AND (b = 1)) +(2 rows) + +SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0; + a | b | arr +---+---+----- + 1 | 1 | +(1 row) + +-- Ensure it is reduced in a join clause +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2 + ON t1.a = t2.a AND t2.b NOT IN (42, NULL); + QUERY PLAN +------------------------------------ + Nested Loop Left Join + Join Filter: false + -> Seq Scan on null_saop_tab t1 + -> Result + Replaces: Scan on t2 + One-Time Filter: false +(6 rows) + +SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2 + ON t1.a = t2.a AND t2.b NOT IN (42, NULL) +ORDER BY t1.b; + a | b | arr | a | b | arr +----+---+-----+---+---+----- + 1 | 1 | | | | + 42 | 2 | {} | | | + | 3 | | | | +(3 rows) + +-- Ensure it is reduced in CASE WHEN conditions and FILTER clauses +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END FROM null_saop_tab; + QUERY PLAN +---------------------------------- + Seq Scan on public.null_saop_tab + Output: b, 0 +(2 rows) + +SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END FROM null_saop_tab; + b | case +---+------ + 1 | 0 + 2 | 0 + 3 | 0 +(3 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab; + QUERY PLAN +----------------------------------------- + Aggregate + Output: count(*) FILTER (WHERE false) + -> Seq Scan on public.null_saop_tab + Output: a, b, arr +(4 rows) + +SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab; + count +------- + 0 +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER () +FROM null_saop_tab; + QUERY PLAN +---------------------------------------------------- + WindowAgg + Output: b, count(*) FILTER (WHERE false) OVER w1 + Window: w1 AS () + -> Seq Scan on public.null_saop_tab + Output: b +(5 rows) + +SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER () +FROM null_saop_tab; + b | count +---+------- + 1 | 0 + 2 | 0 + 3 | 0 +(3 rows) + +-- Ensure it is not reduced under NOT +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL)); + QUERY PLAN +---------------------------------------------- + Seq Scan on null_saop_tab + Filter: (a = ANY ('{42,NULL}'::integer[])) +(2 rows) + +SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL)); + a | b | arr +----+---+----- + 42 | 2 | {} +(1 row) + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS NOT TRUE ORDER BY b; + QUERY PLAN +------------------------------------------------------------------- + Sort + Sort Key: b + -> Seq Scan on null_saop_tab + Filter: ((a <> ALL ('{42,NULL}'::integer[])) IS NOT TRUE) +(4 rows) + +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS NOT TRUE ORDER BY b; + a | b | arr +----+---+----- + 1 | 1 | + 42 | 2 | {} + | 3 | +(3 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END +FROM null_saop_tab; + QUERY PLAN +----------------------------------------------------------------------------- + Seq Scan on public.null_saop_tab + Output: b, CASE WHEN (a = ANY ('{42,NULL}'::integer[])) THEN 1 ELSE 0 END +(2 rows) + +SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END +FROM null_saop_tab; + b | case +---+------ + 1 | 0 + 2 | 1 + 3 | 0 +(3 rows) + +-- Ensure it is not reduced where NULL and FALSE are not equivalent +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, a NOT IN (42, NULL) FROM null_saop_tab; + QUERY PLAN +-------------------------------------------------- + Seq Scan on public.null_saop_tab + Output: b, (a <> ALL ('{42,NULL}'::integer[])) +(2 rows) + +SELECT b, a NOT IN (42, NULL) FROM null_saop_tab; + b | ?column? +---+---------- + 1 | + 2 | f + 3 | +(3 rows) + +-- Ensure it is not reduced for a multidimensional ARRAY[]: NULL sub-arrays +-- don't add NULL elements, and here the array is empty, so ALL is true +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b; + QUERY PLAN +---------------------------------------------------------- + Sort + Sort Key: b + -> Seq Scan on null_saop_tab + Filter: (1 <> ALL (ARRAY[NULL::integer[], arr])) +(4 rows) + +SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b; + a | b | arr +----+---+----- + 1 | 1 | + 42 | 2 | {} + | 3 | +(3 rows) + +DROP TABLE null_saop_tab; diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql index d119158549f..ad5fd21781e 100644 --- a/src/test/regress/sql/insert_conflict.sql +++ b/src/test/regress/sql/insert_conflict.sql @@ -251,6 +251,19 @@ insert into insertconflicttest values (23, 'Blackberry') on conflict (fruit) whe drop index partial_key_index; +-- The inference WHERE clause must not be reduced to constant-FALSE even +-- though it can never be true, else it no longer matches the index predicate +create unique index null_saop_key_index on insertconflicttest(key) where fruit <> all (array['Apple', null]); + +-- Succeeds +explain (costs off) +insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing; +insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) do nothing; +insert into insertconflicttest values (1001, 'Raspberry') on conflict (key) where fruit <> all (array['Apple', null]) and fruit like '%berry' do nothing; + +drop index null_saop_key_index; +delete from insertconflicttest where key = 1001; + -- -- Test that wholerow references to ON CONFLICT's EXCLUDED work -- diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql index 0f92bb52435..129c0957e40 100644 --- a/src/test/regress/sql/predicate.sql +++ b/src/test/regress/sql/predicate.sql @@ -448,3 +448,114 @@ SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; DROP TABLE bool_tab; + +-- +-- Test that "x op ALL (array)" with a strict operator and a NULL array +-- element is reduced to constant-FALSE where NULL and FALSE are equivalent +-- +CREATE TABLE null_saop_tab (a int, b int, arr int[]); +INSERT INTO null_saop_tab VALUES (1, 1, NULL), (42, 2, '{}'), (NULL, 3, NULL); +ANALYZE null_saop_tab; + +-- Ensure NOT IN and <> ALL are reduced to constant-FALSE +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL); +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]); +SELECT * FROM null_saop_tab WHERE a <> ALL (ARRAY[42, b, NULL]); + +-- Ensure equivalent forms are reduced to constant-FALSE as well +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL)); +SELECT * FROM null_saop_tab WHERE NOT (a IN (42, NULL)); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL])); +SELECT * FROM null_saop_tab WHERE NOT (a = ANY (ARRAY[42, NULL])); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL)); +SELECT * FROM null_saop_tab WHERE NOT NOT (a NOT IN (42, NULL)); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true; +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) = true; + +CREATE FUNCTION null_saop_func(int) RETURNS bool LANGUAGE sql IMMUTABLE +AS $$ SELECT $1 NOT IN (42, NULL) $$; +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE null_saop_func(a); +SELECT * FROM null_saop_tab WHERE null_saop_func(a); +DROP FUNCTION null_saop_func(int); + +-- Ensure it is reduced under AND and OR +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1; +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) AND b = 1; + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1; +SELECT * FROM null_saop_tab WHERE a NOT IN (42, NULL) OR b = 1; + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab +WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL))); +SELECT * FROM null_saop_tab +WHERE (b = 1 AND a NOT IN (42, NULL)) OR (b = 2 AND NOT (a IN (7, NULL))); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0; +SELECT * FROM null_saop_tab WHERE (b = 1 OR a NOT IN (42, NULL)) AND a > 0; + +-- Ensure it is reduced in a join clause +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2 + ON t1.a = t2.a AND t2.b NOT IN (42, NULL); +SELECT * FROM null_saop_tab t1 LEFT JOIN null_saop_tab t2 + ON t1.a = t2.a AND t2.b NOT IN (42, NULL) +ORDER BY t1.b; + +-- Ensure it is reduced in CASE WHEN conditions and FILTER clauses +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END FROM null_saop_tab; +SELECT b, CASE WHEN a NOT IN (42, NULL) THEN 1 ELSE 0 END FROM null_saop_tab; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab; +SELECT count(*) FILTER (WHERE a NOT IN (42, NULL)) FROM null_saop_tab; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER () +FROM null_saop_tab; +SELECT b, count(*) FILTER (WHERE a NOT IN (42, NULL)) OVER () +FROM null_saop_tab; + +-- Ensure it is not reduced under NOT +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL)); +SELECT * FROM null_saop_tab WHERE NOT (a NOT IN (42, NULL)); + +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS NOT TRUE ORDER BY b; +SELECT * FROM null_saop_tab WHERE (a NOT IN (42, NULL)) IS NOT TRUE ORDER BY b; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END +FROM null_saop_tab; +SELECT b, CASE WHEN NOT (a NOT IN (42, NULL)) THEN 1 ELSE 0 END +FROM null_saop_tab; + +-- Ensure it is not reduced where NULL and FALSE are not equivalent +EXPLAIN (VERBOSE, COSTS OFF) +SELECT b, a NOT IN (42, NULL) FROM null_saop_tab; +SELECT b, a NOT IN (42, NULL) FROM null_saop_tab; + +-- Ensure it is not reduced for a multidimensional ARRAY[]: NULL sub-arrays +-- don't add NULL elements, and here the array is empty, so ALL is true +EXPLAIN (COSTS OFF) +SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b; +SELECT * FROM null_saop_tab WHERE 1 <> ALL (ARRAY[NULL::int[], arr]) ORDER BY b; + +DROP TABLE null_saop_tab; -- 2.43.0