Updates rows of x that match a row of y (by the by columns) and
inserts the rows of y that have no match, as one atomic MERGE INTO
statement – a single snapshot, with row lineage preserved in the change
feed. A wrapper around dplyr::rows_upsert() with in_place = TRUE as the
default, since DuckLake is designed for in-place modifications.
Arguments
- x
Target table (from get_ducklake_table())
- y
Data frame with rows to update or insert
- by
Column(s) to match on. Defaults to the first column of
y, with a message.- copy
Whether to copy y to the same source as x (default TRUE)
- in_place
Whether to modify the table in place (default TRUE for DuckLake)
- ...
Additional arguments passed to dplyr::rows_upsert()
Details
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.
DuckLake-specific behavior
DuckLake tables have no primary keys or unique constraints, so the usual
database upsert (INSERT ... ON CONFLICT) does not apply. This method
instead generates MERGE INTO, matching on the by columns:
When
ycovers a subset ofx's columns, matched rows are updated in those columns only; inserted rows receive the column's default value in the remaining columns (NULLwhen the table defines none). Note the difference from data-frame upserts, which fill withNA.When
yhas only thebycolumns, there is nothing to update and the call inserts the unmatched rows.Rows where a
bycolumn isNULLnever match and are always inserted.When several rows of
yshare the samebykey, each matched update applies in an unspecified order; the last write wins. Keepykeys unique.
See also
merge_into() for conditional merge clauses and source-driven
deletes.
Other row operations:
merge_into(),
rows_delete(),
rows_insert(),
rows_update()
Examples
if (FALSE) { # \dontrun{
# Update id 2, insert id 4 - one statement, one snapshot
rows_upsert(
get_ducklake_table("my_table"),
data.frame(id = c(2, 4), value = c("updated", "new")),
by = "id"
)
} # }
