From ecf4555cfde8103c143349bb27a6dd5813e3b674 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Sat, 22 Aug 2026 00:53:28 +0800 Subject: [PATCH] fix(remote): fence refresh submissions after add_columns (#4007) A remote backfill submission validates its target column against a table snapshot, but it did not carry the existing read-after-write freshness headers. Immediately after `add_columns`, a stale query node could therefore reject the newly committed column. Route backfill submission through the remote table read fence so it carries the version returned by the preceding write. The shared remote submission path gives synchronous and asynchronous client surfaces the same freshness guarantee. --- rust/lancedb/src/remote/table.rs | 42 ++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 1e5691554..14b869d90 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -2866,8 +2866,7 @@ impl BaseTable for RemoteTable { let mut body = serde_json::json!({ "column": column }); self.apply_branch_body(&mut body); let request = self - .client - .post(&format!("/v1/table/{}/backfill_column", self.identifier)) + .post_read(&format!("/v1/table/{}/backfill_column", self.identifier)) .json(&body); let (request_id, response) = self.send(request, true).await?; let response = self.check_table_response(&request_id, response).await?; @@ -6823,6 +6822,45 @@ mod tests { ); } + #[tokio::test] + async fn test_refresh_submission_uses_add_columns_version_fence() { + let table = Table::new_with_handler("my_table", |request| match request.url().path() { + "/v1/table/my_table/describe/" => simple_describe_response(), + "/v1/table/my_table/add_columns/" => http::Response::builder() + .status(200) + .body(r#"{"version": 7}"#.to_string()) + .unwrap(), + "/v1/table/my_table/backfill_column" => { + let min_version = request + .headers() + .get("x-lancedb-min-version") + .and_then(|value| value.to_str().ok()); + if min_version != Some("7") { + return http::Response::builder() + .status(400) + .body(r#"{"error":"Column not found: doubled"}"#.to_string()) + .unwrap(); + } + http::Response::builder() + .status(202) + .body(r#"{"job_id": "j-43"}"#.to_string()) + .unwrap() + } + path => panic!("unexpected request: {path}"), + }); + + let result = table + .add_columns() + .computed("doubled", "a * 2") + .execute() + .await + .unwrap(); + assert_eq!(result.version, 7); + + let job = table.refresh_column_async("doubled").await.unwrap(); + assert_eq!(job.id(), Some("j-43")); + } + /// The gate's reproducer: after a successful wait, a same-handle read /// must carry a freshness baseline so a stale server cache cannot serve /// the pre-backfill snapshot.