Skip to main content

reddb_file/
zone_map.rs

1use std::fs::{File, OpenOptions};
2use std::io::{BufReader, BufWriter, Read, Write};
3use std::path::Path;
4
5const MAGIC: u32 = 0x5A4D4150; // "ZMAP"
6const VERSION: u32 = 1;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct PersistedZone {
10    pub column_index: u32,
11    pub block_id: u32,
12    pub min_value: String,
13    pub max_value: String,
14    pub null_count: u64,
15    pub row_count: u64,
16}
17
18#[derive(Debug)]
19pub enum ZoneMapPersistError {
20    Io(std::io::Error),
21    BadMagic { found: u32 },
22    BadVersion { found: u32 },
23    Truncated,
24    InvalidUtf8(std::string::FromUtf8Error),
25}
26
27impl From<std::io::Error> for ZoneMapPersistError {
28    fn from(e: std::io::Error) -> Self {
29        Self::Io(e)
30    }
31}
32
33impl From<std::string::FromUtf8Error> for ZoneMapPersistError {
34    fn from(e: std::string::FromUtf8Error) -> Self {
35        Self::InvalidUtf8(e)
36    }
37}
38
39impl std::fmt::Display for ZoneMapPersistError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::Io(e) => write!(f, "zone-map i/o: {e}"),
43            Self::BadMagic { found } => {
44                write!(f, "zone-map bad magic: expected {MAGIC:#x}, got {found:#x}")
45            }
46            Self::BadVersion { found } => {
47                write!(f, "zone-map version {found} not supported (max {VERSION})")
48            }
49            Self::Truncated => write!(f, "zone-map file ended unexpectedly"),
50            Self::InvalidUtf8(e) => write!(f, "zone-map utf8 error: {e}"),
51        }
52    }
53}
54
55impl std::error::Error for ZoneMapPersistError {}
56
57pub fn write_zone_map_sidecar(
58    path: &Path,
59    column_count: u32,
60    zones: &[PersistedZone],
61) -> Result<(), ZoneMapPersistError> {
62    let tmp_path = path.with_extension("zonemap.tmp");
63    {
64        let file = OpenOptions::new()
65            .write(true)
66            .create(true)
67            .truncate(true)
68            .open(&tmp_path)?;
69        let mut w = BufWriter::new(file);
70        w.write_all(&MAGIC.to_le_bytes())?;
71        w.write_all(&VERSION.to_le_bytes())?;
72        w.write_all(&column_count.to_le_bytes())?;
73        w.write_all(&(zones.len() as u32).to_le_bytes())?;
74        for z in zones {
75            w.write_all(&z.column_index.to_le_bytes())?;
76            w.write_all(&z.block_id.to_le_bytes())?;
77            write_str(&mut w, &z.min_value)?;
78            write_str(&mut w, &z.max_value)?;
79            w.write_all(&z.null_count.to_le_bytes())?;
80            w.write_all(&z.row_count.to_le_bytes())?;
81        }
82        w.flush()?;
83    }
84    std::fs::rename(&tmp_path, path)?;
85    Ok(())
86}
87
88pub fn read_zone_map_sidecar(
89    path: &Path,
90) -> Result<(u32, Vec<PersistedZone>), ZoneMapPersistError> {
91    let file = File::open(path)?;
92    let mut r = BufReader::new(file);
93    let magic = read_u32(&mut r)?;
94    if magic != MAGIC {
95        return Err(ZoneMapPersistError::BadMagic { found: magic });
96    }
97    let version = read_u32(&mut r)?;
98    if version != VERSION {
99        return Err(ZoneMapPersistError::BadVersion { found: version });
100    }
101    let column_count = read_u32(&mut r)?;
102    let zone_count = read_u32(&mut r)?;
103    let mut zones = Vec::with_capacity(zone_count as usize);
104    for _ in 0..zone_count {
105        let column_index = read_u32(&mut r)?;
106        let block_id = read_u32(&mut r)?;
107        let min_value = read_str(&mut r)?;
108        let max_value = read_str(&mut r)?;
109        let null_count = read_u64(&mut r)?;
110        let row_count = read_u64(&mut r)?;
111        zones.push(PersistedZone {
112            column_index,
113            block_id,
114            min_value,
115            max_value,
116            null_count,
117            row_count,
118        });
119    }
120    Ok((column_count, zones))
121}
122
123fn write_str<W: Write>(w: &mut W, s: &str) -> Result<(), ZoneMapPersistError> {
124    let bytes = s.as_bytes();
125    w.write_all(&(bytes.len() as u32).to_le_bytes())?;
126    w.write_all(bytes)?;
127    Ok(())
128}
129
130fn read_u32<R: Read>(r: &mut R) -> Result<u32, ZoneMapPersistError> {
131    let mut buf = [0u8; 4];
132    r.read_exact(&mut buf)
133        .map_err(|_| ZoneMapPersistError::Truncated)?;
134    Ok(u32::from_le_bytes(buf))
135}
136
137fn read_u64<R: Read>(r: &mut R) -> Result<u64, ZoneMapPersistError> {
138    let mut buf = [0u8; 8];
139    r.read_exact(&mut buf)
140        .map_err(|_| ZoneMapPersistError::Truncated)?;
141    Ok(u64::from_le_bytes(buf))
142}
143
144fn read_str<R: Read>(r: &mut R) -> Result<String, ZoneMapPersistError> {
145    let len = read_u32(r)?;
146    let mut buf = vec![0u8; len as usize];
147    r.read_exact(&mut buf)
148        .map_err(|_| ZoneMapPersistError::Truncated)?;
149    Ok(String::from_utf8(buf)?)
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use std::time::{SystemTime, UNIX_EPOCH};
156
157    fn temp_path() -> std::path::PathBuf {
158        let suffix = SystemTime::now()
159            .duration_since(UNIX_EPOCH)
160            .expect("clock")
161            .as_nanos();
162        std::env::temp_dir().join(format!("reddb-file-zone-map-{suffix}.zonemap"))
163    }
164
165    #[test]
166    fn zone_map_sidecar_round_trips() {
167        let path = temp_path();
168        let zones = vec![PersistedZone {
169            column_index: 2,
170            block_id: 9,
171            min_value: "a".into(),
172            max_value: "z".into(),
173            null_count: 1,
174            row_count: 50,
175        }];
176
177        write_zone_map_sidecar(&path, 4, &zones).expect("write");
178        let (column_count, decoded) = read_zone_map_sidecar(&path).expect("read");
179        assert_eq!(column_count, 4);
180        assert_eq!(decoded, zones);
181
182        let _ = std::fs::remove_file(path);
183    }
184}