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