Skip to content

Add Relation#on_conflict with a DSL for upserts - #441

Draft
flash-gordon wants to merge 1 commit into
release-3.7from
on-conflict-dsl
Draft

flash-gordon wants to merge 1 commit into
release-3.7from
on-conflict-dsl

Conversation

@flash-gordon

@flash-gordon flash-gordon commented Sep 10, 2026

Copy link
Copy Markdown
Member

Upserts required dropping down to Sequel with dataset.insert_conflict(target: ..., update: { name: Sequel[:excluded][:name] }), and Postgres::Upsert covers single inserts configured at the class level only. This makes ON CONFLICT a part of the relation, so it composes the way where does and any insert through such a relation becomes an upsert:

users.on_conflict(:email).insert(tuple)                        # ON CONFLICT (email) DO NOTHING
users.on_conflict(constraint: :users_email_key).insert(tuple)  # ON CONFLICT ON CONSTRAINT users_email_key DO NOTHING
users.on_conflict(:email) { active.is(true) }.insert(tuple)    # ON CONFLICT (email) WHERE active IS TRUE DO NOTHING

users.on_conflict(:email).do_update.multi_insert(tuples)       # DO UPDATE SET every column but the key and the target from EXCLUDED
users.on_conflict(:email).do_update(:name, :updated_at).multi_insert(tuples)
users.on_conflict(:email).do_update(removed_at: nil).insert(tuple)

do_update accepts a block evaluated with a DSL: attributes refer to the existing row, excluded to the row proposed for insertion, functions are resolved via the virtual row like in where. set and where return immutable values and chain:

user_presence
  .on_conflict(:user_uuid)
  .do_update {
    set(last_seen_at: greatest(last_seen_at, excluded[:last_seen_at]), updated_at: excluded[:updated_at])
      .where(updated_at < excluded[:updated_at])
  }
  .multi_insert(tuples)

Commands::Create is restrictable now, hence the relation flows into commands and changesets, which return the upserted rows via RETURNING on PostgreSQL:

users.on_conflict(:email).do_update(:name).command(:create).call(tuple)
users.on_conflict(:email).changeset(:create, data).commit

Works with PostgreSQL and SQLite, MySQL raises UnsupportedFeatureError. Relation#upsert and Postgres::Upsert are left untouched.

@flash-gordon

Copy link
Copy Markdown
Member Author

It's an AI doing, I'll review it soon. I just accumulated a dozen upserts in the app and I've decided it's enough to model a DSL worth having (and matching our style!).

@flash-gordon
flash-gordon marked this pull request as draft September 10, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant