Skip to content

Commit b6e29a7

Browse files
authored
feat(graph): split catalog crate (#130)
## Summary - add `lance-graph-catalog` crate for namespace/catalog utilities - re-export catalog/namespace types from `lance-graph` for API compatibility - update workspace and crate docs to reference the new catalog crate ## Testing - /home/user/.cargo/bin/cargo check - /home/user/.cargo/bin/cargo test --all
1 parent 8721cf5 commit b6e29a7

12 files changed

Lines changed: 270 additions & 208 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"crates/lance-graph",
4+
"crates/lance-graph-catalog",
45
]
56
exclude = [
67
"python",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "lance-graph-catalog"
3+
version = "0.5.2"
4+
edition = "2021"
5+
license = "Apache-2.0"
6+
authors = ["Lance Devs <dev@lancedb.com>"]
7+
repository = "https://github.com/lancedb/lance-graph"
8+
readme = "README.md"
9+
description = "Catalog and namespace utilities for Lance graph"
10+
keywords = ["lance", "graph", "catalog", "namespace"]
11+
categories = ["database", "data-structures", "science"]
12+
13+
[dependencies]
14+
arrow-schema = "56.2"
15+
async-trait = "0.1"
16+
datafusion = { version = "50.3", default-features = false }
17+
lance-namespace = "1.0.1"
18+
snafu = "0.8"
19+
20+
[dev-dependencies]
21+
tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Lance Graph Catalog
2+
3+
Catalog and namespace utilities shared by the Lance graph query engine.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright The Lance Authors
3+
4+
//! Catalog and namespace utilities for Lance Graph.
5+
6+
pub mod namespace;
7+
pub mod source_catalog;
8+
9+
pub use namespace::DirNamespace;
10+
pub use source_catalog::{GraphSourceCatalog, InMemoryCatalog, SimpleTableSource};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
use async_trait::async_trait;
4+
use lance_namespace::models::{DescribeTableRequest, DescribeTableResponse};
5+
use lance_namespace::{Error as NamespaceError, LanceNamespace, Result};
6+
use snafu::location;
7+
8+
/// A namespace that resolves table names relative to a base directory or URI.
9+
#[derive(Debug, Clone)]
10+
pub struct DirNamespace {
11+
base_uri: String,
12+
}
13+
14+
impl 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]
33+
impl 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)]
67+
mod 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+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod directory;
2+
3+
pub use directory::DirNamespace;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright The Lance Authors
3+
4+
//! Context-free source catalog for DataFusion logical planning.
5+
6+
use std::any::Any;
7+
use std::collections::HashMap;
8+
use std::sync::Arc;
9+
10+
use arrow_schema::{Schema, SchemaRef};
11+
use datafusion::logical_expr::TableSource;
12+
13+
/// A minimal catalog to resolve node labels and relationship types to logical table sources.
14+
pub trait GraphSourceCatalog: Send + Sync {
15+
fn node_source(&self, label: &str) -> Option<Arc<dyn TableSource>>;
16+
fn relationship_source(&self, rel_type: &str) -> Option<Arc<dyn TableSource>>;
17+
}
18+
19+
/// A simple in-memory catalog useful for tests and bootstrap wiring.
20+
pub struct InMemoryCatalog {
21+
node_sources: HashMap<String, Arc<dyn TableSource>>,
22+
rel_sources: HashMap<String, Arc<dyn TableSource>>,
23+
}
24+
25+
impl InMemoryCatalog {
26+
pub fn new() -> Self {
27+
Self {
28+
node_sources: HashMap::new(),
29+
rel_sources: HashMap::new(),
30+
}
31+
}
32+
33+
pub fn with_node_source(
34+
mut self,
35+
label: impl Into<String>,
36+
source: Arc<dyn TableSource>,
37+
) -> Self {
38+
// Normalize key to lowercase for case-insensitive lookup
39+
self.node_sources
40+
.insert(label.into().to_lowercase(), source);
41+
self
42+
}
43+
44+
pub fn with_relationship_source(
45+
mut self,
46+
rel_type: impl Into<String>,
47+
source: Arc<dyn TableSource>,
48+
) -> Self {
49+
// Normalize key to lowercase for case-insensitive lookup
50+
self.rel_sources
51+
.insert(rel_type.into().to_lowercase(), source);
52+
self
53+
}
54+
}
55+
56+
impl Default for InMemoryCatalog {
57+
fn default() -> Self {
58+
Self::new()
59+
}
60+
}
61+
62+
impl GraphSourceCatalog for InMemoryCatalog {
63+
/// Get node source with case-insensitive label lookup
64+
///
65+
/// Note: Keys are stored as lowercase, so this is an O(1) operation.
66+
fn node_source(&self, label: &str) -> Option<Arc<dyn TableSource>> {
67+
self.node_sources.get(&label.to_lowercase()).cloned()
68+
}
69+
70+
/// Get relationship source with case-insensitive type lookup
71+
///
72+
/// Note: Keys are stored as lowercase, so this is an O(1) operation.
73+
fn relationship_source(&self, rel_type: &str) -> Option<Arc<dyn TableSource>> {
74+
self.rel_sources.get(&rel_type.to_lowercase()).cloned()
75+
}
76+
}
77+
78+
/// A trivial logical table source with a fixed schema.
79+
pub struct SimpleTableSource {
80+
schema: SchemaRef,
81+
}
82+
83+
impl SimpleTableSource {
84+
pub fn new(schema: SchemaRef) -> Self {
85+
Self { schema }
86+
}
87+
pub fn empty() -> Self {
88+
Self {
89+
schema: Arc::new(Schema::empty()),
90+
}
91+
}
92+
}
93+
94+
impl TableSource for SimpleTableSource {
95+
fn as_any(&self) -> &dyn Any {
96+
self
97+
}
98+
fn schema(&self) -> SchemaRef {
99+
self.schema.clone()
100+
}
101+
}

crates/lance-graph/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ datafusion-expr = "50.3"
2828
datafusion-sql = "50.3"
2929
datafusion-functions-aggregate = "50.3"
3030
futures = "0.3"
31-
async-trait = "0.1"
31+
lance-graph-catalog = { path = "../lance-graph-catalog", version = "0.5.2" }
3232
lance = "1.0.0"
3333
lance-linalg = "1.0.0"
3434
lance-namespace = "1.0.1"

crates/lance-graph/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,12 @@ Basic aggregations like `COUNT` are supported. Optional matches and subqueries a
120120
- `config` – Graph configuration types and builders.
121121
- `query` – High level `CypherQuery` API and runtime.
122122
- `error``GraphError` and result helpers.
123-
- `source_catalog` – Helpers for looking up table metadata.
123+
- `namespace` – Namespace helpers (re-exported from `lance-graph-catalog`).
124+
- `source_catalog` – Catalog helpers for looking up table metadata (re-exported from `lance-graph-catalog`).
125+
126+
`lance-graph` re-exports the catalog and namespace types from the `lance-graph-catalog` crate for
127+
API compatibility. You can depend on `lance-graph-catalog` directly if you only need catalog or
128+
namespace utilities.
124129

125130
## Error Handling
126131

0 commit comments

Comments
 (0)