Skip to main content

oxgraph_db/
database.rs

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