1#[derive(Debug, Clone)]
8pub struct QueryDbConfig {
9 pub shard_count: usize,
11 pub compaction_fragmentation_threshold: f64,
14 pub compaction_delta_ratio_threshold: f64,
17 pub derived_persistence_filename: String,
20 pub enable_background_compaction: bool,
22 pub max_entry_size_bytes: usize,
30}
31
32impl Default for QueryDbConfig {
33 fn default() -> Self {
34 Self {
35 shard_count: 64,
36 compaction_fragmentation_threshold: 0.20,
37 compaction_delta_ratio_threshold: 0.10,
38 derived_persistence_filename: "derived.sqry".to_owned(),
39 enable_background_compaction: true,
40 max_entry_size_bytes: 1 << 20, }
42 }
43}
44
45impl QueryDbConfig {
46 #[must_use]
48 pub fn builder() -> QueryDbConfigBuilder {
49 QueryDbConfigBuilder(Self::default())
50 }
51
52 #[must_use]
64 pub fn from_env() -> Self {
65 Self::from_env_impl(|key| std::env::var(key).ok())
66 }
67
68 fn from_env_impl(getter: impl Fn(&str) -> Option<String>) -> Self {
72 let mut cfg = Self::default();
73
74 if let Some(v) = getter("SQRY_DB_SHARD_COUNT")
75 && let Ok(n) = v.parse::<usize>()
76 && n.is_power_of_two()
77 && n > 0
78 {
79 cfg.shard_count = n;
80 }
81 if let Some(v) = getter("SQRY_DB_COMPACTION_FRAG")
82 && let Ok(f) = v.parse::<f64>()
83 && (0.0..=1.0).contains(&f)
84 {
85 cfg.compaction_fragmentation_threshold = f;
86 }
87 if let Some(v) = getter("SQRY_DB_COMPACTION_DELTA")
88 && let Ok(f) = v.parse::<f64>()
89 && (0.0..=1.0).contains(&f)
90 {
91 cfg.compaction_delta_ratio_threshold = f;
92 }
93 if let Some(v) = getter("SQRY_DB_DERIVED_FILE")
94 && !v.is_empty()
95 {
96 cfg.derived_persistence_filename = v;
97 }
98 if let Some(v) = getter("SQRY_DB_BG_COMPACTION") {
99 cfg.enable_background_compaction = v != "0";
100 }
101 if let Some(v) = getter("SQRY_DB_MAX_ENTRY_SIZE_BYTES") {
102 match v.parse::<usize>() {
103 Ok(0) => {
104 log::warn!(
105 "SQRY_DB_MAX_ENTRY_SIZE_BYTES=0 is invalid (must be > 0); \
106 using default {}",
107 cfg.max_entry_size_bytes
108 );
109 }
110 Ok(n) => {
111 cfg.max_entry_size_bytes = n;
112 }
113 Err(_) => {
114 log::warn!(
115 "SQRY_DB_MAX_ENTRY_SIZE_BYTES={:?} could not be parsed as usize; \
116 using default {}",
117 v,
118 cfg.max_entry_size_bytes
119 );
120 }
121 }
122 }
123
124 cfg
125 }
126}
127
128pub struct QueryDbConfigBuilder(QueryDbConfig);
130
131impl QueryDbConfigBuilder {
132 #[must_use]
138 pub fn shard_count(mut self, count: usize) -> Self {
139 assert!(
140 count > 0 && count.is_power_of_two(),
141 "shard_count must be a positive power of two"
142 );
143 self.0.shard_count = count;
144 self
145 }
146
147 #[must_use]
149 pub fn compaction_fragmentation_threshold(mut self, threshold: f64) -> Self {
150 self.0.compaction_fragmentation_threshold = threshold.clamp(0.0, 1.0);
151 self
152 }
153
154 #[must_use]
156 pub fn compaction_delta_ratio_threshold(mut self, threshold: f64) -> Self {
157 self.0.compaction_delta_ratio_threshold = threshold.clamp(0.0, 1.0);
158 self
159 }
160
161 #[must_use]
163 pub fn derived_persistence_filename(mut self, filename: impl Into<String>) -> Self {
164 self.0.derived_persistence_filename = filename.into();
165 self
166 }
167
168 #[must_use]
170 pub fn enable_background_compaction(mut self, enable: bool) -> Self {
171 self.0.enable_background_compaction = enable;
172 self
173 }
174
175 #[must_use]
185 pub fn max_entry_size_bytes(mut self, bytes: usize) -> Self {
186 assert!(bytes > 0, "max_entry_size_bytes must be > 0");
187 self.0.max_entry_size_bytes = bytes;
188 self
189 }
190
191 #[must_use]
193 pub fn build(self) -> QueryDbConfig {
194 self.0
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
207 fn max_entry_size_bytes_default_is_1_mib() {
208 let cfg = QueryDbConfig::default();
209 assert_eq!(cfg.max_entry_size_bytes, 1 << 20);
210 assert_eq!(cfg.max_entry_size_bytes, 1_048_576);
211 }
212
213 fn fake_env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
218 move |key| {
219 pairs
220 .iter()
221 .find(|(k, _)| *k == key)
222 .map(|(_, v)| (*v).to_owned())
223 }
224 }
225
226 #[test]
227 fn max_entry_size_bytes_from_env_parses_valid() {
228 let cfg =
229 QueryDbConfig::from_env_impl(fake_env(&[("SQRY_DB_MAX_ENTRY_SIZE_BYTES", "2097152")]));
230 assert_eq!(cfg.max_entry_size_bytes, 2_097_152);
231 }
232
233 #[test]
234 fn max_entry_size_bytes_from_env_rejects_zero() {
235 let cfg = QueryDbConfig::from_env_impl(fake_env(&[("SQRY_DB_MAX_ENTRY_SIZE_BYTES", "0")]));
237 assert_eq!(cfg.max_entry_size_bytes, 1 << 20);
238 }
239
240 #[test]
241 fn max_entry_size_bytes_from_env_rejects_unparseable() {
242 let cfg = QueryDbConfig::from_env_impl(fake_env(&[(
244 "SQRY_DB_MAX_ENTRY_SIZE_BYTES",
245 "not_a_number",
246 )]));
247 assert_eq!(cfg.max_entry_size_bytes, 1 << 20);
248 }
249
250 #[test]
255 #[should_panic(expected = "max_entry_size_bytes must be > 0")]
256 fn builder_rejects_zero_max_entry_size_bytes() {
257 let _ = QueryDbConfig::builder().max_entry_size_bytes(0).build();
258 }
259
260 #[test]
261 fn builder_accepts_nonzero_max_entry_size_bytes() {
262 let cfg = QueryDbConfig::builder()
263 .max_entry_size_bytes(512 * 1024) .build();
265 assert_eq!(cfg.max_entry_size_bytes, 524_288);
266 }
267}