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    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}