diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 5fefabeb2..29b6b698d 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -2791,9 +2791,10 @@ impl BaseTable for RemoteTable { } async fn index_stats(&self, index_name: &str) -> Result> { + let encoded_name = urlencoding::encode(index_name); let mut request = self.post_read(&format!( - "/v1/table/{}/index/{}/stats/", - self.identifier, index_name + "/v1/table/{}/index/{encoded_name}/stats/", + self.identifier )); let version = self.current_version().await; let mut body = serde_json::json!({ "version": version }); @@ -2820,9 +2821,10 @@ impl BaseTable for RemoteTable { } async fn drop_index(&self, index_name: &str) -> Result<()> { + let encoded_name = urlencoding::encode(index_name); let request = self.apply_branch_query(self.client.post(&format!( - "/v1/table/{}/index/{}/drop/", - self.identifier, index_name + "/v1/table/{}/index/{encoded_name}/drop/", + self.identifier ))); let (request_id, response) = self.send(request, true).await?; if response.status() == StatusCode::NOT_FOUND { @@ -2835,9 +2837,10 @@ impl BaseTable for RemoteTable { } async fn prewarm_index(&self, index_name: &str) -> Result<()> { + let encoded_name = urlencoding::encode(index_name); let request = self.client.post(&format!( - "/v1/table/{}/index/{}/prewarm/", - self.identifier, index_name + "/v1/table/{}/index/{encoded_name}/prewarm/", + self.identifier )); let (request_id, response) = self.send(request, true).await?; if response.status() == StatusCode::NOT_FOUND { @@ -6489,6 +6492,41 @@ mod tests { assert!(matches!(e, Error::IndexNotFound { .. })); } + /// Index names are unvalidated, so reserved characters must be + /// percent-encoded or they restructure the request path. + #[tokio::test] + async fn test_per_index_paths_encode_reserved_characters() { + const NAME: &str = "my/index?a#b c"; + const PREFIX: &str = "/v1/table/my_table/index/my%2Findex%3Fa%23b%20c"; + + let table = Table::new_with_handler("my_table", |request| { + assert_eq!(request.url().path(), format!("{PREFIX}/stats/")); + let body = serde_json::json!({ + "num_indexed_rows": 1, + "num_unindexed_rows": 0, + "index_type": "IVF_PQ", + "distance_type": "l2" + }); + http::Response::builder() + .status(200) + .body(serde_json::to_string(&body).unwrap()) + .unwrap() + }); + assert!(table.index_stats(NAME).await.unwrap().is_some()); + + let table = Table::new_with_handler("my_table", |request| { + assert_eq!(request.url().path(), format!("{PREFIX}/drop/")); + http::Response::builder().status(200).body("{}").unwrap() + }); + table.drop_index(NAME).await.unwrap(); + + let table = Table::new_with_handler("my_table", |request| { + assert_eq!(request.url().path(), format!("{PREFIX}/prewarm/")); + http::Response::builder().status(200).body("{}").unwrap() + }); + table.prewarm_index(NAME).await.unwrap(); + } + #[tokio::test] async fn test_set_lsm_write_spec_unsharded() { let table = Table::new_with_handler("my_table", |request| {