use std::collections::BTreeMap;
use nodedb_types::result::QueryResult;
use nodedb_types::timeseries::TimeRange;
use nodedb_types::value::Value;
use crate::error::LiteError;
use crate::query::engine::LiteQueryEngine;
use crate::storage::engine::StorageEngine;
use crate::engine::timeseries::engine::TimeseriesEngine;
pub struct ScanParams {
pub time_range: (i64, i64),
pub projection: Vec<String>,
pub limit: usize,
pub filters: Vec<u8>,
pub bucket_interval_ms: i64,
pub group_by: Vec<String>,
pub aggregates: Vec<(String, String)>,
pub gap_fill: String,
pub computed_columns: Vec<u8>,
pub rls_filters: Vec<u8>,
pub system_as_of_ms: Option<i64>,
pub valid_at_ms: Option<i64>,
}
pub fn scan<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
collection: &str,
params: ScanParams,
) -> Result<QueryResult, LiteError> {
let ScanParams {
time_range,
projection,
limit,
filters: _,
bucket_interval_ms,
group_by: _,
aggregates,
gap_fill,
computed_columns: _,
rls_filters: _,
system_as_of_ms,
valid_at_ms,
} = params;
reject_unsupported_gap_fill(&gap_fill)?;
let range = TimeRange::new(time_range.0, time_range.1);
let ts_engine = engine
.timeseries
.lock()
.map_err(|_| LiteError::LockPoisoned)?;
let raw = ts_engine.scan(collection, &range);
let raw: Vec<(i64, f64, _)> = if let Some(cutoff) = system_as_of_ms {
raw.into_iter().filter(|(ts, _, _)| *ts <= cutoff).collect()
} else {
raw
};
let _ = valid_at_ms;
if bucket_interval_ms > 0 {
bucket_scan(
&ts_engine,
collection,
&BucketScanParams {
range,
bucket_ms: bucket_interval_ms,
aggregates: &aggregates,
gap_fill: &gap_fill,
projection: &projection,
limit,
},
)
} else {
raw_scan(raw, &projection, limit)
}
}
pub struct BucketScanParams<'a> {
pub range: TimeRange,
pub bucket_ms: i64,
pub aggregates: &'a [(String, String)],
pub gap_fill: &'a str,
pub projection: &'a [String],
pub limit: usize,
}
enum GapFill {
Null,
Prev,
Literal(f64),
}
fn parse_gap_fill(gap_fill: &str) -> Result<GapFill, LiteError> {
match gap_fill {
"" | "null" | "none" => Ok(GapFill::Null),
"prev" => Ok(GapFill::Prev),
"linear" | "next" => Err(LiteError::Unsupported {
detail: format!(
"gap_fill = '{gap_fill}' is not supported on the timeseries engine in Lite; \
use 'prev', 'null', or a numeric literal"
),
}),
literal => match literal.parse::<f64>() {
Ok(v) if v.is_finite() => Ok(GapFill::Literal(v)),
_ => Err(LiteError::Unsupported {
detail: format!(
"gap_fill = '{gap_fill}' is not a known strategy on the timeseries \
engine in Lite; use 'prev', 'null', or a numeric literal"
),
}),
},
}
}
pub fn reject_unsupported_gap_fill(gap_fill: &str) -> Result<(), LiteError> {
parse_gap_fill(gap_fill).map(|_| ())
}
pub fn bucket_scan(
ts_engine: &TimeseriesEngine,
collection: &str,
params: &BucketScanParams<'_>,
) -> Result<QueryResult, LiteError> {
let BucketScanParams {
range,
bucket_ms,
aggregates,
gap_fill,
projection,
limit,
} = *params;
let gap_fill = parse_gap_fill(gap_fill)?;
let buckets = ts_engine.aggregate_by_bucket(collection, &range, bucket_ms);
let bucket_map: BTreeMap<i64, (u64, f64, f64, f64)> = buckets
.iter()
.map(|(ts, cnt, sum, min, max)| (*ts, (*cnt, *sum, *min, *max)))
.collect();
let first_bucket = if range.start_ms >= 0 {
(range.start_ms / bucket_ms) * bucket_ms
} else {
range.start_ms
};
let last_bucket = if range.end_ms >= 0 {
(range.end_ms / bucket_ms) * bucket_ms
} else {
range.end_ms
};
let mut expected: Vec<i64> = Vec::new();
let mut b = first_bucket;
while b <= last_bucket {
expected.push(b);
b = b.saturating_add(bucket_ms);
}
let agg_ops: Vec<&str> = if aggregates.is_empty() {
vec!["count", "sum", "min", "max"]
} else {
aggregates.iter().map(|(op, _)| op.as_str()).collect()
};
let mut columns: Vec<String> = vec!["bucket".into()];
columns.extend(agg_ops.iter().map(|op| op.to_string()));
let mut prev_row: Option<Vec<Value>> = None;
let mut rows: Vec<Vec<Value>> = Vec::new();
for bucket_start in &expected {
let row_vals = if let Some(&(cnt, sum, min, max)) = bucket_map.get(bucket_start) {
let agg_vals: Vec<Value> = agg_ops
.iter()
.map(|op| match *op {
"count" => Value::Integer(cnt as i64),
"sum" => Value::Float(sum),
"min" => Value::Float(min),
"max" => Value::Float(max),
"avg" | "mean" => {
if cnt > 0 {
Value::Float(sum / cnt as f64)
} else {
Value::Null
}
}
_ => Value::Null,
})
.collect();
let mut r = vec![Value::Integer(*bucket_start)];
r.extend(agg_vals);
r
} else {
let gap_vals: Vec<Value> = match &gap_fill {
GapFill::Null => agg_ops.iter().map(|_| Value::Null).collect(),
GapFill::Prev => {
if let Some(prev) = &prev_row {
prev[1..].to_vec()
} else {
agg_ops.iter().map(|_| Value::Null).collect()
}
}
GapFill::Literal(v) => agg_ops.iter().map(|_| Value::Float(*v)).collect(),
};
let mut r = vec![Value::Integer(*bucket_start)];
r.extend(gap_vals);
r
};
prev_row = Some(row_vals.clone());
rows.push(row_vals);
if limit > 0 && rows.len() >= limit {
break;
}
}
let (final_columns, final_rows) = if projection.is_empty() {
(columns, rows)
} else {
project_rows(columns, rows, projection)
};
Ok(QueryResult {
columns: final_columns,
rows: final_rows,
rows_affected: 0,
command: None,
})
}
fn raw_scan(
raw: Vec<(i64, f64, nodedb_types::timeseries::SeriesId)>,
projection: &[String],
limit: usize,
) -> Result<QueryResult, LiteError> {
let columns = vec![
"ts".to_string(),
"value".to_string(),
"series_id".to_string(),
];
let cap = if limit > 0 {
raw.len().min(limit)
} else {
raw.len()
};
let rows: Vec<Vec<Value>> = raw
.into_iter()
.take(cap)
.map(|(ts, val, sid)| {
vec![
Value::Integer(ts),
Value::Float(val),
Value::Integer(sid as i64),
]
})
.collect();
let (final_columns, final_rows) = if projection.is_empty() {
(columns, rows)
} else {
project_rows(columns, rows, projection)
};
Ok(QueryResult {
columns: final_columns,
rows: final_rows,
rows_affected: 0,
command: None,
})
}
fn project_rows(
columns: Vec<String>,
rows: Vec<Vec<Value>>,
projection: &[String],
) -> (Vec<String>, Vec<Vec<Value>>) {
let indices: Vec<usize> = projection
.iter()
.filter_map(|p| columns.iter().position(|c| c == p))
.collect();
if indices.is_empty() {
return (columns, rows);
}
let new_cols: Vec<String> = indices.iter().map(|&i| columns[i].clone()).collect();
let new_rows: Vec<Vec<Value>> = rows
.into_iter()
.map(|row| indices.iter().map(|&i| row[i].clone()).collect())
.collect();
(new_cols, new_rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_rows_identity() {
let cols = vec!["a".to_string(), "b".to_string()];
let rows = vec![vec![Value::Integer(1), Value::Integer(2)]];
let (c, r) = project_rows(cols.clone(), rows.clone(), &[]);
assert_eq!(c, cols);
assert_eq!(r, rows);
}
#[test]
fn test_project_rows_subset() {
let cols = vec![
"ts".to_string(),
"value".to_string(),
"series_id".to_string(),
];
let rows = vec![vec![
Value::Integer(100),
Value::Float(1.5),
Value::Integer(0),
]];
let (c, r) = project_rows(cols, rows, &["ts".to_string(), "value".to_string()]);
assert_eq!(c, vec!["ts", "value"]);
assert_eq!(r[0], vec![Value::Integer(100), Value::Float(1.5)]);
}
}