diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index b17e6c2bb..4c3c1bc6f 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -1631,9 +1631,21 @@ impl BaseTable for RemoteTable { let (request_id, response) = self.send(request, true).await?; let response = self.check_table_response(&request_id, response).await?; + // Servers report the creation time either as an RFC 3339 `timestamp` + // (direct-table path) or as `timestamp_millis` in milliseconds since + // epoch (namespace-backed path), and may omit `metadata`. + #[derive(Deserialize)] + struct VersionEntry { + version: u64, + timestamp: Option>, + timestamp_millis: Option, + #[serde(default)] + metadata: std::collections::BTreeMap, + } + #[derive(Deserialize)] struct ListVersionsResponse { - versions: Vec, + versions: Vec, } let body = response.text().await.err_to_http(request_id.clone())?; @@ -1644,11 +1656,37 @@ impl BaseTable for RemoteTable { err, body ) .into(), - request_id, + request_id: request_id.clone(), status_code: None, })?; - Ok(body.versions) + body.versions + .into_iter() + .map(|entry| { + let timestamp = entry + .timestamp + .or_else(|| { + entry + .timestamp_millis + .and_then(DateTime::::from_timestamp_millis) + }) + .ok_or_else(|| Error::Http { + source: format!( + "list_versions response for version {} has neither a valid \ + `timestamp` nor `timestamp_millis` field", + entry.version + ) + .into(), + request_id: request_id.clone(), + status_code: None, + })?; + Ok(Version { + version: entry.version, + timestamp, + metadata: entry.metadata, + }) + }) + .collect() } async fn schema(&self) -> Result { @@ -5246,6 +5284,56 @@ mod tests { // assert_eq!(versions, expected); } + /// Namespace-backed servers report `timestamp_millis` instead of + /// `timestamp`, and may omit `metadata` entirely. + #[tokio::test] + async fn test_list_versions_timestamp_millis() { + let table = Table::new_with_handler("my_table", |request| { + assert_eq!(request.method(), "POST"); + assert_eq!(request.url().path(), "/v1/table/my_table/version/list/"); + + let response_body = serde_json::json!({ + "versions": [ + { + "version": 1, + "manifest_path": "path/to/_versions/1.manifest", + "timestamp_millis": 1704067200000i64, + }, + { + "version": 2, + "manifest_path": "path/to/_versions/2.manifest", + "timestamp_millis": 1706745600000i64, + "metadata": {"key": "value"}, + }, + ] + }); + let response_body = serde_json::to_string(&response_body).unwrap(); + + http::Response::builder() + .status(200) + .body(response_body) + .unwrap() + }); + + let versions = table.list_versions().await.unwrap(); + assert_eq!(versions.len(), 2); + assert_eq!(versions[0].version, 1); + assert_eq!( + versions[0].timestamp, + "2024-01-01T00:00:00Z".parse::>().unwrap() + ); + assert!(versions[0].metadata.is_empty()); + assert_eq!(versions[1].version, 2); + assert_eq!( + versions[1].timestamp, + "2024-02-01T00:00:00Z".parse::>().unwrap() + ); + assert_eq!( + versions[1].metadata.get("key").map(String::as_str), + Some("value") + ); + } + #[tokio::test] async fn test_index_stats() { let table = Table::new_with_handler("my_table", |request| {