nodedb_lite/engine/htap/
bridge.rs1use std::collections::HashMap;
19use std::sync::Mutex;
20
21use nodedb_types::value::Value;
22
23use crate::engine::columnar::ColumnarEngine;
24use crate::storage::engine::StorageEngine;
25
26#[derive(Debug, Clone)]
28pub struct MaterializedView {
29 pub source: String,
31 pub target: String,
33 pub last_replicated_ms: u64,
35 pub rows_replicated: u64,
37}
38
39pub struct HtapBridge {
42 views: Mutex<HashMap<String, Vec<MaterializedView>>>,
44}
45
46impl HtapBridge {
47 pub fn new() -> Self {
49 Self {
50 views: Mutex::new(HashMap::new()),
51 }
52 }
53
54 fn lock_views(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<MaterializedView>>> {
55 match self.views.lock() {
56 Ok(g) => g,
57 Err(p) => p.into_inner(),
58 }
59 }
60
61 pub fn register_view(&self, source: &str, target: &str) {
63 let view = MaterializedView {
64 source: source.to_string(),
65 target: target.to_string(),
66 last_replicated_ms: crate::runtime::now_millis(),
67 rows_replicated: 0,
68 };
69 self.lock_views()
70 .entry(source.to_string())
71 .or_default()
72 .push(view);
73 }
74
75 pub fn remove_view(&self, target: &str) {
77 let mut g = self.lock_views();
78 for views in g.values_mut() {
79 views.retain(|v| v.target != target);
80 }
81 g.retain(|_, views| !views.is_empty());
82 }
83
84 pub fn views_for_source(&self, source: &str) -> Vec<MaterializedView> {
86 self.lock_views().get(source).cloned().unwrap_or_default()
87 }
88
89 pub fn view_by_target(&self, target: &str) -> Option<MaterializedView> {
91 self.lock_views()
92 .values()
93 .flatten()
94 .find(|v| v.target == target)
95 .cloned()
96 }
97
98 pub fn all_targets(&self) -> Vec<String> {
100 self.lock_views()
101 .values()
102 .flatten()
103 .map(|v| v.target.clone())
104 .collect()
105 }
106
107 pub fn replicate_insert<S: StorageEngine>(
110 &self,
111 source: &str,
112 values: &[Value],
113 columnar: &ColumnarEngine<S>,
114 ) {
115 let mut g = self.lock_views();
116 let Some(views) = g.get_mut(source) else {
117 return;
118 };
119 for view in views.iter_mut() {
120 if columnar.insert(&view.target, values).is_ok() {
121 view.rows_replicated += 1;
122 view.last_replicated_ms = crate::runtime::now_millis();
123 }
124 }
125 }
126
127 pub fn replicate_delete<S: StorageEngine>(
130 &self,
131 source: &str,
132 pk: &Value,
133 columnar: &ColumnarEngine<S>,
134 ) {
135 let mut g = self.lock_views();
136 let Some(views) = g.get_mut(source) else {
137 return;
138 };
139 for view in views.iter_mut() {
140 if columnar.delete(&view.target, pk).unwrap_or(false) {
141 view.last_replicated_ms = crate::runtime::now_millis();
142 }
143 }
144 }
145
146 pub fn lag_ms(&self, target: &str) -> u64 {
148 self.view_by_target(target)
149 .map(|v| crate::runtime::now_millis().saturating_sub(v.last_replicated_ms))
150 .unwrap_or(0)
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.lock_views().is_empty()
156 }
157}
158
159impl Default for HtapBridge {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn register_and_lookup_view() {
171 let bridge = HtapBridge::new();
172 bridge.register_view("customers", "customer_analytics");
173
174 assert!(!bridge.is_empty());
175 assert_eq!(bridge.views_for_source("customers").len(), 1);
176 assert_eq!(
177 bridge.views_for_source("customers")[0].target,
178 "customer_analytics"
179 );
180 assert!(bridge.view_by_target("customer_analytics").is_some());
181 assert!(bridge.view_by_target("nonexistent").is_none());
182 }
183
184 #[test]
185 fn remove_view() {
186 let bridge = HtapBridge::new();
187 bridge.register_view("customers", "analytics_1");
188 bridge.register_view("customers", "analytics_2");
189
190 assert_eq!(bridge.views_for_source("customers").len(), 2);
191
192 bridge.remove_view("analytics_1");
193 assert_eq!(bridge.views_for_source("customers").len(), 1);
194 assert_eq!(
195 bridge.views_for_source("customers")[0].target,
196 "analytics_2"
197 );
198 }
199
200 #[test]
201 fn multiple_sources() {
202 let bridge = HtapBridge::new();
203 bridge.register_view("orders", "order_analytics");
204 bridge.register_view("customers", "customer_analytics");
205
206 assert_eq!(bridge.all_targets().len(), 2);
207 }
208}