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; 0] {
564    []
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    /// Retired: the pager double-write sidecar, now the in-file DWB zone.
585    pub const PAGER_DWB_EXTENSION_V0: &str = "rdb-dwb";
586
587    pub fn pager_header_path_v0(data_path: &Path) -> PathBuf {
588        data_path.with_extension(PAGER_HEADER_EXTENSION_V0)
589    }
590
591    pub fn pager_meta_path_v0(data_path: &Path) -> PathBuf {
592        data_path.with_extension(PAGER_META_EXTENSION_V0)
593    }
594
595    pub fn pager_dwb_path_v0(data_path: &Path) -> PathBuf {
596        data_path.with_extension(PAGER_DWB_EXTENSION_V0)
597    }
598
599    pub fn pager_header_shadow_path_v0(data_path: &Path) -> PathBuf {
600        path_with_dash_suffix(data_path, "hdr")
601    }
602
603    pub fn pager_meta_shadow_path_v0(data_path: &Path) -> PathBuf {
604        path_with_dash_suffix(data_path, "meta")
605    }
606
607    pub fn pager_dwb_shadow_path_v0(data_path: &Path) -> PathBuf {
608        path_with_dash_suffix(data_path, "dwb")
609    }
610
611    /// Every retired phase-1 artifact path derived from `data_path`. For a
612    /// `.rdb` data file the extension and shadow forms coincide, so the list
613    /// de-duplicates naturally at the filesystem.
614    pub fn phase1_sidecar_paths(data_path: &Path) -> [PathBuf; 4] {
615        [
616            pager_header_path_v0(data_path),
617            pager_meta_path_v0(data_path),
618            pager_header_shadow_path_v0(data_path),
619            pager_meta_shadow_path_v0(data_path),
620        ]
621    }
622
623    /// The first retired phase-1 sidecar that exists next to `data_path`, if
624    /// any. Drives the didactic open-time refusal and the census assertion.
625    pub fn first_present_phase1_sidecar(data_path: &Path) -> Option<PathBuf> {
626        phase1_sidecar_paths(data_path)
627            .into_iter()
628            .find(|path| path.exists())
629    }
630
631    /// Every retired phase-3 DWB artifact path derived from `data_path`.
632    pub fn phase3_dwb_sidecar_paths(data_path: &Path) -> [PathBuf; 2] {
633        [
634            pager_dwb_path_v0(data_path),
635            pager_dwb_shadow_path_v0(data_path),
636        ]
637    }
638
639    pub fn first_present_phase3_dwb_sidecar(data_path: &Path) -> Option<PathBuf> {
640        phase3_dwb_sidecar_paths(data_path)
641            .into_iter()
642            .find(|path| path.exists())
643    }
644}
645
646pub fn shm_path(data_path: &Path) -> PathBuf {
647    sibling_path(
648        data_path,
649        &format!("{}-{SHM_FILE_SUFFIX}", data_file_name(data_path)),
650    )
651}
652
653pub fn physical_metadata_json_path(data_path: &Path) -> PathBuf {
654    sibling_path(
655        data_path,
656        &format!(
657            "{}.{PHYSICAL_METADATA_JSON_SUFFIX}",
658            data_file_name(data_path)
659        ),
660    )
661}
662
663pub fn physical_metadata_binary_path(data_path: &Path) -> PathBuf {
664    sibling_path(
665        data_path,
666        &format!(
667            "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}",
668            data_file_name(data_path)
669        ),
670    )
671}
672
673pub fn physical_metadata_journal_path(data_path: &Path, sequence: u64) -> PathBuf {
674    sibling_path(
675        data_path,
676        &format!(
677            "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}.seq-{sequence:020}",
678            data_file_name(data_path)
679        ),
680    )
681}
682
683pub fn physical_metadata_journal_prefix(data_path: &Path) -> String {
684    format!(
685        "{}.{PHYSICAL_METADATA_BINARY_EXTENSION}.seq-",
686        data_file_name(data_path)
687    )
688}
689
690pub fn physical_export_data_path(data_path: &Path, name: &str) -> PathBuf {
691    let file_name = data_file_name(data_path);
692    let stem = file_name.strip_suffix(".rdb").unwrap_or(&file_name);
693    sibling_path(
694        data_path,
695        &format!("{stem}.export.{}.rdb", sanitize_export_name(name)),
696    )
697}
698
699pub fn local_cas_lock_path(dest: &Path) -> PathBuf {
700    let file_name = data_file_name(dest);
701    dest.with_file_name(format!(".{file_name}.{LOCAL_CAS_LOCK_SUFFIX}"))
702}
703
704pub fn local_upload_temp_path(dest: &Path, pid: u32, unique: u64) -> PathBuf {
705    let file_name = data_file_name(dest);
706    dest.with_file_name(format!(
707        ".{file_name}.{LOCAL_UPLOAD_TEMP_TAG}-{pid}-{unique}"
708    ))
709}
710
711pub fn rebootstrap_staging_root(data_path: &Path) -> PathBuf {
712    data_path.with_extension(REBOOTSTRAP_STAGING_EXTENSION)
713}
714
715pub fn rebootstrap_pending_path(data_path: &Path) -> PathBuf {
716    data_path.with_extension(REBOOTSTRAP_PENDING_EXTENSION)
717}
718
719pub fn rebootstrap_ready_marker_path(data_path: &Path) -> PathBuf {
720    data_path.with_extension(REBOOTSTRAP_READY_EXTENSION)
721}
722
723pub fn rebootstrap_intent_log_path(data_path: &Path) -> PathBuf {
724    data_path.with_extension(REBOOTSTRAP_INTENT_LOG_EXTENSION)
725}
726
727pub fn rebootstrap_previous_path(data_path: &Path) -> PathBuf {
728    data_path.with_extension(REBOOTSTRAP_PREVIOUS_EXTENSION)
729}
730
731pub fn primary_replica_root(data_path: &Path) -> PathBuf {
732    data_path.with_extension(PRIMARY_REPLICA_ROOT_EXTENSION)
733}
734
735pub fn legacy_logical_slots_path(data_path: &Path) -> PathBuf {
736    let file_name = data_path
737        .file_name()
738        .and_then(|name| name.to_str())
739        .unwrap_or("reddb.rdb");
740    sibling_path(
741        data_path,
742        &format!("{file_name}.{LEGACY_LOGICAL_SLOTS_SUFFIX}"),
743    )
744}
745
746pub fn legacy_logical_slots_temp_path(path: &Path) -> PathBuf {
747    path.with_extension(LEGACY_LOGICAL_SLOTS_TEMP_EXTENSION)
748}
749
750pub fn legacy_audit_log_path(data_path: &Path) -> PathBuf {
751    sibling_path(data_path, LEGACY_AUDIT_LOG_FILE_NAME)
752}
753
754pub fn audit_log_rotated_plain_path(active_path: &Path, timestamp_nanos: u128) -> PathBuf {
755    sibling_path(
756        active_path,
757        &format!("{}.{timestamp_nanos}", audit_log_file_name(active_path)),
758    )
759}
760
761pub fn audit_log_rotated_compressed_path(active_path: &Path, timestamp_nanos: u128) -> PathBuf {
762    sibling_path(
763        active_path,
764        &format!(
765            "{}.{timestamp_nanos}.{AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION}",
766            audit_log_file_name(active_path)
767        ),
768    )
769}
770
771pub fn parse_audit_log_rotated_timestamp(
772    active_path: &Path,
773    candidate_file_name: &str,
774) -> Option<u128> {
775    let active_name = audit_log_file_name(active_path);
776    let rotated = candidate_file_name.strip_prefix(&format!("{active_name}."))?;
777    let timestamp = rotated
778        .strip_suffix(&format!(".{AUDIT_LOG_ROTATED_COMPRESSED_EXTENSION}"))
779        .unwrap_or(rotated);
780    timestamp.parse::<u128>().ok()
781}
782
783pub fn legacy_slow_query_log_path(log_dir: &Path) -> PathBuf {
784    log_dir.join(LEGACY_SLOW_QUERY_LOG_FILE_NAME)
785}
786
787pub fn serverless_root(data_path: &Path) -> PathBuf {
788    data_path.with_extension(SERVERLESS_ROOT_EXTENSION)
789}
790
791pub fn serverless_namespace(data_path: &Path) -> String {
792    data_path
793        .file_stem()
794        .and_then(|stem| stem.to_str())
795        .filter(|stem| !stem.is_empty())
796        .unwrap_or("default")
797        .to_string()
798}
799
800pub fn serverless_cache_root(root: &Path, namespace: &str) -> PathBuf {
801    root.join(namespace).join(SERVERLESS_CACHE_DIR)
802}
803
804fn audit_log_file_name(path: &Path) -> String {
805    path.file_name()
806        .and_then(|name| name.to_str())
807        .unwrap_or(LEGACY_AUDIT_LOG_FILE_NAME)
808        .to_string()
809}
810
811fn push_parent(dirs: &mut Vec<PathBuf>, path: &Path) {
812    if let Some(parent) = path.parent() {
813        if !parent.as_os_str().is_empty() {
814            dirs.push(parent.to_path_buf());
815        }
816    }
817}
818
819fn push_optional(dirs: &mut Vec<PathBuf>, path: Option<&PathBuf>) {
820    if let Some(path) = path {
821        dirs.push(path.clone());
822    }
823}
824
825fn sanitize_export_name(name: &str) -> String {
826    let mut out = String::new();
827    for ch in name.chars() {
828        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
829            out.push(ch);
830        } else {
831            out.push('_');
832        }
833    }
834    if out.is_empty() {
835        "export".to_string()
836    } else {
837        out
838    }
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844
845    #[test]
846    fn derives_standard_sidecars_next_to_database() {
847        let path = Path::new("/var/lib/reddb/main.rdb");
848
849        assert_eq!(
850            support_dir_for(path),
851            PathBuf::from("/var/lib/reddb/main.rdb.red")
852        );
853        assert_eq!(
854            unified_wal_path(path),
855            PathBuf::from("/var/lib/reddb/main.rdb-uwal")
856        );
857        assert_eq!(
858            store_commit_coord_temp_wal_path(Path::new("/tmp"), "burst", 7, 99),
859            PathBuf::from("/tmp/rb_commit_coord_burst_7_99.wal")
860        );
861        assert_eq!(
862            group_commit_temp_wal_path(Path::new("/tmp"), "batch", 7, 99),
863            PathBuf::from("/tmp/rb_group_commit_batch_7_99.wal")
864        );
865        assert_eq!(
866            wal_component_temp_path(Path::new("/tmp"), "writer", "create", 7),
867            PathBuf::from("/tmp/rb_wal_writer_create_7.wal")
868        );
869        assert_eq!(
870            wal_component_temp_path(Path::new("/tmp"), "reader", "empty", 7),
871            PathBuf::from("/tmp/rb_wal_reader_empty_7.wal")
872        );
873        assert_eq!(
874            wal_component_unique_temp_path(Path::new("/tmp"), "coord", "single", 7, 99),
875            PathBuf::from("/tmp/rb_wal_coord_single_7_99.wal")
876        );
877        assert_eq!(
878            backup_temp_json_path(
879                Path::new("/tmp"),
880                "reddb-archived-change-records",
881                7,
882                99,
883                3,
884                Some(10),
885                Some(20)
886            ),
887            PathBuf::from("/tmp/reddb-archived-change-records-7-10-20-99-3.json")
888        );
889        assert_eq!(
890            backup_temp_json_path(Path::new("/tmp"), "reddb-json-object", 7, 99, 3, None, None),
891            PathBuf::from("/tmp/reddb-json-object-7-99-3.json")
892        );
893        assert_eq!(
894            logical_wal_path(path),
895            PathBuf::from("/var/lib/reddb/main.rdb.logical.wal")
896        );
897        assert_eq!(
898            logical_wal_temp_path(&logical_wal_path(path)),
899            PathBuf::from("/var/lib/reddb/main.rdb.logical.logical.wal.tmp")
900        );
901        assert_eq!(
902            temp_path(path),
903            PathBuf::from("/var/lib/reddb/main.rdb-tmp")
904        );
905        assert_eq!(
906            atomic_temp_path(&pager_dwb_path(path)),
907            PathBuf::from("/var/lib/reddb/main.tmp")
908        );
909        assert_eq!(
910            result_cache_l2_path(path),
911            PathBuf::from("/var/lib/reddb/main.result-cache.l2")
912        );
913        assert_eq!(
914            engine_wal_path(path),
915            PathBuf::from("/var/lib/reddb/main.rdb-wal")
916        );
917        assert_eq!(
918            pager_legacy_wal_path(path),
919            PathBuf::from("/var/lib/reddb/main.wal")
920        );
921        assert_eq!(
922            pager_dwb_shadow_path(path),
923            PathBuf::from("/var/lib/reddb/main.rdb-dwb")
924        );
925        assert_eq!(shm_path(path), PathBuf::from("/var/lib/reddb/main.rdb-shm"));
926        assert_eq!(
927            physical_metadata_json_path(path),
928            PathBuf::from("/var/lib/reddb/main.rdb.meta.json")
929        );
930        assert_eq!(
931            physical_metadata_binary_path(path),
932            PathBuf::from("/var/lib/reddb/main.rdb.meta.rdbx")
933        );
934        assert_eq!(
935            physical_metadata_journal_path(path, 7),
936            PathBuf::from("/var/lib/reddb/main.rdb.meta.rdbx.seq-00000000000000000007")
937        );
938        assert_eq!(
939            physical_metadata_journal_prefix(path),
940            "main.rdb.meta.rdbx.seq-"
941        );
942        assert_eq!(
943            physical_export_data_path(path, "nightly backup"),
944            PathBuf::from("/var/lib/reddb/main.export.nightly_backup.rdb")
945        );
946        assert_eq!(
947            local_cas_lock_path(path),
948            PathBuf::from("/var/lib/reddb/.main.rdb.cas.lock")
949        );
950        assert_eq!(
951            local_upload_temp_path(path, 123, 7),
952            PathBuf::from("/var/lib/reddb/.main.rdb.tmp-123-7")
953        );
954        assert_eq!(
955            rebootstrap_staging_root(path),
956            PathBuf::from("/var/lib/reddb/main.rebootstrap.redbase")
957        );
958        assert_eq!(
959            rebootstrap_pending_path(path),
960            PathBuf::from("/var/lib/reddb/main.rebootstrap.pending.rdb")
961        );
962        assert_eq!(
963            rebootstrap_ready_marker_path(path),
964            PathBuf::from("/var/lib/reddb/main.rebootstrap.ready")
965        );
966        assert_eq!(
967            rebootstrap_intent_log_path(path),
968            PathBuf::from("/var/lib/reddb/main.rebootstrap.intent.jsonl")
969        );
970        assert_eq!(
971            rebootstrap_previous_path(path),
972            PathBuf::from("/var/lib/reddb/main.rebootstrap.previous.rdb")
973        );
974        assert_eq!(
975            primary_replica_root(path),
976            PathBuf::from("/var/lib/reddb/main.primary-replica")
977        );
978        assert_eq!(
979            legacy_logical_slots_path(path),
980            PathBuf::from("/var/lib/reddb/main.rdb.logical.slots.json")
981        );
982        assert_eq!(
983            legacy_logical_slots_temp_path(&legacy_logical_slots_path(path)),
984            PathBuf::from("/var/lib/reddb/main.rdb.logical.slots.logical.slots.tmp")
985        );
986        assert_eq!(
987            legacy_audit_log_path(path),
988            PathBuf::from("/var/lib/reddb/.audit.log")
989        );
990        assert_eq!(
991            audit_log_rotated_plain_path(&legacy_audit_log_path(path), 42),
992            PathBuf::from("/var/lib/reddb/.audit.log.42")
993        );
994        assert_eq!(
995            audit_log_rotated_compressed_path(&legacy_audit_log_path(path), 42),
996            PathBuf::from("/var/lib/reddb/.audit.log.42.zst")
997        );
998        assert_eq!(
999            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), ".audit.log.42"),
1000            Some(42)
1001        );
1002        assert_eq!(
1003            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), ".audit.log.42.zst"),
1004            Some(42)
1005        );
1006        assert_eq!(
1007            parse_audit_log_rotated_timestamp(&legacy_audit_log_path(path), "other.42.zst"),
1008            None
1009        );
1010        assert_eq!(
1011            legacy_slow_query_log_path(Path::new("/var/log/reddb")),
1012            PathBuf::from("/var/log/reddb/red-slow.log")
1013        );
1014        assert_eq!(
1015            serverless_root(path),
1016            PathBuf::from("/var/lib/reddb/main.serverless")
1017        );
1018        assert_eq!(serverless_namespace(path), "main");
1019        assert_eq!(
1020            serverless_cache_root(&serverless_root(path), &serverless_namespace(path)),
1021            PathBuf::from("/var/lib/reddb/main.serverless/main/cache")
1022        );
1023    }
1024
1025    #[test]
1026    fn derives_dedicated_support_sidecars() {
1027        let path = Path::new("/var/lib/reddb/main.rdb");
1028        let support = support_dir_for(path);
1029
1030        assert_eq!(
1031            unified_wal_path_in(&support, path),
1032            PathBuf::from("/var/lib/reddb/main.rdb.red/wal/main.rdb-uwal")
1033        );
1034        assert_eq!(
1035            logical_wal_path_in(&support, path),
1036            PathBuf::from("/var/lib/reddb/main.rdb.red/wal/main.rdb.logical.wal")
1037        );
1038        assert_eq!(
1039            temp_path_in(&support, path),
1040            PathBuf::from("/var/lib/reddb/main.rdb.red/tmp/main.rdb-tmp")
1041        );
1042    }
1043
1044    #[test]
1045    fn derives_primary_replica_segment_names() {
1046        assert_eq!(
1047            primary_wal_segment_file_name(2),
1048            "00000000000000000002.redwal"
1049        );
1050        assert_eq!(
1051            relay_segment_relative_path(10, 20),
1052            PathBuf::from("relay-00000000000000000010-00000000000000000020.redwal")
1053        );
1054    }
1055
1056    #[test]
1057    fn the_live_pager_sidecar_set_is_empty_after_phase3() {
1058        let path = Path::new("/var/lib/reddb/main.rdb");
1059        let expected: [PathBuf; 0] = [];
1060        assert_eq!(pager_shadow_sidecar_paths(path), expected);
1061    }
1062
1063    #[test]
1064    fn retired_phase1_and_phase3_names_stay_reachable_only_for_migration_and_refusal() {
1065        let path = Path::new("/var/lib/reddb/main.rdb");
1066        // For a `.rdb` data file the extension and shadow forms coincide,
1067        // which is why a single glob catches both on a real store.
1068        assert_eq!(
1069            retired::phase1_sidecar_paths(path),
1070            [
1071                PathBuf::from("/var/lib/reddb/main.rdb-hdr"),
1072                PathBuf::from("/var/lib/reddb/main.rdb-meta"),
1073                PathBuf::from("/var/lib/reddb/main.rdb-hdr"),
1074                PathBuf::from("/var/lib/reddb/main.rdb-meta"),
1075            ]
1076        );
1077        assert_eq!(
1078            retired::phase3_dwb_sidecar_paths(path),
1079            [
1080                PathBuf::from("/var/lib/reddb/main.rdb-dwb"),
1081                PathBuf::from("/var/lib/reddb/main.rdb-dwb"),
1082            ]
1083        );
1084        // A non-`.rdb` data file (the blob-cache L2 store) shows the two forms
1085        // diverging, so both must be probed.
1086        let l2 = Path::new("/var/lib/reddb/main.result-cache.l2");
1087        assert_eq!(
1088            retired::pager_header_path_v0(l2),
1089            PathBuf::from("/var/lib/reddb/main.result-cache.rdb-hdr")
1090        );
1091        assert_eq!(
1092            retired::pager_header_shadow_path_v0(l2),
1093            PathBuf::from("/var/lib/reddb/main.result-cache.l2-hdr")
1094        );
1095    }
1096}