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, encode_composite_key,
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 directed hypergraph-projection declaration over a set of relation types.
38///
39/// Unlike a [`GraphProjectionSpec`], participation is many-sided: a relation's
40/// incidences whose role is in `source_roles` form the source-side participants
41/// and those in `target_roles` the target-side participants, so one relation can
42/// carry an arbitrary number of participants per side.
43///
44/// # Performance
45///
46/// Cloning is `O(relation-type count + role count + name lengths)`.
47#[derive(Clone, Debug)]
48pub struct HypergraphProjectionSpec {
49    /// Projection name.
50    pub(crate) name: String,
51    /// Relation-type names whose relations the projection treats as hyperedges.
52    pub(crate) relation_types: Vec<String>,
53    /// Incidence role names whose participants are source-side.
54    pub(crate) source_roles: Vec<String>,
55    /// Incidence role names whose participants are target-side.
56    pub(crate) target_roles: Vec<String>,
57}
58
59/// A declarative catalog schema, applied once to obtain a [`Bound`] handle bag.
60///
61/// Built fluently; declaration order is preserved (and irrelevant to the result).
62///
63/// # Performance
64///
65/// Each builder method is `O(1)` amortized; cloning is `O(declared item count)`.
66#[derive(Clone, Debug, Default)]
67pub struct Schema {
68    /// Declared role names.
69    pub(crate) roles: Vec<String>,
70    /// Declared label names.
71    pub(crate) labels: Vec<String>,
72    /// Declared relation-type names.
73    pub(crate) relation_types: Vec<String>,
74    /// Declared property keys: `(name, family, value type)`.
75    pub(crate) keys: Vec<(String, PropertyFamily, PropertyType)>,
76    /// Declared equality indexes: `(index name, indexed key name)`.
77    pub(crate) equality_indexes: Vec<(String, String)>,
78    /// Declared graph projections.
79    pub(crate) graph_projections: Vec<GraphProjectionSpec>,
80    /// Declared hypergraph projections.
81    pub(crate) hypergraph_projections: Vec<HypergraphProjectionSpec>,
82}
83
84impl Schema {
85    /// Starts an empty schema.
86    ///
87    /// # Performance
88    ///
89    /// This function is `O(1)`.
90    #[must_use]
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Declares an incidence role.
96    ///
97    /// # Performance
98    ///
99    /// This method is `O(name length)`.
100    #[must_use]
101    pub fn role(mut self, name: &str) -> Self {
102        self.roles.push(name.to_owned());
103        self
104    }
105
106    /// Declares an element/relation label.
107    ///
108    /// # Performance
109    ///
110    /// This method is `O(name length)`.
111    #[must_use]
112    pub fn label(mut self, name: &str) -> Self {
113        self.labels.push(name.to_owned());
114        self
115    }
116
117    /// Declares a relation type.
118    ///
119    /// # Performance
120    ///
121    /// This method is `O(name length)`.
122    #[must_use]
123    pub fn relation_type(mut self, name: &str) -> Self {
124        self.relation_types.push(name.to_owned());
125        self
126    }
127
128    /// Declares a typed property key in `family` whose value type is `T`.
129    ///
130    /// # Performance
131    ///
132    /// This method is `O(name length)`.
133    #[must_use]
134    pub fn key<T: ValueType>(mut self, name: &str, family: PropertyFamily) -> Self {
135        self.keys.push((name.to_owned(), family, T::TYPE));
136        self
137    }
138
139    /// Declares an equality index named `name` over the property key `key`.
140    ///
141    /// # Performance
142    ///
143    /// This method is `O(name lengths)`.
144    #[must_use]
145    pub fn equality_index(mut self, name: &str, key: &str) -> Self {
146        self.equality_indexes
147            .push((name.to_owned(), key.to_owned()));
148        self
149    }
150
151    /// Declares a binary graph projection over `relation_types`, traversing from
152    /// `source_role` to `target_role`.
153    ///
154    /// # Performance
155    ///
156    /// This method is `O(relation-type count + name lengths)`.
157    #[must_use]
158    pub fn graph_projection(
159        mut self,
160        name: &str,
161        relation_types: &[&str],
162        source_role: &str,
163        target_role: &str,
164    ) -> Self {
165        self.graph_projections.push(GraphProjectionSpec {
166            name: name.to_owned(),
167            relation_types: relation_types
168                .iter()
169                .map(|name| (*name).to_owned())
170                .collect(),
171            source_role: source_role.to_owned(),
172            target_role: target_role.to_owned(),
173        });
174        self
175    }
176
177    /// Declares a directed hypergraph projection over `relation_types`, treating
178    /// incidences in `source_roles` as source-side participants and those in
179    /// `target_roles` as target-side participants.
180    ///
181    /// The role-name slices must be non-empty: a hypergraph projection
182    /// materializes only relations carrying at least one source and one target
183    /// participant (enforced when
184    /// [`Reader::hypergraph_projection`](crate::Reader::hypergraph_projection) builds the
185    /// view).
186    ///
187    /// # Performance
188    ///
189    /// This method is `O(relation-type count + role count + name lengths)`.
190    #[must_use]
191    pub fn hypergraph_projection(
192        mut self,
193        name: &str,
194        relation_types: &[&str],
195        source_roles: &[&str],
196        target_roles: &[&str],
197    ) -> Self {
198        self.hypergraph_projections.push(HypergraphProjectionSpec {
199            name: name.to_owned(),
200            relation_types: relation_types
201                .iter()
202                .map(|name| (*name).to_owned())
203                .collect(),
204            source_roles: source_roles.iter().map(|name| (*name).to_owned()).collect(),
205            target_roles: target_roles.iter().map(|name| (*name).to_owned()).collect(),
206        });
207        self
208    }
209
210    /// Returns a content fingerprint of the declared schema shape: every role,
211    /// label, relation type, typed key, equality index, and projection. Equal
212    /// shapes hash equally regardless of declaration order; any change to a
213    /// declared item changes the fingerprint.
214    ///
215    /// A consumer can fold this into its own index-staleness digest so a schema
216    /// change forces a rebuild even when no source content changed. This is a
217    /// non-cryptographic content hash, not a cross-compiler-version stability
218    /// guarantee.
219    ///
220    /// # Performance
221    ///
222    /// This method is `O(declared items × name lengths + items × log items)`.
223    #[must_use]
224    pub fn fingerprint(&self) -> u64 {
225        use std::hash::{Hash, Hasher};
226
227        let mut items = Vec::new();
228        for role in &self.roles {
229            items.push(encode_composite_key(&["role", role]));
230        }
231        for label in &self.labels {
232            items.push(encode_composite_key(&["label", label]));
233        }
234        for relation_type in &self.relation_types {
235            items.push(encode_composite_key(&["rtype", relation_type]));
236        }
237        for (name, family, value_type) in &self.keys {
238            items.push(encode_composite_key(&[
239                "key",
240                name,
241                &format!("{family:?}"),
242                &format!("{value_type:?}"),
243            ]));
244        }
245        for (name, key) in &self.equality_indexes {
246            items.push(encode_composite_key(&["eqidx", name, key]));
247        }
248        for projection in &self.graph_projections {
249            items.push(encode_composite_key(&[
250                "gproj",
251                &projection.name,
252                &projection.source_role,
253                &projection.target_role,
254                &join_sorted(&projection.relation_types),
255            ]));
256        }
257        for projection in &self.hypergraph_projections {
258            items.push(encode_composite_key(&[
259                "hproj",
260                &projection.name,
261                &join_sorted(&projection.relation_types),
262                &join_sorted(&projection.source_roles),
263                &join_sorted(&projection.target_roles),
264            ]));
265        }
266        items.sort();
267
268        let mut hasher = std::collections::hash_map::DefaultHasher::new();
269        items.len().hash(&mut hasher);
270        for item in &items {
271            item.hash(&mut hasher);
272        }
273        hasher.finish()
274    }
275}
276
277/// Joins a set of names into one order-independent, injective token used inside
278/// [`Schema::fingerprint`].
279fn join_sorted(names: &[String]) -> String {
280    let mut sorted: Vec<&str> = names.iter().map(String::as_str).collect();
281    sorted.sort_unstable();
282    encode_composite_key(&sorted)
283}
284
285/// Resolved name→id handles for an applied [`Schema`].
286///
287/// The single place names resolve to ids; replaces hand-threaded `*Id` maps.
288/// Typed getters return [`Key<T>`]/[`EqualityIndex<T>`] (the value type is
289/// checked against the declaration); a missing name is a [`DbError::UnknownName`].
290///
291/// # Performance
292///
293/// Cloning is `O(handle count)`; every getter is `O(log n + name length)`.
294#[derive(Clone, Debug, Default)]
295pub struct Bound {
296    /// Role ids by name.
297    pub(crate) roles: BTreeMap<String, RoleId>,
298    /// Label ids by name.
299    pub(crate) labels: BTreeMap<String, LabelId>,
300    /// Relation-type ids by name.
301    pub(crate) relation_types: BTreeMap<String, RelationTypeId>,
302    /// Property key ids (with declared value type) by name.
303    pub(crate) keys: BTreeMap<String, (PropertyKeyId, PropertyType)>,
304    /// Equality index ids (with indexed key value type) by name.
305    pub(crate) equality_indexes: BTreeMap<String, (IndexId, PropertyType)>,
306    /// Projection ids by name.
307    pub(crate) projections: BTreeMap<String, ProjectionId>,
308}
309
310impl Bound {
311    /// Resolves a role handle.
312    ///
313    /// # Errors
314    ///
315    /// [`DbError::UnknownName`] when the role was not declared/bound.
316    ///
317    /// # Performance
318    ///
319    /// This method is `O(log n + name length)`.
320    pub fn role(&self, name: &str) -> Result<RoleId, DbError> {
321        self.roles.get(name).copied().ok_or_else(|| {
322            DbError::Catalog(crate::error::CatalogError::UnknownName {
323                kind: "role",
324                name: name.to_owned(),
325            })
326        })
327    }
328
329    /// Resolves a label handle.
330    ///
331    /// # Errors
332    ///
333    /// [`DbError::UnknownName`] when the label was not declared/bound.
334    ///
335    /// # Performance
336    ///
337    /// This method is `O(log n + name length)`.
338    pub fn label(&self, name: &str) -> Result<LabelId, DbError> {
339        self.labels.get(name).copied().ok_or_else(|| {
340            DbError::Catalog(crate::error::CatalogError::UnknownName {
341                kind: "label",
342                name: name.to_owned(),
343            })
344        })
345    }
346
347    /// Resolves a relation-type handle.
348    ///
349    /// # Errors
350    ///
351    /// [`DbError::UnknownName`] when the relation type was not declared/bound.
352    ///
353    /// # Performance
354    ///
355    /// This method is `O(log n + name length)`.
356    pub fn relation_type(&self, name: &str) -> Result<RelationTypeId, DbError> {
357        self.relation_types.get(name).copied().ok_or_else(|| {
358            DbError::Catalog(crate::error::CatalogError::UnknownName {
359                kind: "relation type",
360                name: name.to_owned(),
361            })
362        })
363    }
364
365    /// Resolves a typed property-key handle, checking the value type matches `T`.
366    ///
367    /// # Errors
368    ///
369    /// [`DbError::UnknownName`] when absent, or [`DbError::SchemaConflict`] when
370    /// the declared value type differs from `T`.
371    ///
372    /// # Performance
373    ///
374    /// This method is `O(log n + name length)`.
375    pub fn key<T: ValueType>(&self, name: &str) -> Result<Key<T>, DbError> {
376        let (id, value_type) = self.keys.get(name).copied().ok_or_else(|| {
377            DbError::Catalog(crate::error::CatalogError::UnknownName {
378                kind: "property key",
379                name: name.to_owned(),
380            })
381        })?;
382        if value_type == T::TYPE {
383            Ok(Key::from_id(id))
384        } else {
385            Err(DbError::Catalog(
386                crate::error::CatalogError::SchemaConflict {
387                    name: name.to_owned(),
388                    reason: "property key value type differs from the requested typed handle",
389                },
390            ))
391        }
392    }
393
394    /// Resolves a typed equality-index handle, checking the indexed key's value
395    /// type matches `T`.
396    ///
397    /// # Errors
398    ///
399    /// [`DbError::UnknownName`] when absent, or [`DbError::SchemaConflict`] on a
400    /// value-type mismatch.
401    ///
402    /// # Performance
403    ///
404    /// This method is `O(log n + name length)`.
405    pub fn equality_index<T: ValueType>(&self, name: &str) -> Result<EqualityIndex<T>, DbError> {
406        let (id, value_type) = self.equality_indexes.get(name).copied().ok_or_else(|| {
407            DbError::Catalog(crate::error::CatalogError::UnknownName {
408                kind: "index",
409                name: name.to_owned(),
410            })
411        })?;
412        if value_type == T::TYPE {
413            Ok(EqualityIndex::from_id(id))
414        } else {
415            Err(DbError::Catalog(
416                crate::error::CatalogError::SchemaConflict {
417                    name: name.to_owned(),
418                    reason: "equality index value type differs from the requested typed handle",
419                },
420            ))
421        }
422    }
423
424    /// Resolves a projection handle.
425    ///
426    /// # Errors
427    ///
428    /// [`DbError::UnknownName`] when the projection was not declared/bound.
429    ///
430    /// # Performance
431    ///
432    /// This method is `O(log n + name length)`.
433    pub fn projection(&self, name: &str) -> Result<ProjectionId, DbError> {
434        self.projections.get(name).copied().ok_or_else(|| {
435            DbError::Catalog(crate::error::CatalogError::UnknownName {
436                kind: "projection",
437                name: name.to_owned(),
438            })
439        })
440    }
441}