nodedb_lite/engine/htap/
routing.rs1#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub enum ReadSource {
11 #[default]
14 Source,
15 Auto,
18}
19
20impl ReadSource {
21 pub fn as_str(&self) -> &'static str {
22 match self {
23 Self::Source => "source",
24 Self::Auto => "auto",
25 }
26 }
27}
28
29impl std::str::FromStr for ReadSource {
30 type Err = String;
31
32 fn from_str(s: &str) -> Result<Self, Self::Err> {
33 match s.to_lowercase().as_str() {
34 "source" => Ok(Self::Source),
35 "auto" => Ok(Self::Auto),
36 other => Err(format!(
37 "unknown read_source: '{other}' (use 'source' or 'auto')"
38 )),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45pub enum MaterializedConsistency {
46 #[default]
48 Eventual,
49 Strong,
51}
52
53impl std::str::FromStr for MaterializedConsistency {
54 type Err = String;
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 match s.to_lowercase().as_str() {
58 "eventual" => Ok(Self::Eventual),
59 "strong" => Ok(Self::Strong),
60 other => Err(format!(
61 "unknown materialized_consistency: '{other}' (use 'eventual' or 'strong')"
62 )),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Default)]
71pub struct HtapSession {
72 pub read_source: ReadSource,
73 pub materialized_consistency: MaterializedConsistency,
74}
75
76impl HtapSession {
77 pub fn should_use_materialized(&self, is_analytical: bool) -> bool {
82 self.read_source == ReadSource::Auto && is_analytical
83 }
84
85 pub fn requires_strong_consistency(&self) -> bool {
87 self.materialized_consistency == MaterializedConsistency::Strong
88 }
89}
90
91pub fn is_analytical_query(sql: &str) -> bool {
96 let upper = sql.to_uppercase();
97 upper.contains("GROUP BY")
98 || upper.contains("SUM(")
99 || upper.contains("AVG(")
100 || upper.contains("COUNT(")
101 || upper.contains("MIN(")
102 || upper.contains("MAX(")
103 || (upper.contains("SELECT") && !upper.contains("WHERE") && upper.contains("FROM"))
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn default_session() {
112 let session = HtapSession::default();
113 assert_eq!(session.read_source, ReadSource::Source);
114 assert!(!session.should_use_materialized(true));
115 assert!(!session.should_use_materialized(false));
116 }
117
118 #[test]
119 fn auto_routing() {
120 let session = HtapSession {
121 read_source: ReadSource::Auto,
122 materialized_consistency: MaterializedConsistency::Eventual,
123 };
124 assert!(session.should_use_materialized(true));
125 assert!(!session.should_use_materialized(false));
126 }
127
128 #[test]
129 fn strong_consistency() {
130 let session = HtapSession {
131 read_source: ReadSource::Auto,
132 materialized_consistency: MaterializedConsistency::Strong,
133 };
134 assert!(session.requires_strong_consistency());
135 }
136
137 #[test]
138 fn analytical_detection() {
139 assert!(is_analytical_query(
140 "SELECT SUM(balance) FROM customers GROUP BY status"
141 ));
142 assert!(is_analytical_query("SELECT COUNT(*) FROM orders"));
143 assert!(is_analytical_query("SELECT AVG(score) FROM metrics"));
144 assert!(!is_analytical_query(
145 "SELECT * FROM customers WHERE id = 42"
146 ));
147 assert!(!is_analytical_query(
148 "INSERT INTO customers VALUES (1, 'a')"
149 ));
150 }
151
152 #[test]
153 fn read_source_parse() {
154 assert_eq!("source".parse::<ReadSource>().unwrap(), ReadSource::Source);
155 assert_eq!("auto".parse::<ReadSource>().unwrap(), ReadSource::Auto);
156 assert!("bogus".parse::<ReadSource>().is_err());
157 }
158}