use std::collections::BTreeMap;
use std::sync::Arc;
use arrow::record_batch::RecordBatch;
use crate::promql::ast::{
AtModifier, Duration, Expr, LabelMatcher, MatchOp, PromQLValue, Sample, Series,
};
use crate::promql::metric::{self, ClassicView, MetricRef};
use super::{CompiledMatcher, EvalError, PromQLEvaluator, QueryParams, compile_post_filters};
use super::{SCAN_CACHE_CAPACITY, ScanKey};
type SeriesMap = BTreeMap<Vec<(String, String)>, SeriesSamples>;
#[derive(Default)]
pub(crate) struct SeriesSamples {
floats: Vec<Sample>,
histograms: Vec<crate::promql::ast::HistogramSample>,
}
impl SeriesSamples {
fn latest(&self, labels: Vec<(String, String)>) -> Option<Series> {
if let Some(h) = self.histograms.last() {
return Some(Series::histograms(labels, vec![h.clone()]));
}
let latest = *self.floats.last()?;
Some(Series::floats(labels, vec![latest]))
}
fn all(self, labels: Vec<(String, String)>) -> Series {
if self.histograms.is_empty() {
Series::floats(labels, self.floats)
} else {
Series::histograms(labels, self.histograms)
}
}
fn sort(&mut self) {
self.floats.sort_by_key(|s| s.timestamp);
self.histograms.sort_by_key(|s| s.timestamp);
}
}
impl PromQLEvaluator {
fn fetch_scan(
&self,
measurement: &str,
matchers: &[LabelMatcher],
start: i64,
end: i64,
deadline: Option<super::Deadline>,
) -> Result<Arc<Vec<RecordBatch>>, EvalError> {
let mut tags: Vec<(String, String)> = matchers
.iter()
.filter(|m| {
m.name != "__name__"
&& m.name != chronix_core::NAMESPACE_TAG
&& m.op == MatchOp::Equal
})
.map(|m| (m.name.clone(), m.value.clone()))
.collect();
if let Some(ref ns) = self.namespace {
tags.push((chronix_core::NAMESPACE_TAG.to_string(), ns.clone()));
}
tags.sort();
let key = ScanKey {
measurement: measurement.to_string(),
tags,
start,
end,
};
if let Some(hit) = self.scan_cache.borrow().get(&key) {
let mut stats = self.scan_stats.get();
stats.cache_hits += 1;
self.scan_stats.set(stats);
return Ok(Arc::clone(hit));
}
let mut stats = self.scan_stats.get();
stats.scans += 1;
self.scan_stats.set(stats);
if let Some(deadline) = deadline {
deadline.check()?;
}
let mut builder = self.db.query().measurement(measurement);
for (k, v) in &key.tags {
builder = builder.tag(k, v);
}
let plan = builder
.range(start, end)
.build()
.map_err(|e| EvalError(e.to_string()))?;
let batches = Arc::new(
self.db
.execute_stream(&plan)
.map_err(|e| EvalError(e.to_string()))?,
);
let mut cache = self.scan_cache.borrow_mut();
if cache.len() < SCAN_CACHE_CAPACITY {
cache.insert(key, Arc::clone(&batches));
}
Ok(batches)
}
fn resolve_targets(
&self,
name: &Option<String>,
matchers: &[LabelMatcher],
) -> Result<Vec<MetricRef>, EvalError> {
let registry = self.db.schema_registry();
let named = name.as_deref().or_else(|| {
matchers
.iter()
.find(|m| m.name == "__name__" && m.op == MatchOp::Equal)
.map(|m| m.value.as_str())
});
let name_matchers: Vec<_> = matchers
.iter()
.filter(|m| m.name == "__name__" && m.op != MatchOp::Equal)
.map(CompiledMatcher::compile)
.collect::<Result<_, _>>()?;
let mut candidates = match named {
Some(n) => metric::resolve(registry, n),
None => metric::all_metrics(registry),
};
candidates.retain(|m| self.db.schema(&m.measurement).is_some());
if !name_matchers.is_empty() {
candidates.retain(|m| {
let labels = [("__name__".to_string(), m.name.clone())];
name_matchers.iter().all(|f| f.matches(&labels))
});
}
Ok(candidates)
}
fn read_metric(
&self,
target: &MetricRef,
matchers: &[LabelMatcher],
post_filters: &[CompiledMatcher],
fetch: (i64, i64),
window: (i64, i64),
deadline: Option<super::Deadline>,
) -> Result<SeriesMap, EvalError> {
if target.view == ClassicView::Bucket && matchers.iter().any(is_le_equality) {
let scan_matchers: Vec<LabelMatcher> = matchers
.iter()
.filter(|m| !is_le_equality(m))
.cloned()
.collect();
let filters: Vec<CompiledMatcher> = matchers
.iter()
.filter(|m| m.name != "__name__" && (m.op != MatchOp::Equal || is_le_equality(m)))
.map(CompiledMatcher::compile)
.collect::<Result<_, _>>()?;
let batches = self.fetch_scan(
&target.measurement,
&scan_matchers,
fetch.0,
fetch.1,
deadline,
)?;
return collect_series(&batches, target, &filters, window.0, window.1);
}
let batches = self.fetch_scan(&target.measurement, matchers, fetch.0, fetch.1, deadline)?;
collect_series(&batches, target, post_filters, window.0, window.1)
}
pub(crate) fn eval_vector_selector(
&self,
name: &Option<String>,
matchers: &[LabelMatcher],
offset: &Option<Duration>,
at: Option<AtModifier>,
params: &QueryParams,
) -> Result<PromQLValue, EvalError> {
let targets = self.resolve_targets(name, matchers)?;
let offset_ns = offset.map_or(0, |d| d.as_nanos());
let eval_time = pinned_eval_time(params, at).saturating_sub(offset_ns);
let lookback_start = eval_time - params.lookback_delta;
let fetch = fetch_window(
params,
offset_ns,
params.lookback_delta,
lookback_start,
eval_time,
at.is_some(),
);
let post_filters = compile_post_filters(matchers)?;
let mut result = Vec::new();
for target in &targets {
let series_map = self.read_metric(
target,
matchers,
&post_filters,
fetch,
(lookback_start, eval_time),
params.deadline,
)?;
result.extend(
series_map
.into_iter()
.filter_map(|(labels, samples)| samples.latest(labels)),
);
}
Ok(PromQLValue::Vector(result))
}
#[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) fn eval_matrix_selector(
&self,
vector: &Expr,
range: &Duration,
params: &QueryParams,
) -> Result<PromQLValue, EvalError> {
let Expr::VectorSelector {
name,
matchers,
offset,
at,
} = vector
else {
return Err(EvalError("matrix selector requires vector selector".into()));
};
let targets = self.resolve_targets(name, matchers)?;
let offset_ns = offset.map_or(0, |d| d.as_nanos());
let eval_time = pinned_eval_time(params, *at).saturating_sub(offset_ns);
let range_start = eval_time - range.as_nanos();
let fetch = fetch_window(
params,
offset_ns,
range.as_nanos(),
range_start,
eval_time,
at.is_some(),
);
let post_filters = compile_post_filters(matchers)?;
let mut result = Vec::new();
for target in &targets {
let series_map = self.read_metric(
target,
matchers,
&post_filters,
fetch,
(range_start, eval_time),
params.deadline,
)?;
result.extend(
series_map
.into_iter()
.map(|(labels, samples)| samples.all(labels)),
);
}
Ok(PromQLValue::Matrix(result))
}
}
fn fetch_window(
params: &QueryParams,
offset_ns: i64,
reach_ns: i64,
window_start: i64,
eval_time: i64,
pinned: bool,
) -> (i64, i64) {
if pinned {
return (window_start, eval_time.saturating_add(1));
}
match (params.range_fetch_start, params.range_fetch_end) {
(Some(rs), Some(re)) => {
let query_start = rs.saturating_add(params.lookback_delta);
(
query_start
.saturating_sub(reach_ns)
.saturating_sub(offset_ns),
re.saturating_sub(offset_ns).saturating_add(1),
)
}
_ => (window_start, eval_time + 1),
}
}
fn pinned_eval_time(params: &QueryParams, at: Option<AtModifier>) -> i64 {
match at {
None => params.time,
Some(at) => at.resolve(
params.start.unwrap_or(params.time),
params.end.unwrap_or(params.time),
),
}
}
fn collect_series(
batches: &[RecordBatch],
target: &MetricRef,
post_filters: &[CompiledMatcher],
window_start: i64,
window_end: i64,
) -> Result<SeriesMap, EvalError> {
let mut series_map: SeriesMap = BTreeMap::new();
for batch in batches {
let schema = batch.schema();
let num_rows = batch.num_rows();
let ts_col_idx = schema
.fields()
.iter()
.position(|f| f.name() == chronix_core::TIME_COLUMN)
.ok_or_else(|| EvalError("no timestamp column".into()))?;
let Some(value_col_idx) = schema
.fields()
.iter()
.position(|f| *f.name() == target.field)
else {
continue;
};
let mut tag_cols = Vec::new();
for (idx, field) in schema.fields().iter().enumerate() {
if idx == ts_col_idx || idx == value_col_idx {
continue;
}
if field.name() == chronix_core::NAMESPACE_TAG {
continue;
}
if matches!(field.data_type(), arrow::datatypes::DataType::Utf8) {
tag_cols.push((idx, field.name().clone()));
}
}
let ts_values = batch
.column(ts_col_idx)
.as_any()
.downcast_ref::<arrow::array::Int64Array>()
.ok_or_else(|| EvalError("timestamp column is not Int64".into()))?;
let value_col = batch.column(value_col_idx);
for row in 0..num_rows {
let timestamp = ts_values.value(row);
if timestamp <= window_start || timestamp > window_end {
continue;
}
if arrow::array::Array::is_null(value_col.as_ref(), row) {
continue;
}
let mut labels: Vec<(String, String)> =
vec![("__name__".to_string(), target.name.clone())];
for (idx, name) in &tag_cols {
if let Some(arr) = batch
.column(*idx)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
&& !arrow::array::Array::is_null(arr, row)
{
labels.push((name.clone(), arr.value(row).to_string()));
}
}
labels.sort();
if let Some(bin) = value_col
.as_any()
.downcast_ref::<arrow::array::BinaryArray>()
{
let Ok(h) =
postcard::from_bytes::<chronix_core::histogram::Histogram>(bin.value(row))
else {
continue;
};
match target.view {
ClassicView::Native => {
if !post_filters.iter().all(|f| f.matches(&labels)) {
continue;
}
series_map.entry(labels).or_default().histograms.push(
crate::promql::ast::HistogramSample {
timestamp,
histogram: Box::new(h),
},
);
}
ClassicView::Count => {
push_float(&mut series_map, labels, post_filters, timestamp, h.count);
}
ClassicView::Sum => {
push_float(&mut series_map, labels, post_filters, timestamp, h.sum);
}
ClassicView::Bucket => {
for (le, cumulative) in h.classic_buckets() {
let mut bucket_labels = labels.clone();
bucket_labels.retain(|(k, _)| k != LE_LABEL);
bucket_labels.push((LE_LABEL.to_string(), format_le(le)));
bucket_labels.sort();
push_float(
&mut series_map,
bucket_labels,
post_filters,
timestamp,
cumulative,
);
}
}
}
} else {
if target.view != ClassicView::Native {
continue;
}
if !post_filters.iter().all(|f| f.matches(&labels)) {
continue;
}
series_map.entry(labels).or_default().floats.push(Sample {
timestamp,
value: extract_f64(value_col, row)?,
});
}
}
}
for samples in series_map.values_mut() {
samples.sort();
}
Ok(series_map)
}
const LE_LABEL: &str = "le";
fn is_le_equality(m: &LabelMatcher) -> bool {
m.name == LE_LABEL && m.op == MatchOp::Equal
}
fn push_float(
series_map: &mut SeriesMap,
labels: Vec<(String, String)>,
post_filters: &[CompiledMatcher],
timestamp: i64,
value: f64,
) {
if !post_filters.iter().all(|f| f.matches(&labels)) {
return;
}
series_map
.entry(labels)
.or_default()
.floats
.push(Sample { timestamp, value });
}
fn format_le(le: f64) -> String {
if le.is_infinite() {
if le > 0.0 { "+Inf" } else { "-Inf" }.to_string()
} else {
le.to_string()
}
}
pub(crate) fn extract_f64(col: &arrow::array::ArrayRef, row: usize) -> Result<f64, EvalError> {
if let Some(a) = col.as_any().downcast_ref::<arrow::array::Float32Array>() {
return Ok(f64::from(a.value(row)));
}
if let Some(a) = col.as_any().downcast_ref::<arrow::array::BooleanArray>() {
return Ok(if a.value(row) { 1.0 } else { 0.0 });
}
chronix_query::extract_f64(col.as_ref(), row)
.ok_or_else(|| EvalError(format!("cannot extract f64 from {:?}", col.data_type())))
}