laminar_db/profile/mod.rs
1//! Deployment profiles for `LaminarDB`.
2//!
3//! A [`Profile`] determines which subsystems are activated at startup.
4//! Profiles form a hierarchy: each tier includes all capabilities of
5//! the tiers below it.
6//!
7//! ```text
8//! BareMetal ⊂ Embedded ⊂ Durable ⊂ Cluster
9//! ```
10//!
11//! ## Usage
12//!
13//! ```rust,ignore
14//! use laminar_db::{LaminarDB, Profile};
15//!
16//! let db = LaminarDB::builder()
17//! .profile(Profile::Durable)
18//! .object_store_url("s3://my-bucket/checkpoints")
19//! .build()
20//! .await?;
21//! ```
22
23use std::fmt;
24use std::str::FromStr;
25
26use laminar_core::storage_location::{StorageLocation, StorageProvider};
27
28use crate::config::LaminarConfig;
29
30/// Deployment profile — determines which subsystems are activated.
31///
32/// Profiles are ordered by capability: each tier includes everything
33/// from the tiers below it.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum Profile {
36 /// In-memory only, no persistence. Fastest startup.
37 #[default]
38 BareMetal,
39 /// Local WAL persistence (embedded single-node).
40 Embedded,
41 /// Object-store checkpoints + rkyv snapshots.
42 Durable,
43 /// Full distributed: Durable + gRPC + gossip + cluster primitives.
44 Cluster,
45}
46
47impl Profile {
48 /// Auto-detect the appropriate profile from configuration.
49 ///
50 /// Uses orthogonal signals (checkpoint URL scheme, presence of
51 /// discovery config) rather than requiring an explicit profile choice.
52 ///
53 /// | Signal | Detected Profile |
54 /// |--------|-----------------|
55 /// | `has_discovery` = true | `Cluster` |
56 /// | `object_store_url` uses a supported cloud scheme | `Durable` |
57 /// | `object_store_url` is `file://` or `storage_dir` set | `Embedded` |
58 /// | None of the above | `BareMetal` |
59 #[must_use]
60 pub fn from_config(config: &LaminarConfig, has_discovery: bool) -> Self {
61 if has_discovery {
62 return Self::Cluster;
63 }
64 if let Some(url) = &config.object_store_url {
65 if let Ok(location) = StorageLocation::parse(url) {
66 return if location.provider == StorageProvider::Local {
67 Self::Embedded
68 } else {
69 Self::Durable
70 };
71 }
72 }
73 if config.storage_dir.is_some()
74 || config
75 .checkpoint
76 .as_ref()
77 .and_then(|checkpoint| checkpoint.data_dir.as_ref())
78 .is_some()
79 {
80 return Self::Embedded;
81 }
82 Self::BareMetal
83 }
84
85 /// Validate that the compiled feature flags satisfy this profile's
86 /// requirements. Returns an error if a required feature was not
87 /// compiled in.
88 ///
89 /// # Errors
90 ///
91 /// Returns [`ProfileError::FeatureNotCompiled`] if a required Cargo
92 /// feature is missing.
93 pub fn validate_features(self) -> Result<(), ProfileError> {
94 // Feature gates for durable/cluster were removed — all profiles are
95 // always available. Heavy distributed deps (tonic, chitchat)
96 // are gated on laminar-core's `cluster` feature, which the
97 // server binary enables unconditionally. Library users of laminar-db
98 // get lightweight builds without distributed infrastructure.
99 match self {
100 Self::BareMetal | Self::Embedded | Self::Durable | Self::Cluster => Ok(()),
101 }
102 }
103
104 /// Validate that the given configuration satisfies this profile's
105 /// runtime requirements (e.g., a storage directory for Embedded,
106 /// an object store URL for Durable).
107 ///
108 /// # Errors
109 ///
110 /// Returns [`ProfileError::RequirementNotMet`] if a required config
111 /// field is missing.
112 pub fn validate_config(
113 self,
114 config: &LaminarConfig,
115 object_store_url: Option<&str>,
116 ) -> Result<(), ProfileError> {
117 match self {
118 Self::BareMetal => Ok(()),
119 Self::Embedded => {
120 if let Some(url) = object_store_url.filter(|url| {
121 laminar_core::storage_location::StorageProvider::detect_uri(url)
122 == Some(laminar_core::storage_location::StorageProvider::Local)
123 }) {
124 laminar_core::checkpoint::object_store_builder::file_url_path(url)
125 .map_err(|error| ProfileError::RequirementNotMet(error.to_string()))?;
126 return Ok(());
127 }
128 if config.storage_dir.is_some()
129 || config
130 .checkpoint
131 .as_ref()
132 .and_then(|checkpoint| checkpoint.data_dir.as_ref())
133 .is_some()
134 {
135 return Ok(());
136 }
137 Err(ProfileError::RequirementNotMet(
138 "Embedded profile requires an absolute file:// checkpoint URL or local storage directory"
139 .into(),
140 ))
141 }
142 Self::Durable | Self::Cluster => {
143 if object_store_url.is_none() {
144 return Err(ProfileError::RequirementNotMet(
145 "Durable/Cluster profile requires an \
146 object_store_url"
147 .into(),
148 ));
149 }
150 Ok(())
151 }
152 }
153 }
154
155 /// Apply sensible defaults to a [`LaminarConfig`] for this profile.
156 ///
157 /// Does not override fields that the user has already set.
158 pub fn apply_defaults(self, config: &mut LaminarConfig) {
159 match self {
160 Self::BareMetal => {
161 // No persistence — nothing to configure.
162 }
163 Self::Embedded => {
164 // Ensure a reasonable buffer size for local workloads.
165 if config.default_buffer_size == LaminarConfig::default().default_buffer_size {
166 config.default_buffer_size = 32_768;
167 }
168 }
169 Self::Durable => {
170 // Larger buffers for durable workloads.
171 if config.default_buffer_size == LaminarConfig::default().default_buffer_size {
172 config.default_buffer_size = 131_072;
173 }
174 }
175 Self::Cluster => {
176 // Largest buffers for distributed workloads.
177 if config.default_buffer_size == LaminarConfig::default().default_buffer_size {
178 config.default_buffer_size = 262_144;
179 }
180 }
181 }
182 }
183}
184
185impl FromStr for Profile {
186 type Err = ProfileError;
187
188 fn from_str(s: &str) -> Result<Self, Self::Err> {
189 match s.to_ascii_lowercase().as_str() {
190 "bare_metal" | "baremetal" | "bare-metal" => Ok(Self::BareMetal),
191 "embedded" => Ok(Self::Embedded),
192 "durable" => Ok(Self::Durable),
193 "cluster" => Ok(Self::Cluster),
194 _ => Err(ProfileError::UnknownProfileName(s.into())),
195 }
196 }
197}
198
199impl fmt::Display for Profile {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 Self::BareMetal => write!(f, "bare_metal"),
203 Self::Embedded => write!(f, "embedded"),
204 Self::Durable => write!(f, "durable"),
205 Self::Cluster => write!(f, "cluster"),
206 }
207 }
208}
209
210/// Errors from profile validation.
211#[derive(Debug, thiserror::Error)]
212pub enum ProfileError {
213 /// A runtime requirement (e.g., config field) was not satisfied.
214 #[error("profile requirement not met: {0}")]
215 RequirementNotMet(String),
216
217 /// A required Cargo feature was not compiled in.
218 #[error("feature `{0}` not compiled — enable it in Cargo.toml")]
219 FeatureNotCompiled(String),
220
221 /// The profile name could not be parsed.
222 #[error("unknown profile name: {0}")]
223 UnknownProfileName(String),
224}
225
226#[cfg(test)]
227mod tests;