From ca7da4e3a0403511777d0d47f28008059dee3578 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Mon, 5 Oct 2026 11:45:58 -0400 Subject: [PATCH v1 1/5] Fix ON CONFLICT inference for simplified index expressions An index expression such as COALESCE(b, '') on a NOT NULL column is simplified to a plain Var, in the index and in the inference clause. Only the inference side then counted it as a plain column, so the index was never matched. Count it as one on both sides. --- src/backend/optimizer/util/plancat.c | 59 +++++++++++++------ src/test/regress/expected/insert_conflict.out | 26 ++++++++ src/test/regress/sql/insert_conflict.sql | 16 +++++ 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index d067368cfc1..b73bc574bf7 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -940,6 +940,8 @@ infer_arbiter_indexes(PlannerInfo *root) Form_pg_index idxForm; Bitmapset *indexedAttrs; List *idxExprs; + List *idxOtherExprs; + ListCell *idxExprCell; List *predExprs; AttrNumber natt; bool match; @@ -1044,31 +1046,49 @@ infer_arbiter_indexes(PlannerInfo *root) if (idxForm->indisexclusion) continue; - /* Build BMS representation of plain (non expression) index attrs */ + /* Const-simplify index expressions the way arbiterElems were */ + idxExprs = RelationGetIndexExpressions(idxRel); + if (idxExprs) + { + if (varno != 1) + ChangeVarNodes((Node *) idxExprs, 1, varno, 0); + + idxExprs = (List *) eval_const_expressions(root, (Node *) idxExprs); + } + + /* + * Build BMS representation of plain index attrs. An expression that + * was simplified to a plain Var counts as one, since the same thing + * happened to the matching arbiterElem. + */ indexedAttrs = NULL; + idxOtherExprs = NIL; + idxExprCell = list_head(idxExprs); 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); + if (attno == 0) + { + Node *expr = (Node *) lfirst(idxExprCell); + + idxExprCell = lnext(idxExprs, idxExprCell); + if (!IsA(expr, Var) || ((Var *) expr)->varattno == 0) + { + idxOtherExprs = lappend(idxOtherExprs, expr); + continue; + } + attno = ((Var *) expr)->varattno; + } + + indexedAttrs = bms_add_member(indexedAttrs, + attno - FirstLowInvalidHeapAttributeNumber); } - /* Non-expression attributes (if any) must match */ + /* Plain attributes (if any) must match */ if (!bms_equal(indexedAttrs, inferAttrs)) continue; - /* Expression attributes (if any) must match */ - idxExprs = RelationGetIndexExpressions(idxRel); - if (idxExprs) - { - if (varno != 1) - ChangeVarNodes((Node *) idxExprs, 1, varno, 0); - - idxExprs = (List *) eval_const_expressions(root, (Node *) idxExprs); - } - /* Check the arbiterElems against this index. */ match = true; foreach_ptr(InferenceElem, elem, onconflict->arbiterElems) @@ -1118,7 +1138,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(idxOtherExprs, inferElems) != NIL) continue; predExprs = RelationGetIndexPredicate(idxRel); @@ -1244,12 +1264,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..e88a7e95546 100644 --- a/src/test/regress/expected/insert_conflict.out +++ b/src/test/regress/expected/insert_conflict.out @@ -181,6 +181,32 @@ explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on con drop index cross_match; -- +-- An index expression that is simplified to a plain column, because the +-- column is NOT NULL, must still be matched +-- +create table insertconflict_nn (a int not null, b text not null, v int); +create unique index on insertconflict_nn (a, coalesce(b, '') collate "C" text_pattern_ops); +insert into insertconflict_nn values (1, 'x', 0); +insert into insertconflict_nn values (1, 'x', 1) + on conflict (a, coalesce(b, '')) do update set v = 10 returning v; + v +---- + 10 +(1 row) + +insert into insertconflict_nn values (1, 'x', 1) + on conflict (a, coalesce(b, '') collate "C" text_pattern_ops) do update set v = 20 returning v; + v +---- + 20 +(1 row) + +-- fails +insert into insertconflict_nn values (1, 'x', 1) + on conflict (a, coalesce(b, '') text_ops) 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..786ed109952 100644 --- a/src/test/regress/sql/insert_conflict.sql +++ b/src/test/regress/sql/insert_conflict.sql @@ -78,6 +78,22 @@ explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on con drop index cross_match; +-- +-- An index expression that is simplified to a plain column, because the +-- column is NOT NULL, must still be matched +-- +create table insertconflict_nn (a int not null, b text not null, v int); +create unique index on insertconflict_nn (a, coalesce(b, '') collate "C" text_pattern_ops); +insert into insertconflict_nn values (1, 'x', 0); +insert into insertconflict_nn values (1, 'x', 1) + on conflict (a, coalesce(b, '')) do update set v = 10 returning v; +insert into insertconflict_nn values (1, 'x', 1) + on conflict (a, coalesce(b, '') collate "C" text_pattern_ops) do update set v = 20 returning v; +-- fails +insert into insertconflict_nn values (1, 'x', 1) + on conflict (a, coalesce(b, '') text_ops) do nothing; +drop table insertconflict_nn; + -- -- Single key tests -- -- 2.37.1 (Apple Git-137.1)