1use std::sync::OnceLock;
22
23use regex::Regex;
24use serde_json::{json, Value};
25
26use crate::state::StoredRecord;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum Projection {
31 Star,
33 CountStar,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum CmpOp {
40 Gt,
41 Ge,
42 Lt,
43 Le,
44 Eq,
45}
46
47#[derive(Debug, Clone)]
49pub struct Plan {
50 pub database: String,
51 pub table: String,
52 pub projection: Projection,
53 pub time_filter: Option<(CmpOp, i128)>,
55 pub order_desc: Option<bool>,
57 pub limit: Option<usize>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum QueryError {
63 Validation(String),
65}
66
67impl QueryError {
68 pub fn message(&self) -> &str {
69 match self {
70 QueryError::Validation(m) => m,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
78pub struct QueryResult {
79 pub columns: Vec<Value>,
80 pub rows: Vec<Value>,
81}
82
83fn re_from() -> &'static Regex {
84 static R: OnceLock<Regex> = OnceLock::new();
85 R.get_or_init(|| Regex::new(r#"(?i)\bfrom\s+"?([^"\s.]+)"?\s*\.\s*"?([^"\s.]+)"?"#).unwrap())
86}
87
88fn re_select() -> &'static Regex {
89 static R: OnceLock<Regex> = OnceLock::new();
90 R.get_or_init(|| Regex::new(r#"(?is)^\s*select\s+(.*?)\s+from\b"#).unwrap())
91}
92
93fn re_count() -> &'static Regex {
94 static R: OnceLock<Regex> = OnceLock::new();
95 R.get_or_init(|| Regex::new(r#"(?i)^count\s*\(\s*\*\s*\)$"#).unwrap())
96}
97
98fn re_where_time() -> &'static Regex {
99 static R: OnceLock<Regex> = OnceLock::new();
100 R.get_or_init(|| {
101 Regex::new(r#"(?i)\bwhere\s+time\s*(>=|<=|>|<|=)\s*ago\s*\(\s*(\d+)\s*([a-zA-Z]+)\s*\)"#)
102 .unwrap()
103 })
104}
105
106fn re_where_any() -> &'static Regex {
107 static R: OnceLock<Regex> = OnceLock::new();
108 R.get_or_init(|| Regex::new(r#"(?i)\bwhere\b"#).unwrap())
109}
110
111fn re_order() -> &'static Regex {
112 static R: OnceLock<Regex> = OnceLock::new();
113 R.get_or_init(|| Regex::new(r#"(?i)\border\s+by\s+([A-Za-z0-9_]+)(?:\s+(asc|desc))?"#).unwrap())
114}
115
116fn re_limit() -> &'static Regex {
117 static R: OnceLock<Regex> = OnceLock::new();
118 R.get_or_init(|| Regex::new(r#"(?i)\blimit\s+(\d+)"#).unwrap())
119}
120
121fn unit_nanos(unit: &str) -> Option<i128> {
123 let n: i128 = match unit.to_lowercase().as_str() {
124 "ns" | "nanosecond" | "nanoseconds" => 1,
125 "us" | "microsecond" | "microseconds" => 1_000,
126 "ms" | "millisecond" | "milliseconds" => 1_000_000,
127 "s" | "sec" | "second" | "seconds" => 1_000_000_000,
128 "m" | "min" | "minute" | "minutes" => 60 * 1_000_000_000,
129 "h" | "hour" | "hours" => 3_600 * 1_000_000_000,
130 "d" | "day" | "days" => 86_400 * 1_000_000_000,
131 _ => return None,
132 };
133 Some(n)
134}
135
136fn now_nanos() -> i128 {
137 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as i128
138}
139
140pub fn parse_query(sql: &str) -> Result<Plan, QueryError> {
143 let trimmed = sql.trim().trim_end_matches(';').trim();
144 if trimmed.is_empty() {
145 return Err(QueryError::Validation("The query string is empty.".into()));
146 }
147
148 let select_caps = re_select().captures(trimmed).ok_or_else(|| {
149 QueryError::Validation(
150 "Only 'SELECT ... FROM \"database\".\"table\"' queries are supported.".into(),
151 )
152 })?;
153 let select_list = select_caps[1].trim();
154
155 let projection = if select_list == "*" {
156 Projection::Star
157 } else if re_count().is_match(select_list) {
158 Projection::CountStar
159 } else {
160 return Err(QueryError::Validation(format!(
161 "Unsupported projection '{select_list}'. Supported: '*' or 'COUNT(*)'."
162 )));
163 };
164
165 let from_caps = re_from().captures(trimmed).ok_or_else(|| {
166 QueryError::Validation(
167 "Missing or malformed FROM clause; expected FROM \"database\".\"table\".".into(),
168 )
169 })?;
170 let database = from_caps[1].to_string();
171 let table = from_caps[2].to_string();
172
173 let time_filter = if re_where_any().is_match(trimmed) {
176 let caps = re_where_time().captures(trimmed).ok_or_else(|| {
177 QueryError::Validation(
178 "Unsupported WHERE clause. Supported: WHERE time <op> ago(<n><unit>).".into(),
179 )
180 })?;
181 let op = match &caps[1] {
182 ">" => CmpOp::Gt,
183 ">=" => CmpOp::Ge,
184 "<" => CmpOp::Lt,
185 "<=" => CmpOp::Le,
186 "=" => CmpOp::Eq,
187 other => {
188 return Err(QueryError::Validation(format!(
189 "Unsupported comparison operator '{other}'."
190 )))
191 }
192 };
193 let n: i128 = caps[2].parse().map_err(|_| {
194 QueryError::Validation(format!("Invalid duration magnitude '{}'.", &caps[2]))
195 })?;
196 let unit = &caps[3];
197 let per = unit_nanos(unit).ok_or_else(|| {
198 QueryError::Validation(format!("Unsupported ago() time unit '{unit}'."))
199 })?;
200 Some((op, now_nanos() - n * per))
201 } else {
202 None
203 };
204
205 let order_desc = if let Some(caps) = re_order().captures(trimmed) {
206 let col = caps[1].to_lowercase();
207 if col != "time" {
208 return Err(QueryError::Validation(format!(
209 "ORDER BY is only supported on the 'time' column, not '{}'.",
210 &caps[1]
211 )));
212 }
213 Some(
214 caps.get(2)
215 .map(|m| m.as_str().eq_ignore_ascii_case("desc"))
216 .unwrap_or(false),
217 )
218 } else {
219 None
220 };
221
222 let limit = re_limit()
223 .captures(trimmed)
224 .and_then(|c| c[1].parse::<usize>().ok());
225
226 Ok(Plan {
227 database,
228 table,
229 projection,
230 time_filter,
231 order_desc,
232 limit,
233 })
234}
235
236fn scalar_type_for_measure(ty: &str) -> &'static str {
238 match ty {
239 "DOUBLE" => "DOUBLE",
240 "BIGINT" => "BIGINT",
241 "BOOLEAN" => "BOOLEAN",
242 "TIMESTAMP" => "TIMESTAMP",
243 _ => "VARCHAR",
244 }
245}
246
247fn format_timestamp(nanos: i128) -> String {
250 let secs = (nanos.div_euclid(1_000_000_000)) as i64;
251 let sub = (nanos.rem_euclid(1_000_000_000)) as u32;
252 match chrono::DateTime::from_timestamp(secs, sub) {
253 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S%.9f").to_string(),
254 None => "1970-01-01 00:00:00.000000000".to_string(),
255 }
256}
257
258fn scalar(v: &str) -> Value {
259 json!({ "ScalarValue": v })
260}
261
262fn null_datum() -> Value {
263 json!({ "NullValue": true })
264}
265
266pub fn execute(plan: &Plan, records: &[StoredRecord]) -> QueryResult {
269 let mut filtered: Vec<&StoredRecord> = records
271 .iter()
272 .filter(|r| match plan.time_filter {
273 None => true,
274 Some((op, threshold)) => match op {
275 CmpOp::Gt => r.time_nanos > threshold,
276 CmpOp::Ge => r.time_nanos >= threshold,
277 CmpOp::Lt => r.time_nanos < threshold,
278 CmpOp::Le => r.time_nanos <= threshold,
279 CmpOp::Eq => r.time_nanos == threshold,
280 },
281 })
282 .collect();
283
284 if let Some(desc) = plan.order_desc {
286 filtered.sort_by(|a, b| {
287 if desc {
288 b.time_nanos.cmp(&a.time_nanos)
289 } else {
290 a.time_nanos.cmp(&b.time_nanos)
291 }
292 });
293 }
294
295 if let Projection::CountStar = plan.projection {
296 let count = plan
297 .limit
298 .map(|l| filtered.len().min(l))
299 .unwrap_or(filtered.len());
300 return QueryResult {
301 columns: vec![json!({ "Name": "_col0", "Type": { "ScalarType": "BIGINT" } })],
302 rows: vec![json!({ "Data": [scalar(&count.to_string())] })],
303 };
304 }
305
306 if let Some(limit) = plan.limit {
309 filtered.truncate(limit);
310 }
311
312 let mut dim_names: Vec<String> = Vec::new();
313 let mut measure_types: Vec<String> = Vec::new();
314 for r in &filtered {
315 for (name, _) in &r.dimensions {
316 if !dim_names.iter().any(|d| d == name) {
317 dim_names.push(name.clone());
318 }
319 }
320 if !measure_types.iter().any(|t| t == &r.measure_value_type) {
321 measure_types.push(r.measure_value_type.clone());
322 }
323 }
324 dim_names.sort();
325 measure_types.sort();
326
327 let mut columns: Vec<Value> = Vec::new();
328 for d in &dim_names {
329 columns.push(json!({ "Name": d, "Type": { "ScalarType": "VARCHAR" } }));
330 }
331 columns.push(json!({ "Name": "measure_name", "Type": { "ScalarType": "VARCHAR" } }));
332 for t in &measure_types {
333 columns.push(json!({
334 "Name": format!("measure_value::{}", t.to_lowercase()),
335 "Type": { "ScalarType": scalar_type_for_measure(t) }
336 }));
337 }
338 columns.push(json!({ "Name": "time", "Type": { "ScalarType": "TIMESTAMP" } }));
339
340 let mut rows: Vec<Value> = Vec::with_capacity(filtered.len());
341 for r in &filtered {
342 let mut data: Vec<Value> = Vec::with_capacity(columns.len());
343 for d in &dim_names {
344 match r.dimensions.iter().find(|(n, _)| n == d) {
345 Some((_, v)) => data.push(scalar(v)),
346 None => data.push(null_datum()),
347 }
348 }
349 data.push(scalar(&r.measure_name));
350 for t in &measure_types {
351 if &r.measure_value_type == t {
352 data.push(scalar(&r.measure_value));
353 } else {
354 data.push(null_datum());
355 }
356 }
357 data.push(scalar(&format_timestamp(r.time_nanos)));
358 rows.push(json!({ "Data": data }));
359 }
360
361 QueryResult { columns, rows }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 fn rec(dims: &[(&str, &str)], mn: &str, mv: &str, ty: &str, t: i128) -> StoredRecord {
369 StoredRecord {
370 time_nanos: t,
371 dimensions: dims
372 .iter()
373 .map(|(a, b)| (a.to_string(), b.to_string()))
374 .collect(),
375 measure_name: mn.to_string(),
376 measure_value: mv.to_string(),
377 measure_value_type: ty.to_string(),
378 }
379 }
380
381 #[test]
382 fn parse_select_star() {
383 let p = parse_query(r#"SELECT * FROM "metrics"."cpu""#).unwrap();
384 assert_eq!(p.database, "metrics");
385 assert_eq!(p.table, "cpu");
386 assert_eq!(p.projection, Projection::Star);
387 }
388
389 #[test]
390 fn parse_count_and_order_and_limit() {
391 let p = parse_query(r#"SELECT COUNT(*) FROM "m"."t" ORDER BY time DESC LIMIT 10"#).unwrap();
392 assert_eq!(p.projection, Projection::CountStar);
393 assert_eq!(p.order_desc, Some(true));
394 assert_eq!(p.limit, Some(10));
395 }
396
397 #[test]
398 fn parse_where_ago() {
399 let p = parse_query(r#"SELECT * FROM "m"."t" WHERE time > ago(1h)"#).unwrap();
400 assert!(matches!(p.time_filter, Some((CmpOp::Gt, _))));
401 }
402
403 #[test]
404 fn unsupported_projection_is_validation() {
405 let e = parse_query(r#"SELECT avg(x) FROM "m"."t""#).unwrap_err();
406 assert!(e.message().contains("Unsupported projection"));
407 }
408
409 #[test]
410 fn unsupported_where_is_validation() {
411 let e = parse_query(r#"SELECT * FROM "m"."t" WHERE host = 'a'"#).unwrap_err();
412 assert!(e.message().contains("Unsupported WHERE"));
413 }
414
415 #[test]
416 fn execute_star_projects_dimension_measure_time() {
417 let recs = vec![
418 rec(&[("host", "a")], "cpu", "1.5", "DOUBLE", 1_000_000_000),
419 rec(&[("host", "b")], "cpu", "2.5", "DOUBLE", 2_000_000_000),
420 ];
421 let p = parse_query(r#"SELECT * FROM "m"."t" ORDER BY time ASC"#).unwrap();
422 let out = execute(&p, &recs);
423 let col_names: Vec<&str> = out
424 .columns
425 .iter()
426 .map(|c| c["Name"].as_str().unwrap())
427 .collect();
428 assert_eq!(
429 col_names,
430 vec!["host", "measure_name", "measure_value::double", "time"]
431 );
432 assert_eq!(out.rows.len(), 2);
433 assert_eq!(out.rows[0]["Data"][0]["ScalarValue"], "a");
434 }
435
436 #[test]
437 fn execute_count() {
438 let recs = vec![
439 rec(&[("host", "a")], "cpu", "1", "DOUBLE", 1),
440 rec(&[("host", "b")], "cpu", "2", "DOUBLE", 2),
441 ];
442 let p = parse_query(r#"SELECT COUNT(*) FROM "m"."t""#).unwrap();
443 let out = execute(&p, &recs);
444 assert_eq!(out.rows.len(), 1);
445 assert_eq!(out.rows[0]["Data"][0]["ScalarValue"], "2");
446 }
447
448 #[test]
449 fn execute_limit_truncates() {
450 let recs = vec![
451 rec(&[("host", "a")], "cpu", "1", "DOUBLE", 1),
452 rec(&[("host", "b")], "cpu", "2", "DOUBLE", 2),
453 rec(&[("host", "c")], "cpu", "3", "DOUBLE", 3),
454 ];
455 let p = parse_query(r#"SELECT * FROM "m"."t" ORDER BY time ASC LIMIT 2"#).unwrap();
456 let out = execute(&p, &recs);
457 assert_eq!(out.rows.len(), 2);
458 }
459}