From f79dc017c4d189d000dd3d6aaffb8cc38eebd2ee Mon Sep 17 00:00:00 2001 From: Adityaj0 <93090622+Adityaj0@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:49:52 -0700 Subject: [PATCH] fix: when_not_matched_by_source_delete() doesn't reset a previously-set condition (#3771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `LanceMergeInsertBuilder.when_not_matched_by_source_delete()` didn't clear a previously-set condition when called again with no argument (or a different condition type). Per the docstring, `condition=None` means "delete all unmatched rows," but if the builder had already been configured with a string/Expr condition, a later no-arg call left the stale condition in place instead of widening the delete to unconditional. Fixes #3767 ## Change Each call now unconditionally sets both `_when_not_matched_by_source_condition` and `_when_not_matched_by_source_condition_expr` (one to the new value, the other to `None`), so the latest call always wins — consistent with every other setter on this builder (e.g. `when_matched_update_all(where=...)`). ## Test plan - [x] New regression test `test_merge_insert_by_source_delete_reconfigure` in `python/python/tests/test_table.py` - [x] `uv run --extra tests pytest python/tests/test_table.py::test_merge_insert_by_source_delete_reconfigure python/tests/test_table.py::test_merge_insert_by_source_delete_expr python/tests/test_table.py::test_merge_insert_by_source_delete_expr_async -vv` — 3 passed - [x] `uv run --extra dev ruff format` / `ruff check` — clean Co-authored-by: Claude Sonnet 5 --- python/python/lancedb/merge.py | 4 +++- python/python/tests/test_table.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/python/python/lancedb/merge.py b/python/python/lancedb/merge.py index 3debed97b..115a47e2d 100644 --- a/python/python/lancedb/merge.py +++ b/python/python/lancedb/merge.py @@ -92,8 +92,10 @@ class LanceMergeInsertBuilder(object): self._when_not_matched_by_source_delete = True if isinstance(condition, Expr): self._when_not_matched_by_source_condition_expr = condition._inner - elif condition is not None: + self._when_not_matched_by_source_condition = None + else: self._when_not_matched_by_source_condition = condition + self._when_not_matched_by_source_condition_expr = None return self def use_index(self, use_index: bool) -> LanceMergeInsertBuilder: diff --git a/python/python/tests/test_table.py b/python/python/tests/test_table.py index a355cf101..542c944c8 100644 --- a/python/python/tests/test_table.py +++ b/python/python/tests/test_table.py @@ -2364,6 +2364,29 @@ def test_merge_insert_by_source_delete_expr(mem_db: DBConnection): assert table.to_arrow().sort_by("a") == expected +def test_merge_insert_by_source_delete_reconfigure(mem_db: DBConnection): + # Calling when_not_matched_by_source_delete() again with no condition must + # widen the delete to unconditional, not keep the earlier condition around. + table = mem_db.create_table( + "my_table", + data=pa.table({"a": [1, 2, 3], "b": ["a", "b", "c"]}), + ) + new_data = pa.table({"a": [2, 4], "b": ["x", "z"]}) + + merge_insert_res = ( + table.merge_insert("a") + .when_matched_update_all() + .when_not_matched_insert_all() + .when_not_matched_by_source_delete("a > 2") + .when_not_matched_by_source_delete() + .execute(new_data) + ) + assert merge_insert_res.num_deleted_rows == 2 + + expected = pa.table({"a": [2, 4], "b": ["x", "z"]}) + assert table.to_arrow().sort_by("a") == expected + + @pytest.mark.asyncio async def test_merge_insert_by_source_delete_expr_async( mem_db_async: AsyncConnection,