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}