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