MERGE
MERGE INTO ... USING ... ON ... WHEN MATCHED ... WHEN NOT MATCHED ... — explicit upsert semantics. K3 picks one of three strategies based on the source shape. For the RPC contract and full enum reference, see the Execute hub.
MERGE_ROWS — source is inline VALUES
MERGE INTO events AS t
USING (VALUES
(1, 'u-101', TIMESTAMP '2026-05-27 10:00:00', 'click_pricing', '{"page":"/pricing","variant":"B"}'),
(4, 'u-103', TIMESTAMP '2026-05-27 10:03:00', 'signup', '{"plan":"pro"}')
) AS s(id, user_id, occurred_at, event_type, payload)
ON t.id = s.id AND t.user_id = s.user_id
WHEN MATCHED THEN
UPDATE SET event_type = s.event_type, payload = s.payload
WHEN NOT MATCHED THEN
INSERT (id, user_id, occurred_at, event_type, payload)
VALUES (s.id, s.user_id, s.occurred_at, s.event_type, s.payload);Response: strategy: WRITE_STRATEGY_MERGE_ROWS, rowsWritten: "2" (one log append per source row).
MERGE_QUERY — source is a SELECT
-- Merge today's events from a staging table
MERGE INTO events AS t
USING (
SELECT * FROM events_staging
WHERE occurred_at >= DATE_TRUNC('day', CURRENT_TIMESTAMP)
) AS s
ON t.id = s.id AND t.user_id = s.user_id
WHEN MATCHED THEN UPDATE SET
event_type = s.event_type,
payload = s.payload
WHEN NOT MATCHED THEN INSERT (id, user_id, occurred_at, event_type, payload)
VALUES (s.id, s.user_id, s.occurred_at, s.event_type, s.payload);Response: strategy: WRITE_STRATEGY_MERGE_QUERY.
MERGE_TABLE — source is another table by name
-- Merge an entire staging table — equivalent to MERGE_QUERY with `SELECT * FROM events_staging`,
-- but the planner can short-circuit metadata lookups on the source.
MERGE INTO events AS t
USING events_staging AS s
ON t.id = s.id AND t.user_id = s.user_id
WHEN MATCHED THEN UPDATE SET event_type = s.event_type, payload = s.payload
WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.user_id, s.occurred_at, s.event_type, s.payload);Response: strategy: WRITE_STRATEGY_MERGE_TABLE.
WHEN MATCHED THEN DELETE
Both UPDATE and DELETE actions are supported in WHEN MATCHED clauses:
-- Drop matched rows entirely (e.g. soft-delete inversion)
MERGE INTO events AS t
USING events_to_purge AS s
ON t.id = s.id AND t.user_id = s.user_id
WHEN MATCHED THEN DELETE;Pre-drain MERGE response semantics
rows_written on MERGE always counts source rows accepted into the write log — pre-drain we cannot tell inserts apart from updates. The post-drain insert/update/delete classification comes back via DescribeTable.last_drain_*:
dodil k3 table describe events --bucket kb-prod -o json | jq '{
lastDrainInsertedRows, lastDrainUpdatedRows, lastDrainDeletedRows
}'See also
- Execute — Overview — RPC contract, enums, refusals,
noopsemantics - SELECT · INSERT · UPDATE · DELETE — sibling SQL shapes
- Data → Merge — typed shortcut
- Tables → DescribeTable — post-drain insert/update/delete row classification (
last_drain_*) - Core Concepts → WriteStrategy — every routing variant
- CLI Guide —
dodil k3 table execute "MERGE ..."