Skip to main content

osmic_repl/
store.rs

1use std::path::Path;
2
3use redb::{Database, ReadableTableMetadata, TableDefinition};
4
5use osmic_core::bbox::BBox;
6use osmic_core::error::{OsmicError, OsmicResult};
7
8/// redb table: osm_id (i64) -> bbox bytes (4 x f64 = 32 bytes).
9/// We store the bbox so we can mark dirty tiles on delete without
10/// needing the full feature.
11const BBOXES: TableDefinition<i64, &[u8]> = TableDefinition::new("bboxes");
12
13/// Persistent bbox store backed by redb.
14///
15/// For incremental tile updates, we only need to know WHICH tiles are
16/// affected by a change. We store the bounding box of each feature by
17/// OSM ID. The full feature data (geometry, tags) lives in the PMTiles
18/// output and is regenerated from the PBF + changes.
19pub struct FeatureStore {
20    db: Database,
21}
22
23impl FeatureStore {
24    /// Open or create a feature store at the given path.
25    pub fn open(path: &Path) -> OsmicResult<Self> {
26        let db = Database::create(path)
27            .map_err(|e| OsmicError::Other(format!("Failed to open feature store: {e}")))?;
28
29        // Ensure table exists
30        let txn = db
31            .begin_write()
32            .map_err(|e| OsmicError::Other(format!("Failed to begin write: {e}")))?;
33        {
34            let _table = txn
35                .open_table(BBOXES)
36                .map_err(|e| OsmicError::Other(format!("Failed to open table: {e}")))?;
37        }
38        txn.commit()
39            .map_err(|e| OsmicError::Other(format!("Failed to commit: {e}")))?;
40
41        Ok(Self { db })
42    }
43
44    /// Store a feature's bounding box. Returns the old bbox if it existed.
45    pub fn upsert(&self, id: i64, bbox: &BBox) -> OsmicResult<Option<BBox>> {
46        let old = self.get_bbox(id)?;
47
48        let bytes = bbox_to_bytes(bbox);
49        let txn = self
50            .db
51            .begin_write()
52            .map_err(|e| OsmicError::Other(format!("Write txn failed: {e}")))?;
53        {
54            let mut table = txn
55                .open_table(BBOXES)
56                .map_err(|e| OsmicError::Other(format!("Open table failed: {e}")))?;
57            table
58                .insert(id, bytes.as_slice())
59                .map_err(|e| OsmicError::Other(format!("Insert failed: {e}")))?;
60        }
61        txn.commit()
62            .map_err(|e| OsmicError::Other(format!("Commit failed: {e}")))?;
63
64        Ok(old)
65    }
66
67    /// Delete a feature's bbox by ID. Returns the old bbox if it existed.
68    pub fn delete(&self, id: i64) -> OsmicResult<Option<BBox>> {
69        let old = self.get_bbox(id)?;
70
71        let txn = self
72            .db
73            .begin_write()
74            .map_err(|e| OsmicError::Other(format!("Write txn failed: {e}")))?;
75        {
76            let mut table = txn
77                .open_table(BBOXES)
78                .map_err(|e| OsmicError::Other(format!("Open table failed: {e}")))?;
79            let _ = table.remove(id);
80        }
81        txn.commit()
82            .map_err(|e| OsmicError::Other(format!("Commit failed: {e}")))?;
83
84        Ok(old)
85    }
86
87    /// Get a stored bbox by ID.
88    pub fn get_bbox(&self, id: i64) -> OsmicResult<Option<BBox>> {
89        let txn = self
90            .db
91            .begin_read()
92            .map_err(|e| OsmicError::Other(format!("Read txn failed: {e}")))?;
93        let table = txn
94            .open_table(BBOXES)
95            .map_err(|e| OsmicError::Other(format!("Open table failed: {e}")))?;
96
97        match table.get(id) {
98            Ok(Some(guard)) => {
99                let bytes = guard.value();
100                Ok(Some(bbox_from_bytes(bytes)))
101            }
102            Ok(None) => Ok(None),
103            Err(e) => Err(OsmicError::Other(format!("Get failed: {e}"))),
104        }
105    }
106
107    /// Count of features in the store.
108    pub fn len(&self) -> OsmicResult<u64> {
109        let txn = self
110            .db
111            .begin_read()
112            .map_err(|e| OsmicError::Other(format!("Read txn failed: {e}")))?;
113        let table = txn
114            .open_table(BBOXES)
115            .map_err(|e| OsmicError::Other(format!("Open table failed: {e}")))?;
116        Ok(table.len().unwrap_or(0))
117    }
118
119    pub fn is_empty(&self) -> OsmicResult<bool> {
120        self.len().map(|n| n == 0)
121    }
122}
123
124fn bbox_to_bytes(bbox: &BBox) -> [u8; 32] {
125    let mut buf = [0u8; 32];
126    buf[0..8].copy_from_slice(&bbox.min_lon.to_le_bytes());
127    buf[8..16].copy_from_slice(&bbox.min_lat.to_le_bytes());
128    buf[16..24].copy_from_slice(&bbox.max_lon.to_le_bytes());
129    buf[24..32].copy_from_slice(&bbox.max_lat.to_le_bytes());
130    buf
131}
132
133fn bbox_from_bytes(bytes: &[u8]) -> BBox {
134    BBox {
135        min_lon: f64::from_le_bytes(bytes[0..8].try_into().unwrap()),
136        min_lat: f64::from_le_bytes(bytes[8..16].try_into().unwrap()),
137        max_lon: f64::from_le_bytes(bytes[16..24].try_into().unwrap()),
138        max_lat: f64::from_le_bytes(bytes[24..32].try_into().unwrap()),
139    }
140}