trine_kv/options.rs
1use std::path::{Path, PathBuf};
2
3use crate::{codec::CodecId, prefix::PrefixExtractor, runtime::RuntimeOptions};
4
5/// Storage location and host backend selected for a database.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum StorageMode {
8 /// Keep all data in memory and discard it when the handle closes.
9 InMemory,
10 /// Store data in a native filesystem directory.
11 Persistent {
12 /// Native filesystem database directory.
13 path: PathBuf,
14 },
15 /// Store data through an explicit host-provided backend.
16 HostPersistent {
17 /// Host backend used for persistent storage.
18 backend: HostStorageBackend,
19 },
20}
21
22/// Host storage backend selected for non-native persistent modes.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum HostStorageBackend {
25 /// Use a WASI preopened filesystem path.
26 Wasi {
27 /// WASI preopened filesystem path.
28 path: PathBuf,
29 },
30 /// Use browser storage.
31 Browser,
32}
33
34impl HostStorageBackend {
35 pub(crate) const fn as_str(&self) -> &'static str {
36 match self {
37 Self::Wasi { .. } => "WASI persistent storage backend",
38 Self::Browser => "browser persistent storage backend",
39 }
40 }
41}
42
43impl StorageMode {
44 pub(crate) fn persistent_path(&self) -> Option<&Path> {
45 match self {
46 Self::Persistent { path }
47 | Self::HostPersistent {
48 backend: HostStorageBackend::Wasi { path },
49 } => Some(path.as_path()),
50 Self::InMemory
51 | Self::HostPersistent {
52 backend: HostStorageBackend::Browser,
53 } => None,
54 }
55 }
56
57 pub(crate) const fn is_wasi_persistent(&self) -> bool {
58 matches!(
59 self,
60 Self::HostPersistent {
61 backend: HostStorageBackend::Wasi { .. }
62 }
63 )
64 }
65
66 pub(crate) const fn is_browser_persistent(&self) -> bool {
67 matches!(
68 self,
69 Self::HostPersistent {
70 backend: HostStorageBackend::Browser
71 }
72 )
73 }
74}
75
76impl Default for StorageMode {
77 fn default() -> Self {
78 Self::InMemory
79 }
80}
81
82/// Durability requested for committed writes.
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
84pub enum DurabilityMode {
85 /// Accept writes after buffering them in the storage backend.
86 #[default]
87 Buffered,
88 /// Flush buffered bytes to the backend.
89 Flush,
90 /// Sync file data without requiring metadata sync where the backend allows it.
91 SyncData,
92 /// Sync file data and metadata where the backend allows it.
93 SyncAll,
94}
95
96impl DurabilityMode {
97 pub(crate) const fn as_str(self) -> &'static str {
98 match self {
99 Self::Buffered => "buffered",
100 Self::Flush => "flush",
101 Self::SyncData => "sync-data",
102 Self::SyncAll => "sync-all",
103 }
104 }
105}
106
107/// Compression codec profile used for table blocks.
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
109pub enum CompressionProfile {
110 /// Store table blocks without compression.
111 None,
112 /// Use the v1 fast LZ4 block codec.
113 #[default]
114 Fast,
115}
116
117impl CompressionProfile {
118 #[must_use]
119 pub(crate) const fn codec_id(self) -> CodecId {
120 match self {
121 Self::None => CodecId::None,
122 Self::Fast => CodecId::FastLz4Block,
123 }
124 }
125}
126
127/// Point-read filter policy for table keys.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum FilterPolicy {
130 /// Do not write table key filters.
131 Disabled,
132 /// Write Bloom filters with the requested bit budget per key.
133 Bloom {
134 /// Bloom-filter bit budget per key.
135 bits_per_key: u8,
136 },
137}
138
139impl Default for FilterPolicy {
140 fn default() -> Self {
141 Self::Bloom { bits_per_key: 10 }
142 }
143}
144
145/// Prefix-read filter policy for extracted key prefixes.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum PrefixFilterPolicy {
148 /// Do not write prefix filters.
149 Disabled,
150 /// Write Bloom filters with the requested bit budget per prefix.
151 Bloom {
152 /// Bloom-filter bit budget per extracted prefix.
153 bits_per_prefix: u8,
154 },
155}
156
157impl Default for PrefixFilterPolicy {
158 fn default() -> Self {
159 Self::Bloom {
160 bits_per_prefix: 10,
161 }
162 }
163}
164
165/// Search strategy used inside table indexes.
166#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
167pub enum IndexSearchPolicy {
168 /// Scan index entries linearly.
169 Linear,
170 /// Search index entries with binary search.
171 Binary,
172 /// Let Trine choose based on index size.
173 #[default]
174 Auto,
175}
176
177/// Startup policy when repairable temporary files are found.
178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
179pub enum FailOnCorruptionPolicy {
180 /// Report the files as corruption and leave them untouched.
181 #[default]
182 FailClosed,
183 /// Delete safe temporary files and write a recovery report.
184 RepairSafeTemporaryFiles,
185}
186
187/// Options used when opening a database.
188///
189/// `DbOptions` controls where data is stored, whether missing storage is
190/// created, the default write durability, bucket defaults, runtime behavior,
191/// maintenance thresholds, and startup recovery policy. Path-like calls to
192/// [`crate::Db::open`] and [`crate::Db::open_sync`] are converted with
193/// [`DbOptions::new`], which selects persistent native filesystem storage.
194///
195/// # Examples
196///
197/// ```rust
198/// use trine_kv::{Db, DbOptions, DurabilityMode};
199///
200/// # fn main() -> trine_kv::Result<()> {
201/// let persistent = Db::open_sync(
202/// DbOptions::new("target/doc-example-options")
203/// .with_durability(DurabilityMode::SyncAll),
204/// )?;
205///
206/// let memory = Db::open_sync(DbOptions::memory())?;
207/// assert_eq!(persistent.options().read_only, false);
208/// assert_eq!(memory.options().read_only, false);
209/// # Ok(())
210/// # }
211/// ```
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct DbOptions {
214 /// Storage location and host backend.
215 pub storage_mode: StorageMode,
216 /// Create the database directory and metadata when missing.
217 pub create_if_missing: bool,
218 /// Open without allowing writes or maintenance that mutates storage.
219 pub read_only: bool,
220 /// Options used when the built-in default bucket is first created.
221 pub default_bucket_options: BucketOptions,
222 /// Default durability used by write helpers that do not pass `WriteOptions`.
223 pub durability: DurabilityMode,
224 /// Active memtable target size before flush.
225 pub write_buffer_bytes: usize,
226 /// Maximum queued immutable memtables before writes apply backpressure.
227 pub max_immutable_memtables: usize,
228 /// Target size for newly written table files.
229 pub target_table_bytes: usize,
230 /// Per-level size multiplier used by compaction.
231 pub level_size_multiplier: usize,
232 /// Maximum L0 table count before compaction is requested.
233 pub max_l0_files: usize,
234 /// Approximate bytes reserved for cached table blocks.
235 pub block_cache_bytes: usize,
236 /// Number of background workers used by maintenance-capable runtimes.
237 pub background_worker_count: usize,
238 /// Runtime used for async, blocking, and background work.
239 pub runtime: RuntimeOptions,
240 /// Startup policy for safe temporary files left by interrupted writes.
241 pub fail_on_corruption: FailOnCorruptionPolicy,
242 /// Enable garbage collection of obsolete blob bytes.
243 pub blob_gc_enabled: bool,
244 /// Minimum discardable-byte ratio required before a blob file is collected.
245 pub blob_gc_discardable_ratio: BlobGcRatio,
246 /// Minimum blob file size considered for garbage collection.
247 pub blob_gc_min_file_bytes: u64,
248}
249
250impl DbOptions {
251 /// Default active memtable target size.
252 pub const DEFAULT_WRITE_BUFFER_BYTES: usize = 64 * 1024 * 1024;
253 /// Default target size for table files.
254 pub const DEFAULT_TARGET_TABLE_BYTES: usize = 64 * 1024 * 1024;
255 /// Default block-cache byte budget.
256 pub const DEFAULT_BLOCK_CACHE_BYTES: usize = 256 * 1024 * 1024;
257 /// Default minimum blob file size for garbage collection.
258 pub const DEFAULT_BLOB_GC_MIN_FILE_BYTES: u64 = 64 * 1024 * 1024;
259
260 /// Creates persistent database options for `path`.
261 ///
262 /// This is the path-first constructor used by `Db::open(path)` and
263 /// `Db::open_sync(path)`. It selects [`StorageMode::Persistent`], sets
264 /// safety-first default durability for confirmed writes, and leaves
265 /// `create_if_missing` enabled.
266 ///
267 /// # Parameters
268 ///
269 /// - `path`: native filesystem database directory.
270 #[must_use]
271 pub fn new(path: impl Into<PathBuf>) -> Self {
272 Self::persistent(path)
273 }
274
275 /// Creates in-memory database options.
276 ///
277 /// In-memory databases keep memtables, tables, metadata, and WAL state only
278 /// in process memory. Closing the handle drops all data. This is useful for
279 /// tests, temporary indexes, and caches that can be rebuilt.
280 #[must_use]
281 pub fn memory() -> Self {
282 Self::default()
283 }
284
285 /// Creates persistent native-filesystem options for `path`.
286 ///
287 /// This is equivalent to [`DbOptions::new`].
288 #[must_use]
289 pub fn persistent(path: impl Into<PathBuf>) -> Self {
290 Self {
291 storage_mode: StorageMode::Persistent { path: path.into() },
292 durability: DurabilityMode::SyncAll,
293 ..Self::default()
294 }
295 }
296
297 /// Creates persistent WASI host-filesystem options for `path`.
298 #[must_use]
299 pub fn wasi_persistent(path: impl Into<PathBuf>) -> Self {
300 Self {
301 storage_mode: StorageMode::HostPersistent {
302 backend: HostStorageBackend::Wasi { path: path.into() },
303 },
304 background_worker_count: 0,
305 durability: DurabilityMode::Flush,
306 runtime: RuntimeOptions::inline(),
307 ..Self::default()
308 }
309 }
310
311 /// Creates read-only WASI host-filesystem options for `path`.
312 #[must_use]
313 pub fn wasi_persistent_read_only(path: impl Into<PathBuf>) -> Self {
314 Self::wasi_persistent(path).read_only()
315 }
316
317 /// Creates browser persistent options.
318 #[must_use]
319 pub fn browser_persistent() -> Self {
320 Self {
321 storage_mode: StorageMode::HostPersistent {
322 backend: HostStorageBackend::Browser,
323 },
324 background_worker_count: 0,
325 durability: DurabilityMode::Flush,
326 runtime: RuntimeOptions::inline(),
327 ..Self::default()
328 }
329 }
330
331 /// Creates read-only browser persistent options.
332 #[must_use]
333 pub fn browser_persistent_read_only() -> Self {
334 Self::browser_persistent().read_only()
335 }
336
337 /// Creates read-only native-filesystem options for `path`.
338 #[must_use]
339 pub fn persistent_read_only(path: impl Into<PathBuf>) -> Self {
340 Self::persistent(path).read_only()
341 }
342
343 /// Sets the default durability for writes that do not pass `WriteOptions`.
344 ///
345 /// This affects helpers such as [`crate::Db::put_sync`] and
346 /// [`crate::Db::put`]. Calls that pass [`WriteOptions`] use their explicit
347 /// durability instead.
348 #[must_use]
349 pub const fn with_durability(mut self, durability: DurabilityMode) -> Self {
350 self.durability = durability;
351 self
352 }
353
354 /// Sets the options used by the built-in default bucket.
355 ///
356 /// Named buckets still use the options passed to `Db::bucket_with_options`.
357 #[must_use]
358 pub fn with_default_bucket_options(mut self, options: BucketOptions) -> Self {
359 self.default_bucket_options = options;
360 self
361 }
362
363 /// Marks these options read-only and disables creation of missing storage.
364 ///
365 /// Read-only handles can read existing persistent data, but reject writes,
366 /// flushes, compactions, and repairs that would mutate storage.
367 #[must_use]
368 pub const fn read_only(mut self) -> Self {
369 self.read_only = true;
370 self.create_if_missing = false;
371 self
372 }
373}
374
375impl Default for DbOptions {
376 fn default() -> Self {
377 Self {
378 storage_mode: StorageMode::InMemory,
379 create_if_missing: true,
380 read_only: false,
381 default_bucket_options: BucketOptions::default(),
382 durability: DurabilityMode::Buffered,
383 write_buffer_bytes: Self::DEFAULT_WRITE_BUFFER_BYTES,
384 max_immutable_memtables: 4,
385 target_table_bytes: Self::DEFAULT_TARGET_TABLE_BYTES,
386 level_size_multiplier: 10,
387 max_l0_files: 8,
388 block_cache_bytes: Self::DEFAULT_BLOCK_CACHE_BYTES,
389 background_worker_count: 1,
390 runtime: RuntimeOptions::default(),
391 fail_on_corruption: FailOnCorruptionPolicy::FailClosed,
392 blob_gc_enabled: true,
393 blob_gc_discardable_ratio: BlobGcRatio::HALF,
394 blob_gc_min_file_bytes: Self::DEFAULT_BLOB_GC_MIN_FILE_BYTES,
395 }
396 }
397}
398
399/// Ratio threshold used by blob garbage collection.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub struct BlobGcRatio {
402 millionths: u32,
403}
404
405impl BlobGcRatio {
406 /// A 50% discardable-byte threshold.
407 pub const HALF: Self = Self {
408 millionths: 500_000,
409 };
410 /// A 100% discardable-byte threshold.
411 pub const FULL: Self = Self {
412 millionths: 1_000_000,
413 };
414
415 /// Creates a ratio from millionths, where `1_000_000` means 100%.
416 #[must_use]
417 pub const fn from_millionths(millionths: u32) -> Self {
418 Self { millionths }
419 }
420
421 /// Returns this ratio in millionths.
422 #[must_use]
423 pub const fn millionths(self) -> u32 {
424 self.millionths
425 }
426
427 pub(crate) fn should_collect(self, discardable_bytes: u64, total_bytes: u64) -> bool {
428 if total_bytes == 0 {
429 return false;
430 }
431 u128::from(discardable_bytes).saturating_mul(1_000_000)
432 >= u128::from(total_bytes).saturating_mul(u128::from(self.millionths))
433 }
434}
435
436impl Default for BlobGcRatio {
437 fn default() -> Self {
438 Self::HALF
439 }
440}
441
442/// Policy for merging blob values back into table levels during compaction.
443#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
444pub enum BlobLevelMergePolicy {
445 /// Keep blob values in blob files during compaction.
446 Disabled,
447 /// Let Trine decide based on level and value size.
448 #[default]
449 Auto,
450 /// Always merge blob values back into table files when compacting.
451 Always,
452}
453
454/// Options fixed for a bucket when the bucket is created.
455#[derive(Debug, Clone, PartialEq, Eq)]
456pub struct BucketOptions {
457 /// Allow empty user keys.
458 pub allow_empty_keys: bool,
459 /// Compression profile used for table blocks.
460 pub compression: CompressionProfile,
461 /// Target uncompressed data-block size.
462 pub block_bytes: usize,
463 /// Point-read filter policy for table keys.
464 pub filter_policy: FilterPolicy,
465 /// Prefix extractor used by prefix filters.
466 pub prefix_extractor: PrefixExtractor,
467 /// Prefix-read filter policy.
468 pub prefix_filter_policy: PrefixFilterPolicy,
469 /// Search strategy used inside table indexes.
470 pub index_search_policy: IndexSearchPolicy,
471 /// Values at or above this size are stored in blob files.
472 pub blob_threshold_bytes: usize,
473 /// Policy for merging blob values during compaction.
474 pub blob_level_merge_policy: BlobLevelMergePolicy,
475}
476
477impl BucketOptions {
478 /// Default target data-block size.
479 pub const DEFAULT_BLOCK_BYTES: usize = 16 * 1024;
480 /// Default threshold for storing values in blob files.
481 pub const DEFAULT_BLOB_THRESHOLD_BYTES: usize = 1024 * 1024;
482
483 /// Sets the prefix extractor for this bucket.
484 #[must_use]
485 pub fn with_prefix_extractor(mut self, prefix_extractor: PrefixExtractor) -> Self {
486 self.prefix_extractor = prefix_extractor;
487 self
488 }
489
490 /// Sets the value-size threshold for blob storage.
491 #[must_use]
492 pub const fn with_blob_threshold_bytes(mut self, blob_threshold_bytes: usize) -> Self {
493 self.blob_threshold_bytes = blob_threshold_bytes;
494 self
495 }
496
497 /// Sets the blob-level merge policy.
498 #[must_use]
499 pub const fn with_blob_level_merge_policy(mut self, policy: BlobLevelMergePolicy) -> Self {
500 self.blob_level_merge_policy = policy;
501 self
502 }
503
504 /// Enables or disables blob-level merge with a boolean convenience flag.
505 #[must_use]
506 pub const fn with_blob_level_merge_enabled(mut self, enabled: bool) -> Self {
507 self.blob_level_merge_policy = if enabled {
508 BlobLevelMergePolicy::Always
509 } else {
510 BlobLevelMergePolicy::Disabled
511 };
512 self
513 }
514}
515
516impl Default for BucketOptions {
517 fn default() -> Self {
518 Self {
519 allow_empty_keys: true,
520 compression: CompressionProfile::Fast,
521 block_bytes: Self::DEFAULT_BLOCK_BYTES,
522 filter_policy: FilterPolicy::default(),
523 prefix_extractor: PrefixExtractor::default(),
524 prefix_filter_policy: PrefixFilterPolicy::default(),
525 index_search_policy: IndexSearchPolicy::Auto,
526 blob_threshold_bytes: Self::DEFAULT_BLOB_THRESHOLD_BYTES,
527 blob_level_merge_policy: BlobLevelMergePolicy::Auto,
528 }
529 }
530}
531
532/// Per-write options passed to commit operations.
533///
534/// `WriteOptions` lets a single write or batch request a durability level
535/// different from the database default. The options are evaluated when the
536/// write is accepted; changing a `WriteOptions` value later has no effect on an
537/// already committed write.
538///
539/// # Examples
540///
541/// ```rust
542/// use trine_kv::{Db, WriteOptions};
543///
544/// # fn main() -> trine_kv::Result<()> {
545/// let db = Db::open_sync(trine_kv::DbOptions::memory())?;
546/// let commit = db.put_with_options_sync(b"k", b"v", WriteOptions::sync_all())?;
547/// assert!(commit.sequence().get() > 0);
548/// # Ok(())
549/// # }
550/// ```
551#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
552pub struct WriteOptions {
553 /// Durability requested for this write.
554 pub durability: DurabilityMode,
555}
556
557impl WriteOptions {
558 /// Creates write options with an explicit durability mode.
559 ///
560 /// # Parameters
561 ///
562 /// - `durability`: durability requested for the write or batch.
563 #[must_use]
564 pub const fn new(durability: DurabilityMode) -> Self {
565 Self { durability }
566 }
567
568 /// Creates buffered write options.
569 ///
570 /// The write may return after bytes are accepted by the storage backend but
571 /// before a flush or sync is requested. This is the lowest-latency and
572 /// weakest durability mode.
573 #[must_use]
574 pub const fn buffered() -> Self {
575 Self::new(DurabilityMode::Buffered)
576 }
577
578 /// Creates flush write options.
579 ///
580 /// The write requests a backend flush for accepted WAL bytes. Exact meaning
581 /// depends on the selected backend capabilities.
582 #[must_use]
583 pub const fn flush() -> Self {
584 Self::new(DurabilityMode::Flush)
585 }
586
587 /// Creates data-sync write options.
588 ///
589 /// The write requests durable file data without requiring metadata sync
590 /// where the backend can distinguish those operations.
591 #[must_use]
592 pub const fn sync_data() -> Self {
593 Self::new(DurabilityMode::SyncData)
594 }
595
596 /// Creates full-sync write options.
597 ///
598 /// The write requests the strongest durability mode supported by the
599 /// backend for WAL data and required metadata.
600 #[must_use]
601 pub const fn sync_all() -> Self {
602 Self::new(DurabilityMode::SyncAll)
603 }
604}