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    unique: u64,
470    start_lsn: Option<u64>,
471    end_lsn: Option<u64>,
472) -> PathBuf {
473    temp_dir.join(backup_temp_json_file_name(
474        prefix, process_id, nanos, unique, start_lsn, end_lsn,
475    ))
476}
477
478pub fn backup_temp_json_file_name(
479    prefix: &str,
480    process_id: u32,
481    nanos: u128,
482    unique: u64,
483    start_lsn: Option<u64>,
484    end_lsn: Option<u64>,
485) -> String {
486    // `unique` is a process-global monotonic counter so two stagings can
487    // never share a path even when `nanos` collides under coarse clock
488    // resolution or when concurrent archives stage the same LSN range.
489    match (start_lsn, end_lsn) {
490        (Some(start_lsn), Some(end_lsn)) => {
491            format!("{prefix}-{process_id}-{start_lsn}-{end_lsn}-{nanos}-{unique}.json")
492        }
493        _ => format!("{prefix}-{process_id}-{nanos}-{unique}.json"),
494    }
495}
496
497pub fn logical_wal_path(data_path: &Path) -> PathBuf {
498    sibling_path(
499        data_path,
500        &format!("{}.{}", data_file_name(data_path), LOGICAL_WAL_SUFFIX),
501    )
502}
503
504pub fn logical_wal_temp_path(logical_wal_path: &Path) -> PathBuf {
505    logical_wal_path.with_extension("logical.wal.tmp")
506}
507
508pub fn logical_wal_path_in(support_dir: &Path, data_path: &Path) -> PathBuf {
509    support_dir.join("wal").join(format!(
510        "{}.{}",
511        data_file_name(data_path),
512        LOGICAL_WAL_SUFFIX
513    ))
514}
515
516pub fn temp_path(data_path: &Path) -> PathBuf {
517    data_path.with_extension(TEMP_EXTENSION)
518}
519
520pub fn atomic_temp_path(path: &Path) -> PathBuf {
521    path.with_extension(ATOMIC_TEMP_EXTENSION)
522}
523
524pub fn result_cache_l2_path(data_path: &Path) -> PathBuf {
525    data_path.with_extension(RESULT_CACHE_L2_EXTENSION)
526}
527
528pub fn temp_path_in(support_dir: &Path, data_path: &Path) -> PathBuf {
529    support_dir
530        .join("tmp")
531        .join(sidecar_file_name(data_path, TEMP_EXTENSION))
532}
533
534pub fn primary_wal_segment_file_name(segment_index: u64) -> String {
535    format!("{segment_index:020}.{PRIMARY_WAL_EXTENSION}")
536}
537
538pub fn relay_segment_relative_path(start_lsn: u64, end_lsn: u64) -> PathBuf {
539    PathBuf::from(format!(
540        "relay-{start_lsn:020}-{end_lsn:020}.{PRIMARY_WAL_EXTENSION}"
541    ))
542}
543
544pub fn pager_legacy_wal_path(data_path: &Path) -> PathBuf {
545    data_path.with_extension(PAGER_LEGACY_WAL_EXTENSION)
546}
547
548pub fn engine_wal_path(data_path: &Path) -> PathBuf {
549    data_path.with_extension(ENGINE_WAL_EXTENSION)
550}
551
552pub fn pager_header_path(data_path: &Path) -> PathBuf {
553    data_path.with_extension(PAGER_HEADER_EXTENSION)
554}
555
556pub fn pager_meta_path(data_path: &Path) -> PathBuf {
557    data_path.with_extension(PAGER_META_EXTENSION)
558}
559
560pub fn pager_dwb_path(data_path: &Path) -> PathBuf {
561    data_path.with_extension(PAGER_DWB_EXTENSION)
562}
563
564fn path_with_dash_suffix(data_path: &Path, suffix: &str) -> PathBuf {
565    let mut path = data_path.to_path_buf().into_os_string();
566    path.push("-");
567    path.push(suffix);
568    PathBuf::from(path)
569}
570
571pub fn pager_header_shadow_path(data_path: &Path) -> PathBuf {
572    path_with_dash_suffix(data_path, PAGER_HEADER_SHADOW_SUFFIX)
573}
574
575pub fn pager_meta_shadow_path(data_path: &Path) -> PathBuf {
576    path_with_dash_suffix(data_path, PAGER_META_SHADOW_SUFFIX)
577}
578
579pub fn pager_dwb_shadow_path(data_path: &Path) -> PathBuf {
580    path_with_dash_suffix(data_path, PAGER_DWB_SHADOW_SUFFIX)
581}
582
583pub fn pager_shadow_sidecar_paths(data_path: &Path) -> [PathBuf; 3] {
584    [
585        pager_header_shadow_path(data_path),
586        pager_meta_shadow_path(data_path),
587        pager_dwb_shadow_path(data_path),
588    ]
589}
590
591pub fn shm_path(data_path: &Path) -> PathBuf {
592    sibling_path(
593        data_path,
594        &format!("{}-{SHM_FILE_SUFFIX}", data_file_name(data_path)),
595    )
596}
597
598pub fn physical_metadata_json_path(data_path: &Path) -> PathBuf {
599    sibling_path(
600        data_path,
601        &format!(
602            "{}.{PHYSICAL_METADATA_JSON_SUFFIX}",
603            data_file_name(data_path)
604        ),
605    )
606}
607
608pub fn physical_metadata_binary_path(data_path: &Path) -> PathBuf {
609    sibling_path(
610        data_path,
611        &format!(
612            "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}",
613            data_file_name(data_path)
614        ),
615    )
616}
617
618pub fn physical_metadata_journal_path(data_path: &Path, sequence: u64) -> PathBuf {
619    sibling_path(
620        data_path,
621        &format!(
622            "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}.seq-{sequence:020}",
623            data_file_name(data_path)
624        ),
625    )
626}
627
628pub fn physical_metadata_journal_prefix(data_path: &Path) -> String {
629    format!(
630        "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}.seq-",
631        data_file_name(data_path)
632    )
633}
634
635pub fn physical_export_data_path(data_path: &Path, name: &str) -> PathBuf {
636    let file_name = data_file_name(data_path);
637    let stem = file_name.strip_suffix(".rdb").unwrap_or(&file_name);
638    sibling_path(
639        data_path,
640        &format!("{stem}.export.{}.rdb", sanitize_export_name(name)),
641    )
642}
643
644pub fn local_cas_lock_path(dest: &Path) -> PathBuf {
645    let file_name = data_file_name(dest);
646    dest.with_file_name(format!(".{file_name}.{LOCAL_CAS_LOCK_SUFFIX}"))
647}
648
649pub fn local_upload_temp_path(dest: &Path, pid: u32, unique: u64) -> PathBuf {
650    let file_name = data_file_name(dest);
651    dest.with_file_name(format!(
652        ".{file_name}.{LOCAL_UPLOAD_TEMP_TAG}-{pid}-{unique}"
653    ))
654}
655
656pub fn rebootstrap_staging_root(data_path: &Path) -> PathBuf {
657    data_path.with_extension(REBOOTSTRAP_STAGING_EXTENSION)
658}
659
660pub fn rebootstrap_pending_path(data_path: &Path) -> PathBuf {
661    data_path.with_extension(REBOOTSTRAP_PENDING_EXTENSION)
662}
663
664pub fn rebootstrap_ready_marker_path(data_path: &Path) -> PathBuf {
665    data_path.with_extension(REBOOTSTRAP_READY_EXTENSION)
666}
667
668pub fn rebootstrap_intent_log_path(data_path: &Path) -> PathBuf {
669    data_path.with_extension(REBOOTSTRAP_INTENT_LOG_EXTENSION)
670}
671
672pub fn rebootstrap_previous_path(data_path: &Path) -> PathBuf {
673    data_path.with_extension(REBOOTSTRAP_PREVIOUS_EXTENSION)
674}
675
676pub fn primary_replica_root(data_path: &Path) -> PathBuf {
677    data_path.with_extension(PRIMARY_REPLICA_ROOT_EXTENSION)
678}
679
680pub fn legacy_logical_slots_path(data_path: &Path) -> PathBuf {
681    let file_name = data_path
682        .file_name()
683        .and_then(|name| name.to_str())
684        .unwrap_or("reddb.rdb");
685    sibling_path(
686        data_path,
687        &format!("{file_name}.{LEGACY_LOGICAL_SLOTS_SUFFIX}"),
688    )
689}
690
691pub fn legacy_logical_slots_temp_path(path: &Path) -> PathBuf {
692    path.with_extension(LEGACY_LOGICAL_SLOTS_TEMP_EXTENSION)
693}
694
695pub fn legacy_audit_log_path(data_path: &Path) -> PathBuf {
696    sibling_path(data_path, LEGACY_AUDIT_LOG_FILE_NAME)
697}
698
699pub fn audit_log_rotated_plain_path(active_path: &Path, timestamp_nanos: u128) -> PathBuf {
700    sibling_path(
701        active_path,
702        &format!("{}.{timestamp_nanos}", audit_log_file_name(active_path)),
703    )
704}
705
706pub fn audit_log_rotated_compressed_path(active_path: &Path, timestamp_nanos: u128) -> PathBuf {
707    sibling_path(
708        active_path,
709        &format!(
710            "{}.{timestamp_nanos}.{AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION}",
711            audit_log_file_name(active_path)
712        ),
713    )
714}
715
716pub fn parse_audit_log_rotated_timestamp(
717    active_path: &Path,
718    candidate_file_name: &str,
719) -> Option<u128> {
720    let active_name = audit_log_file_name(active_path);
721    let rotated = candidate_file_name.strip_prefix(&format!("{active_name}."))?;
722    let timestamp = rotated
723        .strip_suffix(&format!(".{AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION}"))
724        .unwrap_or(rotated);
725    timestamp.parse::<u128>().ok()
726}
727
728pub fn legacy_slow_query_log_path(log_dir: &Path) -> PathBuf {
729    log_dir.join(LEGACY_SLOW_QUERY_LOG_FILE_NAME)
730}
731
732pub fn serverless_root(data_path: &Path) -> PathBuf {
733    data_path.with_extension(SERVERLESS_ROOT_EXTENSION)
734}
735
736pub fn serverless_namespace(data_path: &Path) -> String {
737    data_path
738        .file_stem()
739        .and_then(|stem| stem.to_str())
740        .filter(|stem| !stem.is_empty())
741        .unwrap_or("default")
742        .to_string()
743}
744
745pub fn serverless_cache_root(root: &Path, namespace: &str) -> PathBuf {
746    root.join(namespace).join(SERVERLESS_CACHE_DIR)
747}
748
749fn audit_log_file_name(path: &Path) -> String {
750    path.file_name()
751        .and_then(|name| name.to_str())
752        .unwrap_or(LEGACY_AUDIT_LOG_FILE_NAME)
753        .to_string()
754}
755
756fn push_parent(dirs: &mut Vec<PathBuf>, path: &Path) {
757    if let Some(parent) = path.parent() {
758        if !parent.as_os_str().is_empty() {
759            dirs.push(parent.to_path_buf());
760        }
761    }
762}
763
764fn push_optional(dirs: &mut Vec<PathBuf>, path: Option<&PathBuf>) {
765    if let Some(path) = path {
766        dirs.push(path.clone());
767    }
768}
769
770fn sanitize_export_name(name: &str) -> String {
771    let mut out = String::new();
772    for ch in name.chars() {
773        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
774            out.push(ch);
775        } else {
776            out.push('_');
777        }
778    }
779    if out.is_empty() {
780        "export".to_string()
781    } else {
782        out
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    #[test]
791    fn derives_standard_sidecars_next_to_database() {
792        let path = Path::new("/var/lib/reddb/main.rdb");
793
794        assert_eq!(
795            support_dir_for(path),
796            PathBuf::from("/var/lib/reddb/main.rdb.red")
797        );
798        assert_eq!(
799            unified_wal_path(path),
800            PathBuf::from("/var/lib/reddb/main.rdb-uwal")
801        );
802        assert_eq!(
803            store_commit_coord_temp_wal_path(Path::new("/tmp"), "burst", 7, 99),
804            PathBuf::from("/tmp/rb_commit_coord_burst_7_99.wal")
805        );
806        assert_eq!(
807            group_commit_temp_wal_path(Path::new("/tmp"), "batch", 7, 99),
808            PathBuf::from("/tmp/rb_group_commit_batch_7_99.wal")
809        );
810        assert_eq!(
811            wal_component_temp_path(Path::new("/tmp"), "writer", "create", 7),
812            PathBuf::from("/tmp/rb_wal_writer_create_7.wal")
813        );
814        assert_eq!(
815            wal_component_temp_path(Path::new("/tmp"), "reader", "empty", 7),
816            PathBuf::from("/tmp/rb_wal_reader_empty_7.wal")
817        );
818        assert_eq!(
819            wal_component_unique_temp_path(Path::new("/tmp"), "coord", "single", 7, 99),
820            PathBuf::from("/tmp/rb_wal_coord_single_7_99.wal")
821        );
822        assert_eq!(
823            backup_temp_json_path(
824                Path::new("/tmp"),
825                "reddb-archived-change-records",
826                7,
827                99,
828                3,
829                Some(10),
830                Some(20)
831            ),
832            PathBuf::from("/tmp/reddb-archived-change-records-7-10-20-99-3.json")
833        );
834        assert_eq!(
835            backup_temp_json_path(Path::new("/tmp"), "reddb-json-object", 7, 99, 3, None, None),
836            PathBuf::from("/tmp/reddb-json-object-7-99-3.json")
837        );
838        assert_eq!(
839            logical_wal_path(path),
840            PathBuf::from("/var/lib/reddb/main.rdb.logical.wal")
841        );
842        assert_eq!(
843            logical_wal_temp_path(&logical_wal_path(path)),
844            PathBuf::from("/var/lib/reddb/main.rdb.logical.logical.wal.tmp")
845        );
846        assert_eq!(
847            temp_path(path),
848            PathBuf::from("/var/lib/reddb/main.rdb-tmp")
849        );
850        assert_eq!(
851            atomic_temp_path(&pager_meta_path(path)),
852            PathBuf::from("/var/lib/reddb/main.tmp")
853        );
854        assert_eq!(
855            result_cache_l2_path(path),
856            PathBuf::from("/var/lib/reddb/main.result-cache.l2")
857        );
858        assert_eq!(
859            engine_wal_path(path),
860            PathBuf::from("/var/lib/reddb/main.rdb-wal")
861        );
862        assert_eq!(
863            pager_legacy_wal_path(path),
864            PathBuf::from("/var/lib/reddb/main.wal")
865        );
866        assert_eq!(
867            pager_header_shadow_path(path),
868            PathBuf::from("/var/lib/reddb/main.rdb-hdr")
869        );
870        assert_eq!(
871            pager_meta_shadow_path(path),
872            PathBuf::from("/var/lib/reddb/main.rdb-meta")
873        );
874        assert_eq!(
875            pager_dwb_shadow_path(path),
876            PathBuf::from("/var/lib/reddb/main.rdb-dwb")
877        );
878        assert_eq!(shm_path(path), PathBuf::from("/var/lib/reddb/main.rdb-shm"));
879        assert_eq!(
880            physical_metadata_json_path(path),
881            PathBuf::from("/var/lib/reddb/main.rdb.meta.json")
882        );
883        assert_eq!(
884            physical_metadata_binary_path(path),
885            PathBuf::from("/var/lib/reddb/main.rdb.meta.rdbx")
886        );
887        assert_eq!(
888            physical_metadata_journal_path(path, 7),
889            PathBuf::from("/var/lib/reddb/main.rdb.meta.rdbx.seq-00000000000000000007")
890        );
891        assert_eq!(
892            physical_metadata_journal_prefix(path),
893            "main.rdb.meta.rdbx.seq-"
894        );
895        assert_eq!(
896            physical_export_data_path(path, "nightly backup"),
897            PathBuf::from("/var/lib/reddb/main.export.nightly_backup.rdb")
898        );
899        assert_eq!(
900            local_cas_lock_path(path),
901            PathBuf::from("/var/lib/reddb/.main.rdb.cas.lock")
902        );
903        assert_eq!(
904            local_upload_temp_path(path, 123, 7),
905            PathBuf::from("/var/lib/reddb/.main.rdb.tmp-123-7")
906        );
907        assert_eq!(
908            rebootstrap_staging_root(path),
909            PathBuf::from("/var/lib/reddb/main.rebootstrap.redbase")
910        );
911        assert_eq!(
912            rebootstrap_pending_path(path),
913            PathBuf::from("/var/lib/reddb/main.rebootstrap.pending.rdb")
914        );
915        assert_eq!(
916            rebootstrap_ready_marker_path(path),
917            PathBuf::from("/var/lib/reddb/main.rebootstrap.ready")
918        );
919        assert_eq!(
920            rebootstrap_intent_log_path(path),
921            PathBuf::from("/var/lib/reddb/main.rebootstrap.intent.jsonl")
922        );
923        assert_eq!(
924            rebootstrap_previous_path(path),
925            PathBuf::from("/var/lib/reddb/main.rebootstrap.previous.rdb")
926        );
927        assert_eq!(
928            primary_replica_root(path),
929            PathBuf::from("/var/lib/reddb/main.primary-replica")
930        );
931        assert_eq!(
932            legacy_logical_slots_path(path),
933            PathBuf::from("/var/lib/reddb/main.rdb.logical.slots.json")
934        );
935        assert_eq!(
936            legacy_logical_slots_temp_path(&legacy_logical_slots_path(path)),
937            PathBuf::from("/var/lib/reddb/main.rdb.logical.slots.logical.slots.tmp")
938        );
939        assert_eq!(
940            legacy_audit_log_path(path),
941            PathBuf::from("/var/lib/reddb/.audit.log")
942        );
943        assert_eq!(
944            audit_log_rotated_plain_path(&legacy_audit_log_path(path), 42),
945            PathBuf::from("/var/lib/reddb/.audit.log.42")
946        );
947        assert_eq!(
948            audit_log_rotated_compressed_path(&legacy_audit_log_path(path), 42),
949            PathBuf::from("/var/lib/reddb/.audit.log.42.zst")
950        );
951        assert_eq!(
952            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), ".audit.log.42"),
953            Some(42)
954        );
955        assert_eq!(
956            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), ".audit.log.42.zst"),
957            Some(42)
958        );
959        assert_eq!(
960            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), "other.42.zst"),
961            None
962        );
963        assert_eq!(
964            legacy_slow_query_log_path(Path::new("/var/log/reddb")),
965            PathBuf::from("/var/log/reddb/red-slow.log")
966        );
967        assert_eq!(
968            serverless_root(path),
969            PathBuf::from("/var/lib/reddb/main.serverless")
970        );
971        assert_eq!(serverless_namespace(path), "main");
972        assert_eq!(
973            serverless_cache_root(&serverless_root(path), &serverless_namespace(path)),
974            PathBuf::from("/var/lib/reddb/main.serverless/main/cache")
975        );
976    }
977
978    #[test]
979    fn derives_dedicated_support_sidecars() {
980        let path = Path::new("/var/lib/reddb/main.rdb");
981        let support = support_dir_for(path);
982
983        assert_eq!(
984            unified_wal_path_in(&support, path),
985            PathBuf::from("/var/lib/reddb/main.rdb.red/wal/main.rdb-uwal")
986        );
987        assert_eq!(
988            logical_wal_path_in(&support, path),
989            PathBuf::from("/var/lib/reddb/main.rdb.red/wal/main.rdb.logical.wal")
990        );
991        assert_eq!(
992            temp_path_in(&support, path),
993            PathBuf::from("/var/lib/reddb/main.rdb.red/tmp/main.rdb-tmp")
994        );
995    }
996
997    #[test]
998    fn derives_primary_replica_segment_names() {
999        assert_eq!(
1000            primary_wal_segment_file_name(2),
1001            "00000000000000000002.redwal"
1002        );
1003        assert_eq!(
1004            relay_segment_relative_path(10, 20),
1005            PathBuf::from("relay-00000000000000000010-00000000000000000020.redwal")
1006        );
1007    }
1008}