use nodedb_physical::PhysicalTaskVisitor;
use nodedb_physical::physical_plan::TimeseriesOp;
use nodedb_sql::TimeseriesScanVisitArgs;
use nodedb_sql::temporal::TemporalScope;
use nodedb_sql::types::filter::Filter;
use nodedb_sql::types::query::AggregateExpr;
use nodedb_sql::types_expr::SqlExpr;
use nodedb_sql::types_expr::SqlValue;
use crate::error::LiteError;
use crate::query::engine::LiteQueryEngine;
use crate::query::filter_convert::sql_filters_to_metadata;
use crate::query::physical_visitor::LiteDataPlaneVisitor;
use crate::query::visitor::scan_post::sort_rows;
use crate::storage::engine::StorageEngine;
use super::adapter::LiteFut;
use super::having_eval::{apply_having_result, make_agg_alias_map};
use super::projection::projection_names;
fn encode_filters(filters: &[Filter]) -> Result<Vec<u8>, LiteError> {
if filters.is_empty() {
return Ok(Vec::new());
}
match sql_filters_to_metadata(filters, &[])?.meta {
None => Ok(Vec::new()),
Some(mf) => zerompk::to_msgpack_vec(&mf).map_err(|e| LiteError::Serialization {
detail: format!("encode timeseries filters: {e}"),
}),
}
}
fn convert_aggregates(aggregates: &[AggregateExpr]) -> Result<Vec<(String, String)>, LiteError> {
aggregates
.iter()
.map(|agg| {
let field = match agg.args.first() {
None | Some(SqlExpr::Wildcard) => "*".to_string(),
Some(SqlExpr::Column { name, .. }) => name.clone(),
Some(_) => {
return Err(LiteError::Unsupported {
detail: format!(
"aggregate '{}' has a computed argument the grouped timeseries \
scan cannot read in Lite",
agg.alias
),
});
}
};
Ok((
nodedb_sql::planner::agg_naming::aggregate_function_name(agg),
field,
))
})
.collect()
}
pub(super) fn lower_timeseries_scan<'a, S: StorageEngine + 'a>(
engine: &'a LiteQueryEngine<S>,
args: TimeseriesScanVisitArgs<'_>,
) -> Result<LiteFut<'a>, LiteError> {
let TimeseriesScanVisitArgs {
collection,
time_range,
bucket_interval_ms,
group_by,
aggregates,
filters,
having,
projection,
gap_fill,
limit,
sort_keys,
temporal,
..
} = args;
let filter_bytes = encode_filters(filters)?;
let proj_cols = projection_names(projection, "timeseries scan")?;
let agg_pairs = convert_aggregates(aggregates)?;
let (system_time, valid_at_ms) = extract_temporal(temporal);
let post_processed = !sort_keys.is_empty() || !having.is_empty();
let scan_limit = if post_processed { 0 } else { limit };
let op = TimeseriesOp::Scan {
collection: nodedb_types::QualifiedCollection::new(
nodedb_types::DatabaseId::DEFAULT,
collection,
),
time_range,
projection: proj_cols,
limit: scan_limit,
filters: filter_bytes,
bucket_interval_ms,
group_by: group_by.to_vec(),
aggregates: agg_pairs,
gap_fill: gap_fill.to_string(),
computed_columns: Vec::new(),
rls_filters: Vec::new(),
system_time,
valid_at_ms,
sort_keys: Vec::new(),
having: Vec::new(),
tiering: None,
};
let mut phys = LiteDataPlaneVisitor::new(engine);
let fut = phys.timeseries(&op)?;
if !post_processed {
return Ok(Box::pin(fut));
}
let sort_keys = sort_keys.to_vec();
let having = having.to_vec();
let agg_alias_map = make_agg_alias_map(aggregates);
Ok(Box::pin(async move {
let result = fut.await?;
let mut result = apply_having_result(result, &having, &agg_alias_map);
sort_rows(&mut result, &sort_keys)?;
if limit > 0 {
result.rows.truncate(limit);
}
Ok(result)
}))
}
fn extract_temporal(scope: &TemporalScope) -> (nodedb_types::SystemTimeScope, Option<i64>) {
use nodedb_sql::temporal::ValidTime;
let sys = scope.system_time;
let valid = match &scope.valid_time {
ValidTime::At(ms) => Some(*ms),
_ => None,
};
(sys, valid)
}
pub(super) fn lower_timeseries_ingest<'a, S: StorageEngine + 'a>(
engine: &'a LiteQueryEngine<S>,
permit: &'a crate::engine::fts::coordinator::TextMutationPermit,
collection: &str,
rows: &[Vec<(String, SqlValue)>],
) -> Result<LiteFut<'a>, LiteError> {
use nodedb_types::value::Value;
use std::collections::HashMap;
let row_maps: Vec<HashMap<String, Value>> = rows
.iter()
.map(|row| {
row.iter()
.map(|(col, sv)| {
let v = crate::query::filter_convert::sql_value_to_value(sv)?;
Ok((col.clone(), v))
})
.collect::<Result<HashMap<String, Value>, LiteError>>()
})
.collect::<Result<Vec<_>, LiteError>>()?;
let payload = zerompk::to_msgpack_vec(&row_maps).map_err(|e| LiteError::Serialization {
detail: format!("encode timeseries ingest payload: {e}"),
})?;
let op = TimeseriesOp::Ingest {
collection: nodedb_types::QualifiedCollection::new(
nodedb_types::DatabaseId::DEFAULT,
collection,
),
payload,
format: "samples".to_string(),
wal_lsn: None,
surrogates: Vec::new(),
provenance: None,
rls_write_check: nodedb_types::RlsWriteCheck::NoPolicyApplies,
returning: None,
rls_filters: Vec::new(),
};
let mut phys = LiteDataPlaneVisitor {
engine,
permit: Some(permit),
};
let fut = phys.timeseries(&op)?;
Ok(Box::pin(fut))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::PagedbStorageMem;
use crate::query::engine::{LiteQueryEngine, LiteQueryEngineParams};
async fn make_engine() -> LiteQueryEngine<PagedbStorageMem> {
use std::sync::Mutex;
let storage = Arc::new(
PagedbStorageMem::open_in_memory()
.await
.expect("in-memory pagedb"),
);
let crdt = Arc::new(Mutex::new(
crate::engine::crdt::CrdtEngine::new(1).expect("crdt"),
));
let governor = crate::query::engine::test_governor();
let strict = Arc::new(crate::engine::strict::StrictEngine::new(Arc::clone(
&storage,
)));
let columnar = Arc::new(crate::engine::columnar::ColumnarEngine::new(
Arc::clone(&storage),
crate::query::engine::test_scoped_memory(&governor, nodedb_mem::EngineId::Columnar),
));
let htap = Arc::new(crate::engine::htap::HtapBridge::new());
let timeseries = Arc::new(Mutex::new(
crate::engine::timeseries::engine::TimeseriesEngine::new(),
));
let vector_state = Arc::new(crate::engine::vector::VectorState::new(
Arc::clone(&storage),
100,
crate::query::engine::test_scoped_memory(&governor, nodedb_mem::EngineId::Vector),
));
let array_state = Arc::new(tokio::sync::Mutex::new(
crate::engine::array::engine::ArrayEngineState::open(&storage)
.await
.expect("array"),
));
let fts_state = Arc::new(crate::engine::fts::FtsState::new(Arc::clone(&governor)));
let spatial = Arc::new(Mutex::new(
crate::engine::spatial::SpatialIndexManager::new(
crate::query::engine::test_scoped_memory(&governor, nodedb_mem::EngineId::Spatial),
),
));
LiteQueryEngine::new(LiteQueryEngineParams {
crdt,
strict,
columnar,
htap,
storage,
timeseries,
vector_state,
array_state,
fts_state,
sparse_state: Arc::new(crate::engine::sparse_vector::SparseVectorState::new()),
spatial,
csr: Arc::new(Mutex::new(std::collections::HashMap::new())),
governor,
kv_local: crate::query::engine::test_kv_local(),
})
}
#[tokio::test]
async fn test_timeseries_scan_lower() {
use nodedb_sql::temporal::TemporalScope;
let engine = make_engine().await;
let result = super::lower_timeseries_scan(
&engine,
nodedb_sql::TimeseriesScanVisitArgs {
collection: "metrics",
time_range: (0, i64::MAX),
bucket_interval_ms: 0,
group_by: &[],
aggregates: &[],
filters: &[],
having: &[],
projection: &[],
gap_fill: "",
limit: 100,
sort_keys: &[],
tiered: false,
temporal: &TemporalScope::default(),
},
);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_timeseries_ingest_lower() {
use nodedb_sql::types_expr::SqlValue;
let engine = make_engine().await;
let rows = vec![vec![
("ts".to_string(), SqlValue::Int(1_700_000_000_000)),
("value".to_string(), SqlValue::Float(42.0)),
]];
let guard = engine.fts_state.admit_mutation().await;
let result = super::lower_timeseries_ingest(&engine, guard.permit(), "metrics", &rows);
assert!(result.is_ok());
}
}