TLDR
- SQLite and PostGreSQL: Use EXCLUDEDto reference the would-have-been inserted row
- MySQL: Use VALUES()function to reference the would-have-been inserted row
Syntax
-- sqlite
-- psql
INSERT INTO users (email, last_name, first_name)
VALUES
    ('john.doe@example.com', 'Doe', 'John'),
    ('jane.doe@example.com', 'Doe', 'Jane')
ON CONFLICT (email) DO UPDATE SET
    last_name = EXCLUDED.last_name,
    first_name = EXCLUDED.first_name;
 
-- mysql
INSERT INTO users (email, last_name, first_name)
VALUES
    ('john.doe@example.com', 'Doe', 'John'),
    ('jane.doe@example.com', 'Doe', 'Jane')
ON DUPLICATE KEY UPDATE
    last_name = VALUES(last_name),
    first_name = VALUES(first_name);Gotchas
Upsert Might not Work Like How You Thought It Would
Per documents, the such ONLY works on columns with an uniqueness constraint, like UNIQUE or PRIMARY KEY. And, the upsert flow for each row is decided separately.
Should other qualifiers, like NOT NULL, fail to pass, the row insertion/update would still fail.
With hindsight, this was already stated in MySQL's "DUPLICATE KEY but not having to specify which" part.
Unwanted Row Locks
The affected rows will be locked during the query12, even though they might not be updated in the end. Treat them with care.
Anecdote
SQLite follows PostgreSQL in its upsert syntax, and it is documented.