Skip to content

feat: Added support for upserting data#231

Open
puzzledpolymath wants to merge 21 commits into
2.xfrom
feature/upsert
Open

feat: Added support for upserting data#231
puzzledpolymath wants to merge 21 commits into
2.xfrom
feature/upsert

Conversation

@puzzledpolymath
Copy link
Copy Markdown
Contributor

@puzzledpolymath puzzledpolymath commented Jul 11, 2025

This pull request adds support for performing upsert queries. Ticket #50 is what sparked interest for this feature.

🔍 What was changed

  • Each driver (MySQL, Postgres, SQLite and SQLServer) compiler supports an UpsertQuery.
  • Differences between SQL dialects have been accounted for.

🤔 Why?

  1. Upsert allows developers to combine insert and update operations into a single command, reducing the need for explicit existence checks and multiple database calls.
  2. Modern databases support atomic upsert operations, which help maintain data integrity even in concurrent environments.
  3. Upsert reduces the number of database round-trips, eliminating separate existence checks followed by insert or update, likely leading to better performance, especially in high-volume scenarios.

📝 Checklist

📃 Documentation

Note that specifying conflict index column names is redundant and not required for MySQL.

PHP

$upsert = $this->database->upsert('users')
    ->conflicts('email')
    ->columns('email', 'name')
    ->values('adam@email.com', 'Adam')
    ->values('bill@email.com', 'Bill');
$upsert = $this->database->upsert('users')
    ->conflicts(['email'])
    ->values([
        ['email' => 'adam@email.com', 'name' => 'Adam'],
        ['email' => 'bill@email.com', 'name' => 'Bill'],
    ]);

MySQL

INSERT INTO `users` (`email`, `name`)
VALUES
    ('adam@email.com', 'Adam'),
    ('bill@email.com', 'Bill')
ON DUPLICATE KEY UPDATE
    `email` = VALUES(`email`),
    `name` = VALUES(`name`);

Postgres / SQLite

INSERT INTO `users` (`email`, `name`)
VALUES
    ('adam@email.com', 'Adam'),
    ('bill@email.com', 'Bill')
ON CONFLICT (`email`) DO UPDATE SET
    `email` = EXCLUDED.`email`,
    `name` = EXCLUDED.`name`

MSSQL

MERGE INTO [users] WITH (holdlock) AS [target] 
USING ( VALUES 
    ('adam@email.com', 'Adam'),
    ('bill@email.com', 'Bill') 
) AS [source] ([email], [name]) 
ON [target].[email] = [source].[email] 
WHEN MATCHED THEN 
    UPDATE SET 
        [target].[email] = [source].[email], 
        [target].[name] = [source].[name] 
WHEN NOT MATCHED THEN 
    INSERT ([email], [name]) 
    VALUES ([source].[email], [source].[name])

@cycle cycle deleted a comment from codecov Bot Jul 11, 2025
@puzzledpolymath puzzledpolymath requested a review from a team as a code owner July 11, 2025 07:19
@cycle cycle deleted a comment from codecov Bot Jul 11, 2025
@cycle cycle deleted a comment from codecov Bot Jul 11, 2025
@cycle cycle deleted a comment from codecov Bot Jul 11, 2025
@codecov
Copy link
Copy Markdown

codecov Bot commented Jul 11, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.62%. Comparing base (2acc10e) to head (22dfb17).

Additional details and impacted files
@@             Coverage Diff              @@
##                2.x     #231      +/-   ##
============================================
+ Coverage     95.43%   95.62%   +0.19%     
- Complexity     1942     2003      +61     
============================================
  Files           133      136       +3     
  Lines          5414     5652     +238     
============================================
+ Hits           5167     5405     +238     
  Misses          247      247              

☔ View full report in Codecov by Sentry.
📢 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.

@puzzledpolymath puzzledpolymath mentioned this pull request Jul 11, 2025
1 task
@puzzledpolymath puzzledpolymath changed the title Upsert feature feat: Added support for upserting data Apr 12, 2026
@puzzledpolymath
Copy link
Copy Markdown
Contributor Author

@cycle/core-contributors I've just recently merged 2.x updates, ensuring everything in this PR is up-to-date. I'd really like to see this make it's way into the codebase, as upsert functionality is something I've become accustomed to across other ORM's and languages, and something I feel others would appreciate. Let me know if there's anything further needed from me, to get this over the line.

@roxblnfk
Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

# Conflicts:
#	src/Driver/Postgres/PostgresDriver.php
#	src/Driver/SQLite/SQLiteDriver.php

Co-authored-by: roxblnfk <4152481+roxblnfk@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented May 12, 2026

@copilot resolve the merge conflicts in this pull request

Resolved and pushed in commit 22dfb17. I merged the latest 2.x into this branch and fixed the conflicts in src/Driver/Postgres/PostgresDriver.php and src/Driver/SQLite/SQLiteDriver.php.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/OndraM/ci-detector/zipball/8b0223b5ed235fd377c75fdd1bfcad05c0f168b8
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/80fd29f44a736136a2f05bae5464816a444b91d1
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/buggregator/trap/zipball/2530255bee5e1c5fa9b251018376f97ea7e54e64
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/colinodell/json5/zipball/5724d21bc5c910c2560af1b8915f0cc0163579c8
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/cweiske/jsonmapper/zipball/980674efdda65913492d29a8fd51c82270dd37bb
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/ae703ea7b4811797a10590b6078de05b3b33dd91
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/ergebnis/composer-normalize/zipball/80971fe24ff10709789942bcbe9368b2c704097c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/ergebnis/json-normalizer/zipball/77961faf2c651c3f05977b53c6c68e8434febf62
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/ergebnis/json-pointer/zipball/43bef355184e9542635e35dd2705910a3df4c236
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/ergebnis/json-printer/zipball/211d73fc7ec6daf98568ee6ed6e6d133dee8503e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/ergebnis/json-schema-validator/zipball/b739527a480a9e3651360ad351ea77e7e9019df2
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/ergebnis/json/zipball/7b56d2b5d9e897e75b43e2e753075a0904c921b1
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/infection/abstract-testframework-adapter/zipball/18925e20d15d1a5995bb85c9dc09e8751e1e069b
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/infection/extension-installer/zipball/9b351d2910b9a23ab4815542e93d541e0ca0cdcf
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/infection/include-interceptor/zipball/0cc76d95a79d9832d74e74492b0a30139904bdf7
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/infection/infection/zipball/beac2ca971b37dd7feb92fe2d3e705c175b2360b
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/infection/mutator/zipball/3c976d721b02b32f851ee4e15d553ef1e9186d1d
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/jsonrainbow/json-schema/zipball/2f7abf648939847a789c55c206d4cb9dd0d53e2c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/localheinz/diff/zipball/33bd840935970cda6691c23fc7d94ae764c0734c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/mRR9Jo /usr/bin/composer install --no-interaction --prefer-dist --ignore-platform-req=php (http block)
  • https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/php-internal/destroy/zipball/93068c4f7da218034f5373e31407f564b74b4a06
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sanmai/later/zipball/cf5164557d19930295892094996f049ea12ba14d
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sanmai/pipeline/zipball/ad7dbc3f773eeafb90d5459522fbd8f188532e25
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spatie/array-to-xml/zipball/88b2f3852a922dd73177a68938f8eb2ec70c7224
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/code-style/zipball/a0407ffcb300c9d2977cb3d1c36af0d13b2ffb72
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/core/zipball/fc9b81afd87a61ae9e523d86d1bff277a3e81b95
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/hmvc/zipball/63c6b9c5ac84c30926fdc4488a8c44f6f5d5664d
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/interceptors/zipball/306ab72c945acc03ef67398155de95ee3adbe617
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/logger/zipball/133f5069311114809984f3d53634c4e6246ae650
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/pagination/zipball/3da266cf0b4cefbe516786d5236caf73d53b8e75
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/security/zipball/15c7aa202fd91f7b93218350ebcf5107724a8656
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/spiral/tokenizer/zipball/59cda1f8fdac4a618b76d7e0ee01199bdbef72fb
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/console/zipball/49257c96304c508223815ee965c251e7c79e614e
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/finder/zipball/9590e86be1d1c57bfbb16d0dd040345378c20896
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/symfony/var-dumper/zipball/131fc9915e0343052af5ed5040401b481ca192aa
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/thecodingmachine/safe/zipball/4fbc0088994d486b0012d67116b90825f17a9309
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/vimeo/psalm/zipball/7cf3e8b988edd75e0766963b0b9e671b220f5785
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68
    • Triggering command: REDACTED, pid is -1 (http block)
  • https://api.github.com/repos/yiisoft/injector/zipball/d3f718256b734933670ad11143cca724d340fc90
    • Triggering command: REDACTED, pid is -1 (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from roxblnfk May 12, 2026 13:42
@roxblnfk
Copy link
Copy Markdown
Member

Hi. It's time! :D
Thank you for the substantial work on this feature - it provided an excellent foundation for brainstorming.

After reviewing the PR with ClaudeCode in detail, I plan to implement upsert support with a different architecture in a separate PR. The rationale and the alternative API are summarised at the end of this review.

The alternative we plan to implement

Rather than introducing a new query type, the upsert will live as an optional state on InsertQuery, configured by a small immutable DTO. In short:

$db->insert('users')
    ->values($row)
    ->onConflict(
        OnConflict::target('email')->doUpdate(['name'])->where(...)
    )
    ->returning('id')
    ->run();

// short form for the common case
$db->insert('users')
    ->values($row)
    ->onConflict('email')   // accepted as shorthand: DO UPDATE on all columns
    ->run();

// no-op variant
$db->insert('users')
    ->values($row)
    ->onConflict(OnConflict::target('email')->doNothing())
    ->run();

Key differences from this PR's design:

  • No new query class. Upsert is a state of InsertQuery, not a sibling. This eliminates the duplication of into/columns/values and lets Postgres/SQLServer RETURNING be shared without further class hierarchy.
  • OnConflict is a value object, not a sub-builder. InsertQuery::onConflict() is a plain setter.
  • Full expressive power: doUpdate() accepts either a column list (overwrite from EXCLUDED) or a column→expression map (counters, conditional updates); doNothing() is a first-class action; where() is available for Postgres.
  • Driver-specific rendering stays in the existing Postgres/MSSQL/MySQL compiler overrides for insertQuery() — they just branch on the presence of the onConflict token, and the MSSQL branch emits MERGE instead of INSERT.
  • Public API surface stays minimal: one new method (InsertQuery::onConflict()) plus the OnConflict DTO. No DatabaseInterface::upsert(), no Table::upsertOne(), no BuilderInterface::upsertQuery(), no UPSERT_QUERY type to maintain backwards compatibility on.

A separate feature request issue will describe this in detail.

What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants