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