Skip to main content

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}