Skip to main content

genegraph_storage/
catalog.rs

1//! M-C1: catalog contract for table discovery and registration (#75).
2//!
3//! The [`TableDescriptor`] shape and the [`Catalog`] operations mirror the
4//! **Lance Namespace** client spec whose Apache Polaris implementation maps
5//! onto Polaris' **Generic Table API** (`name`, `format`, `base-location`,
6//! `properties`); see the Polaris + Lance integration announcement
7//! (2026-01-06). Per decision D1 in #75 this is a standards-hygiene
8//! contract only: no catalog server client is provided here.
9//!
10//! [`LocalRegistry`] implements the contract over the existing JSON metadata
11//! registry (a `GeneMetadata` instance), mirroring Lance Namespace's
12//! *Directory* semantics: every `*.lance` dataset registered in the
13//! metadata's files map is a table.
14
15use std::collections::BTreeMap;
16use std::path::PathBuf;
17
18use crate::StorageError;
19use crate::StorageResult;
20use crate::metadata::{FileInfo, GeneMetadata};
21
22/// Generic Table API-compatible table descriptor.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct TableDescriptor {
25    /// Unique table name within the registry (the metadata files-map key).
26    pub name: String,
27    /// Table format; always `lance` for this crate.
28    pub format: String,
29    /// Location of the table root (a `*.lance` dataset directory).
30    pub base_location: PathBuf,
31    /// Free-form key/value properties (filetype, shape, ...).
32    pub properties: BTreeMap<String, String>,
33}
34
35/// Registry of Lance tables backing a storage instance.
36pub trait Catalog {
37    /// All tables known to this registry.
38    fn list_tables(&self) -> StorageResult<Vec<TableDescriptor>>;
39    /// Whether a table with `name` is registered.
40    fn table_exists(&self, name: &str) -> StorageResult<bool>;
41    /// Descriptor for `name`, or `StorageError::Invalid` if absent.
42    fn describe_table(&self, name: &str) -> StorageResult<TableDescriptor>;
43    /// Adds `table` to the registry (replaces an existing entry of the same
44    /// name, mirroring Generic Table create-or-replace semantics).
45    fn register_table(&mut self, table: TableDescriptor) -> StorageResult<()>;
46    /// Removes `name` from the registry without deleting the dataset.
47    fn deregister_table(&mut self, name: &str) -> StorageResult<()>;
48}
49
50/// [`Catalog`] implementation over the existing JSON metadata registry.
51///
52/// The registry wraps an owned [`GeneMetadata`]; mutate through the
53/// [`Catalog`] methods, then persist the instance with
54/// `StorageBackend::save_metadata` (or take it back with
55/// [`LocalRegistry::into_metadata`]).
56#[derive(Debug, Clone)]
57pub struct LocalRegistry {
58    metadata: GeneMetadata,
59    base: PathBuf,
60}
61
62impl LocalRegistry {
63    /// Wraps `metadata`, resolving table locations under `base`.
64    pub fn new(metadata: GeneMetadata, base: PathBuf) -> Self {
65        Self { metadata, base }
66    }
67
68    /// The wrapped metadata (for persistence).
69    pub fn metadata(&self) -> &GeneMetadata {
70        &self.metadata
71    }
72
73    /// Consumes the registry, returning the mutated metadata.
74    pub fn into_metadata(self) -> GeneMetadata {
75        self.metadata
76    }
77
78    fn descriptor(&self, key: &str, info: &FileInfo) -> TableDescriptor {
79        let mut properties = BTreeMap::new();
80        properties.insert("filetype".to_string(), info.filetype.clone());
81        properties.insert("storage_format".to_string(), info.storage_format.clone());
82        properties.insert("rows".to_string(), info.rows.to_string());
83        properties.insert("cols".to_string(), info.cols.to_string());
84        if let Some(nnz) = info.nnz {
85            properties.insert("nnz".to_string(), nnz.to_string());
86        }
87        if let Some(size) = info.size_bytes {
88            properties.insert("size_bytes".to_string(), size.to_string());
89        }
90        TableDescriptor {
91            name: key.to_string(),
92            format: "lance".to_string(),
93            base_location: self.base.join(&info.filename),
94            properties,
95        }
96    }
97}
98
99impl Catalog for LocalRegistry {
100    fn list_tables(&self) -> StorageResult<Vec<TableDescriptor>> {
101        let mut tables: Vec<TableDescriptor> = self
102            .metadata
103            .files
104            .iter()
105            .map(|(key, info)| self.descriptor(key, info))
106            .collect();
107        tables.sort_by(|a, b| a.name.cmp(&b.name));
108        Ok(tables)
109    }
110
111    fn table_exists(&self, name: &str) -> StorageResult<bool> {
112        Ok(self.metadata.files.contains_key(name))
113    }
114
115    fn describe_table(&self, name: &str) -> StorageResult<TableDescriptor> {
116        let info =
117            self.metadata.files.get(name).ok_or_else(|| {
118                StorageError::Invalid(format!("table '{name}' is not registered"))
119            })?;
120        Ok(self.descriptor(name, info))
121    }
122
123    fn register_table(&mut self, table: TableDescriptor) -> StorageResult<()> {
124        if table.format != "lance" {
125            return Err(StorageError::UnsupportedFormat(format!(
126                "unsupported table format '{}'",
127                table.format
128            )));
129        }
130        let prop = |key: &str| table.properties.get(key).cloned();
131        let parse = |key: &str| -> StorageResult<Option<usize>> {
132            prop(key)
133                .map(|v| {
134                    v.parse::<usize>().map_err(|_| {
135                        StorageError::Invalid(format!(
136                            "property '{key}' = '{v}' is not a valid integer"
137                        ))
138                    })
139                })
140                .transpose()
141        };
142        let info = FileInfo::new(
143            table
144                .base_location
145                .file_name()
146                .map(|n| n.to_string_lossy().to_string())
147                .unwrap_or_else(|| format!("{}_{}.lance", self.metadata.name_id, table.name)),
148            prop("filetype").as_deref().unwrap_or("vector"),
149            (parse("rows")?.unwrap_or(0), parse("cols")?.unwrap_or(0)),
150            parse("nnz")?,
151            None,
152        )?;
153        self.metadata.files.insert(table.name, info);
154        Ok(())
155    }
156
157    fn deregister_table(&mut self, name: &str) -> StorageResult<()> {
158        if self.metadata.files.remove(name).is_none() {
159            return Err(StorageError::Invalid(format!(
160                "table '{name}' is not registered"
161            )));
162        }
163        Ok(())
164    }
165}