Skip to main content

lora_database/
named.rs

1use std::fmt;
2use std::path::{Path, PathBuf};
3
4use lora_wal::{SyncMode, WalConfig};
5
6/// Hard ceiling for one portable database archive/root.
7///
8/// The current WAL backend still stores segment files under this root, but
9/// callers should treat the resolved `.lora` path as the database artifact.
10/// The archive backend will use the same limit when it starts writing framed
11/// compressed chunks directly into portable files.
12pub const DEFAULT_DATABASE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
13
14/// Options for opening a named filesystem-backed database.
15#[derive(Debug, Clone)]
16pub struct DatabaseOpenOptions {
17    pub database_dir: PathBuf,
18    pub sync_mode: SyncMode,
19    pub segment_target_bytes: u64,
20    pub max_database_bytes: u64,
21}
22
23impl Default for DatabaseOpenOptions {
24    fn default() -> Self {
25        Self {
26            database_dir: PathBuf::from("."),
27            sync_mode: SyncMode::Group { interval_ms: 1_000 },
28            segment_target_bytes: 8 * 1024 * 1024,
29            max_database_bytes: DEFAULT_DATABASE_MAX_BYTES,
30        }
31    }
32}
33
34impl DatabaseOpenOptions {
35    pub fn with_database_dir(mut self, database_dir: impl Into<PathBuf>) -> Self {
36        self.database_dir = database_dir.into();
37        self
38    }
39
40    pub fn wal_config_for(&self, name: &DatabaseName) -> WalConfig {
41        WalConfig::Enabled {
42            dir: self.database_path_for(name),
43            sync_mode: self.sync_mode,
44            segment_target_bytes: self.segment_target_bytes,
45        }
46    }
47
48    pub fn database_path_for(&self, name: &DatabaseName) -> PathBuf {
49        self.database_dir.join(format!("{}.lora", name.as_str()))
50    }
51}
52
53/// Validated logical database name.
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
55pub struct DatabaseName(String);
56
57impl DatabaseName {
58    pub fn parse(value: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
59        let value = value.as_ref();
60        if value.is_empty() {
61            return Err(DatabaseNameError::Empty);
62        }
63        if value == "." || value == ".." {
64            return Err(DatabaseNameError::Reserved(value.to_string()));
65        }
66        if !value
67            .bytes()
68            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
69        {
70            return Err(DatabaseNameError::InvalidCharacters(value.to_string()));
71        }
72        Ok(Self(value.to_string()))
73    }
74
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78}
79
80impl fmt::Display for DatabaseName {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        self.0.fmt(f)
83    }
84}
85
86impl TryFrom<&str> for DatabaseName {
87    type Error = DatabaseNameError;
88
89    fn try_from(value: &str) -> Result<Self, Self::Error> {
90        Self::parse(value)
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum DatabaseNameError {
96    Empty,
97    Reserved(String),
98    InvalidCharacters(String),
99}
100
101impl fmt::Display for DatabaseNameError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::Empty => write!(f, "database name must not be empty"),
105            Self::Reserved(name) => write!(f, "database name '{name}' is reserved"),
106            Self::InvalidCharacters(name) => write!(
107                f,
108                "invalid database name '{name}': use only letters, digits, '_', '-', and '.'"
109            ),
110        }
111    }
112}
113
114impl std::error::Error for DatabaseNameError {}
115
116pub fn resolve_database_path(
117    database_name: &str,
118    database_dir: impl AsRef<Path>,
119) -> Result<PathBuf, DatabaseNameError> {
120    let name = DatabaseName::parse(database_name)?;
121    Ok(database_dir
122        .as_ref()
123        .join(format!("{}.lora", name.as_str())))
124}