Skip to main content

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