From cf81b6419fc4b44f8bddfbe1988d60b36c391af9 Mon Sep 17 00:00:00 2001 From: Wyatt Alt Date: Mon, 2 Mar 2026 08:37:14 -0800 Subject: [PATCH] feat: add `num_deleted_rows` to delete result (#3077) --- docs/src/js/interfaces/DeleteResult.md | 8 ++++++ nodejs/src/table.rs | 2 ++ python/python/lancedb/table.py | 8 +++--- python/src/table.rs | 7 ++++- rust/lancedb/src/remote/table.rs | 5 +++- rust/lancedb/src/table/delete.rs | 37 ++++++++++++++++++++++++-- 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/docs/src/js/interfaces/DeleteResult.md b/docs/src/js/interfaces/DeleteResult.md index f2b18633f..a11ac9663 100644 --- a/docs/src/js/interfaces/DeleteResult.md +++ b/docs/src/js/interfaces/DeleteResult.md @@ -8,6 +8,14 @@ ## Properties +### numDeletedRows + +```ts +numDeletedRows: number; +``` + +*** + ### version ```ts diff --git a/nodejs/src/table.rs b/nodejs/src/table.rs index ed39b9bae..69788e1e9 100644 --- a/nodejs/src/table.rs +++ b/nodejs/src/table.rs @@ -753,12 +753,14 @@ impl From for AddResult { #[napi(object)] pub struct DeleteResult { + pub num_deleted_rows: i64, pub version: i64, } impl From for DeleteResult { fn from(value: lancedb::table::DeleteResult) -> Self { Self { + num_deleted_rows: value.num_deleted_rows as i64, version: value.version as i64, } } diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 029d234f9..e19449cc8 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -1331,7 +1331,7 @@ class Table(ABC): 1 2 [3.0, 4.0] 2 3 [5.0, 6.0] >>> table.delete("x = 2") - DeleteResult(version=2) + DeleteResult(num_deleted_rows=1, version=2) >>> table.to_pandas() x vector 0 1 [1.0, 2.0] @@ -1345,7 +1345,7 @@ class Table(ABC): >>> to_remove '1, 5' >>> table.delete(f"x IN ({to_remove})") - DeleteResult(version=3) + DeleteResult(num_deleted_rows=1, version=3) >>> table.to_pandas() x vector 0 3 [5.0, 6.0] @@ -4215,7 +4215,7 @@ class AsyncTable: 1 2 [3.0, 4.0] 2 3 [5.0, 6.0] >>> table.delete("x = 2") - DeleteResult(version=2) + DeleteResult(num_deleted_rows=1, version=2) >>> table.to_pandas() x vector 0 1 [1.0, 2.0] @@ -4229,7 +4229,7 @@ class AsyncTable: >>> to_remove '1, 5' >>> table.delete(f"x IN ({to_remove})") - DeleteResult(version=3) + DeleteResult(num_deleted_rows=1, version=3) >>> table.to_pandas() x vector 0 3 [5.0, 6.0] diff --git a/python/src/table.rs b/python/src/table.rs index 31279847d..3766f3f19 100644 --- a/python/src/table.rs +++ b/python/src/table.rs @@ -112,19 +112,24 @@ impl From for AddResult { #[pyclass(get_all)] #[derive(Clone, Debug)] pub struct DeleteResult { + pub num_deleted_rows: u64, pub version: u64, } #[pymethods] impl DeleteResult { pub fn __repr__(&self) -> String { - format!("DeleteResult(version={})", self.version) + format!( + "DeleteResult(num_deleted_rows={}, version={})", + self.num_deleted_rows, self.version + ) } } impl From for DeleteResult { fn from(result: lancedb::table::DeleteResult) -> Self { Self { + num_deleted_rows: result.num_deleted_rows, version: result.version, } } diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 181adeff4..aa3c3fe97 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -1227,7 +1227,10 @@ impl BaseTable for RemoteTable { let body = response.text().await.err_to_http(request_id.clone())?; if body.trim().is_empty() { // Backward compatible with old servers - return Ok(DeleteResult { version: 0 }); + return Ok(DeleteResult { + num_deleted_rows: 0, + version: 0, + }); } let delete_response: DeleteResult = serde_json::from_str(&body).map_err(|e| Error::Http { diff --git a/rust/lancedb/src/table/delete.rs b/rust/lancedb/src/table/delete.rs index dbc24ceb7..add8e9e50 100644 --- a/rust/lancedb/src/table/delete.rs +++ b/rust/lancedb/src/table/delete.rs @@ -7,6 +7,9 @@ use crate::Result; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct DeleteResult { + /// The number of rows that were deleted. + #[serde(default)] + pub num_deleted_rows: u64, // The commit version associated with the operation. // A version of `0` indicates compatibility with legacy servers that do not return /// a commit version. @@ -20,10 +23,14 @@ pub struct DeleteResult { pub(crate) async fn execute_delete(table: &NativeTable, predicate: &str) -> Result { table.dataset.ensure_mutable()?; let mut dataset = (*table.dataset.get().await?).clone(); - dataset.delete(predicate).await?; + let delete_result = dataset.delete(predicate).await?; + let num_deleted_rows = delete_result.num_deleted_rows; let version = dataset.version().version; table.dataset.update(dataset); - Ok(DeleteResult { version }) + Ok(DeleteResult { + num_deleted_rows, + version, + }) } #[cfg(test)] @@ -108,6 +115,32 @@ mod tests { assert_eq!(current_schema, original_schema); } + #[tokio::test] + async fn test_delete_returns_num_deleted_rows() { + let conn = connect("memory://").execute().await.unwrap(); + let batch = record_batch!(("id", Int32, [1, 2, 3, 4, 5])).unwrap(); + let table = conn + .create_table("test_num_deleted", batch) + .execute() + .await + .unwrap(); + + // Delete 2 rows (id > 3 means id=4 and id=5) + let result = table.delete("id > 3").await.unwrap(); + assert_eq!(result.num_deleted_rows, 2); + assert_eq!(table.count_rows(None).await.unwrap(), 3); + + // Delete 0 rows (no rows match) + let result = table.delete("id > 100").await.unwrap(); + assert_eq!(result.num_deleted_rows, 0); + assert_eq!(table.count_rows(None).await.unwrap(), 3); + + // Delete remaining rows + let result = table.delete("true").await.unwrap(); + assert_eq!(result.num_deleted_rows, 3); + assert_eq!(table.count_rows(None).await.unwrap(), 0); + } + #[tokio::test] async fn test_delete_false_increments_version() { let conn = connect("memory://").execute().await.unwrap();