1use std::fmt;
2use std::path::{Path, PathBuf};
3
4use lora_wal::{SyncMode, WalConfig};
5
6pub const DEFAULT_DATABASE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
13
14#[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#[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}