Skip to main content

reddb_file/
layout.rs

1//! Canonical file and sidecar path derivation.
2//!
3//! These helpers are pure: they do not touch the filesystem. Runtime crates
4//! use them so filename contracts live in `reddb-file` instead of being
5//! reassembled at call sites.
6
7use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11pub const SUPPORT_DIR_SUFFIX: &str = "red";
12pub const UNIFIED_WAL_EXTENSION: &str = "rdb-uwal";
13pub const LOGICAL_WAL_SUFFIX: &str = "logical.wal";
14pub const TEMP_EXTENSION: &str = "rdb-tmp";
15pub const ATOMIC_TEMP_EXTENSION: &str = "tmp";
16pub const PRIMARY_WAL_EXTENSION: &str = "redwal";
17pub const PAGER_LEGACY_WAL_EXTENSION: &str = "wal";
18pub const ENGINE_WAL_EXTENSION: &str = "rdb-wal";
19pub const PAGER_HEADER_EXTENSION: &str = "rdb-hdr";
20pub const PAGER_META_EXTENSION: &str = "rdb-meta";
21pub const PAGER_DWB_EXTENSION: &str = "rdb-dwb";
22pub const PAGER_HEADER_SHADOW_SUFFIX: &str = "hdr";
23pub const PAGER_META_SHADOW_SUFFIX: &str = "meta";
24pub const PAGER_DWB_SHADOW_SUFFIX: &str = "dwb";
25pub const SHM_FILE_SUFFIX: &str = "shm";
26pub const PHYSICAL_METADATA_JSON_SUFFIX: &str = "meta.json";
27pub const PHYSICAL_METADATA_BINARY_EXTENSION: &str = "meta.rdbx";
28pub const REBOOTSTRAP_STAGING_EXTENSION: &str = "rebootstrap.redbase";
29pub const REBOOTSTRAP_PENDING_EXTENSION: &str = "rebootstrap.pending.rdb";
30pub const REBOOTSTRAP_READY_EXTENSION: &str = "rebootstrap.ready";
31pub const REBOOTSTRAP_INTENT_LOG_EXTENSION: &str = "rebootstrap.intent.jsonl";
32pub const REBOOTSTRAP_PREVIOUS_EXTENSION: &str = "rebootstrap.previous.rdb";
33pub const PRIMARY_REPLICA_ROOT_EXTENSION: &str = "primary-replica";
34pub const LEGACY_LOGICAL_SLOTS_SUFFIX: &str = "logical.slots.json";
35pub const LEGACY_LOGICAL_SLOTS_TEMP_EXTENSION: &str = "logical.slots.tmp";
36pub const LEGACY_AUDIT_LOG_FILE_NAME: &str = ".audit.log";
37pub const AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION: &str = "zst";
38pub const LEGACY_SLOW_QUERY_LOG_FILE_NAME: &str = "red-slow.log";
39pub const SERVERLESS_ROOT_EXTENSION: &str = "serverless";
40pub const SERVERLESS_CACHE_DIR: &str = "cache";
41pub const RESULT_CACHE_L2_EXTENSION: &str = "result-cache.l2";
42pub const LOCAL_CAS_LOCK_SUFFIX: &str = "cas.lock";
43pub const LOCAL_UPLOAD_TEMP_TAG: &str = "tmp";
44
45/// Storage layout preset for tier-aware RedDB file placement.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(rename_all = "kebab-case")]
48#[derive(Default)]
49pub enum StorageLayout {
50    /// Keep only required durability sidecars next to the data file.
51    Minimal,
52    /// Default balance: shared support directory for durable metadata.
53    #[default]
54    Standard,
55    /// Put hot write/read artifacts into dedicated directories.
56    Performance,
57    /// Enable every known dedicated tier directory.
58    Max,
59}
60
61/// Optional per-toggle override applied after preset expansion.
62#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(default)]
64pub struct LayoutOverrides {
65    pub dedicated_wal_dir: Option<bool>,
66    pub dedicated_index_dir: Option<bool>,
67    pub dedicated_cache_dir: Option<bool>,
68    pub dedicated_snapshot_dir: Option<bool>,
69    pub dedicated_blob_dir: Option<bool>,
70    pub dedicated_temp_dir: Option<bool>,
71    pub dedicated_metrics_dir: Option<bool>,
72    /// Per-log routing overrides. See [`LogRoutingOverrides`].
73    #[serde(default)]
74    pub logs: LogRoutingOverrides,
75}
76
77/// Where a log stream should be written.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "kebab-case", tag = "kind", content = "path")]
80pub enum LogDestination {
81    Stderr,
82    File(PathBuf),
83    Syslog,
84}
85
86impl LogDestination {
87    /// Human-readable destination tag for status and diagnostics.
88    pub fn describe(&self) -> String {
89        match self {
90            Self::Stderr => "stderr".to_string(),
91            Self::Syslog => "syslog".to_string(),
92            Self::File(path) => format!("file:{}", path.display()),
93        }
94    }
95
96    /// Returns the file path if this destination writes to a file.
97    pub fn file_path(&self) -> Option<&Path> {
98        match self {
99            Self::File(path) => Some(path.as_path()),
100            _ => None,
101        }
102    }
103}
104
105/// Per-log destination overrides. `None` keeps the tier default.
106#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(default)]
108pub struct LogRoutingOverrides {
109    pub audit_log: Option<LogDestination>,
110    pub slow_log: Option<LogDestination>,
111}
112
113/// Fully expanded layout toggles after applying a preset and overrides.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115pub struct LayoutToggles {
116    pub dedicated_wal_dir: bool,
117    pub dedicated_index_dir: bool,
118    pub dedicated_cache_dir: bool,
119    pub dedicated_snapshot_dir: bool,
120    pub dedicated_blob_dir: bool,
121    pub dedicated_temp_dir: bool,
122    pub dedicated_metrics_dir: bool,
123}
124
125impl StorageLayout {
126    /// Default audit-log destination for this tier, before any override.
127    pub fn default_audit_log_in(self, support_dir: &Path) -> LogDestination {
128        match self {
129            Self::Performance | Self::Max => {
130                LogDestination::File(support_dir.join("logs").join("audit.log"))
131            }
132            Self::Minimal | Self::Standard => LogDestination::Stderr,
133        }
134    }
135
136    /// Default slow-query log destination for this tier, before any override.
137    pub fn default_slow_log_in(self, support_dir: &Path) -> LogDestination {
138        match self {
139            Self::Performance | Self::Max => {
140                LogDestination::File(support_dir.join("logs").join("slow.log"))
141            }
142            Self::Minimal | Self::Standard => LogDestination::Stderr,
143        }
144    }
145
146    pub fn expand(self, overrides: &LayoutOverrides) -> LayoutToggles {
147        let mut toggles = match self {
148            Self::Minimal => LayoutToggles {
149                dedicated_wal_dir: false,
150                dedicated_index_dir: false,
151                dedicated_cache_dir: false,
152                dedicated_snapshot_dir: false,
153                dedicated_blob_dir: false,
154                dedicated_temp_dir: false,
155                dedicated_metrics_dir: false,
156            },
157            Self::Standard => LayoutToggles {
158                dedicated_wal_dir: false,
159                dedicated_index_dir: true,
160                dedicated_cache_dir: false,
161                dedicated_snapshot_dir: true,
162                dedicated_blob_dir: false,
163                dedicated_temp_dir: false,
164                dedicated_metrics_dir: false,
165            },
166            Self::Performance => LayoutToggles {
167                dedicated_wal_dir: true,
168                dedicated_index_dir: true,
169                dedicated_cache_dir: true,
170                dedicated_snapshot_dir: true,
171                dedicated_blob_dir: true,
172                dedicated_temp_dir: false,
173                dedicated_metrics_dir: false,
174            },
175            Self::Max => LayoutToggles {
176                dedicated_wal_dir: true,
177                dedicated_index_dir: true,
178                dedicated_cache_dir: true,
179                dedicated_snapshot_dir: true,
180                dedicated_blob_dir: true,
181                dedicated_temp_dir: true,
182                dedicated_metrics_dir: true,
183            },
184        };
185
186        if let Some(value) = overrides.dedicated_wal_dir {
187            toggles.dedicated_wal_dir = value;
188        }
189        if let Some(value) = overrides.dedicated_index_dir {
190            toggles.dedicated_index_dir = value;
191        }
192        if let Some(value) = overrides.dedicated_cache_dir {
193            toggles.dedicated_cache_dir = value;
194        }
195        if let Some(value) = overrides.dedicated_snapshot_dir {
196            toggles.dedicated_snapshot_dir = value;
197        }
198        if let Some(value) = overrides.dedicated_blob_dir {
199            toggles.dedicated_blob_dir = value;
200        }
201        if let Some(value) = overrides.dedicated_temp_dir {
202            toggles.dedicated_temp_dir = value;
203        }
204        if let Some(value) = overrides.dedicated_metrics_dir {
205            toggles.dedicated_metrics_dir = value;
206        }
207
208        toggles
209    }
210}
211
212/// Deterministic paths derived from a data file and expanded layout.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct TieredLayoutPaths {
215    pub data_file: PathBuf,
216    pub support_dir: PathBuf,
217    pub wal_file: PathBuf,
218    pub logical_wal_file: PathBuf,
219    pub temp_file: PathBuf,
220    pub snapshot_dir: Option<PathBuf>,
221    pub index_dir: Option<PathBuf>,
222    pub cache_dir: Option<PathBuf>,
223    pub blob_dir: Option<PathBuf>,
224    pub metrics_dir: Option<PathBuf>,
225    pub logs_dir: Option<PathBuf>,
226    pub audit_log_destination: LogDestination,
227    pub slow_log_destination: LogDestination,
228    pub toggles: LayoutToggles,
229}
230
231impl TieredLayoutPaths {
232    pub fn new(
233        data_path: &Path,
234        layout: StorageLayout,
235        overrides: LayoutOverrides,
236    ) -> TieredLayoutPaths {
237        let toggles = layout.expand(&overrides);
238        let data_file = data_path.to_path_buf();
239        let support_dir = support_dir_for(data_path);
240
241        let wal_file = if toggles.dedicated_wal_dir {
242            unified_wal_path_in(&support_dir, data_path)
243        } else {
244            unified_wal_path(data_path)
245        };
246        let logical_wal_file = if toggles.dedicated_wal_dir {
247            logical_wal_path_in(&support_dir, data_path)
248        } else {
249            logical_wal_path(data_path)
250        };
251        let temp_file = if toggles.dedicated_temp_dir {
252            temp_path_in(&support_dir, data_path)
253        } else {
254            temp_path(data_path)
255        };
256
257        let audit_log_destination = overrides
258            .logs
259            .audit_log
260            .clone()
261            .unwrap_or_else(|| layout.default_audit_log_in(&support_dir));
262        let slow_log_destination = overrides
263            .logs
264            .slow_log
265            .clone()
266            .unwrap_or_else(|| layout.default_slow_log_in(&support_dir));
267        let logs_dir = match (
268            audit_log_destination.file_path(),
269            slow_log_destination.file_path(),
270        ) {
271            (None, None) => None,
272            _ => Some(support_dir.join("logs")),
273        };
274
275        TieredLayoutPaths {
276            data_file,
277            support_dir: support_dir.clone(),
278            wal_file,
279            logical_wal_file,
280            temp_file,
281            snapshot_dir: toggles
282                .dedicated_snapshot_dir
283                .then(|| support_dir.join("snapshots")),
284            index_dir: toggles
285                .dedicated_index_dir
286                .then(|| support_dir.join("indexes")),
287            cache_dir: toggles
288                .dedicated_cache_dir
289                .then(|| support_dir.join("cache")),
290            blob_dir: toggles
291                .dedicated_blob_dir
292                .then(|| support_dir.join("blobs")),
293            metrics_dir: toggles
294                .dedicated_metrics_dir
295                .then(|| support_dir.join("metrics")),
296            logs_dir,
297            audit_log_destination,
298            slow_log_destination,
299            toggles,
300        }
301    }
302
303    pub fn dirs_to_create(&self) -> Vec<PathBuf> {
304        let mut dirs = Vec::new();
305        push_parent(&mut dirs, &self.data_file);
306        push_parent(&mut dirs, &self.wal_file);
307        push_parent(&mut dirs, &self.logical_wal_file);
308        push_parent(&mut dirs, &self.temp_file);
309        push_optional(&mut dirs, self.snapshot_dir.as_ref());
310        push_optional(&mut dirs, self.index_dir.as_ref());
311        push_optional(&mut dirs, self.cache_dir.as_ref());
312        push_optional(&mut dirs, self.blob_dir.as_ref());
313        push_optional(&mut dirs, self.metrics_dir.as_ref());
314        push_optional(&mut dirs, self.logs_dir.as_ref());
315        if let Some(path) = self.audit_log_destination.file_path() {
316            push_parent(&mut dirs, path);
317        }
318        if let Some(path) = self.slow_log_destination.file_path() {
319            push_parent(&mut dirs, path);
320        }
321        dirs.sort();
322        dirs.dedup();
323        dirs
324    }
325
326    pub fn ensure_dirs(&self) -> std::io::Result<()> {
327        for dir in self.dirs_to_create() {
328            std::fs::create_dir_all(dir)?;
329        }
330        Ok(())
331    }
332
333    /// Path for a `vector.turbo` collection's `.tv` snapshot.
334    pub fn turbo_snapshot_path(&self, collection: &str) -> Option<PathBuf> {
335        if !self.toggles.dedicated_snapshot_dir {
336            return None;
337        }
338        if let Some(dir) = &self.snapshot_dir {
339            return Some(dir.join(format!("{collection}.tv")));
340        }
341        let stem = data_file_name(&self.data_file);
342        Some(sibling_path(
343            &self.data_file,
344            &format!("{stem}.{collection}.tv"),
345        ))
346    }
347}
348
349pub fn data_file_name(path: &Path) -> String {
350    path.file_name()
351        .and_then(|name| name.to_str())
352        .unwrap_or(DEFAULT_DATABASE_FILE_NAME)
353        .to_string()
354}
355
356pub const DEFAULT_DATABASE_FILE_NAME: &str = "data.rdb";
357pub const DEFAULT_SERVICE_DATABASE_PATH: &str = "/var/lib/reddb/data.rdb";
358pub const TRANSACTION_WAL_FILE_NAME: &str = "wal.log";
359
360pub fn default_database_path() -> PathBuf {
361    PathBuf::from(DEFAULT_DATABASE_FILE_NAME)
362}
363
364pub fn default_service_database_path() -> PathBuf {
365    PathBuf::from(DEFAULT_SERVICE_DATABASE_PATH)
366}
367
368pub fn default_transaction_wal_path() -> PathBuf {
369    PathBuf::from(TRANSACTION_WAL_FILE_NAME)
370}
371
372pub fn sibling_path(path: &Path, file_name: &str) -> PathBuf {
373    match path.parent() {
374        Some(parent) if !parent.as_os_str().is_empty() => parent.join(file_name),
375        _ => PathBuf::from(file_name),
376    }
377}
378
379pub fn sidecar_file_name(path: &Path, extension: &str) -> String {
380    path.with_extension(extension)
381        .file_name()
382        .and_then(|name| name.to_str())
383        .unwrap_or(DEFAULT_DATABASE_FILE_NAME)
384        .to_string()
385}
386
387pub fn support_dir_for(data_path: &Path) -> PathBuf {
388    let file_name = data_file_name(data_path);
389    sibling_path(data_path, &format!("{file_name}.{SUPPORT_DIR_SUFFIX}"))
390}
391
392pub fn unified_wal_path(data_path: &Path) -> PathBuf {
393    data_path.with_extension(UNIFIED_WAL_EXTENSION)
394}
395
396pub fn unified_wal_path_in(support_dir: &Path, data_path: &Path) -> PathBuf {
397    support_dir
398        .join("wal")
399        .join(sidecar_file_name(data_path, UNIFIED_WAL_EXTENSION))
400}
401
402pub fn store_commit_coord_temp_wal_path(
403    temp_dir: &Path,
404    name: &str,
405    process_id: u32,
406    nanos: u128,
407) -> PathBuf {
408    temp_dir.join(store_commit_coord_temp_wal_file_name(
409        name, process_id, nanos,
410    ))
411}
412
413pub fn store_commit_coord_temp_wal_file_name(name: &str, process_id: u32, nanos: u128) -> String {
414    format!("rb_commit_coord_{name}_{process_id}_{nanos}.wal")
415}
416
417pub fn group_commit_temp_wal_path(
418    temp_dir: &Path,
419    name: &str,
420    process_id: u32,
421    nanos: u128,
422) -> PathBuf {
423    temp_dir.join(group_commit_temp_wal_file_name(name, process_id, nanos))
424}
425
426pub fn group_commit_temp_wal_file_name(name: &str, process_id: u32, nanos: u128) -> String {
427    format!("rb_group_commit_{name}_{process_id}_{nanos}.wal")
428}
429
430pub fn wal_component_temp_path(
431    temp_dir: &Path,
432    component: &str,
433    name: &str,
434    process_id: u32,
435) -> PathBuf {
436    temp_dir.join(wal_component_temp_file_name(component, name, process_id))
437}
438
439pub fn wal_component_temp_file_name(component: &str, name: &str, process_id: u32) -> String {
440    format!("rb_wal_{component}_{name}_{process_id}.wal")
441}
442
443pub fn wal_component_unique_temp_path(
444    temp_dir: &Path,
445    component: &str,
446    name: &str,
447    process_id: u32,
448    nanos: u128,
449) -> PathBuf {
450    temp_dir.join(wal_component_unique_temp_file_name(
451        component, name, process_id, nanos,
452    ))
453}
454
455pub fn wal_component_unique_temp_file_name(
456    component: &str,
457    name: &str,
458    process_id: u32,
459    nanos: u128,
460) -> String {
461    format!("rb_wal_{component}_{name}_{process_id}_{nanos}.wal")
462}
463
464pub fn backup_temp_json_path(
465    temp_dir: &Path,
466    prefix: &str,
467    process_id: u32,
468    nanos: u128,
469    start_lsn: Option<u64>,
470    end_lsn: Option<u64>,
471) -> PathBuf {
472    temp_dir.join(backup_temp_json_file_name(
473        prefix, process_id, nanos, start_lsn, end_lsn,
474    ))
475}
476
477pub fn backup_temp_json_file_name(
478    prefix: &str,
479    process_id: u32,
480    nanos: u128,
481    start_lsn: Option<u64>,
482    end_lsn: Option<u64>,
483) -> String {
484    match (start_lsn, end_lsn) {
485        (Some(start_lsn), Some(end_lsn)) => {
486            format!("{prefix}-{process_id}-{start_lsn}-{end_lsn}-{nanos}.json")
487        }
488        _ => format!("{prefix}-{process_id}-{nanos}.json"),
489    }
490}
491
492pub fn logical_wal_path(data_path: &Path) -> PathBuf {
493    sibling_path(
494        data_path,
495        &format!("{}.{}", data_file_name(data_path), LOGICAL_WAL_SUFFIX),
496    )
497}
498
499pub fn logical_wal_temp_path(logical_wal_path: &Path) -> PathBuf {
500    logical_wal_path.with_extension("logical.wal.tmp")
501}
502
503pub fn logical_wal_path_in(support_dir: &Path, data_path: &Path) -> PathBuf {
504    support_dir.join("wal").join(format!(
505        "{}.{}",
506        data_file_name(data_path),
507        LOGICAL_WAL_SUFFIX
508    ))
509}
510
511pub fn temp_path(data_path: &Path) -> PathBuf {
512    data_path.with_extension(TEMP_EXTENSION)
513}
514
515pub fn atomic_temp_path(path: &Path) -> PathBuf {
516    path.with_extension(ATOMIC_TEMP_EXTENSION)
517}
518
519pub fn result_cache_l2_path(data_path: &Path) -> PathBuf {
520    data_path.with_extension(RESULT_CACHE_L2_EXTENSION)
521}
522
523pub fn temp_path_in(support_dir: &Path, data_path: &Path) -> PathBuf {
524    support_dir
525        .join("tmp")
526        .join(sidecar_file_name(data_path, TEMP_EXTENSION))
527}
528
529pub fn primary_wal_segment_file_name(segment_index: u64) -> String {
530    format!("{segment_index:020}.{PRIMARY_WAL_EXTENSION}")
531}
532
533pub fn relay_segment_relative_path(start_lsn: u64, end_lsn: u64) -> PathBuf {
534    PathBuf::from(format!(
535        "relay-{start_lsn:020}-{end_lsn:020}.{PRIMARY_WAL_EXTENSION}"
536    ))
537}
538
539pub fn pager_legacy_wal_path(data_path: &Path) -> PathBuf {
540    data_path.with_extension(PAGER_LEGACY_WAL_EXTENSION)
541}
542
543pub fn engine_wal_path(data_path: &Path) -> PathBuf {
544    data_path.with_extension(ENGINE_WAL_EXTENSION)
545}
546
547pub fn pager_header_path(data_path: &Path) -> PathBuf {
548    data_path.with_extension(PAGER_HEADER_EXTENSION)
549}
550
551pub fn pager_meta_path(data_path: &Path) -> PathBuf {
552    data_path.with_extension(PAGER_META_EXTENSION)
553}
554
555pub fn pager_dwb_path(data_path: &Path) -> PathBuf {
556    data_path.with_extension(PAGER_DWB_EXTENSION)
557}
558
559fn path_with_dash_suffix(data_path: &Path, suffix: &str) -> PathBuf {
560    let mut path = data_path.to_path_buf().into_os_string();
561    path.push("-");
562    path.push(suffix);
563    PathBuf::from(path)
564}
565
566pub fn pager_header_shadow_path(data_path: &Path) -> PathBuf {
567    path_with_dash_suffix(data_path, PAGER_HEADER_SHADOW_SUFFIX)
568}
569
570pub fn pager_meta_shadow_path(data_path: &Path) -> PathBuf {
571    path_with_dash_suffix(data_path, PAGER_META_SHADOW_SUFFIX)
572}
573
574pub fn pager_dwb_shadow_path(data_path: &Path) -> PathBuf {
575    path_with_dash_suffix(data_path, PAGER_DWB_SHADOW_SUFFIX)
576}
577
578pub fn pager_shadow_sidecar_paths(data_path: &Path) -> [PathBuf; 3] {
579    [
580        pager_header_shadow_path(data_path),
581        pager_meta_shadow_path(data_path),
582        pager_dwb_shadow_path(data_path),
583    ]
584}
585
586pub fn shm_path(data_path: &Path) -> PathBuf {
587    sibling_path(
588        data_path,
589        &format!("{}-{SHM_FILE_SUFFIX}", data_file_name(data_path)),
590    )
591}
592
593pub fn physical_metadata_json_path(data_path: &Path) -> PathBuf {
594    sibling_path(
595        data_path,
596        &format!(
597            "{}.{PHYSICAL_METADATA_JSON_SUFFIX}",
598            data_file_name(data_path)
599        ),
600    )
601}
602
603pub fn physical_metadata_binary_path(data_path: &Path) -> PathBuf {
604    sibling_path(
605        data_path,
606        &format!(
607            "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}",
608            data_file_name(data_path)
609        ),
610    )
611}
612
613pub fn physical_metadata_journal_path(data_path: &Path, sequence: u64) -> PathBuf {
614    sibling_path(
615        data_path,
616        &format!(
617            "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}.seq-{sequence:020}",
618            data_file_name(data_path)
619        ),
620    )
621}
622
623pub fn physical_metadata_journal_prefix(data_path: &Path) -> String {
624    format!(
625        "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}.seq-",
626        data_file_name(data_path)
627    )
628}
629
630pub fn physical_export_data_path(data_path: &Path, name: &str) -> PathBuf {
631    let file_name = data_file_name(data_path);
632    let stem = file_name.strip_suffix(".rdb").unwrap_or(&file_name);
633    sibling_path(
634        data_path,
635        &format!("{stem}.export.{}.rdb", sanitize_export_name(name)),
636    )
637}
638
639pub fn local_cas_lock_path(dest: &Path) -> PathBuf {
640    let file_name = data_file_name(dest);
641    dest.with_file_name(format!(".{file_name}.{LOCAL_CAS_LOCK_SUFFIX}"))
642}
643
644pub fn local_upload_temp_path(dest: &Path, pid: u32, unique: u64) -> PathBuf {
645    let file_name = data_file_name(dest);
646    dest.with_file_name(format!(
647        ".{file_name}.{LOCAL_UPLOAD_TEMP_TAG}-{pid}-{unique}"
648    ))
649}
650
651pub fn rebootstrap_staging_root(data_path: &Path) -> PathBuf {
652    data_path.with_extension(REBOOTSTRAP_STAGING_EXTENSION)
653}
654
655pub fn rebootstrap_pending_path(data_path: &Path) -> PathBuf {
656    data_path.with_extension(REBOOTSTRAP_PENDING_EXTENSION)
657}
658
659pub fn rebootstrap_ready_marker_path(data_path: &Path) -> PathBuf {
660    data_path.with_extension(REBOOTSTRAP_READY_EXTENSION)
661}
662
663pub fn rebootstrap_intent_log_path(data_path: &Path) -> PathBuf {
664    data_path.with_extension(REBOOTSTRAP_INTENT_LOG_EXTENSION)
665}
666
667pub fn rebootstrap_previous_path(data_path: &Path) -> PathBuf {
668    data_path.with_extension(REBOOTSTRAP_PREVIOUS_EXTENSION)
669}
670
671pub fn primary_replica_root(data_path: &Path) -> PathBuf {
672    data_path.with_extension(PRIMARY_REPLICA_ROOT_EXTENSION)
673}
674
675pub fn legacy_logical_slots_path(data_path: &Path) -> PathBuf {
676    let file_name = data_path
677        .file_name()
678        .and_then(|name| name.to_str())
679        .unwrap_or("reddb.rdb");
680    sibling_path(
681        data_path,
682        &format!("{file_name}.{LEGACY_LOGICAL_SLOTS_SUFFIX}"),
683    )
684}
685
686pub fn legacy_logical_slots_temp_path(path: &Path) -> PathBuf {
687    path.with_extension(LEGACY_LOGICAL_SLOTS_TEMP_EXTENSION)
688}
689
690pub fn legacy_audit_log_path(data_path: &Path) -> PathBuf {
691    sibling_path(data_path, LEGACY_AUDIT_LOG_FILE_NAME)
692}
693
694pub fn audit_log_rotated_plain_path(active_path: &Path, timestamp_nanos: u128) -> PathBuf {
695    sibling_path(
696        active_path,
697        &format!("{}.{timestamp_nanos}", audit_log_file_name(active_path)),
698    )
699}
700
701pub fn audit_log_rotated_compressed_path(active_path: &Path, timestamp_nanos: u128) -> PathBuf {
702    sibling_path(
703        active_path,
704        &format!(
705            "{}.{timestamp_nanos}.{AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION}",
706            audit_log_file_name(active_path)
707        ),
708    )
709}
710
711pub fn parse_audit_log_rotated_timestamp(
712    active_path: &Path,
713    candidate_file_name: &str,
714) -> Option<u128> {
715    let active_name = audit_log_file_name(active_path);
716    let rotated = candidate_file_name.strip_prefix(&format!("{active_name}."))?;
717    let timestamp = rotated
718        .strip_suffix(&format!(".{AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION}"))
719        .unwrap_or(rotated);
720    timestamp.parse::<u128>().ok()
721}
722
723pub fn legacy_slow_query_log_path(log_dir: &Path) -> PathBuf {
724    log_dir.join(LEGACY_SLOW_QUERY_LOG_FILE_NAME)
725}
726
727pub fn serverless_root(data_path: &Path) -> PathBuf {
728    data_path.with_extension(SERVERLESS_ROOT_EXTENSION)
729}
730
731pub fn serverless_namespace(data_path: &Path) -> String {
732    data_path
733        .file_stem()
734        .and_then(|stem| stem.to_str())
735        .filter(|stem| !stem.is_empty())
736        .unwrap_or("default")
737        .to_string()
738}
739
740pub fn serverless_cache_root(root: &Path, namespace: &str) -> PathBuf {
741    root.join(namespace).join(SERVERLESS_CACHE_DIR)
742}
743
744fn audit_log_file_name(path: &Path) -> String {
745    path.file_name()
746        .and_then(|name| name.to_str())
747        .unwrap_or(LEGACY_AUDIT_LOG_FILE_NAME)
748        .to_string()
749}
750
751fn push_parent(dirs: &mut Vec<PathBuf>, path: &Path) {
752    if let Some(parent) = path.parent() {
753        if !parent.as_os_str().is_empty() {
754            dirs.push(parent.to_path_buf());
755        }
756    }
757}
758
759fn push_optional(dirs: &mut Vec<PathBuf>, path: Option<&PathBuf>) {
760    if let Some(path) = path {
761        dirs.push(path.clone());
762    }
763}
764
765fn sanitize_export_name(name: &str) -> String {
766    let mut out = String::new();
767    for ch in name.chars() {
768        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
769            out.push(ch);
770        } else {
771            out.push('_');
772        }
773    }
774    if out.is_empty() {
775        "export".to_string()
776    } else {
777        out
778    }
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784
785    #[test]
786    fn derives_standard_sidecars_next_to_database() {
787        let path = Path::new("/var/lib/reddb/main.rdb");
788
789        assert_eq!(
790            support_dir_for(path),
791            PathBuf::from("/var/lib/reddb/main.rdb.red")
792        );
793        assert_eq!(
794            unified_wal_path(path),
795            PathBuf::from("/var/lib/reddb/main.rdb-uwal")
796        );
797        assert_eq!(
798            store_commit_coord_temp_wal_path(Path::new("/tmp"), "burst", 7, 99),
799            PathBuf::from("/tmp/rb_commit_coord_burst_7_99.wal")
800        );
801        assert_eq!(
802            group_commit_temp_wal_path(Path::new("/tmp"), "batch", 7, 99),
803            PathBuf::from("/tmp/rb_group_commit_batch_7_99.wal")
804        );
805        assert_eq!(
806            wal_component_temp_path(Path::new("/tmp"), "writer", "create", 7),
807            PathBuf::from("/tmp/rb_wal_writer_create_7.wal")
808        );
809        assert_eq!(
810            wal_component_temp_path(Path::new("/tmp"), "reader", "empty", 7),
811            PathBuf::from("/tmp/rb_wal_reader_empty_7.wal")
812        );
813        assert_eq!(
814            wal_component_unique_temp_path(Path::new("/tmp"), "coord", "single", 7, 99),
815            PathBuf::from("/tmp/rb_wal_coord_single_7_99.wal")
816        );
817        assert_eq!(
818            backup_temp_json_path(
819                Path::new("/tmp"),
820                "reddb-archived-change-records",
821                7,
822                99,
823                Some(10),
824                Some(20)
825            ),
826            PathBuf::from("/tmp/reddb-archived-change-records-7-10-20-99.json")
827        );
828        assert_eq!(
829            backup_temp_json_path(Path::new("/tmp"), "reddb-json-object", 7, 99, None, None),
830            PathBuf::from("/tmp/reddb-json-object-7-99.json")
831        );
832        assert_eq!(
833            logical_wal_path(path),
834            PathBuf::from("/var/lib/reddb/main.rdb.logical.wal")
835        );
836        assert_eq!(
837            logical_wal_temp_path(&logical_wal_path(path)),
838            PathBuf::from("/var/lib/reddb/main.rdb.logical.logical.wal.tmp")
839        );
840        assert_eq!(
841            temp_path(path),
842            PathBuf::from("/var/lib/reddb/main.rdb-tmp")
843        );
844        assert_eq!(
845            atomic_temp_path(&pager_meta_path(path)),
846            PathBuf::from("/var/lib/reddb/main.tmp")
847        );
848        assert_eq!(
849            result_cache_l2_path(path),
850            PathBuf::from("/var/lib/reddb/main.result-cache.l2")
851        );
852        assert_eq!(
853            engine_wal_path(path),
854            PathBuf::from("/var/lib/reddb/main.rdb-wal")
855        );
856        assert_eq!(
857            pager_legacy_wal_path(path),
858            PathBuf::from("/var/lib/reddb/main.wal")
859        );
860        assert_eq!(
861            pager_header_shadow_path(path),
862            PathBuf::from("/var/lib/reddb/main.rdb-hdr")
863        );
864        assert_eq!(
865            pager_meta_shadow_path(path),
866            PathBuf::from("/var/lib/reddb/main.rdb-meta")
867        );
868        assert_eq!(
869            pager_dwb_shadow_path(path),
870            PathBuf::from("/var/lib/reddb/main.rdb-dwb")
871        );
872        assert_eq!(shm_path(path), PathBuf::from("/var/lib/reddb/main.rdb-shm"));
873        assert_eq!(
874            physical_metadata_json_path(path),
875            PathBuf::from("/var/lib/reddb/main.rdb.meta.json")
876        );
877        assert_eq!(
878            physical_metadata_binary_path(path),
879            PathBuf::from("/var/lib/reddb/main.rdb.meta.rdbx")
880        );
881        assert_eq!(
882            physical_metadata_journal_path(path, 7),
883            PathBuf::from("/var/lib/reddb/main.rdb.meta.rdbx.seq-00000000000000000007")
884        );
885        assert_eq!(
886            physical_metadata_journal_prefix(path),
887            "main.rdb.meta.rdbx.seq-"
888        );
889        assert_eq!(
890            physical_export_data_path(path, "nightly backup"),
891            PathBuf::from("/var/lib/reddb/main.export.nightly_backup.rdb")
892        );
893        assert_eq!(
894            local_cas_lock_path(path),
895            PathBuf::from("/var/lib/reddb/.main.rdb.cas.lock")
896        );
897        assert_eq!(
898            local_upload_temp_path(path, 123, 7),
899            PathBuf::from("/var/lib/reddb/.main.rdb.tmp-123-7")
900        );
901        assert_eq!(
902            rebootstrap_staging_root(path),
903            PathBuf::from("/var/lib/reddb/main.rebootstrap.redbase")
904        );
905        assert_eq!(
906            rebootstrap_pending_path(path),
907            PathBuf::from("/var/lib/reddb/main.rebootstrap.pending.rdb")
908        );
909        assert_eq!(
910            rebootstrap_ready_marker_path(path),
911            PathBuf::from("/var/lib/reddb/main.rebootstrap.ready")
912        );
913        assert_eq!(
914            rebootstrap_intent_log_path(path),
915            PathBuf::from("/var/lib/reddb/main.rebootstrap.intent.jsonl")
916        );
917        assert_eq!(
918            rebootstrap_previous_path(path),
919            PathBuf::from("/var/lib/reddb/main.rebootstrap.previous.rdb")
920        );
921        assert_eq!(
922            primary_replica_root(path),
923            PathBuf::from("/var/lib/reddb/main.primary-replica")
924        );
925        assert_eq!(
926            legacy_logical_slots_path(path),
927            PathBuf::from("/var/lib/reddb/main.rdb.logical.slots.json")
928        );
929        assert_eq!(
930            legacy_logical_slots_temp_path(&legacy_logical_slots_path(path)),
931            PathBuf::from("/var/lib/reddb/main.rdb.logical.slots.logical.slots.tmp")
932        );
933        assert_eq!(
934            legacy_audit_log_path(path),
935            PathBuf::from("/var/lib/reddb/.audit.log")
936        );
937        assert_eq!(
938            audit_log_rotated_plain_path(&legacy_audit_log_path(path), 42),
939            PathBuf::from("/var/lib/reddb/.audit.log.42")
940        );
941        assert_eq!(
942            audit_log_rotated_compressed_path(&legacy_audit_log_path(path), 42),
943            PathBuf::from("/var/lib/reddb/.audit.log.42.zst")
944        );
945        assert_eq!(
946            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), ".audit.log.42"),
947            Some(42)
948        );
949        assert_eq!(
950            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), ".audit.log.42.zst"),
951            Some(42)
952        );
953        assert_eq!(
954            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), "other.42.zst"),
955            None
956        );
957        assert_eq!(
958            legacy_slow_query_log_path(Path::new("/var/log/reddb")),
959            PathBuf::from("/var/log/reddb/red-slow.log")
960        );
961        assert_eq!(
962            serverless_root(path),
963            PathBuf::from("/var/lib/reddb/main.serverless")
964        );
965        assert_eq!(serverless_namespace(path), "main");
966        assert_eq!(
967            serverless_cache_root(&serverless_root(path), &serverless_namespace(path)),
968            PathBuf::from("/var/lib/reddb/main.serverless/main/cache")
969        );
970    }
971
972    #[test]
973    fn derives_dedicated_support_sidecars() {
974        let path = Path::new("/var/lib/reddb/main.rdb");
975        let support = support_dir_for(path);
976
977        assert_eq!(
978            unified_wal_path_in(&support, path),
979            PathBuf::from("/var/lib/reddb/main.rdb.red/wal/main.rdb-uwal")
980        );
981        assert_eq!(
982            logical_wal_path_in(&support, path),
983            PathBuf::from("/var/lib/reddb/main.rdb.red/wal/main.rdb.logical.wal")
984        );
985        assert_eq!(
986            temp_path_in(&support, path),
987            PathBuf::from("/var/lib/reddb/main.rdb.red/tmp/main.rdb-tmp")
988        );
989    }
990
991    #[test]
992    fn derives_primary_replica_segment_names() {
993        assert_eq!(
994            primary_wal_segment_file_name(2),
995            "00000000000000000002.redwal"
996        );
997        assert_eq!(
998            relay_segment_relative_path(10, 20),
999            PathBuf::from("relay-00000000000000000010-00000000000000000020.redwal")
1000        );
1001    }
1002}