Skip to content

Postgres: support WHERE index_predicate in the ON CONFLICT target - #2627

Open
zvonimir-dd wants to merge 4 commits into
apache:mainfrom
zvonimir-dd:postgres-on-conflict-target-predicate
Open

zvonimir-dd wants to merge 4 commits into
apache:mainfrom
zvonimir-dd:postgres-on-conflict-target-predicate

Conversation

@zvonimir-dd

@zvonimir-dd zvonimir-dd commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Dialects: PostgreSQL

Closes #2625.

In PostgreSQL a column-list ON CONFLICT target may end with WHERE index_predicate, which lets a partial unique index be inferred as the arbiter:

INSERT INTO t (a, b) VALUES (1, 2) ON CONFLICT (a) WHERE b > 0 DO NOTHING
  • ConflictTarget::Columns becomes a struct variant, Columns { columns: Vec<Ident>, predicate: Option<Expr> }. The predicate is parsed after the column list and before DO, and printed back after it.
  • Accepted only after a column list, as in PostgreSQL's grammar. ON CONSTRAINT … WHERE and a WHERE with no target are still rejected.
  • Separate from the action's DO UPDATE SET … WHERE condition (Postgres: [pg on conflict method] does not support 'where' condition. #733, update on conflict with condition #735). A statement can carry both; the tests check that.
  • The predicate is a regular Expr field, so visitors see the columns it names, and the target's span covers it.
  • The syntax is added to the fuzz seed corpus.

This changes the shape of ConflictTarget::Columns (it was Columns(Vec<Ident>)), so code matching on it needs updating. The struct variant also leaves room for the ( index_expression ) form requested in #1977.

In PostgreSQL a column-list conflict target may end with
`WHERE index_predicate`, which lets a partial unique index be inferred as
the arbiter: `ON CONFLICT (a) WHERE b > 0 DO NOTHING`. Parse it into a new
`OnConflict::conflict_target_predicate` and print it back after the target.
It is only accepted after a column list, as in PostgreSQL's grammar, and it
is separate from the action's `DO UPDATE ... WHERE`.

Fixes apache#2625.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.25%. Comparing base (dacdbc9) to head (8936117).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/ast/mod.rs 66.66% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2627      +/-   ##
==========================================
+ Coverage   81.16%   81.25%   +0.09%     
==========================================
  Files          42       42              
  Lines       33769    33902     +133     
  Branches    33769    33902     +133     
==========================================
+ Hits        27408    27547     +139     
+ Misses       2797     2780      -17     
- Partials     3564     3575      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Covers `OnConflict`'s span with and without the predicate, a predicate cut off at the end of the
input, and a predicate with no action after it.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@LucaCappelletti94 LucaCappelletti94 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. The parsing and the tests look right. I'd go with the struct variant you offered in #2625, ConflictTarget::Columns { columns: Vec<Ident>, predicate: Option<Expr> }, instead of the separate OnConflict::conflict_target_predicate field.

Also do add an example of this syntax for the fuzzer corpus.

As suggested in review: `ConflictTarget::Columns` is now a struct variant, `Columns { columns,
predicate }`, instead of `OnConflict` carrying a separate `conflict_target_predicate`. The
predicate belongs to the column-list target only, and the variant now says so; it also leaves
room for the `( index_expression )` form of apache#1977.

Adds the syntax to the fuzz seed corpus.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zvonimir-dd

Copy link
Copy Markdown
Contributor Author

Thanks! Done in ff41dab: ConflictTarget::Columns is now Columns { columns, predicate }, and OnConflict::conflict_target_predicate is gone. I added fuzz/fuzz_seeds/postgres_on_conflict_target_predicate.sql, and checked that both of its statements run on PostgreSQL 17. The description is updated to match.

@LucaCappelletti94 LucaCappelletti94 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than this nit, the code looks okay. If you have time, I would appreciate if you could also do some reviews of the other PRs so as to speed up the whole queue.

Comment thread src/ast/mod.rs
Comment on lines +6919 to +6920
///
/// See <https://www.postgresql.org/docs/current/sql-insert.html>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should say SQLite accepts this predicate too. Its upsert grammar has the same ( indexed-column, ... ) WHERE expr target, and the parser reads it in every dialect. SQLite 3.51.1 runs INSERT INTO t VALUES (1, 2, 3) ON CONFLICT (a) WHERE b > 0 DO NOTHING and rejects ON CONFLICT WHERE b > 0. With the doc naming only PostgreSQL, a later change could gate the parse to PostgreSQL and drop SQLite without anyone noticing.

Suggested change
///
/// See <https://www.postgresql.org/docs/current/sql-insert.html>
/// Target specified as a list of columns, optionally followed by
/// `WHERE index_predicate`, which lets a partial unique index be inferred as the arbiter:
/// `ON CONFLICT (a) WHERE b > 0 DO ...`. PostgreSQL and SQLite both accept the predicate.
///
/// See <https://www.postgresql.org/docs/current/sql-insert.html>
/// and <https://www.sqlite.org/lang_upsert.html>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, applied your suggestion as written in 8936117. I also added parse_on_conflict_target_predicate to tests/sqlparser_sqlite.rs, so restricting the parse to PostgreSQL would fail a test rather than only contradict the doc.

I checked it against SQLite 3.37.2 as well. With a partial unique index on t (a) WHERE b > 0, INSERT INTO t VALUES (1, 2, 3) ON CONFLICT (a) WHERE b > 0 DO NOTHING runs, and the second insert is a no-op. ON CONFLICT WHERE b > 0 is a syntax error.

@LucaCappelletti94 LucaCappelletti94 added enhancement New feature or request waiting on contributor The review needs further refinements by its author labels Oct 15, 2026
As suggested in review: SQLite's upsert has the same `( indexed-column, ... ) WHERE expr` target,
and the parser reads it in every dialect. The doc on `ConflictTarget::Columns` now names both, and
a SQLite test pins the parse, so a change that restricted it to PostgreSQL would fail a test rather
than only contradict the doc.

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request PostgreSQL waiting on contributor The review needs further refinements by its author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Postgres: support WHERE index_predicate in the ON CONFLICT conflict target

3 participants