feat: Add InsertQuery::onConflict() method#249
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class upsert support by extending InsertQuery with an onConflict() method that flips the query type to a new UPSERT_QUERY constant; a new immutable OnConflict DTO (with driver-specific PostgresOnConflict, MySQLOnConflict, SQLServerOnConflict subclasses) describes target/action/update spec; each driver compiler implements upsertQuery() (SQLite/Postgres ON CONFLICT, MySQL INSERT ... AS new_row ON DUPLICATE KEY UPDATE, SQL Server MERGE ... WITH (HOLDLOCK)), with single-row upsert caching in CompilerCache.
Changes:
- New
OnConflictvalue object hierarchy andConflictActionenum, with sharedupsertUpdateClause()helper in baseCompiler. InsertQuery::onConflict()setter (string / array / DTO) plus updatedgetType()/getTokens(); PostgresInsertQuerypropagatesonConflictinto its tokens.- Per-driver
upsertQuery()compilers,CompilerInterface::UPSERT_QUERY = 10,CompilerCache::hashUpsertQuery(), and unit + functional tests across drivers.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Query/OnConflict.php | New immutable upsert DTO with target/action/update spec and cache-key generation. |
| src/Query/ConflictAction.php | New enum for Update / Nothing actions. |
| src/Query/InsertQuery.php | Adds onConflict() setter, type-flip in getType(), and onConflict token. |
| src/Query/ActiveQuery.php | Tightens fetchIdentifiers return type annotation. |
| src/Driver/CompilerInterface.php | Adds UPSERT_QUERY = 10 constant. |
| src/Driver/Compiler.php | Default SQLite-style upsertQuery() and shared upsertUpdateClause() helper. |
| src/Driver/CompilerCache.php | Single-row upsert caching via hashUpsertQuery(). |
| src/Driver/MySQL/MySQLOnConflict.php | MySQL DTO with withRowAlias(); doc claim that target is "ignored" is misleading. |
| src/Driver/MySQL/MySQLCompiler.php | INSERT ... AS <alias> ON DUPLICATE KEY UPDATE compilation; DO NOTHING emulated via self-assignment. |
| src/Driver/Postgres/PostgresOnConflict.php | Postgres DTO adding onConstraint() target. |
| src/Driver/Postgres/PostgresCompiler.php | ON CONFLICT (...) DO UPDATE/NOTHING [RETURNING] compilation; appendReturning() extracted. |
| src/Driver/Postgres/Query/PostgresInsertQuery.php | Propagates onConflict token. |
| src/Driver/SQLServer/SQLServerOnConflict.php | SQL Server DTO placeholder; rejects mismatched subclasses in from(). |
| src/Driver/SQLServer/SQLServerCompiler.php | MERGE ... WITH (HOLDLOCK) compilation with optional OUTPUT. |
| tests/Database/Unit/Query/OnConflictTest.php | DTO semantics, immutability, cache-key tests. |
| tests/Database/Unit/Driver/{Postgres,MySQL}/*OnConflictTest.php | Driver-specific DTO / from() narrowing tests. |
| tests/Database/Unit/Query/Tokens/InsertQueryTest.php | Adds onConflict => null to expected tokens. |
| tests/Database/Functional/Driver/Common/Query/UpsertQueryTest.php | Cross-driver behavior expectations. |
| tests/Database/Functional/Driver/{SQLite,MySQL,Postgres,SQLServer}/Query/UpsertQueryTest.php | Per-driver SQL assertions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 2.x #249 +/- ##
============================================
- Coverage 95.43% 95.39% -0.05%
- Complexity 1942 2032 +90
============================================
Files 133 137 +4
Lines 5414 5755 +341
============================================
+ Hits 5167 5490 +323
- Misses 247 265 +18 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
🔍 What was changed
Adds first-class upsert support as a state on
InsertQuery, not a new query class.InsertQuery::onConflict(OnConflict|string|array)— new setter. Presence of state flipsgetType()fromINSERT_QUERYtoUPSERT_QUERY.Cycle\Database\Query\OnConflict— immutable value object describing conflict-resolution policy: target columns,doUpdate(null|list|map)(supports column expressions viaFragmentInterface),doNothing().CursorOptionspattern):Postgres\PostgresOnConflict::onConstraint($name)—ON CONFLICT ON CONSTRAINT.MySQL\MySQLOnConflict::withRowAlias($alias)— customize theINSERT ... AS <alias>row alias.SQLServer\SQLServerOnConflict— placeholder for future MERGE-specific options.static from(OnConflict)that accepts base or self and rejects mismatched subclasses.CompilerInterface::UPSERT_QUERYconstant:Compiler::upsertQuery()→ SQLite (INSERT ... ON CONFLICT ... DO UPDATE|NOTHING).PostgresCompiler→z same +RETURNING+ constraint target.MySQLCompiler→INSERT ... AS <alias> ON DUPLICATE KEY UPDATE(requires MySQL 8.0.19+).SQLServerCompiler→MERGE ... WITH (HOLDLOCK) WHEN MATCHED THEN UPDATE WHEN NOT MATCHED THEN INSERT [OUTPUT].CompilerCache::hashUpsertQuery()— caches single-row upserts, parallel to single-row insert caching. Hash delegates to polymorphicOnConflict::getCacheKey()so driver-specific fields are correctly disambiguated.Note
Closes the same gap as #231 with a different architecture. No new query class, no widening of
DatabaseInterface/BuilderInterface/Table/QueryBuilder— the only new public surface isInsertQuery::onConflict()and theOnConflictDTO hierarchy. Seereview.mdandfr.mdon the branch for the design discussion.🤔 Why?
INSERTfor 3 of 4 drivers — modelling it as a separate query class duplicatedinto/columns/valuesmachinery and required a parallelDatabase::upsert(),Table::upsertOne(),BuilderInterface::upsertQuery()surface.ON CONSTRAINT, MySQL row alias, SQLServer MERGE quirks) doesn't belong on a single base DTO — handling it with runtimeCompilerExceptions pollutes the abstraction. Driver-specific subclasses move the check to type level and match the existingCursorOptionspattern in the codebase.MySQLCompileruses modernINSERT ... AS new_row ON DUPLICATE KEY UPDATE col = new_row.colrather than the deprecatedVALUES(col)form.📝 Checklist
OnConflictTest,PostgresOnConflictTest,MySQLOnConflictTest— 27 new tests covering DTO semantics, immutability,from()narrowing/rejection, cache-key stability)Common, SQLite, MySQL, Postgres, SQLServer — exercising shorthand, explicitdoUpdatecolumns/expressions,doNothing, multi-row upsert, constraint target, custom row alias, cross-driver subclass rejection)📃 Documentation
Database::cursor()-style PHPDoc onInsertQuery::onConflict()andOnConflictdescribes both shorthand and DTO forms.InsertQuery::run()PHPDoc notes thatlastInsertIDis unreliable on the update branch for MySQL/SQLite withoutRETURNING— driver-specific subclasses withReturningInterface(Postgres, SQLServer) are recommended for code that needs the row identity back.Known scope limitations (follow-ups)
where()clause onDO UPDATE(Postgres) /WHEN MATCHED AND ...(SQLServer) is intentionally deferred — would require integratingWhereTraitinto the immutable DTO; the subclass hierarchy is ready for it without polluting the base.VALUES(col)not implemented (current target: MySQL 8.0.19+, the row-alias syntax cutoff).