oxgraph_db/database.rs
1//! Embedded `OxGraph` database engine API.
2
3use std::{
4 cell::RefCell,
5 collections::BTreeMap,
6 path::{Path, PathBuf},
7 rc::Rc,
8};
9
10use crate::{
11 Catalog, CommitSeq, DbError, ElementId, ElementRecord, GraphProjection, HypergraphProjection,
12 IncidenceId, IncidenceRecord, IndexId, LabelId, PreparedQuery, ProjectionDefinition,
13 ProjectionId, PropertyKeyId, PropertySubject, PropertyType, PropertyValue, QueryLanguage,
14 QueryResult, RelationId, RelationRecord, RelationTypeId, RoleId, TransactionId,
15 catalog::{IndexDefinition, PropertyFamily},
16 projection::{self},
17 state::DatabaseState,
18 storage::{self, StoredDatabase},
19 traversal::{self, TraversalOptions, TraversalResult},
20};
21
22/// Lookup input for a cataloged index.
23///
24/// This type makes index lookup shape explicit: membership indexes accept
25/// [`IndexLookup::All`], single-property indexes accept scalar equality or
26/// range inputs, and composite equality indexes accept an ordered value tuple.
27///
28/// # Performance
29///
30/// Copying this value is `O(1)`.
31#[derive(Clone, Copy, Debug)]
32pub enum IndexLookup<'value> {
33 /// Lookup every subject represented by a membership-style index.
34 All,
35 /// Lookup one scalar equality value.
36 Equal(&'value PropertyValue),
37 /// Lookup one inclusive scalar range.
38 Range {
39 /// Inclusive lower bound.
40 min: &'value PropertyValue,
41 /// Inclusive upper bound.
42 max: &'value PropertyValue,
43 },
44 /// Lookup one ordered composite equality tuple.
45 CompositeEqual(&'value [PropertyValue]),
46}
47
48/// Open OXGDB database handle.
49///
50/// # Performance
51///
52/// Moving a handle is `O(n)` for the owned in-memory database state.
53pub struct Database {
54 /// Root database directory.
55 path: PathBuf,
56 /// Visible canonical state.
57 state: DatabaseState,
58 /// Last visible commit sequence.
59 visible_commit_seq: CommitSeq,
60 /// Last writer transaction ID burned by this handle.
61 ///
62 /// Rollback burns are session-local. Committed and empty-committed IDs are
63 /// durable because commit publication persists the current high-water mark.
64 last_transaction_id: TransactionId,
65}
66
67impl Database {
68 /// Creates a new empty OXGDB database at `path`.
69 ///
70 /// # Errors
71 ///
72 /// Returns [`DbError::AlreadyExists`] when a greenfield store already
73 /// exists, or [`DbError::Io`] when creation fails.
74 ///
75 /// # Performance
76 ///
77 /// This function is `O(path length + empty store bytes)`.
78 pub fn create(path: impl AsRef<Path>) -> Result<Self, DbError> {
79 let path = path.as_ref().to_path_buf();
80 if storage::store_path(&path).exists() {
81 return Err(DbError::AlreadyExists);
82 }
83 let stored = StoredDatabase::empty();
84 storage::write_store(&path, &stored)?;
85 Ok(Self::from_stored(path, stored))
86 }
87
88 /// Opens an existing OXGDB database.
89 ///
90 /// # Errors
91 ///
92 /// Returns [`DbError`] when the store is missing, malformed, or
93 /// semantically invalid.
94 ///
95 /// # Performance
96 ///
97 /// This function is `O(serialized database bytes)`.
98 pub fn open(path: impl AsRef<Path>) -> Result<Self, DbError> {
99 let path = path.as_ref().to_path_buf();
100 let stored = storage::read_store(&path)?;
101 Ok(Self::from_stored(path, stored))
102 }
103
104 /// Validates an OXGDB database at `path`.
105 ///
106 /// # Errors
107 ///
108 /// Returns [`DbError`] when store or semantic validation fails.
109 ///
110 /// # Performance
111 ///
112 /// This function is `O(serialized database bytes)`.
113 pub fn validate_path(path: impl AsRef<Path>) -> Result<(), DbError> {
114 storage::validate_store(path.as_ref())
115 }
116
117 /// Rewrites the store in the current greenfield format.
118 ///
119 /// # Errors
120 ///
121 /// Returns [`DbError`] when validation, encoding, writing, or replacement
122 /// fails.
123 ///
124 /// # Performance
125 ///
126 /// This method is `O(serialized database bytes)`.
127 pub fn compact(&mut self) -> Result<(), DbError> {
128 self.state.validate()?;
129 storage::write_store(&self.path, &self.to_stored())
130 }
131
132 /// Validates this open handle's store and in-memory state.
133 ///
134 /// # Errors
135 ///
136 /// Returns [`DbError`] when validation fails.
137 ///
138 /// # Performance
139 ///
140 /// This method is `O(serialized database bytes)`.
141 pub fn validate(&self) -> Result<(), DbError> {
142 self.state.validate()?;
143 storage::validate_store(&self.path)
144 }
145
146 /// Returns operational status for this handle.
147 ///
148 /// # Performance
149 ///
150 /// This method is `O(1)`.
151 #[must_use]
152 pub fn status(&self) -> DatabaseStatus {
153 DatabaseStatus {
154 visible_commit_seq: self.visible_commit_seq,
155 last_transaction_id: self.last_transaction_id,
156 element_count: self.state.element_count(),
157 relation_count: self.state.relation_count(),
158 incidence_count: self.state.incidence_count(),
159 catalog: self.catalog_summary(),
160 }
161 }
162
163 /// Returns a catalog-size summary.
164 ///
165 /// # Performance
166 ///
167 /// This method is `O(catalog entry count)`.
168 #[must_use]
169 pub fn catalog_summary(&self) -> CatalogSummary {
170 CatalogSummary::from_catalog(self.state.catalog())
171 }
172
173 /// Starts a read transaction pinned to the current visible generation.
174 ///
175 /// # Performance
176 ///
177 /// This method is `O(database state size)` because readers own immutable
178 /// snapshots.
179 #[must_use]
180 pub fn begin_read(&self) -> ReadTransaction {
181 ReadTransaction {
182 pin: ReadPin {
183 visible_commit_seq: self.visible_commit_seq,
184 last_transaction_id: self.last_transaction_id,
185 },
186 state: self.state.clone(),
187 graph_projections: RefCell::new(BTreeMap::new()),
188 hypergraph_projections: RefCell::new(BTreeMap::new()),
189 }
190 }
191
192 /// Starts the single writer transaction.
193 ///
194 /// # Errors
195 ///
196 /// Returns [`DbError::TransactionIdOverflow`] when writer IDs are
197 /// exhausted.
198 ///
199 /// # Performance
200 ///
201 /// This method is `O(database state size)` because writes stage an owned
202 /// copy.
203 pub fn begin_write(&mut self) -> Result<WriteTransaction<'_>, DbError> {
204 let transaction_id = self
205 .last_transaction_id
206 .checked_next()
207 .ok_or(DbError::TransactionIdOverflow)?;
208 let state = self.state.clone();
209 self.last_transaction_id = transaction_id;
210 Ok(WriteTransaction {
211 database: self,
212 state,
213 transaction_id,
214 dirty: false,
215 })
216 }
217
218 /// Prepares a query against the current catalog.
219 ///
220 /// # Errors
221 ///
222 /// Returns [`DbError`] when parsing or semantic analysis fails.
223 ///
224 /// # Performance
225 ///
226 /// This method is `O(query length + catalog lookup cost)`.
227 pub fn prepare(&self, language: QueryLanguage, query: &str) -> Result<PreparedQuery, DbError> {
228 PreparedQuery::prepare(language, query, &self.state)
229 }
230
231 /// Builds a handle from stored state.
232 fn from_stored(path: PathBuf, stored: StoredDatabase) -> Self {
233 Self {
234 path,
235 state: stored.state,
236 visible_commit_seq: stored.commit_seq,
237 last_transaction_id: stored.transaction_id,
238 }
239 }
240
241 /// Converts this handle into the durable payload.
242 fn to_stored(&self) -> StoredDatabase {
243 StoredDatabase {
244 commit_seq: self.visible_commit_seq,
245 transaction_id: self.last_transaction_id,
246 state: self.state.clone(),
247 }
248 }
249
250 /// Allocates the next commit sequence.
251 fn next_commit_seq(&self) -> Result<CommitSeq, DbError> {
252 self.visible_commit_seq
253 .checked_next()
254 .ok_or(DbError::CommitSeqOverflow)
255 }
256}
257
258/// Snapshot of database status.
259///
260/// # Performance
261///
262/// Copying and comparing status is `O(1)`.
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub struct DatabaseStatus {
265 /// Last visible commit sequence.
266 pub visible_commit_seq: CommitSeq,
267 /// Last writer transaction ID burned by this handle.
268 ///
269 /// This value is durable after commit and session-local after rollback.
270 pub last_transaction_id: TransactionId,
271 /// Visible element count.
272 pub element_count: usize,
273 /// Visible relation count.
274 pub relation_count: usize,
275 /// Visible incidence count.
276 pub incidence_count: usize,
277 /// Catalog-size summary.
278 pub catalog: CatalogSummary,
279}
280
281/// Catalog-size summary.
282///
283/// # Performance
284///
285/// Copying and comparing are `O(1)`.
286#[derive(Clone, Copy, Debug, Eq, PartialEq)]
287pub struct CatalogSummary {
288 /// Role count.
289 pub role_count: usize,
290 /// Label count.
291 pub label_count: usize,
292 /// Relation type count.
293 pub relation_type_count: usize,
294 /// Property key count.
295 pub property_key_count: usize,
296 /// Projection count.
297 pub projection_count: usize,
298 /// Index count.
299 pub index_count: usize,
300}
301
302impl CatalogSummary {
303 /// Builds a summary from a catalog.
304 ///
305 /// # Performance
306 ///
307 /// This function is `O(catalog entry count)`.
308 #[must_use]
309 pub fn from_catalog(catalog: &Catalog) -> Self {
310 Self {
311 role_count: catalog.roles().count(),
312 label_count: catalog.labels().count(),
313 relation_type_count: catalog.relation_types().count(),
314 property_key_count: catalog.property_keys().count(),
315 projection_count: catalog.projections().count(),
316 index_count: catalog.indexes().count(),
317 }
318 }
319}
320
321/// Reader pin identifying the visible database generation.
322///
323/// # Performance
324///
325/// Copying and comparing a pin is `O(1)`.
326#[derive(Clone, Copy, Debug, Eq, PartialEq)]
327pub struct ReadPin {
328 /// Pinned visible commit sequence.
329 pub visible_commit_seq: CommitSeq,
330 /// Pinned writer transaction high-water mark visible to this handle.
331 pub last_transaction_id: TransactionId,
332}
333
334/// Read transaction over a pinned state snapshot.
335///
336/// # Performance
337///
338/// Moving a read transaction is `O(database state size)`.
339pub struct ReadTransaction {
340 /// Pinned generation coordinates.
341 pin: ReadPin,
342 /// Cloned visible state.
343 state: DatabaseState,
344 /// Materialized graph projections cached for the pinned commit, keyed by
345 /// projection ID. The stored [`CommitSeq`] guards against reuse across a
346 /// commit-sequence advance; within one read it is always the pin's seq.
347 graph_projections: ProjectionCache<GraphProjection>,
348 /// Materialized hypergraph projections cached for the pinned commit.
349 hypergraph_projections: ProjectionCache<HypergraphProjection>,
350}
351
352/// Per-read materialized-projection cache keyed by projection ID.
353///
354/// Each entry carries the [`CommitSeq`] it was built against so a stale entry
355/// is discarded when the visible commit sequence advances.
356///
357/// # Performance
358///
359/// Lookups and inserts are `O(log projection count)`.
360type ProjectionCache<P> = RefCell<BTreeMap<ProjectionId, (CommitSeq, Rc<P>)>>;
361
362impl ReadTransaction {
363 /// Returns this transaction's reader pin.
364 ///
365 /// # Performance
366 ///
367 /// This method is `O(1)`.
368 #[must_use]
369 pub const fn pin(&self) -> ReadPin {
370 self.pin
371 }
372
373 /// Returns catalog metadata.
374 ///
375 /// # Performance
376 ///
377 /// This method is `O(1)`.
378 #[must_use]
379 pub const fn catalog(&self) -> &Catalog {
380 self.state.catalog()
381 }
382
383 /// Returns visible element count.
384 ///
385 /// # Performance
386 ///
387 /// This method is `O(1)`.
388 #[must_use]
389 pub fn element_count(&self) -> usize {
390 self.state.element_count()
391 }
392
393 /// Returns visible relation count.
394 ///
395 /// # Performance
396 ///
397 /// This method is `O(1)`.
398 #[must_use]
399 pub fn relation_count(&self) -> usize {
400 self.state.relation_count()
401 }
402
403 /// Returns visible incidence count.
404 ///
405 /// # Performance
406 ///
407 /// This method is `O(1)`.
408 #[must_use]
409 pub fn incidence_count(&self) -> usize {
410 self.state.incidence_count()
411 }
412
413 /// Returns whether an element exists.
414 ///
415 /// # Performance
416 ///
417 /// This method is `O(log n)`.
418 #[must_use]
419 pub fn contains_element(&self, id: ElementId) -> bool {
420 self.state.contains_element(id)
421 }
422
423 /// Returns whether a relation exists.
424 ///
425 /// # Performance
426 ///
427 /// This method is `O(log n)`.
428 #[must_use]
429 pub fn contains_relation(&self, id: RelationId) -> bool {
430 self.state.contains_relation(id)
431 }
432
433 /// Returns whether an incidence exists.
434 ///
435 /// # Performance
436 ///
437 /// This method is `O(log n)`.
438 #[must_use]
439 pub fn contains_incidence(&self, id: IncidenceId) -> bool {
440 self.state.contains_incidence(id)
441 }
442
443 /// Returns an element record.
444 ///
445 /// # Performance
446 ///
447 /// This method is `O(log n)`.
448 #[must_use]
449 pub fn element(&self, id: ElementId) -> Option<&ElementRecord> {
450 self.state.element(id)
451 }
452
453 /// Returns a relation record.
454 ///
455 /// # Performance
456 ///
457 /// This method is `O(log n)`.
458 #[must_use]
459 pub fn relation(&self, id: RelationId) -> Option<&RelationRecord> {
460 self.state.relation(id)
461 }
462
463 /// Returns an incidence record.
464 ///
465 /// # Performance
466 ///
467 /// This method is `O(log n)`.
468 #[must_use]
469 pub fn incidence(&self, id: IncidenceId) -> Option<&IncidenceRecord> {
470 self.state.incidence(id)
471 }
472
473 /// Iterates incidences attached to an element.
474 ///
475 /// # Performance
476 ///
477 /// This method is `O(i)` for visible incidence count.
478 pub fn element_incidences(&self, id: ElementId) -> impl Iterator<Item = &IncidenceRecord> {
479 self.state.element_incidences(id)
480 }
481
482 /// Returns one property value.
483 ///
484 /// # Performance
485 ///
486 /// This method is `O(log subjects + log keys)`.
487 #[must_use]
488 pub fn property(&self, subject: PropertySubject, key: PropertyKeyId) -> Option<&PropertyValue> {
489 self.state.property(subject, key)
490 }
491
492 /// Looks up subjects with a property value.
493 ///
494 /// # Errors
495 ///
496 /// Returns [`DbError`] when the property key is unknown or `value` does not
497 /// match the key schema.
498 ///
499 /// # Performance
500 ///
501 /// This method is `O(property subject count)`.
502 pub fn lookup_property_equal(
503 &self,
504 key: PropertyKeyId,
505 value: &PropertyValue,
506 ) -> Result<Vec<PropertySubject>, DbError> {
507 self.state.typed_property_equal(key, value)
508 }
509
510 /// Looks up subjects with a property inside an inclusive range.
511 ///
512 /// # Errors
513 ///
514 /// Returns [`DbError`] when the property key is unknown or either bound
515 /// does not match the key schema.
516 ///
517 /// # Performance
518 ///
519 /// This method is `O(property subject count)`.
520 pub fn lookup_property_range(
521 &self,
522 key: PropertyKeyId,
523 min: &PropertyValue,
524 max: &PropertyValue,
525 ) -> Result<Vec<PropertySubject>, DbError> {
526 self.state.typed_property_range(key, min, max)
527 }
528
529 /// Executes an index lookup.
530 ///
531 /// # Errors
532 ///
533 /// Returns [`DbError`] when the index is unknown, the lookup shape does not
534 /// match the index kind, or supplied property values do not match catalog
535 /// schemas.
536 ///
537 /// # Performance
538 ///
539 /// This method is `O(indexed family size)` for the greenfield embedded
540 /// implementation.
541 pub fn lookup_index(
542 &self,
543 index: IndexId,
544 lookup: IndexLookup<'_>,
545 ) -> Result<Vec<PropertySubject>, DbError> {
546 let entry = self
547 .state
548 .catalog()
549 .index(index)
550 .ok_or(DbError::UnknownIndex { id: index })?;
551 match (&entry.definition, lookup) {
552 (IndexDefinition::Label { label }, IndexLookup::All) => Ok(self
553 .state
554 .elements_with_label(*label)
555 .into_iter()
556 .map(PropertySubject::Element)
557 .collect()),
558 (IndexDefinition::Label { .. }, _lookup) => {
559 Err(DbError::unsupported("label index expects all lookup"))
560 }
561 (IndexDefinition::RelationType { relation_type }, IndexLookup::All) => Ok(self
562 .state
563 .relations_with_type(*relation_type)
564 .into_iter()
565 .map(PropertySubject::Relation)
566 .collect()),
567 (IndexDefinition::RelationType { .. }, _lookup) => Err(DbError::unsupported(
568 "relation type index expects all lookup",
569 )),
570 (IndexDefinition::PropertyEquality { key }, IndexLookup::Equal(value)) => {
571 self.state.typed_property_equal(*key, value)
572 }
573 (IndexDefinition::PropertyEquality { .. }, _lookup) => Err(DbError::unsupported(
574 "property equality index expects equality lookup",
575 )),
576 (IndexDefinition::PropertyRange { key }, IndexLookup::Range { min, max }) => {
577 self.state.typed_property_range(*key, min, max)
578 }
579 (IndexDefinition::PropertyRange { .. }, _lookup) => Err(DbError::unsupported(
580 "property range index expects range lookup",
581 )),
582 (IndexDefinition::CompositeEquality { keys }, IndexLookup::CompositeEqual(values)) => {
583 self.state.typed_property_composite_equal(keys, values)
584 }
585 (IndexDefinition::CompositeEquality { .. }, _lookup) => Err(DbError::unsupported(
586 "composite equality index expects composite equality lookup",
587 )),
588 (IndexDefinition::Projection { projection }, IndexLookup::All) => {
589 self.projection_index_subjects(*projection)
590 }
591 (IndexDefinition::Projection { .. }, _lookup) => {
592 Err(DbError::unsupported("projection index expects all lookup"))
593 }
594 }
595 }
596
597 /// Materializes a graph projection.
598 ///
599 /// # Errors
600 ///
601 /// Returns [`DbError`] when the projection is unknown, is not a graph, or
602 /// fails validation against current topology.
603 ///
604 /// # Performance
605 ///
606 /// This method is `O(relation count * incidence count)`.
607 pub fn graph_projection(&self, id: ProjectionId) -> Result<GraphProjection, DbError> {
608 self.cached_graph_projection(id)
609 .map(|graph| (*graph).clone())
610 }
611
612 /// Returns a cached graph projection, materializing it on first use.
613 ///
614 /// The projection is keyed by ID and tagged with the pinned commit
615 /// sequence, so repeated traverse/query/index calls within one read reuse
616 /// one materialization instead of rebuilding it `O(relation * incidence)`
617 /// each time. A stale-seq entry is discarded and rebuilt.
618 ///
619 /// # Errors
620 ///
621 /// Returns [`DbError`] when the projection is unknown, is not a graph, or
622 /// fails validation against current topology.
623 ///
624 /// # Performance
625 ///
626 /// This method is `O(1)` on a cache hit and
627 /// `O(relation count * incidence count)` on a miss.
628 fn cached_graph_projection(&self, id: ProjectionId) -> Result<Rc<GraphProjection>, DbError> {
629 let seq = self.pin.visible_commit_seq;
630 if let Some((cached_seq, graph)) = self.graph_projections.borrow().get(&id)
631 && *cached_seq == seq
632 {
633 return Ok(Rc::clone(graph));
634 }
635 let entry = self
636 .state
637 .catalog()
638 .projection(id)
639 .ok_or(DbError::UnknownProjection { id })?;
640 let graph = match &entry.definition {
641 ProjectionDefinition::Graph(definition) => {
642 projection::GraphProjection::from_state(&self.state, definition.clone())?
643 }
644 ProjectionDefinition::Hypergraph(_definition) => {
645 return Err(DbError::invalid_projection("projection is not a graph"));
646 }
647 };
648 let graph = Rc::new(graph);
649 self.graph_projections
650 .borrow_mut()
651 .insert(id, (seq, Rc::clone(&graph)));
652 Ok(graph)
653 }
654
655 /// Materializes a graph projection by catalog name.
656 ///
657 /// # Errors
658 ///
659 /// Returns [`DbError`] when the projection is unknown, is not a graph, or
660 /// fails validation against current topology.
661 ///
662 /// # Performance
663 ///
664 /// This method is `O(log projection count + relation count * incidence count)`.
665 pub fn graph_projection_by_name(&self, name: &str) -> Result<GraphProjection, DbError> {
666 let id = self
667 .state
668 .catalog()
669 .projection_id(name)
670 .ok_or_else(|| DbError::unsupported(format!("unknown projection {name}")))?;
671 self.graph_projection(id)
672 }
673
674 /// Traverses a cataloged graph projection from canonical seed elements.
675 ///
676 /// Rows are unique canonical elements in BFS first-discovery order. Depth is
677 /// the shortest discovered hop count from any seed.
678 ///
679 /// # Errors
680 ///
681 /// Returns [`DbError`] when the projection is unknown, is not a graph,
682 /// cannot be materialized, or a seed element is not part of the projection.
683 ///
684 /// # Performance
685 ///
686 /// This method is `O(relation count * incidence count + visited edges)`.
687 pub fn traverse_graph(
688 &self,
689 projection: ProjectionId,
690 seeds: &[ElementId],
691 options: TraversalOptions,
692 ) -> Result<TraversalResult, DbError> {
693 if seeds.is_empty() || options.limit == 0 {
694 return Ok(TraversalResult::new(Vec::new()));
695 }
696 let graph = self.cached_graph_projection(projection)?;
697 traversal::traverse_graph_projection(&graph, seeds, options)
698 }
699
700 /// Materializes a hypergraph projection.
701 ///
702 /// # Errors
703 ///
704 /// Returns [`DbError`] when the projection is unknown, is not a hypergraph,
705 /// or fails validation against current topology.
706 ///
707 /// # Performance
708 ///
709 /// This method is `O(relation count * incidence count)`.
710 pub fn hypergraph_projection(&self, id: ProjectionId) -> Result<HypergraphProjection, DbError> {
711 self.cached_hypergraph_projection(id)
712 .map(|hyper| (*hyper).clone())
713 }
714
715 /// Returns a cached hypergraph projection, materializing it on first use.
716 ///
717 /// Mirrors [`Self::cached_graph_projection`]: keyed by ID, tagged with the
718 /// pinned commit sequence, and rebuilt only on a miss or stale seq.
719 ///
720 /// # Errors
721 ///
722 /// Returns [`DbError`] when the projection is unknown, is not a hypergraph,
723 /// or fails validation against current topology.
724 ///
725 /// # Performance
726 ///
727 /// This method is `O(1)` on a cache hit and
728 /// `O(relation count * incidence count)` on a miss.
729 fn cached_hypergraph_projection(
730 &self,
731 id: ProjectionId,
732 ) -> Result<Rc<HypergraphProjection>, DbError> {
733 let seq = self.pin.visible_commit_seq;
734 if let Some((cached_seq, hyper)) = self.hypergraph_projections.borrow().get(&id)
735 && *cached_seq == seq
736 {
737 return Ok(Rc::clone(hyper));
738 }
739 let entry = self
740 .state
741 .catalog()
742 .projection(id)
743 .ok_or(DbError::UnknownProjection { id })?;
744 let hyper = match &entry.definition {
745 ProjectionDefinition::Hypergraph(definition) => {
746 projection::HypergraphProjection::from_state(&self.state, definition.clone())?
747 }
748 ProjectionDefinition::Graph(_definition) => {
749 return Err(DbError::invalid_projection(
750 "projection is not a hypergraph",
751 ));
752 }
753 };
754 let hyper = Rc::new(hyper);
755 self.hypergraph_projections
756 .borrow_mut()
757 .insert(id, (seq, Rc::clone(&hyper)));
758 Ok(hyper)
759 }
760
761 /// Executes a prepared query.
762 ///
763 /// # Errors
764 ///
765 /// Returns [`DbError`] when execution cannot materialize a referenced
766 /// projection.
767 ///
768 /// # Performance
769 ///
770 /// This method is `O(plan output + projection build cost when used)`.
771 pub fn execute(&self, query: &PreparedQuery) -> Result<QueryResult, DbError> {
772 query.execute(&self.state)
773 }
774
775 /// Explains a prepared query.
776 ///
777 /// # Performance
778 ///
779 /// This method is `O(plan size)`.
780 #[must_use]
781 pub fn explain(&self, query: &PreparedQuery) -> String {
782 query.explain()
783 }
784
785 /// Materializes subjects represented by a projection index.
786 fn projection_index_subjects(
787 &self,
788 projection: ProjectionId,
789 ) -> Result<Vec<PropertySubject>, DbError> {
790 let entry = self
791 .state
792 .catalog()
793 .projection(projection)
794 .ok_or(DbError::UnknownProjection { id: projection })?;
795 match &entry.definition {
796 ProjectionDefinition::Graph(_definition) => {
797 Ok(self.cached_graph_projection(projection)?.subjects())
798 }
799 ProjectionDefinition::Hypergraph(_definition) => {
800 Ok(self.cached_hypergraph_projection(projection)?.subjects())
801 }
802 }
803 }
804}
805
806/// Single writer transaction.
807///
808/// # Performance
809///
810/// Moving a writer is `O(database state size)`.
811pub struct WriteTransaction<'db> {
812 /// Database receiving the commit.
813 database: &'db mut Database,
814 /// Staged state after mutations.
815 state: DatabaseState,
816 /// Writer transaction ID.
817 transaction_id: TransactionId,
818 /// Whether this transaction changed visible state.
819 dirty: bool,
820}
821
822impl WriteTransaction<'_> {
823 /// Registers a structural incidence role.
824 ///
825 /// # Errors
826 ///
827 /// Returns [`DbError`] when the name already exists or ID allocation fails.
828 ///
829 /// # Performance
830 ///
831 /// This method is `O(log role count + name length)`.
832 pub fn register_role(&mut self, name: impl Into<String>) -> Result<RoleId, DbError> {
833 let id = self.state.register_role(name.into())?;
834 self.dirty = true;
835 Ok(id)
836 }
837
838 /// Registers an element or relation label.
839 ///
840 /// # Errors
841 ///
842 /// Returns [`DbError`] when the name already exists or ID allocation fails.
843 ///
844 /// # Performance
845 ///
846 /// This method is `O(log label count + name length)`.
847 pub fn register_label(&mut self, name: impl Into<String>) -> Result<LabelId, DbError> {
848 let id = self.state.register_label(name.into())?;
849 self.dirty = true;
850 Ok(id)
851 }
852
853 /// Registers a relation type.
854 ///
855 /// # Errors
856 ///
857 /// Returns [`DbError`] when the name already exists or ID allocation fails.
858 ///
859 /// # Performance
860 ///
861 /// This method is `O(log relation type count + name length)`.
862 pub fn register_relation_type(
863 &mut self,
864 name: impl Into<String>,
865 ) -> Result<RelationTypeId, DbError> {
866 let id = self.state.register_relation_type(name.into())?;
867 self.dirty = true;
868 Ok(id)
869 }
870
871 /// Registers a typed property key.
872 ///
873 /// # Errors
874 ///
875 /// Returns [`DbError`] when the name already exists or ID allocation fails.
876 ///
877 /// # Performance
878 ///
879 /// This method is `O(log property key count + name length)`.
880 pub fn register_property_key(
881 &mut self,
882 name: impl Into<String>,
883 family: PropertyFamily,
884 value_type: PropertyType,
885 ) -> Result<PropertyKeyId, DbError> {
886 let id = self
887 .state
888 .register_property_key(name.into(), family, value_type)?;
889 self.dirty = true;
890 Ok(id)
891 }
892
893 /// Defines a physical projection.
894 ///
895 /// # Errors
896 ///
897 /// Returns [`DbError`] when referenced catalog IDs are unknown, the
898 /// projection name already exists, or ID allocation fails.
899 ///
900 /// # Performance
901 ///
902 /// This method is `O(definition size + catalog lookup cost)`.
903 pub fn define_projection(
904 &mut self,
905 definition: ProjectionDefinition,
906 ) -> Result<ProjectionId, DbError> {
907 let id = self.state.define_projection(definition)?;
908 self.dirty = true;
909 Ok(id)
910 }
911
912 /// Defines an index.
913 ///
914 /// # Errors
915 ///
916 /// Returns [`DbError`] when referenced catalog IDs are unknown, the index
917 /// name already exists, or ID allocation fails.
918 ///
919 /// # Performance
920 ///
921 /// This method is `O(definition size + catalog lookup cost)`.
922 pub fn define_index(
923 &mut self,
924 name: impl Into<String>,
925 definition: IndexDefinition,
926 ) -> Result<IndexId, DbError> {
927 let id = self.state.define_index(name.into(), definition)?;
928 self.dirty = true;
929 Ok(id)
930 }
931
932 /// Creates a canonical element.
933 ///
934 /// # Errors
935 ///
936 /// Returns [`DbError::IdOverflow`] when element IDs are exhausted.
937 ///
938 /// # Performance
939 ///
940 /// This method is `O(log element count)`.
941 pub fn create_element(&mut self) -> Result<ElementId, DbError> {
942 let id = self.state.create_element()?;
943 self.dirty = true;
944 Ok(id)
945 }
946
947 /// Creates a canonical relation.
948 ///
949 /// # Errors
950 ///
951 /// Returns [`DbError::IdOverflow`] when relation IDs are exhausted.
952 ///
953 /// # Performance
954 ///
955 /// This method is `O(log relation count)`.
956 pub fn create_relation(&mut self) -> Result<RelationId, DbError> {
957 let id = self.state.create_relation()?;
958 self.dirty = true;
959 Ok(id)
960 }
961
962 /// Creates a canonical incidence.
963 ///
964 /// # Errors
965 ///
966 /// Returns [`DbError`] when referenced IDs are unknown or incidence IDs are
967 /// exhausted.
968 ///
969 /// # Performance
970 ///
971 /// This method is `O(log incidence count + reference lookup cost)`.
972 pub fn create_incidence(
973 &mut self,
974 relation: RelationId,
975 element: ElementId,
976 role: RoleId,
977 ) -> Result<IncidenceId, DbError> {
978 let id = self.state.create_incidence(relation, element, role)?;
979 self.dirty = true;
980 Ok(id)
981 }
982
983 /// Tombstones a canonical element and its incidences.
984 ///
985 /// # Errors
986 ///
987 /// Returns [`DbError::UnknownElement`] when the element is not visible.
988 ///
989 /// # Performance
990 ///
991 /// This method is `O(incidence count)`.
992 pub fn tombstone_element(&mut self, id: ElementId) -> Result<(), DbError> {
993 self.state.tombstone_element(id)?;
994 self.dirty = true;
995 Ok(())
996 }
997
998 /// Tombstones a canonical relation and its incidences.
999 ///
1000 /// # Errors
1001 ///
1002 /// Returns [`DbError::UnknownRelation`] when the relation is not visible.
1003 ///
1004 /// # Performance
1005 ///
1006 /// This method is `O(incidence count)`.
1007 pub fn tombstone_relation(&mut self, id: RelationId) -> Result<(), DbError> {
1008 self.state.tombstone_relation(id)?;
1009 self.dirty = true;
1010 Ok(())
1011 }
1012
1013 /// Tombstones a canonical incidence.
1014 ///
1015 /// # Errors
1016 ///
1017 /// Returns [`DbError::UnknownIncidence`] when the incidence is not visible.
1018 ///
1019 /// # Performance
1020 ///
1021 /// This method is `O(log incidence count)`.
1022 pub fn tombstone_incidence(&mut self, id: IncidenceId) -> Result<(), DbError> {
1023 self.state.tombstone_incidence(id)?;
1024 self.dirty = true;
1025 Ok(())
1026 }
1027
1028 /// Adds a label to an element.
1029 ///
1030 /// # Errors
1031 ///
1032 /// Returns [`DbError`] when the element or label is unknown.
1033 ///
1034 /// # Performance
1035 ///
1036 /// This method is `O(log element count + log label count)`.
1037 pub fn add_element_label(&mut self, element: ElementId, label: LabelId) -> Result<(), DbError> {
1038 self.state.add_element_label(element, label)?;
1039 self.dirty = true;
1040 Ok(())
1041 }
1042
1043 /// Adds a label to a relation.
1044 ///
1045 /// # Errors
1046 ///
1047 /// Returns [`DbError`] when the relation or label is unknown.
1048 ///
1049 /// # Performance
1050 ///
1051 /// This method is `O(log relation count + log label count)`.
1052 pub fn add_relation_label(
1053 &mut self,
1054 relation: RelationId,
1055 label: LabelId,
1056 ) -> Result<(), DbError> {
1057 self.state.add_relation_label(relation, label)?;
1058 self.dirty = true;
1059 Ok(())
1060 }
1061
1062 /// Sets a relation type.
1063 ///
1064 /// # Errors
1065 ///
1066 /// Returns [`DbError`] when the relation or relation type is unknown.
1067 ///
1068 /// # Performance
1069 ///
1070 /// This method is `O(log relation count + log relation type count)`.
1071 pub fn set_relation_type(
1072 &mut self,
1073 relation: RelationId,
1074 relation_type: RelationTypeId,
1075 ) -> Result<(), DbError> {
1076 self.state.set_relation_type(relation, relation_type)?;
1077 self.dirty = true;
1078 Ok(())
1079 }
1080
1081 /// Sets a property value.
1082 ///
1083 /// # Errors
1084 ///
1085 /// Returns [`DbError`] when the subject or key is unknown, or the value
1086 /// does not match the key schema.
1087 ///
1088 /// # Performance
1089 ///
1090 /// This method is `O(log subject count + log key count)`.
1091 pub fn set_property(
1092 &mut self,
1093 subject: PropertySubject,
1094 key: PropertyKeyId,
1095 value: PropertyValue,
1096 ) -> Result<(), DbError> {
1097 self.state.set_property(subject, key, value)?;
1098 self.dirty = true;
1099 Ok(())
1100 }
1101
1102 /// Removes a property value.
1103 ///
1104 /// # Errors
1105 ///
1106 /// Returns [`DbError`] when the subject or key is unknown.
1107 ///
1108 /// # Performance
1109 ///
1110 /// This method is `O(log subject count + log key count)`.
1111 pub fn remove_property(
1112 &mut self,
1113 subject: PropertySubject,
1114 key: PropertyKeyId,
1115 ) -> Result<(), DbError> {
1116 self.state.remove_property(subject, key)?;
1117 self.dirty = true;
1118 Ok(())
1119 }
1120
1121 /// Commits this write transaction durably.
1122 ///
1123 /// # Errors
1124 ///
1125 /// Returns [`DbError`] when commit sequence allocation, validation,
1126 /// encoding, writing, or store replacement fails.
1127 ///
1128 /// # Performance
1129 ///
1130 /// This method is `O(serialized database bytes)`.
1131 pub fn commit(self) -> Result<CommitSeq, DbError> {
1132 let commit_seq = if self.dirty {
1133 self.database.next_commit_seq()?
1134 } else {
1135 self.database.visible_commit_seq
1136 };
1137 let stored = StoredDatabase {
1138 commit_seq,
1139 transaction_id: self.transaction_id,
1140 state: self.state.clone(),
1141 };
1142 storage::write_store(&self.database.path, &stored)?;
1143 self.database.state = self.state;
1144 self.database.visible_commit_seq = commit_seq;
1145 self.database.last_transaction_id = self.transaction_id;
1146 Ok(commit_seq)
1147 }
1148
1149 /// Drops this write transaction without committing.
1150 ///
1151 /// # Performance
1152 ///
1153 /// This method is `O(1)` excluding staged-state drop cost.
1154 pub fn rollback(self) {}
1155}