From f9ccc5e4f92bc39f7c31905ff611785ff29ab9bb Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Sat, 10 Oct 2026 09:20:27 +0900 Subject: [PATCH] Fix ON CONFLICT inference for index expressions simplified to a Var infer_arbiter_indexes() classifies each inference element by its const-simplified form: a plain Var is matched against the index's plain attributes, and anything else against its expressions. Index columns, however, were classified by indkey alone, so an expression column stayed an expression even when const-simplification reduced it to a plain Var. Since commit 10c4fe074, COALESCE on a NOT NULL column is reduced to the column itself, so an inference element such as COALESCE(b, '') became a plain Var that no longer matched the unique index on that same expression, and the statement failed with "there is no unique or exclusion constraint matching the ON CONFLICT specification". To fix, classify index columns the same way as inference elements: an index expression that is simplified to a plain Var is treated as a plain attribute. Also, teach infer_collation_opclass_match() to compare such a column as an expression, so that an inference element that specifies a collation or operator class can still match it. As a consequence, an inference element naming the column directly now matches such an index too, which is consistent with how an element written as the expression already matches a plain index on the column. Author: Shihao Zhong Reviewed-by: jian he Reviewed-by: Richard Guo Discussion: https://postgr.es/m/CAGRkXqR7VXacmRP41UBaM5u4aseeCH5ECVmQgn_YS=L7=xdhTw@mail.gmail.com Backpatch-through: 19 --- src/backend/optimizer/util/plancat.c | 66 +++++++++++++------ src/test/regress/expected/insert_conflict.out | 27 ++++++++ src/test/regress/sql/insert_conflict.sql | 13 ++++ 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 85c25d7d0b6..84b9c188fb5 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -942,6 +942,8 @@ infer_arbiter_indexes(PlannerInfo *root) Form_pg_index idxForm; Bitmapset *indexedAttrs; List *idxExprs; + List *idxNonVarExprs; + ListCell *idxExprItem; List *predExprs; AttrNumber natt; bool match; @@ -1046,22 +1048,7 @@ infer_arbiter_indexes(PlannerInfo *root) if (idxForm->indisexclusion) continue; - /* Build BMS representation of plain (non expression) index attrs */ - indexedAttrs = NULL; - for (natt = 0; natt < idxForm->indnkeyatts; natt++) - { - int attno = idxRel->rd_index->indkey.values[natt]; - - if (attno != 0) - indexedAttrs = bms_add_member(indexedAttrs, - attno - FirstLowInvalidHeapAttributeNumber); - } - - /* Non-expression attributes (if any) must match */ - if (!bms_equal(indexedAttrs, inferAttrs)) - continue; - - /* Expression attributes (if any) must match */ + /* Simplify the index expressions the same way arbiterElems were */ idxExprs = RelationGetIndexExpressions(idxRel); if (idxExprs) { @@ -1071,6 +1058,44 @@ infer_arbiter_indexes(PlannerInfo *root) idxExprs = (List *) eval_const_expressions(root, (Node *) idxExprs); } + /* + * Build BMS representation of plain index attrs. An index expression + * that was simplified to a plain Var is treated as a plain attr too, + * to match how arbiterElems are classified above. + */ + indexedAttrs = NULL; + idxNonVarExprs = NIL; + idxExprItem = list_head(idxExprs); + for (natt = 0; natt < idxForm->indnkeyatts; natt++) + { + int attno = idxRel->rd_index->indkey.values[natt]; + + if (attno == 0) + { + Node *idxExpr; + + if (idxExprItem == NULL) + elog(ERROR, "wrong number of index expressions"); + idxExpr = (Node *) lfirst(idxExprItem); + idxExprItem = lnext(idxExprs, idxExprItem); + + if (!IsA(idxExpr, Var) || ((Var *) idxExpr)->varattno == 0) + { + idxNonVarExprs = lappend(idxNonVarExprs, idxExpr); + continue; + } + + attno = ((Var *) idxExpr)->varattno; + } + + indexedAttrs = bms_add_member(indexedAttrs, + attno - FirstLowInvalidHeapAttributeNumber); + } + + /* Plain attributes (if any) must match */ + if (!bms_equal(indexedAttrs, inferAttrs)) + continue; + /* Check the arbiterElems against this index. */ match = true; foreach_ptr(InferenceElem, elem, onconflict->arbiterElems) @@ -1120,7 +1145,7 @@ infer_arbiter_indexes(PlannerInfo *root) * indexes redundantly repeat the same attribute, or if attributes * redundantly appear multiple times within an inference clause. */ - if (list_difference(idxExprs, inferElems) != NIL) + if (list_difference(idxNonVarExprs, inferElems) != NIL) continue; predExprs = RelationGetIndexPredicate(idxRel); @@ -1246,12 +1271,13 @@ infer_collation_opclass_match(InferenceElem *elem, Relation idxRel, } /* If one matching index att found, good enough -- return true */ - if (IsA(elem->expr, Var)) + if (attno != 0) { - if (((Var *) elem->expr)->varattno == attno) + if (IsA(elem->expr, Var) && + ((Var *) elem->expr)->varattno == attno) return true; } - else if (attno == 0) + else { Node *nattExpr = list_nth(idxExprs, (natt - 1) - nplain); diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out index b4c9bdb0e55..0dd8bc734a0 100644 --- a/src/test/regress/expected/insert_conflict.out +++ b/src/test/regress/expected/insert_conflict.out @@ -181,6 +181,33 @@ explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on con drop index cross_match; -- +-- Index expressions that are simplified to a plain column can be inferred +-- +create table insertconflict_nn (a int not null, b text); +create unique index nn_expr_key on insertconflict_nn(coalesce(a, 0), lower(b)); +explain (costs off) insert into insertconflict_nn values(1, 'x') on conflict (coalesce(a, 0), lower(b)) do nothing; + QUERY PLAN +----------------------------------------- + Insert on insertconflict_nn + Conflict Resolution: NOTHING + Conflict Arbiter Indexes: nn_expr_key + -> Result +(4 rows) + +explain (costs off) insert into insertconflict_nn values(1, 'x') on conflict (a int4_ops, lower(b)) do nothing; + QUERY PLAN +----------------------------------------- + Insert on insertconflict_nn + Conflict Resolution: NOTHING + Conflict Arbiter Indexes: nn_expr_key + -> Result +(4 rows) + +-- fails: +explain (costs off) insert into insertconflict_nn values(1, 'x') on conflict (a) do nothing; +ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification +drop table insertconflict_nn; +-- -- Single key tests -- create unique index key_index on insertconflicttest(key); diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql index d119158549f..8539c49c939 100644 --- a/src/test/regress/sql/insert_conflict.sql +++ b/src/test/regress/sql/insert_conflict.sql @@ -78,6 +78,19 @@ explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on con drop index cross_match; +-- +-- Index expressions that are simplified to a plain column can be inferred +-- +create table insertconflict_nn (a int not null, b text); +create unique index nn_expr_key on insertconflict_nn(coalesce(a, 0), lower(b)); + +explain (costs off) insert into insertconflict_nn values(1, 'x') on conflict (coalesce(a, 0), lower(b)) do nothing; +explain (costs off) insert into insertconflict_nn values(1, 'x') on conflict (a int4_ops, lower(b)) do nothing; +-- fails: +explain (costs off) insert into insertconflict_nn values(1, 'x') on conflict (a) do nothing; + +drop table insertconflict_nn; + -- -- Single key tests --