use std::collections::HashMap;
use std::sync::Mutex;
use nodedb_types::value::Value;
use crate::engine::columnar::ColumnarEngine;
use crate::storage::engine::StorageEngine;
#[derive(Debug, Clone)]
pub struct MaterializedView {
pub source: String,
pub target: String,
pub last_replicated_ms: u64,
pub rows_replicated: u64,
}
pub struct HtapBridge {
views: Mutex<HashMap<String, Vec<MaterializedView>>>,
}
impl HtapBridge {
pub fn new() -> Self {
Self {
views: Mutex::new(HashMap::new()),
}
}
fn lock_views(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<MaterializedView>>> {
match self.views.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
}
}
pub fn register_view(&self, source: &str, target: &str) {
let view = MaterializedView {
source: source.to_string(),
target: target.to_string(),
last_replicated_ms: crate::runtime::now_millis(),
rows_replicated: 0,
};
self.lock_views()
.entry(source.to_string())
.or_default()
.push(view);
}
pub fn remove_view(&self, target: &str) {
let mut g = self.lock_views();
for views in g.values_mut() {
views.retain(|v| v.target != target);
}
g.retain(|_, views| !views.is_empty());
}
pub fn views_for_source(&self, source: &str) -> Vec<MaterializedView> {
self.lock_views().get(source).cloned().unwrap_or_default()
}
pub fn view_by_target(&self, target: &str) -> Option<MaterializedView> {
self.lock_views()
.values()
.flatten()
.find(|v| v.target == target)
.cloned()
}
pub fn all_targets(&self) -> Vec<String> {
self.lock_views()
.values()
.flatten()
.map(|v| v.target.clone())
.collect()
}
pub fn replicate_insert<S: StorageEngine>(
&self,
source: &str,
values: &[Value],
columnar: &ColumnarEngine<S>,
) {
let mut g = self.lock_views();
let Some(views) = g.get_mut(source) else {
return;
};
for view in views.iter_mut() {
if columnar.insert(&view.target, values).is_ok() {
view.rows_replicated += 1;
view.last_replicated_ms = crate::runtime::now_millis();
}
}
}
pub fn replicate_delete<S: StorageEngine>(
&self,
source: &str,
pk: &Value,
columnar: &ColumnarEngine<S>,
) {
let mut g = self.lock_views();
let Some(views) = g.get_mut(source) else {
return;
};
for view in views.iter_mut() {
if columnar.delete(&view.target, pk).unwrap_or(false) {
view.last_replicated_ms = crate::runtime::now_millis();
}
}
}
pub fn lag_ms(&self, target: &str) -> u64 {
self.view_by_target(target)
.map(|v| crate::runtime::now_millis().saturating_sub(v.last_replicated_ms))
.unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.lock_views().is_empty()
}
}
impl Default for HtapBridge {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_and_lookup_view() {
let bridge = HtapBridge::new();
bridge.register_view("customers", "customer_analytics");
assert!(!bridge.is_empty());
assert_eq!(bridge.views_for_source("customers").len(), 1);
assert_eq!(
bridge.views_for_source("customers")[0].target,
"customer_analytics"
);
assert!(bridge.view_by_target("customer_analytics").is_some());
assert!(bridge.view_by_target("nonexistent").is_none());
}
#[test]
fn remove_view() {
let bridge = HtapBridge::new();
bridge.register_view("customers", "analytics_1");
bridge.register_view("customers", "analytics_2");
assert_eq!(bridge.views_for_source("customers").len(), 2);
bridge.remove_view("analytics_1");
assert_eq!(bridge.views_for_source("customers").len(), 1);
assert_eq!(
bridge.views_for_source("customers")[0].target,
"analytics_2"
);
}
#[test]
fn multiple_sources() {
let bridge = HtapBridge::new();
bridge.register_view("orders", "order_analytics");
bridge.register_view("customers", "customer_analytics");
assert_eq!(bridge.all_targets().len(), 2);
}
}