Skip to main content

mz_deploy/
fs.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Filesystem abstraction with optional in-memory overlays.
11//!
12//! The overlay only intercepts content reads. Directory walks, file
13//! existence checks, and sibling metadata are still served from disk.
14
15use std::collections::BTreeMap;
16use std::io;
17use std::path::{Path, PathBuf};
18
19/// Read-through filesystem with an optional in-memory overlay.
20pub(crate) struct FileSystem {
21    overlay: BTreeMap<PathBuf, String>,
22}
23
24impl FileSystem {
25    /// Construct a filesystem with no overlay; reads always go to disk.
26    pub(crate) fn new() -> Self {
27        Self {
28            overlay: BTreeMap::new(),
29        }
30    }
31
32    /// Construct a filesystem with the given overlay; a read for a path
33    /// present in `overlay` returns the overlay bytes, otherwise it falls
34    /// back to disk.
35    pub(crate) fn with_overlay(overlay: BTreeMap<PathBuf, String>) -> Self {
36        Self { overlay }
37    }
38
39    /// Construct a filesystem from a JSON file mapping absolute paths to
40    /// their contents (`{ "/abs/path": "contents", ... }`). Used by the
41    /// `--overlay` flag on `test` and `explain` so the VSCode extension can
42    /// surface unsaved buffers without writing them to disk.
43    pub(crate) fn from_overlay_file(path: &Path) -> io::Result<Self> {
44        let raw = std::fs::read_to_string(path)?;
45        let map: BTreeMap<PathBuf, String> = serde_json::from_str(&raw)
46            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
47        Ok(Self::with_overlay(map))
48    }
49
50    /// Read the file at `path`, consulting the overlay first.
51    pub(crate) fn read_to_string(&self, path: &Path) -> io::Result<String> {
52        if let Some(text) = self.overlay.get(path) {
53            return Ok(text.clone());
54        }
55        std::fs::read_to_string(path)
56    }
57
58    /// Whether `path` is covered by an overlay entry. Used by callers that
59    /// maintain disk-keyed caches: when a path is overlay-covered, the
60    /// disk-derived cache key is meaningless and the cache must be
61    /// bypassed.
62    pub(crate) fn is_overlay(&self, path: &Path) -> bool {
63        self.overlay.contains_key(path)
64    }
65}
66
67impl Default for FileSystem {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[mz_ore::test]
78    fn overlay_intercepts_read() {
79        let tmp = tempfile::tempdir().unwrap();
80        let file = tmp.path().join("a.sql");
81        std::fs::write(&file, "disk").unwrap();
82
83        let mut overlay = BTreeMap::new();
84        overlay.insert(file.clone(), "buffer".to_string());
85        let fs = FileSystem::with_overlay(overlay);
86
87        assert_eq!(fs.read_to_string(&file).unwrap(), "buffer");
88        assert!(fs.is_overlay(&file));
89    }
90
91    #[mz_ore::test]
92    fn no_overlay_reads_disk() {
93        let tmp = tempfile::tempdir().unwrap();
94        let file = tmp.path().join("a.sql");
95        std::fs::write(&file, "disk").unwrap();
96        let fs = FileSystem::new();
97        assert_eq!(fs.read_to_string(&file).unwrap(), "disk");
98        assert!(!fs.is_overlay(&file));
99    }
100
101    #[mz_ore::test]
102    fn from_overlay_file_round_trips() {
103        let tmp = tempfile::tempdir().unwrap();
104        let target = tmp.path().join("models/x.sql");
105        let overlay_json = tmp.path().join("overlay.json");
106
107        // Write the JSON manually so the format we accept is locked in.
108        let body = format!(
109            "{{\"{}\":\"SELECT 42\"}}",
110            target.display().to_string().replace('\\', "\\\\"),
111        );
112        std::fs::write(&overlay_json, body).unwrap();
113
114        let fs = FileSystem::from_overlay_file(&overlay_json).unwrap();
115        assert_eq!(fs.read_to_string(&target).unwrap(), "SELECT 42");
116        assert!(fs.is_overlay(&target));
117    }
118
119    #[mz_ore::test]
120    fn from_overlay_file_rejects_garbage() {
121        let tmp = tempfile::tempdir().unwrap();
122        let overlay_json = tmp.path().join("overlay.json");
123        std::fs::write(&overlay_json, "not json").unwrap();
124
125        let err = FileSystem::from_overlay_file(&overlay_json)
126            .err()
127            .expect("garbage JSON must be rejected");
128        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
129    }
130}