Skip to main content

lance_csr_catalog/namespace/
directory.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use async_trait::async_trait;
4use lance_namespace::models::{DescribeTableRequest, DescribeTableResponse};
5use lance_namespace::{Error as NamespaceError, LanceNamespace, Result};
6
7/// A namespace that resolves table names relative to a base directory or URI.
8#[derive(Debug, Clone)]
9pub struct DirNamespace {
10    base_uri: String,
11}
12
13impl DirNamespace {
14    /// Create a new directory-backed namespace rooted at `base_uri`.
15    ///
16    /// The URI is normalized so that it does not end with a trailing slash.
17    pub fn new(base_uri: impl Into<String>) -> Self {
18        let uri = base_uri.into();
19        let clean_uri = uri.trim_end_matches('/').to_string();
20        Self {
21            base_uri: clean_uri,
22        }
23    }
24
25    /// Return the normalized base URI.
26    pub fn base_uri(&self) -> &str {
27        &self.base_uri
28    }
29}
30
31#[async_trait]
32impl LanceNamespace for DirNamespace {
33    fn namespace_id(&self) -> String {
34        format!("DirNamespace {{ base_uri: '{}' }}", self.base_uri)
35    }
36
37    async fn describe_table(&self, request: DescribeTableRequest) -> Result<DescribeTableResponse> {
38        let id = request.id.ok_or_else(|| {
39            NamespaceError::invalid_input(
40                "DirNamespace requires the table identifier to be provided",
41            )
42        })?;
43
44        if id.len() != 1 {
45            return Err(NamespaceError::invalid_input(format!(
46                "DirNamespace expects identifiers with a single component, got {:?}",
47                id
48            )));
49        }
50
51        let table_name = &id[0];
52        let location = format!("{}/{}.lance", self.base_uri, table_name);
53
54        let mut response = DescribeTableResponse::new();
55        response.location = Some(location);
56        response.storage_options = None;
57        Ok(response)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[tokio::test]
66    async fn describe_table_returns_clean_location() {
67        let namespace = DirNamespace::new("s3://bucket/path/");
68        let mut request = DescribeTableRequest::new();
69        request.id = Some(vec!["users".to_string()]);
70
71        let response = namespace.describe_table(request).await.unwrap();
72        assert_eq!(
73            response.location.as_deref(),
74            Some("s3://bucket/path/users.lance")
75        );
76    }
77
78    #[tokio::test]
79    async fn describe_table_rejects_missing_identifier() {
80        let namespace = DirNamespace::new("file:///tmp");
81        let request = DescribeTableRequest::new();
82
83        let err = namespace.describe_table(request).await.unwrap_err();
84        assert!(
85            err.to_string()
86                .contains("DirNamespace requires the table identifier"),
87            "unexpected error: {err}"
88        );
89    }
90
91    #[tokio::test]
92    async fn describe_table_rejects_multi_component_identifier() {
93        let namespace = DirNamespace::new("memory://namespace");
94        let mut request = DescribeTableRequest::new();
95        request.id = Some(vec!["foo".into(), "bar".into()]);
96
97        let err = namespace.describe_table(request).await.unwrap_err();
98        assert!(
99            err.to_string()
100                .contains("expects identifiers with a single component"),
101            "unexpected error: {err}"
102        );
103    }
104}