Skip to main content

oxgraph_db/
schema.rs

1//! Declarative catalog schema.
2//!
3//! Declare a store's roles, labels, relation types, typed property keys,
4//! equality indexes, and graph projections by name ONCE as a [`Schema`], then
5//! apply it idempotently with [`Writer::apply_schema`](crate::Writer::apply_schema)
6//! (register-or-get) or resolve an already-bootstrapped store with
7//! [`Db::bind`](crate::Db::bind). Both return a [`Bound`] handle bag whose typed
8//! getters hand back [`Key<T>`](crate::Key)/[`EqualityIndex<T>`](crate::EqualityIndex)
9//! and plain id newtypes — so a consumer never threads name→id maps by hand and a
10//! name typo is a typed [`DbError::UnknownName`].
11
12use std::collections::BTreeMap;
13
14use crate::{
15    DbError, IndexId, LabelId, ProjectionId, PropertyFamily, PropertyKeyId, PropertyType,
16    RelationTypeId, RoleId,
17    typed::{EqualityIndex, Key, ValueType},
18};
19
20/// A binary graph-projection declaration over a set of relation types.
21///
22/// # Performance
23///
24/// Cloning is `O(relation-type count + name lengths)`.
25#[derive(Clone, Debug)]
26pub struct GraphProjectionSpec {
27    /// Projection name.
28    pub(crate) name: String,
29    /// Relation-type names whose edges the projection traverses.
30    pub(crate) relation_types: Vec<String>,
31    /// Source incidence role name.
32    pub(crate) source_role: String,
33    /// Target incidence role name.
34    pub(crate) target_role: String,
35}
36
37/// A declarative catalog schema, applied once to obtain a [`Bound`] handle bag.
38///
39/// Built fluently; declaration order is preserved (and irrelevant to the result).
40///
41/// # Performance
42///
43/// Each builder method is `O(1)` amortized; cloning is `O(declared item count)`.
44#[derive(Clone, Debug, Default)]
45pub struct Schema {
46    /// Declared role names.
47    pub(crate) roles: Vec<String>,
48    /// Declared label names.
49    pub(crate) labels: Vec<String>,
50    /// Declared relation-type names.
51    pub(crate) relation_types: Vec<String>,
52    /// Declared property keys: `(name, family, value type)`.
53    pub(crate) keys: Vec<(String, PropertyFamily, PropertyType)>,
54    /// Declared equality indexes: `(index name, indexed key name)`.
55    pub(crate) equality_indexes: Vec<(String, String)>,
56    /// Declared graph projections.
57    pub(crate) graph_projections: Vec<GraphProjectionSpec>,
58}
59
60impl Schema {
61    /// Starts an empty schema.
62    ///
63    /// # Performance
64    ///
65    /// This function is `O(1)`.
66    #[must_use]
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Declares an incidence role.
72    ///
73    /// # Performance
74    ///
75    /// This method is `O(name length)`.
76    #[must_use]
77    pub fn role(mut self, name: &str) -> Self {
78        self.roles.push(name.to_owned());
79        self
80    }
81
82    /// Declares an element/relation label.
83    ///
84    /// # Performance
85    ///
86    /// This method is `O(name length)`.
87    #[must_use]
88    pub fn label(mut self, name: &str) -> Self {
89        self.labels.push(name.to_owned());
90        self
91    }
92
93    /// Declares a relation type.
94    ///
95    /// # Performance
96    ///
97    /// This method is `O(name length)`.
98    #[must_use]
99    pub fn relation_type(mut self, name: &str) -> Self {
100        self.relation_types.push(name.to_owned());
101        self
102    }
103
104    /// Declares a typed property key in `family` whose value type is `T`.
105    ///
106    /// # Performance
107    ///
108    /// This method is `O(name length)`.
109    #[must_use]
110    pub fn key<T: ValueType>(mut self, name: &str, family: PropertyFamily) -> Self {
111        self.keys.push((name.to_owned(), family, T::TYPE));
112        self
113    }
114
115    /// Declares an equality index named `name` over the property key `key`.
116    ///
117    /// # Performance
118    ///
119    /// This method is `O(name lengths)`.
120    #[must_use]
121    pub fn equality_index(mut self, name: &str, key: &str) -> Self {
122        self.equality_indexes
123            .push((name.to_owned(), key.to_owned()));
124        self
125    }
126
127    /// Declares a binary graph projection over `relation_types`, traversing from
128    /// `source_role` to `target_role`.
129    ///
130    /// # Performance
131    ///
132    /// This method is `O(relation-type count + name lengths)`.
133    #[must_use]
134    pub fn graph_projection(
135        mut self,
136        name: &str,
137        relation_types: &[&str],
138        source_role: &str,
139        target_role: &str,
140    ) -> Self {
141        self.graph_projections.push(GraphProjectionSpec {
142            name: name.to_owned(),
143            relation_types: relation_types
144                .iter()
145                .map(|name| (*name).to_owned())
146                .collect(),
147            source_role: source_role.to_owned(),
148            target_role: target_role.to_owned(),
149        });
150        self
151    }
152}
153
154/// Resolved name→id handles for an applied [`Schema`].
155///
156/// The single place names resolve to ids; replaces hand-threaded `*Id` maps.
157/// Typed getters return [`Key<T>`]/[`EqualityIndex<T>`] (the value type is
158/// checked against the declaration); a missing name is a [`DbError::UnknownName`].
159///
160/// # Performance
161///
162/// Cloning is `O(handle count)`; every getter is `O(log n + name length)`.
163#[derive(Clone, Debug, Default)]
164pub struct Bound {
165    /// Role ids by name.
166    pub(crate) roles: BTreeMap<String, RoleId>,
167    /// Label ids by name.
168    pub(crate) labels: BTreeMap<String, LabelId>,
169    /// Relation-type ids by name.
170    pub(crate) relation_types: BTreeMap<String, RelationTypeId>,
171    /// Property key ids (with declared value type) by name.
172    pub(crate) keys: BTreeMap<String, (PropertyKeyId, PropertyType)>,
173    /// Equality index ids (with indexed key value type) by name.
174    pub(crate) equality_indexes: BTreeMap<String, (IndexId, PropertyType)>,
175    /// Projection ids by name.
176    pub(crate) projections: BTreeMap<String, ProjectionId>,
177}
178
179impl Bound {
180    /// Resolves a role handle.
181    ///
182    /// # Errors
183    ///
184    /// [`DbError::UnknownName`] when the role was not declared/bound.
185    ///
186    /// # Performance
187    ///
188    /// This method is `O(log n + name length)`.
189    pub fn role(&self, name: &str) -> Result<RoleId, DbError> {
190        self.roles.get(name).copied().ok_or_else(|| {
191            DbError::Catalog(crate::error::CatalogError::UnknownName {
192                kind: "role",
193                name: name.to_owned(),
194            })
195        })
196    }
197
198    /// Resolves a label handle.
199    ///
200    /// # Errors
201    ///
202    /// [`DbError::UnknownName`] when the label was not declared/bound.
203    ///
204    /// # Performance
205    ///
206    /// This method is `O(log n + name length)`.
207    pub fn label(&self, name: &str) -> Result<LabelId, DbError> {
208        self.labels.get(name).copied().ok_or_else(|| {
209            DbError::Catalog(crate::error::CatalogError::UnknownName {
210                kind: "label",
211                name: name.to_owned(),
212            })
213        })
214    }
215
216    /// Resolves a relation-type handle.
217    ///
218    /// # Errors
219    ///
220    /// [`DbError::UnknownName`] when the relation type was not declared/bound.
221    ///
222    /// # Performance
223    ///
224    /// This method is `O(log n + name length)`.
225    pub fn relation_type(&self, name: &str) -> Result<RelationTypeId, DbError> {
226        self.relation_types.get(name).copied().ok_or_else(|| {
227            DbError::Catalog(crate::error::CatalogError::UnknownName {
228                kind: "relation type",
229                name: name.to_owned(),
230            })
231        })
232    }
233
234    /// Resolves a typed property-key handle, checking the value type matches `T`.
235    ///
236    /// # Errors
237    ///
238    /// [`DbError::UnknownName`] when absent, or [`DbError::SchemaConflict`] when
239    /// the declared value type differs from `T`.
240    ///
241    /// # Performance
242    ///
243    /// This method is `O(log n + name length)`.
244    pub fn key<T: ValueType>(&self, name: &str) -> Result<Key<T>, DbError> {
245        let (id, value_type) = self.keys.get(name).copied().ok_or_else(|| {
246            DbError::Catalog(crate::error::CatalogError::UnknownName {
247                kind: "property key",
248                name: name.to_owned(),
249            })
250        })?;
251        if value_type == T::TYPE {
252            Ok(Key::from_id(id))
253        } else {
254            Err(DbError::Catalog(
255                crate::error::CatalogError::SchemaConflict {
256                    name: name.to_owned(),
257                    reason: "property key value type differs from the requested typed handle",
258                },
259            ))
260        }
261    }
262
263    /// Resolves a typed equality-index handle, checking the indexed key's value
264    /// type matches `T`.
265    ///
266    /// # Errors
267    ///
268    /// [`DbError::UnknownName`] when absent, or [`DbError::SchemaConflict`] on a
269    /// value-type mismatch.
270    ///
271    /// # Performance
272    ///
273    /// This method is `O(log n + name length)`.
274    pub fn equality_index<T: ValueType>(&self, name: &str) -> Result<EqualityIndex<T>, DbError> {
275        let (id, value_type) = self.equality_indexes.get(name).copied().ok_or_else(|| {
276            DbError::Catalog(crate::error::CatalogError::UnknownName {
277                kind: "index",
278                name: name.to_owned(),
279            })
280        })?;
281        if value_type == T::TYPE {
282            Ok(EqualityIndex::from_id(id))
283        } else {
284            Err(DbError::Catalog(
285                crate::error::CatalogError::SchemaConflict {
286                    name: name.to_owned(),
287                    reason: "equality index value type differs from the requested typed handle",
288                },
289            ))
290        }
291    }
292
293    /// Resolves a projection handle.
294    ///
295    /// # Errors
296    ///
297    /// [`DbError::UnknownName`] when the projection was not declared/bound.
298    ///
299    /// # Performance
300    ///
301    /// This method is `O(log n + name length)`.
302    pub fn projection(&self, name: &str) -> Result<ProjectionId, DbError> {
303        self.projections.get(name).copied().ok_or_else(|| {
304            DbError::Catalog(crate::error::CatalogError::UnknownName {
305                kind: "projection",
306                name: name.to_owned(),
307            })
308        })
309    }
310}