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}