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}