Skip to content

Add upsert() for INSERT ... ON DUPLICATE KEY UPDATE - #1

Merged
kasapdev merged 1 commit into
mainfrom
improve/upsert-support
Sep 6, 2026
Merged

kasapdev merged 1 commit into
mainfrom
improve/upsert-support

Conversation

@kasapdev

@kasapdev kasapdev commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds QueryBuilder::upsert(string $table, array $data, array $updateColumns = []): array compiling MySQL/MariaDB's INSERT ... ON DUPLICATE KEY UPDATE.
  • Built on top of the existing insert() (reuses its column/placeholder compilation, its bindings-only value handling, and its empty-$data guard) rather than duplicating that logic.
  • $updateColumns defaults to refreshing every column in $data on conflict; pass a subset to only refresh those. Every entry must be a key of $data, or upsert() throws InvalidArgumentException.

Why

insert(), update(), and delete() cover every other write path, but "insert this row, or update it if a unique/primary key already exists" — one of the most common write patterns (idempotent writes, sync jobs, counters) — had no terminal helper. Without it, a caller needs this pattern has to hand-write raw SQL just for that one case, stepping outside the bindings-only guarantee that's the whole point of this library.

Changes

  • src/QueryBuilder.php — new upsert() method.
  • tests/run.php — 8 new test cases: default update-columns behavior, explicit $updateColumns subset, the SQL-injection regression check applied to upsert() (matching the existing pattern for where()/insert()/delete()), the inherited empty-$data guard, and a new guard for an $updateColumns entry that isn't a key of $data.
  • README.md / CHANGELOG.md — documented the new method with usage examples.

Test plan

  • php -l on src/QueryBuilder.php and tests/run.php — clean.
  • php tests/run.php — all 55 checks pass (47 existing + 8 new), including the full end-to-end SQLite execution suite for the pre-existing methods. ON DUPLICATE KEY UPDATE is MySQL/MariaDB-specific syntax (SQLite has no equivalent), so upsert() is verified via SQL-string/bindings assertions, the same way the majority of this suite already verifies insert()/update()/delete(), rather than added to the live-SQLite integration section.

🤖 Generated with Claude Code

insert()/update()/delete() cover every other write path, but "insert
this row, or update it if a unique key already exists" -- one of the
most common write patterns (sync jobs, idempotent writes, counters) --
had no terminal helper, forcing callers to hand-write raw SQL just for
this one case and step outside the bindings-only guarantee the rest of
the library exists to provide.

upsert() builds on the existing insert() (reusing its empty-data guard
and column/binding handling) and appends ON DUPLICATE KEY UPDATE for
the given columns, defaulting to refreshing every inserted column.
@kasapdev
kasapdev merged commit d630862 into main Sep 6, 2026
2 checks passed
@kasapdev
kasapdev deleted the improve/upsert-support branch September 6, 2026 07:45
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