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