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