Runs a SQL MERGE INTO statement: rows of target are matched against
rows of source on the by columns, then updated, deleted, or left
alone, while unmatched source rows can be inserted and target rows
missing from the source can be removed. The whole operation is atomic –
one snapshot, with row lineage preserved in the change feed.
Arguments
- target
The table to modify: a table from
get_ducklake_table()or a table name.- source
The rows to merge in: a data frame or a lazy table on the same connection.
- by
Character vector of key column(s) to match on. Rows with
NULLkey values never match.- when_matched
What to do with target rows that match a source row:
"update"(default) sets the columns the two tables share,"delete"removes the row,"nothing"leaves it alone.- when_not_matched
What to do with source rows that match no target row:
"insert"(default) adds them,"nothing"skips them.- matched_condition
Optional SQL expression further restricting the
when_matchedaction, written against the aliasestargetandsource, e.g."source.amt > target.amt".- not_matched_condition
Optional SQL expression further restricting the
when_not_matchedaction.- delete_missing
Also delete target rows that have no match in the source (
WHEN NOT MATCHED BY SOURCE THEN DELETE). Combined with the update action this synchronizestargettosource.- .quiet
Logical, whether to suppress the row-count message (default TRUE).
Details
This is not a join. Joins (left_join() and friends) read from the lake
and build a new result without touching either table; merge_into()
changes the rows of target in place. For the common update-or-insert
case, reach for rows_upsert() first – merge_into() is for the cases
it cannot express: conditional clauses, deletes of matched rows, and
synchronizing a table with a staging source.
Choosing how to change a table
To look up or combine data for analysis, use dplyr joins (
left_join()and friends). Joins read from the lake and build a new result; they never modify a lake table.To append, correct, or remove specific rows, use
rows_insert(),rows_update(), orrows_delete(). Each call is a single SQL statement against the existing table – no data leaves the database, and with data inlining enabled (DuckLake's default) small changes land in the catalog without creating tiny Parquet files.To update rows that exist and insert the ones that don't in one atomic statement, use
rows_upsert().For conditional merge logic or deletes driven by a staging table, use
merge_into().To change a table's shape without touching its data – add, drop, or rename columns, widen a type – use the schema evolution family (
add_table_column()and friends): metadata-only changes that rewrite nothing.For bulk transformations that touch most rows, use
replace_table(). It collects the transformed data into R and rewrites the whole table – heavier than the row operations, and it resets the row lineage that the in-place operations preserve in the change feed.
A tempting alternative – joining the source to the table and calling
replace_table() on the result – rewrites every row and records the
change as a wholesale replacement. merge_into() touches only the
affected rows, so get_table_changes() afterward shows exactly which
rows were inserted, updated, or deleted.
Conditions are SQL
matched_condition and not_matched_condition are raw SQL expressions,
not dplyr code. They are pasted into the statement as written (only a
; is rejected), so build them from trusted input only.
DuckLake-specific behavior
Update sets the columns present in both tables (minus
by); inserted rows receive the column default (NULLwhen none is defined) in target columns the source lacks.MERGE ... RETURNINGis not implemented by DuckLake.DuckLake currently supports one update/delete action per MERGE statement.
delete_missing = TRUEtogether with awhen_matchedaction therefore runs as a MERGE plus aDELETEof the unmatched rows, wrapped in one transaction (a single snapshot). When you already opened a transaction, both statements simply join it.When several source rows share a
bykey, matched updates apply in an unspecified order; keep source keys unique.
See also
rows_upsert() for the plain update-or-insert case;
get_table_changes() to inspect what a merge did.
Other row operations:
rows_delete(),
rows_insert(),
rows_update(),
rows_upsert()
Examples
if (FALSE) { # \dontrun{
# Update only when the source amount is higher; insert new ids
merge_into(
get_ducklake_table("sales"), new_sales, by = "sls_id",
matched_condition = "source.sls_amt > target.sls_amt"
)
# Synchronize to a staging table: upsert + drop rows gone from the source
merge_into(
"sales", staging_sales, by = "sls_id",
delete_missing = TRUE
)
# Remove rows flagged in the source
merge_into(
"sales", withdrawn, by = "sls_id",
when_matched = "delete", when_not_matched = "nothing"
)
} # }
