Upserts
An upsert inserts a row, or updates the existing one when a unique conflict
occurs. CDO writes the right dialect for you — ON CONFLICT on PostgreSQL,
ON DUPLICATE KEY UPDATE on MySQL/MariaDB — and gives you two portable tokens,
:new and :current, to describe the update.
Insert-or-ignore
Pass the conflict columns and no update columns to skip existing rows:
// PostgreSQL: ON CONFLICT (email) DO NOTHING
// MySQL/MariaDB: INSERT IGNORE
$cdo->upsert('users',
['email' => 'alice@example.com', 'name' => 'Alice'],
conflictColumns: ['email']
);conflictColumns must name the column(s) that define uniqueness. Passing an empty
array throws a CDOException.
Insert-or-update
Add updateColumns — a map of column => expression — to update on conflict.
Expressions use two placeholders:
| Token | Means | PostgreSQL | MySQL / MariaDB |
|---|---|---|---|
:new |
the incoming value | EXCLUDED.column |
VALUES(column) |
:current |
the existing row value | table.column |
column |
$cdo->upsert('products',
['sku' => 'ABC-001', 'name' => 'Widget', 'price' => 9.99, 'stock' => 50],
conflictColumns: ['sku'],
updateColumns: [
'name' => ':new', // replace with incoming value
'price' => ':new',
'stock' => ':current + :new', // accumulate onto the existing value
]
);An expression that references neither token is emitted verbatim, so SQL functions work directly:
updateColumns: [
'quantity' => ':current + :new',
'updated_at' => 'NOW()',
]updateColumns is not bound
The column => expression strings are written into the SQL as-is (only :new /
:current are substituted). Never build them from user input. Row values are
still bound safely; only the expression text is verbatim. See the
upsert placeholders reference for the full
substitution rules.
Batch upsert
upsertGroup() upserts many rows with chunking, defaulting to 500 rows per
statement:
$cdo->upsertGroup('inventory', $items,
conflictColumns: ['warehouse_id', 'product_id'],
updateColumns: [
'cost' => ':new',
'quantity' => ':current + :new',
'updated_at' => 'NOW()',
],
chunkSize: 500
);Like insertGroup, an empty input array is a no-op; an empty conflictColumns
throws.
Return values
upsert()returns the primary key only on PostgreSQL (viaRETURNING). On MySQL/MariaDB it returnslastInsertId(), which isnullwhen no new row was inserted (i.e. an existing row was updated or ignored).upsertGroup()returns nothing.
Portability of the returned id
Because RETURNING on upsert is PostgreSQL-only here, don’t rely on upsert()’s
return value for a stable id across drivers. If you need the id on MySQL/MariaDB,
read it back with a follow-up query keyed on your conflict columns.
Common expression patterns
| Goal | Expression |
|---|---|
| Replace with new value | :new |
| Add to current | :current + :new |
| Keep the larger | GREATEST(:current, :new) |
| Keep the smaller | LEAST(:current, :new) |
| New value or keep current | COALESCE(:new, :current) |
| Stamp the change time | NOW() |
Related
- Upsert placeholders — full token reference
- Inserting records — plain inserts and batches
- Batch & chunking — how chunked statements are built
- CDO API —
upsert/upsertGroupsignatures