1use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5use std::sync::Arc;
6use std::time::Duration;
7
8use haematite::branch::history::SERVICE_BRANCH as SERVICE;
9use haematite::branch::{
10 KeptLimit, Kind as BranchKind, RefRecord as BranchRefRecord, RefStore as BranchRefStore,
11 Registry as BranchRegistry, ShardRef as BranchShardRef, create_branch_with_kind,
12 current_timestamp, fork_from_with_kind, open as open_branch,
13};
14use haematite::db::vacuum::CANONICAL_REFS_DIR;
15use haematite::db::{
16 Database, DatabaseConfig, DatabaseError, ReadOnlyDatabase, branches::BranchDatabase,
17};
18use haematite::store::NodeCacheBudget;
19use haematite::{Retention, RuntimeOptions};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22
23use super::protocol::{Selection, Version};
24
25mod admission;
26pub use admission::Admission;
27
28pub mod attribution;
29pub mod embed_inherit;
30pub mod embed_job;
31pub mod embed_pending;
32pub mod embed_prune;
33pub mod embed_rewalk;
34pub mod embed_search;
35pub mod embedded;
36
37#[cfg(test)]
38#[path = "storage/envelope_tests.rs"]
39mod envelope_tests;
40#[cfg(test)]
41#[path = "storage/retention_tests.rs"]
42mod retention_tests;
43#[cfg(test)]
44#[path = "storage/span_measure_tests.rs"]
45mod span_measure_tests;
46#[cfg(test)]
47#[path = "storage/write_cost_tests.rs"]
48mod write_cost_tests;
49
50#[cfg(test)]
51#[path = "storage/batch_test_support.rs"]
52mod batch_test_support;
53#[cfg(test)]
54#[path = "storage/documents_delete_many_tests.rs"]
55mod documents_delete_many_tests;
56#[cfg(test)]
57#[path = "storage/edges_batch_cost_tests.rs"]
58mod edges_batch_cost_tests;
59#[cfg(test)]
60#[path = "storage/vectors_many_tests.rs"]
61mod vectors_many_tests;
62
63mod branches;
64mod catalogue;
65mod discards;
66mod document_delete_many;
67pub use discards::DiscardPasses;
68pub(super) mod edges;
69pub(super) mod field_index;
70pub(super) mod graph;
71mod history;
72pub use history::{KEPT_VERSIONS_DEFAULT, KEPT_VERSIONS_MOST};
73mod counts;
74mod dead;
75mod merge_holders;
76mod merge_journal;
77mod merges;
78mod mutations;
79#[cfg(test)]
80pub(in crate::serve) use mutations::author_write;
81mod retention;
82pub use retention::AppendPruneLimits;
83pub mod reads;
84pub(super) mod relations;
85mod release;
86pub(super) mod review;
87mod vector_import;
88pub(super) mod vectors;
89pub use branches::ForkProvenance;
90#[cfg(test)]
91pub use catalogue::after_held;
92pub use catalogue::{Catalogue, Held};
93pub use merge_holders::{MergePin, MergePlan, MergeState};
94pub use merge_journal::{
95 CandidateAction, CandidateAdvance, MergeChoice, MergeChoiceRow, MergeChoices, MergeChoose,
96 MergeCoverage, MergeOwner, MissingChoice, RecordedChoice,
97};
98pub use release::{
99 Answer as ReleaseAnswer, HeldBy, IntentListing, Keep, Rules as ReleaseRules,
100 Source as ReleaseSource, UndatedGeneration,
101};
102#[cfg(test)]
103pub use release::{Crash, crash_at};
104
105const BRANCH_PREFIX: &str = haematite::branch::history::DATA_BRANCH_PREFIX;
106
107mod failure;
108pub use failure::Failure;
109#[cfg(test)]
110#[path = "storage/failure_tests.rs"]
111mod failure_tests;
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct Receipt {
116 pub request_id: String,
117 pub fingerprint: String,
118 pub branch: String,
119 pub version: Version,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub retention: Option<super::protocol::RetentionResult>,
122}
123
124#[derive(Debug, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct Collection {
127 pub name: String,
128 pub schema: Option<Value>,
129 pub changed_in: String,
130 pub changed_at: Version,
131}
132
133#[derive(Debug, Serialize, Deserialize)]
134#[serde(deny_unknown_fields)]
135pub struct DocumentRevision {
136 pub collection_branch: String,
137 pub collection_version: Version,
138 pub written_in: String,
139 pub written_at: Version,
140}
141
142#[derive(Debug, Clone)]
144pub struct Captured {
145 pub branch: String,
146 pub version: Version,
147 pub shards: Vec<BranchShardRef>,
148}
149
150pub struct Storage {
153 refs: BranchRefStore,
154 visible: Catalogue,
155 registry: BranchRegistry,
156 reader: Arc<ReadOnlyDatabase>,
157 writer: BranchDatabase,
158 pending_discards: BTreeSet<String>,
159 confirmed_candidates: BTreeMap<String, Version>,
160 kept_rows: history::KeptRows,
162 discard_passes: DiscardPasses,
164}
165
166pub(super) const fn runtime_options(
167 long_wait_interval: Duration,
168) -> Result<RuntimeOptions, DatabaseError> {
169 if long_wait_interval.is_zero() {
170 return Err(DatabaseError::InvalidLongWaitInterval {
171 given: long_wait_interval,
172 });
173 }
174 Ok(RuntimeOptions {
175 long_wait_interval,
176 reclaim_start: haematite::ReclaimStart::AtOpen,
177 })
178}
179
180impl Storage {
181 pub fn create(
188 config: DatabaseConfig,
189 branch_cache: NodeCacheBudget,
190 long_wait_interval: Duration,
191 ) -> Result<Self, Failure> {
192 Self::create_kept(
193 config,
194 branch_cache,
195 history::default_kept()?,
196 long_wait_interval,
197 )
198 }
199
200 pub fn create_kept(
207 config: DatabaseConfig,
208 branch_cache: NodeCacheBudget,
209 kept: KeptLimit,
210 long_wait_interval: Duration,
211 ) -> Result<Self, Failure> {
212 let options = runtime_options(long_wait_interval).map_err(Failure::from)?;
213 if config.data_dir.try_exists().map_err(Failure::storage)? {
214 return Err(Failure::new(
215 "directory_exists",
216 "create requires a new directory",
217 ));
218 }
219 let database = Database::create(config, options).map_err(Failure::from)?;
220 database.require_path_writer().map_err(Failure::from)?;
221 let mut writer = BranchDatabase::new(database, branch_cache).map_err(Failure::database)?;
222 let mut refs = BranchRefStore::open_writing(
223 writer.data_dir().join(CANONICAL_REFS_DIR),
224 writer.writer_lock(),
225 )
226 .map_err(Failure::branch_ref)?;
227 refs.keep_versions(kept);
228 let registry = BranchRegistry::new();
229 create_branch_with_kind(
230 SERVICE,
231 writer.base_roots().map_err(Failure::database)?,
232 BranchKind::Namespace,
233 &mut refs,
234 ®istry,
235 current_timestamp(),
236 )
237 .map_err(Failure::branch_commit)?;
238 counts::initialize_new_store(&mut writer, &mut refs, ®istry)?;
239 Self::finish_open(writer, refs, registry, kept)
240 }
241
242 pub fn open(
249 path: &Path,
250 branch_cache: NodeCacheBudget,
251 long_wait_interval: Duration,
252 ) -> Result<Self, Failure> {
253 Self::open_kept(
254 path,
255 branch_cache,
256 history::default_kept()?,
257 long_wait_interval,
258 )
259 }
260
261 pub fn open_kept(
268 path: &Path,
269 branch_cache: NodeCacheBudget,
270 kept: KeptLimit,
271 long_wait_interval: Duration,
272 ) -> Result<Self, Failure> {
273 Self::open_kept_starting(
274 path,
275 branch_cache,
276 kept,
277 long_wait_interval,
278 haematite::ReclaimStart::AtOpen,
279 )
280 }
281
282 #[doc(hidden)]
291 pub fn open_reclaim_held(
292 path: &Path,
293 branch_cache: NodeCacheBudget,
294 long_wait_interval: Duration,
295 ) -> Result<Self, Failure> {
296 Self::open_kept_starting(
297 path,
298 branch_cache,
299 history::default_kept()?,
300 long_wait_interval,
301 haematite::ReclaimStart::Held,
302 )
303 }
304
305 fn open_kept_starting(
306 path: &Path,
307 branch_cache: NodeCacheBudget,
308 kept: KeptLimit,
309 long_wait_interval: Duration,
310 reclaim_start: haematite::ReclaimStart,
311 ) -> Result<Self, Failure> {
312 let options = RuntimeOptions {
313 reclaim_start,
314 ..runtime_options(long_wait_interval).map_err(Failure::from)?
315 };
316 if !path.join(CANONICAL_REFS_DIR).is_dir() {
317 return Err(Failure::new(
318 "not_served",
319 "directory has no served branch references",
320 ));
321 }
322 match BranchRefStore::read_record(path.join(CANONICAL_REFS_DIR), SERVICE) {
325 Ok(Some(_record)) => {}
326 Ok(None) => {
327 return Err(Failure::new(
328 "not_served",
329 "directory is not a haem/1 service database",
330 ));
331 }
332 Err(error) => {
333 return Err(Failure::new(
334 "service_record_unreadable",
335 format!("the service's branch record cannot be read: {error}"),
336 ));
337 }
338 }
339 let database = Database::open_path_writer(path, options).map_err(Failure::from)?;
340 let database = if database.reclaim_mark_bytes().is_some() {
341 database
342 } else {
343 drop(database);
347 let moved = Database::open_with_retention(path, options, Retention::reclaim(0))
348 .map_err(Failure::from)?;
349 moved.require_path_writer().map_err(Failure::from)?;
350 moved
351 };
352 let writer = BranchDatabase::new(database, branch_cache).map_err(Failure::database)?;
353 let mut refs =
354 BranchRefStore::open_writing(path.join(CANONICAL_REFS_DIR), writer.writer_lock())
355 .map_err(Failure::branch_ref)?;
356 refs.keep_versions(kept);
357 if refs.get(SERVICE).is_none() {
358 return Err(Failure::new(
359 "not_served",
360 "directory is not a haem/1 service database",
361 ));
362 }
363 Self::finish_open(writer, refs, BranchRegistry::new(), kept)
364 }
365
366 fn finish_open(
367 writer: BranchDatabase,
368 mut refs: BranchRefStore,
369 registry: BranchRegistry,
370 kept: KeptLimit,
371 ) -> Result<Self, Failure> {
372 if refs.get(SERVICE).is_none() {
373 return Err(Failure::new("not_served", "missing service namespace"));
374 }
375 let reader =
376 Arc::new(ReadOnlyDatabase::open(writer.data_dir()).map_err(Failure::observer)?);
377 history::require_file_rows(&reader, &refs)?;
378 discards::require_file_rows(&reader, &refs)?;
379 let main = branch_name("main");
380 if refs.get(&main).is_none() {
381 let parent = open_branch(SERVICE, &refs, ®istry).map_err(Failure::branch_commit)?;
382 fork_from_with_kind(
383 &parent,
384 &main,
385 BranchKind::Work,
386 &mut refs,
387 ®istry,
388 current_timestamp(),
389 )
390 .map_err(Failure::branch_commit)?;
391 }
392 refs.confirm_directory().map_err(Failure::branch_ref)?;
396 let visible: BTreeMap<_, _> = refs
397 .list()
398 .map(|record| {
399 let kept = refs.kept(&record.name).to_vec();
400 (record.name.clone(), (record.clone(), kept))
401 })
402 .collect();
403 let holder = writer.read_holder();
406 let mut storage = Self {
407 refs,
408 visible: Catalogue::new(visible, Arc::clone(&reader), holder),
409 registry,
410 reader,
411 writer,
412 pending_discards: BTreeSet::new(),
413 confirmed_candidates: BTreeMap::new(),
414 kept_rows: history::KeptRows::new(kept),
415 discard_passes: DiscardPasses::default(),
416 };
417 storage.reconcile_discards()?;
418 Ok(storage)
419 }
420
421 pub fn capture(&self, selection: &Selection) -> Result<Captured, Failure> {
425 self.visible.capture(selection)
426 }
427
428 pub(in crate::serve) fn activate_reclaim(&self) -> Result<(), Failure> {
429 self.writer.activate_reclaim().map_err(Failure::from)
430 }
431
432 #[cfg(test)]
437 pub fn quiesce_reclaim(&self) -> Result<(), Failure> {
438 self.writer.quiesce_reclaim().map_err(Failure::from)
439 }
440
441 #[cfg(test)]
444 pub fn reclaim_status(&self) -> haematite::db::ReclaimStatus {
445 self.writer.reclaim_status()
446 }
447
448 #[cfg(test)]
450 pub fn live_read_holds(&self) -> usize {
451 self.writer.read_holder().status().live
452 }
453
454 pub fn catalogue(&self) -> Catalogue {
455 self.visible.clone()
456 }
457
458 #[must_use]
461 pub fn discard_passes(&self) -> DiscardPasses {
462 self.discard_passes.clone()
463 }
464
465 pub fn reader(&self) -> Result<ReadOnlyDatabase, Failure> {
471 ReadOnlyDatabase::open(self.writer.data_dir()).map_err(Failure::observer)
472 }
473
474 fn retain_current(&mut self, branch: &str, record: &BranchRefRecord) -> Result<(), Failure> {
475 self.retain_history_row(branch, record)
476 }
477
478 fn publish_record(&self, name: &str) -> Result<(), Failure> {
479 let record = self
480 .refs
481 .get(name)
482 .ok_or_else(|| Failure::new("reference_missing", "committed reference disappeared"))?;
483 self.visible
484 .publish(record.clone(), self.refs.kept(name).to_vec())
485 }
486}
487
488const fn version(record: &BranchRefRecord) -> Version {
489 Version {
490 generation: record.created,
491 sequence: record.seq,
492 }
493}
494
495fn branch_name(name: &str) -> String {
496 format!("{BRANCH_PREFIX}{name}")
497}
498
499fn identifier(value: &str) -> Result<(), Failure> {
500 if value.is_empty() || value.len() > 512 || value.contains('\0') {
501 return Err(Failure::new(
502 "invalid_identifier",
503 "names must contain 1 to 512 UTF-8 bytes and no NUL",
504 ));
505 }
506 Ok(())
507}
508
509fn collection_key(name: &str) -> Vec<u8> {
510 format!("c\0{name}").into_bytes()
511}
512pub(in crate::serve) fn document_key(collection: &str, id: &str) -> Vec<u8> {
513 format!("d\0{collection}\0{id}").into_bytes()
514}
515
516pub(in crate::serve) fn revision_key(collection: &str, id: &str) -> Vec<u8> {
517 format!("m\0{collection}\0{id}").into_bytes()
518}
519fn receipt_key(branch: &str, generation: u64, id: &str) -> Vec<u8> {
520 format!("r\0{branch}\0{generation}\0{id}").into_bytes()
521}
522
523#[cfg(test)]
524mod long_wait_tests {
525 use std::error::Error;
526 use std::time::Duration;
527
528 use haematite::DatabaseError;
529 use haematite::db::DatabaseConfig;
530 use haematite::store::NodeCacheBudget;
531
532 use super::{Storage, runtime_options};
533
534 #[test]
535 fn zero_is_the_typed_engine_refusal() {
536 assert!(matches!(
537 runtime_options(Duration::ZERO),
538 Err(DatabaseError::InvalidLongWaitInterval { given }) if given == Duration::ZERO
539 ));
540 }
541
542 #[test]
543 fn zero_refuses_create_and_open_before_directory_checks() -> Result<(), Box<dyn Error>> {
544 let directory = tempfile::tempdir()?;
545 let path = directory.path().join("absent");
546 let config = DatabaseConfig {
547 data_dir: path.clone(),
548 shard_count: 1,
549 distributed: None,
550 executor_threads: Some(1),
551 node_cache_budget: Some(NodeCacheBudget::Unlimited),
552 retention: haematite::Retention::reclaim(0),
553 };
554 for refused in [
555 Storage::create(config, NodeCacheBudget::Unlimited, Duration::ZERO),
556 Storage::open(&path, NodeCacheBudget::Unlimited, Duration::ZERO),
557 ] {
558 let failure = refused.err().ok_or("zero interval was accepted")?;
559 assert_eq!(failure.code, "storage_unavailable");
560 assert_eq!(
561 failure.message,
562 DatabaseError::InvalidLongWaitInterval {
563 given: Duration::ZERO
564 }
565 .to_string()
566 );
567 assert!(!path.try_exists()?);
568 }
569 Ok(())
570 }
571}