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 let options = runtime_options(long_wait_interval).map_err(Failure::from)?;
273 if !path.join(CANONICAL_REFS_DIR).is_dir() {
274 return Err(Failure::new(
275 "not_served",
276 "directory has no served branch references",
277 ));
278 }
279 match BranchRefStore::read_record(path.join(CANONICAL_REFS_DIR), SERVICE) {
282 Ok(Some(_record)) => {}
283 Ok(None) => {
284 return Err(Failure::new(
285 "not_served",
286 "directory is not a haem/1 service database",
287 ));
288 }
289 Err(error) => {
290 return Err(Failure::new(
291 "service_record_unreadable",
292 format!("the service's branch record cannot be read: {error}"),
293 ));
294 }
295 }
296 let database = Database::open_path_writer(path, options).map_err(Failure::from)?;
297 let database = if database.reclaim_mark_bytes().is_some() {
298 database
299 } else {
300 drop(database);
304 let moved = Database::open_with_retention(
305 path,
306 runtime_options(long_wait_interval).map_err(Failure::from)?,
307 Retention::reclaim(0),
308 )
309 .map_err(Failure::from)?;
310 moved.require_path_writer().map_err(Failure::from)?;
311 moved
312 };
313 let writer = BranchDatabase::new(database, branch_cache).map_err(Failure::database)?;
314 let mut refs =
315 BranchRefStore::open_writing(path.join(CANONICAL_REFS_DIR), writer.writer_lock())
316 .map_err(Failure::branch_ref)?;
317 refs.keep_versions(kept);
318 if refs.get(SERVICE).is_none() {
319 return Err(Failure::new(
320 "not_served",
321 "directory is not a haem/1 service database",
322 ));
323 }
324 Self::finish_open(writer, refs, BranchRegistry::new(), kept)
325 }
326
327 fn finish_open(
328 writer: BranchDatabase,
329 mut refs: BranchRefStore,
330 registry: BranchRegistry,
331 kept: KeptLimit,
332 ) -> Result<Self, Failure> {
333 if refs.get(SERVICE).is_none() {
334 return Err(Failure::new("not_served", "missing service namespace"));
335 }
336 let reader =
337 Arc::new(ReadOnlyDatabase::open(writer.data_dir()).map_err(Failure::observer)?);
338 history::require_file_rows(&reader, &refs)?;
339 discards::require_file_rows(&reader, &refs)?;
340 let main = branch_name("main");
341 if refs.get(&main).is_none() {
342 let parent = open_branch(SERVICE, &refs, ®istry).map_err(Failure::branch_commit)?;
343 fork_from_with_kind(
344 &parent,
345 &main,
346 BranchKind::Work,
347 &mut refs,
348 ®istry,
349 current_timestamp(),
350 )
351 .map_err(Failure::branch_commit)?;
352 }
353 refs.confirm_directory().map_err(Failure::branch_ref)?;
357 let visible: BTreeMap<_, _> = refs
358 .list()
359 .map(|record| {
360 let kept = refs.kept(&record.name).to_vec();
361 (record.name.clone(), (record.clone(), kept))
362 })
363 .collect();
364 let holder = writer.read_holder();
367 let mut storage = Self {
368 refs,
369 visible: Catalogue::new(visible, Arc::clone(&reader), holder),
370 registry,
371 reader,
372 writer,
373 pending_discards: BTreeSet::new(),
374 confirmed_candidates: BTreeMap::new(),
375 kept_rows: history::KeptRows::new(kept),
376 discard_passes: DiscardPasses::default(),
377 };
378 storage.reconcile_discards()?;
379 Ok(storage)
380 }
381
382 pub fn capture(&self, selection: &Selection) -> Result<Captured, Failure> {
386 self.visible.capture(selection)
387 }
388
389 pub(in crate::serve) fn activate_reclaim(&self) -> Result<(), Failure> {
390 self.writer.activate_reclaim().map_err(Failure::from)
391 }
392
393 #[cfg(test)]
398 pub fn quiesce_reclaim(&self) -> Result<(), Failure> {
399 self.writer.quiesce_reclaim().map_err(Failure::from)
400 }
401
402 #[cfg(test)]
405 pub fn reclaim_status(&self) -> haematite::db::ReclaimStatus {
406 self.writer.reclaim_status()
407 }
408
409 #[cfg(test)]
411 pub fn live_read_holds(&self) -> usize {
412 self.writer.read_holder().status().live
413 }
414
415 pub fn catalogue(&self) -> Catalogue {
416 self.visible.clone()
417 }
418
419 #[must_use]
422 pub fn discard_passes(&self) -> DiscardPasses {
423 self.discard_passes.clone()
424 }
425
426 pub fn reader(&self) -> Result<ReadOnlyDatabase, Failure> {
432 ReadOnlyDatabase::open(self.writer.data_dir()).map_err(Failure::observer)
433 }
434
435 fn retain_current(&mut self, branch: &str, record: &BranchRefRecord) -> Result<(), Failure> {
436 self.retain_history_row(branch, record)
437 }
438
439 fn publish_record(&self, name: &str) -> Result<(), Failure> {
440 let record = self
441 .refs
442 .get(name)
443 .ok_or_else(|| Failure::new("reference_missing", "committed reference disappeared"))?;
444 self.visible
445 .publish(record.clone(), self.refs.kept(name).to_vec())
446 }
447}
448
449const fn version(record: &BranchRefRecord) -> Version {
450 Version {
451 generation: record.created,
452 sequence: record.seq,
453 }
454}
455
456fn branch_name(name: &str) -> String {
457 format!("{BRANCH_PREFIX}{name}")
458}
459
460fn identifier(value: &str) -> Result<(), Failure> {
461 if value.is_empty() || value.len() > 512 || value.contains('\0') {
462 return Err(Failure::new(
463 "invalid_identifier",
464 "names must contain 1 to 512 UTF-8 bytes and no NUL",
465 ));
466 }
467 Ok(())
468}
469
470fn collection_key(name: &str) -> Vec<u8> {
471 format!("c\0{name}").into_bytes()
472}
473pub(in crate::serve) fn document_key(collection: &str, id: &str) -> Vec<u8> {
474 format!("d\0{collection}\0{id}").into_bytes()
475}
476
477pub(in crate::serve) fn revision_key(collection: &str, id: &str) -> Vec<u8> {
478 format!("m\0{collection}\0{id}").into_bytes()
479}
480fn receipt_key(branch: &str, generation: u64, id: &str) -> Vec<u8> {
481 format!("r\0{branch}\0{generation}\0{id}").into_bytes()
482}
483
484#[cfg(test)]
485mod long_wait_tests {
486 use std::error::Error;
487 use std::time::Duration;
488
489 use haematite::DatabaseError;
490 use haematite::db::DatabaseConfig;
491 use haematite::store::NodeCacheBudget;
492
493 use super::{Storage, runtime_options};
494
495 #[test]
496 fn zero_is_the_typed_engine_refusal() {
497 assert!(matches!(
498 runtime_options(Duration::ZERO),
499 Err(DatabaseError::InvalidLongWaitInterval { given }) if given == Duration::ZERO
500 ));
501 }
502
503 #[test]
504 fn zero_refuses_create_and_open_before_directory_checks() -> Result<(), Box<dyn Error>> {
505 let directory = tempfile::tempdir()?;
506 let path = directory.path().join("absent");
507 let config = DatabaseConfig {
508 data_dir: path.clone(),
509 shard_count: 1,
510 distributed: None,
511 executor_threads: Some(1),
512 node_cache_budget: Some(NodeCacheBudget::Unlimited),
513 retention: haematite::Retention::reclaim(0),
514 };
515 for refused in [
516 Storage::create(config, NodeCacheBudget::Unlimited, Duration::ZERO),
517 Storage::open(&path, NodeCacheBudget::Unlimited, Duration::ZERO),
518 ] {
519 let failure = refused.err().ok_or("zero interval was accepted")?;
520 assert_eq!(failure.code, "storage_unavailable");
521 assert_eq!(
522 failure.message,
523 DatabaseError::InvalidLongWaitInterval {
524 given: Duration::ZERO
525 }
526 .to_string()
527 );
528 assert!(!path.try_exists()?);
529 }
530 Ok(())
531 }
532}