Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/lance-graph",
"crates/lance-graph-catalog",
]
exclude = [
"python",
Expand Down
21 changes: 21 additions & 0 deletions crates/lance-graph-catalog/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "lance-graph-catalog"
version = "0.5.2"
edition = "2021"
license = "Apache-2.0"
authors = ["Lance Devs <dev@lancedb.com>"]
repository = "https://github.com/lancedb/lance-graph"
readme = "README.md"
description = "Catalog and namespace utilities for Lance graph"
keywords = ["lance", "graph", "catalog", "namespace"]
categories = ["database", "data-structures", "science"]

[dependencies]
arrow-schema = "56.2"
async-trait = "0.1"
datafusion = { version = "50.3", default-features = false }
lance-namespace = "1.0.1"
snafu = "0.8"

[dev-dependencies]
tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] }
3 changes: 3 additions & 0 deletions crates/lance-graph-catalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Lance Graph Catalog

Catalog and namespace utilities shared by the Lance graph query engine.
10 changes: 10 additions & 0 deletions crates/lance-graph-catalog/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors

//! Catalog and namespace utilities for Lance Graph.

pub mod namespace;
pub mod source_catalog;

pub use namespace::DirNamespace;
pub use source_catalog::{GraphSourceCatalog, InMemoryCatalog, SimpleTableSource};
109 changes: 109 additions & 0 deletions crates/lance-graph-catalog/src/namespace/directory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0

use async_trait::async_trait;
use lance_namespace::models::{DescribeTableRequest, DescribeTableResponse};
use lance_namespace::{Error as NamespaceError, LanceNamespace, Result};
use snafu::location;

/// A namespace that resolves table names relative to a base directory or URI.
#[derive(Debug, Clone)]
pub struct DirNamespace {
base_uri: String,
}

impl DirNamespace {
/// Create a new directory-backed namespace rooted at `base_uri`.
///
/// The URI is normalized so that it does not end with a trailing slash.
pub fn new(base_uri: impl Into<String>) -> Self {
let uri = base_uri.into();
let clean_uri = uri.trim_end_matches('/').to_string();
Self {
base_uri: clean_uri,
}
}

/// Return the normalized base URI.
pub fn base_uri(&self) -> &str {
&self.base_uri
}
}

#[async_trait]
impl LanceNamespace for DirNamespace {
fn namespace_id(&self) -> String {
format!("DirNamespace {{ base_uri: '{}' }}", self.base_uri)
}

async fn describe_table(&self, request: DescribeTableRequest) -> Result<DescribeTableResponse> {
let id = request.id.ok_or_else(|| {
NamespaceError::invalid_input(
"DirNamespace requires the table identifier to be provided",
location!(),
)
})?;

if id.len() != 1 {
return Err(NamespaceError::invalid_input(
format!(
"DirNamespace expects identifiers with a single component, got {:?}",
id
),
location!(),
));
}

let table_name = &id[0];
let location = format!("{}/{}.lance", self.base_uri, table_name);

let mut response = DescribeTableResponse::new();
response.location = Some(location);
response.storage_options = None;
Ok(response)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn describe_table_returns_clean_location() {
let namespace = DirNamespace::new("s3://bucket/path/");
let mut request = DescribeTableRequest::new();
request.id = Some(vec!["users".to_string()]);

let response = namespace.describe_table(request).await.unwrap();
assert_eq!(
response.location.as_deref(),
Some("s3://bucket/path/users.lance")
);
}

#[tokio::test]
async fn describe_table_rejects_missing_identifier() {
let namespace = DirNamespace::new("file:///tmp");
let request = DescribeTableRequest::new();

let err = namespace.describe_table(request).await.unwrap_err();
assert!(
err.to_string()
.contains("DirNamespace requires the table identifier"),
"unexpected error: {err}"
);
}

#[tokio::test]
async fn describe_table_rejects_multi_component_identifier() {
let namespace = DirNamespace::new("memory://namespace");
let mut request = DescribeTableRequest::new();
request.id = Some(vec!["foo".into(), "bar".into()]);

let err = namespace.describe_table(request).await.unwrap_err();
assert!(
err.to_string()
.contains("expects identifiers with a single component"),
"unexpected error: {err}"
);
}
}
3 changes: 3 additions & 0 deletions crates/lance-graph-catalog/src/namespace/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod directory;

pub use directory::DirNamespace;
101 changes: 101 additions & 0 deletions crates/lance-graph-catalog/src/source_catalog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors

//! Context-free source catalog for DataFusion logical planning.

use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;

use arrow_schema::{Schema, SchemaRef};
use datafusion::logical_expr::TableSource;

/// A minimal catalog to resolve node labels and relationship types to logical table sources.
pub trait GraphSourceCatalog: Send + Sync {
fn node_source(&self, label: &str) -> Option<Arc<dyn TableSource>>;
fn relationship_source(&self, rel_type: &str) -> Option<Arc<dyn TableSource>>;
}

/// A simple in-memory catalog useful for tests and bootstrap wiring.
pub struct InMemoryCatalog {
node_sources: HashMap<String, Arc<dyn TableSource>>,
rel_sources: HashMap<String, Arc<dyn TableSource>>,
}

impl InMemoryCatalog {
pub fn new() -> Self {
Self {
node_sources: HashMap::new(),
rel_sources: HashMap::new(),
}
}

pub fn with_node_source(
mut self,
label: impl Into<String>,
source: Arc<dyn TableSource>,
) -> Self {
// Normalize key to lowercase for case-insensitive lookup
self.node_sources
.insert(label.into().to_lowercase(), source);
self
}

pub fn with_relationship_source(
mut self,
rel_type: impl Into<String>,
source: Arc<dyn TableSource>,
) -> Self {
// Normalize key to lowercase for case-insensitive lookup
self.rel_sources
.insert(rel_type.into().to_lowercase(), source);
self
}
}

impl Default for InMemoryCatalog {
fn default() -> Self {
Self::new()
}
}

impl GraphSourceCatalog for InMemoryCatalog {
/// Get node source with case-insensitive label lookup
///
/// Note: Keys are stored as lowercase, so this is an O(1) operation.
fn node_source(&self, label: &str) -> Option<Arc<dyn TableSource>> {
self.node_sources.get(&label.to_lowercase()).cloned()
}

/// Get relationship source with case-insensitive type lookup
///
/// Note: Keys are stored as lowercase, so this is an O(1) operation.
fn relationship_source(&self, rel_type: &str) -> Option<Arc<dyn TableSource>> {
self.rel_sources.get(&rel_type.to_lowercase()).cloned()
}
}

/// A trivial logical table source with a fixed schema.
pub struct SimpleTableSource {
schema: SchemaRef,
}

impl SimpleTableSource {
pub fn new(schema: SchemaRef) -> Self {
Self { schema }
}
pub fn empty() -> Self {
Self {
schema: Arc::new(Schema::empty()),
}
}
}

impl TableSource for SimpleTableSource {
fn as_any(&self) -> &dyn Any {
self
}
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
}
2 changes: 1 addition & 1 deletion crates/lance-graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ datafusion-expr = "50.3"
datafusion-sql = "50.3"
datafusion-functions-aggregate = "50.3"
futures = "0.3"
async-trait = "0.1"
lance-graph-catalog = { path = "../lance-graph-catalog", version = "0.5.2" }
lance = "1.0.0"
lance-linalg = "1.0.0"
lance-namespace = "1.0.1"
Expand Down
7 changes: 6 additions & 1 deletion crates/lance-graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,12 @@ Basic aggregations like `COUNT` are supported. Optional matches and subqueries a
- `config` – Graph configuration types and builders.
- `query` – High level `CypherQuery` API and runtime.
- `error` – `GraphError` and result helpers.
- `source_catalog` – Helpers for looking up table metadata.
- `namespace` – Namespace helpers (re-exported from `lance-graph-catalog`).
- `source_catalog` – Catalog helpers for looking up table metadata (re-exported from `lance-graph-catalog`).

`lance-graph` re-exports the catalog and namespace types from the `lance-graph-catalog` crate for
API compatibility. You can depend on `lance-graph-catalog` directly if you only need catalog or
namespace utilities.

## Error Handling

Expand Down
Loading
Loading