mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-11-14 14:56:54 +00:00
1203 lines
50 KiB
Rust
1203 lines
50 KiB
Rust
// Copyright 2023 Greptime Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Range-vector window evaluation of raw series onto an evaluation grid.
|
|
|
|
use std::ops::Range;
|
|
use std::sync::Arc;
|
|
|
|
use common_query::prometheus::is_prometheus_stale_nan;
|
|
use datafusion::arrow::array::{Array, Float64Array};
|
|
use datafusion::arrow::buffer::Buffer;
|
|
use datafusion::error::{DataFusionError, Result};
|
|
use datatypes::series::MetricName;
|
|
use datatypes::series::histogram::CounterResetHint;
|
|
use datatypes::series::raw::RawSeriesChunk;
|
|
use datatypes::series::samples::{
|
|
DenseSamplesRef, SampleRef, Samples, Series, SeriesFields, SeriesSchema,
|
|
};
|
|
use datatypes::series::time::{EvalGrid, GridRange, GridSpec, TimeAxis};
|
|
|
|
use crate::evaluated::EvaluatedChunk;
|
|
use crate::grid_series::{GridSample, GridSeriesBuilder, METADATA_LABELS, external};
|
|
use crate::histogram_eval::float_histogram::FloatHistogram;
|
|
|
|
mod function;
|
|
mod histogram;
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
pub use function::RangeFunction;
|
|
pub(crate) use function::quantile;
|
|
|
|
/// The samples a range selector reads at evaluation time `T`: source timestamps in
|
|
/// `(E - range_ms, E]`, where `E` is `T - offset_ms`, or `anchor_ms - offset_ms` with `@`.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct RangeWindow {
|
|
/// Positive window length in milliseconds.
|
|
pub range_ms: i64,
|
|
/// Selector offset in milliseconds; positive reads earlier samples.
|
|
pub offset_ms: i64,
|
|
/// The resolved `@` timestamp in milliseconds, with `start()` and `end()` already replaced
|
|
/// by the statement's start and end.
|
|
pub anchor_ms: Option<i64>,
|
|
}
|
|
|
|
/// The valid samples of one field inside one window, in source order.
|
|
pub(crate) struct FloatWindow<'a> {
|
|
/// Sample timestamps shifted by the offset onto the evaluation timeline; non-empty.
|
|
pub timestamps: &'a [i64],
|
|
pub values: &'a [f64],
|
|
/// The counter resets this window contains, ascending by sample. Empty for every function
|
|
/// that does not correct counters and for every window of a series that never reset.
|
|
pub counter_resets: &'a [CounterReset],
|
|
/// Evaluation timestamp `T`. `timestamps` lie in `(T - range_ms, T]`, except for an
|
|
/// `@`-anchored `predict_linear`, which evaluates the first point's window at every `T`.
|
|
pub eval_ts: i64,
|
|
pub range_ms: i64,
|
|
}
|
|
|
|
/// Computes one window result; `None` means the window has no result.
|
|
pub(crate) type WindowKernel = Box<dyn FnMut(FloatWindow<'_>) -> Option<f64> + Send>;
|
|
|
|
/// The windows of one field at consecutive grid points, as ranges into the field's retained
|
|
/// samples.
|
|
pub(crate) struct FloatWindows<'a> {
|
|
/// The retained samples, in time order.
|
|
pub timestamps: &'a [i64],
|
|
pub values: &'a [f64],
|
|
/// Counter resets over the retained samples, ascending by position.
|
|
pub resets: &'a [CounterReset],
|
|
/// Absolute position of `timestamps[0]`: a reset position is `base` plus a sample index.
|
|
pub base: u64,
|
|
/// One range per window, ascending and left-open at the start.
|
|
pub windows: &'a [Range<usize>],
|
|
/// The evaluation timestamp of each window.
|
|
pub eval_ts: &'a [i64],
|
|
pub range_ms: i64,
|
|
}
|
|
|
|
impl FloatWindows<'_> {
|
|
/// The resets inside window `index`.
|
|
///
|
|
/// `windows` are absolute indices into `timestamps`, so a window whose start has already
|
|
/// been released still maps to the right reset.
|
|
pub(crate) fn resets_of(&self, index: usize) -> &[CounterReset] {
|
|
let span = &self.windows[index];
|
|
resets_between(
|
|
self.resets,
|
|
self.base + span.start as u64,
|
|
self.base + span.end as u64,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Computes a batch of one field's windows at once, appending one result per window to `out`.
|
|
pub(crate) type BatchKernel = Box<dyn FnMut(&FloatWindows<'_>, &mut Vec<Option<f64>>) + Send>;
|
|
|
|
/// Grid points accumulated before a batch kernel call. Sized to keep the per-field span and
|
|
/// column buffers in cache while staying large enough to amortise the call.
|
|
const EVALUATION_BATCH: usize = 512;
|
|
|
|
/// Evaluates range windows over raw series, one series at a time, handing out its results as they
|
|
/// become final.
|
|
///
|
|
/// Each series is pushed as its chunks in order. A push evaluates the grid points whose windows
|
|
/// the pushed samples complete, and hands them out as one [`EvaluatedChunk`], so a consumer can
|
|
/// stream a long series instead of waiting for its last chunk. One chunk may hold several runs of
|
|
/// positions, separated by stretches holding no value, and the axis names them. The series closes
|
|
/// on a chunk marked `is_last`, which is handed out even when it holds no position.
|
|
///
|
|
/// Fields are evaluated independently over their own valid samples; a row holding a stale
|
|
/// marker in any field is dropped, as the ordinary range selector does. Only the samples later
|
|
/// windows can still use are retained. Native histogram samples need a [`RangeFunction`]; an
|
|
/// evaluator built from a bare kernel rejects them.
|
|
pub struct RangeEvaluator {
|
|
grid: Arc<EvalGrid>,
|
|
/// The window with `offset_ms` replaced by the offset samples are shifted by.
|
|
window: RangeWindow,
|
|
replay: Replay,
|
|
kernel: WindowKernel,
|
|
function: Option<RangeFunction>,
|
|
output_schema: Arc<SeriesSchema>,
|
|
output: GridSeriesBuilder,
|
|
/// The grid of evaluated input series, such as the inner result of a subquery; `None` for
|
|
/// raw input.
|
|
input_grid: Option<GridSpec>,
|
|
series: Option<SeriesState>,
|
|
fields: Vec<FieldSamples>,
|
|
row: Vec<Option<GridSample>>,
|
|
drops_metric_name: bool,
|
|
/// Raised by the first non-empty window, where Prometheus validates function arguments.
|
|
window_error: Option<String>,
|
|
/// Chunks appended by the bulk path, so a test can prove that path is reached rather than
|
|
/// only that the output is right, which the per-sample path would also produce.
|
|
#[cfg(test)]
|
|
bulk_ingests: usize,
|
|
/// The batch form of `function`, when it has one and the input allows it. Present only for
|
|
/// `Replay::PerPoint`, whose windows advance with the grid; an anchored selector replays one
|
|
/// window and gains nothing from batching.
|
|
batch: Option<BatchKernel>,
|
|
/// Windows accumulated for the next batch call.
|
|
pending: PendingBatch,
|
|
/// Batch kernel calls made so far. Test-only: both dispatch shapes produce the same output,
|
|
/// so a test comparing only output could not tell which one ran.
|
|
#[cfg(test)]
|
|
batch_calls: usize,
|
|
}
|
|
|
|
/// Windows accumulated for the next batch call: one list per field.
|
|
struct PendingBatch {
|
|
/// Grid index of each accumulated point, ascending.
|
|
///
|
|
/// Not assumed contiguous: `evaluate` skips the grid points whose window ends before the
|
|
/// series has a sample, so a batch can span such a gap and must write each point to its own
|
|
/// row.
|
|
indices: Vec<usize>,
|
|
/// Evaluation timestamp of each accumulated point.
|
|
eval_ts: Vec<i64>,
|
|
/// The retained-sample range of each point's window, one list per field.
|
|
spans: Vec<Vec<Range<usize>>>,
|
|
/// One output column per field, written by the batch kernel.
|
|
columns: Vec<Vec<Option<f64>>>,
|
|
}
|
|
|
|
impl PendingBatch {
|
|
fn with_fields(fields: usize) -> Self {
|
|
Self {
|
|
indices: Vec::new(),
|
|
eval_ts: Vec::new(),
|
|
spans: vec![Vec::new(); fields],
|
|
columns: vec![Vec::new(); fields],
|
|
}
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
self.eval_ts.len()
|
|
}
|
|
|
|
fn is_empty(&self) -> bool {
|
|
self.eval_ts.is_empty()
|
|
}
|
|
|
|
/// Drops the accumulated points, keeping the allocations.
|
|
fn clear_points(&mut self) {
|
|
self.indices.clear();
|
|
self.eval_ts.clear();
|
|
for spans in &mut self.spans {
|
|
spans.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Records one point for the next batch call: its evaluation timestamp and, per field, the range
|
|
/// of retained samples its window covers.
|
|
///
|
|
/// The ranges are relative to the retained samples, so samples dropped before the batch flushes
|
|
/// would shift them; [`flush_batch`] therefore runs before any `advance`.
|
|
fn accumulate(pending: &mut PendingBatch, index: usize, eval_ts: i64, fields: &[FieldSamples]) {
|
|
debug_assert_eq!(pending.spans.len(), fields.len());
|
|
pending.indices.push(index);
|
|
pending.eval_ts.push(eval_ts);
|
|
for (spans, field) in pending.spans.iter_mut().zip(fields) {
|
|
let end = field.head
|
|
+ field.timestamps[field.head..].partition_point(|×tamp| timestamp <= eval_ts);
|
|
spans.push(field.head..end);
|
|
}
|
|
}
|
|
|
|
/// Runs the batch kernel over the accumulated points and appends its columns.
|
|
fn flush_batch(
|
|
kernel: &mut BatchKernel,
|
|
pending: &mut PendingBatch,
|
|
fields: &[FieldSamples],
|
|
range_ms: i64,
|
|
row: &mut Vec<Option<GridSample>>,
|
|
output: &mut GridSeriesBuilder,
|
|
#[cfg(test)] batch_calls: &mut usize,
|
|
) -> Result<()> {
|
|
if pending.is_empty() {
|
|
return Ok(());
|
|
}
|
|
// Counted here rather than at the call site, so the number a test reads is by construction
|
|
// the number of kernel calls that happened.
|
|
#[cfg(test)]
|
|
{
|
|
*batch_calls += 1;
|
|
}
|
|
for (index, field) in fields.iter().enumerate() {
|
|
// `pending.spans` index the retained arrays absolutely, so no adjustment is needed for
|
|
// samples `advance` has released: those sit before every span start.
|
|
let batch = FloatWindows {
|
|
timestamps: &field.timestamps,
|
|
values: &field.values,
|
|
resets: &field.resets,
|
|
base: field.base,
|
|
windows: &pending.spans[index],
|
|
eval_ts: &pending.eval_ts,
|
|
range_ms,
|
|
};
|
|
kernel(&batch, &mut pending.columns[index]);
|
|
}
|
|
for (offset, &grid_index) in pending.indices.iter().enumerate() {
|
|
row.clear();
|
|
for column in &pending.columns {
|
|
row.push(column[offset].map(GridSample::Float));
|
|
}
|
|
output.push_samples(grid_index, row)?;
|
|
}
|
|
pending.clear_points();
|
|
Ok(())
|
|
}
|
|
|
|
struct SeriesState {
|
|
/// `None` for evaluated input, which arrives as one complete series.
|
|
primary_key: Option<Buffer>,
|
|
metric: MetricName,
|
|
/// Largest source timestamp retained so far, or `None` while the series holds no sample: a
|
|
/// closing chunk of a series that never produced a value carries no position to read.
|
|
last_ts: Option<i64>,
|
|
/// Next grid index to evaluate.
|
|
cursor: usize,
|
|
/// Whether the input, or the evaluated series, has ended. The state stays until the buffered
|
|
/// rows have been handed out: the closing chunk still needs its metric.
|
|
closing: bool,
|
|
/// Grid index after the last position handed out to the consumer.
|
|
emitted_end: usize,
|
|
}
|
|
|
|
/// How the windows of an `@`-anchored selector map to the grid.
|
|
///
|
|
/// Like Prometheus, an anchored selector is read once, as the window of the first grid point
|
|
/// with the offset rewritten to `start - (anchor - offset)`.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum Replay {
|
|
/// Not anchored: every grid point reads its own window.
|
|
PerPoint,
|
|
/// The first point's result is reported at every point (step-invariant functions).
|
|
Once,
|
|
/// The first point's window is evaluated at every point (`predict_linear`).
|
|
EveryPoint,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct FieldSamples {
|
|
timestamps: Vec<i64>,
|
|
values: Vec<f64>,
|
|
/// Samples before `head` are behind every remaining window.
|
|
head: usize,
|
|
/// Native histogram samples, kept apart from the floats as Prometheus does.
|
|
histograms: Vec<(i64, FloatHistogram)>,
|
|
histogram_head: usize,
|
|
/// Sequence number of `values[0]`. Absolute and monotonic across the series this evaluator
|
|
/// evaluates, so a reset keeps its position when `head` samples are dropped instead of every
|
|
/// index being renumbered. An empty field holds the number of samples already dropped, which
|
|
/// keeps positions increasing when the next series starts.
|
|
base: u64,
|
|
/// Resets between consecutive retained samples, ascending by position: exactly the ones a
|
|
/// window over these samples can still contain. Maintained only for the counter-correcting
|
|
/// functions, and released as the samples they reach back to are dropped.
|
|
resets: Vec<CounterReset>,
|
|
/// Whether `resets` is maintained for this field.
|
|
index_resets: bool,
|
|
}
|
|
|
|
/// One counter reset: a sample that dropped below the sample before it.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) struct CounterReset {
|
|
/// Position of the later sample, as an absolute sequence number. Its predecessor is
|
|
/// `position - 1`, so a window holding the samples `[start, end)` contains this reset
|
|
/// exactly when `start < position < end`.
|
|
pub position: u64,
|
|
/// The value the reset adds back: the sample at `position - 1`, which is inside the window
|
|
/// whenever the reset is.
|
|
pub previous_value: f64,
|
|
}
|
|
|
|
/// The resets inside the window over the absolute positions `[start, end)`.
|
|
///
|
|
/// Left-open: a reset at `start` would add back a sample behind the window.
|
|
fn resets_between(resets: &[CounterReset], start: u64, end: u64) -> &[CounterReset] {
|
|
let first = resets.partition_point(|reset| reset.position <= start);
|
|
let count = resets[first..].partition_point(|reset| reset.position < end);
|
|
&resets[first..first + count]
|
|
}
|
|
|
|
impl FieldSamples {
|
|
fn new(index_resets: bool) -> Self {
|
|
Self {
|
|
index_resets,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Appends one valid sample, recording a counter reset when it dropped below the sample
|
|
/// before it.
|
|
///
|
|
/// "Before it" means the previous entry of `values`, which leaves native histogram samples
|
|
/// out. A drop across an intervening histogram is therefore recorded as a reset of two
|
|
/// samples that are not neighbours. That is unobservable: a window holding both floats also
|
|
/// holds the histogram between them, so it is a mixed window, whose counter functions report
|
|
/// nothing; and a window starting after the earlier float excludes a reset on its first
|
|
/// sample. Should the mixed-window rules ever start reading the float resets, this has to
|
|
/// compare timestamps instead.
|
|
fn push(&mut self, timestamp: i64, value: f64) {
|
|
if self.index_resets
|
|
&& let Some(&previous_value) = self.values.last()
|
|
&& value < previous_value
|
|
{
|
|
self.resets.push(CounterReset {
|
|
position: self.base + self.values.len() as u64,
|
|
previous_value,
|
|
});
|
|
}
|
|
self.timestamps.push(timestamp);
|
|
self.values.push(value);
|
|
}
|
|
|
|
/// Appends samples that are all present, in bulk.
|
|
///
|
|
/// Same result as calling [`push`](Self::push) once per sample, including the reset scan,
|
|
/// which stays per-sample for a field that maintains the index; the two appends become two
|
|
/// `memcpy`s.
|
|
fn push_bulk(&mut self, timestamps: &[i64], values: &[f64]) {
|
|
debug_assert_eq!(timestamps.len(), values.len());
|
|
if self.index_resets {
|
|
let start = self.values.len();
|
|
let mut previous = self.values.last().copied();
|
|
for (offset, &value) in values.iter().enumerate() {
|
|
if let Some(previous_value) = previous
|
|
&& value < previous_value
|
|
{
|
|
self.resets.push(CounterReset {
|
|
position: self.base + (start + offset) as u64,
|
|
previous_value,
|
|
});
|
|
}
|
|
previous = Some(value);
|
|
}
|
|
}
|
|
self.timestamps.extend_from_slice(timestamps);
|
|
self.values.extend_from_slice(values);
|
|
}
|
|
|
|
/// Drops the samples before `head` and the resets whose predecessor is one of them.
|
|
fn advance(&mut self) {
|
|
let head = std::mem::take(&mut self.head);
|
|
if head == 0 {
|
|
return;
|
|
}
|
|
self.base += head as u64;
|
|
self.timestamps.drain(..head);
|
|
self.values.drain(..head);
|
|
// A reset at `base` would add back the last dropped sample, so it goes with it.
|
|
let dropped = self
|
|
.resets
|
|
.partition_point(|reset| reset.position <= self.base);
|
|
self.resets.drain(..dropped);
|
|
}
|
|
|
|
/// The resets inside the window holding `len` samples from `head`.
|
|
fn resets_in(&self, len: usize) -> &[CounterReset] {
|
|
let start = self.base + self.head as u64;
|
|
resets_between(&self.resets, start, start + len as u64)
|
|
}
|
|
}
|
|
|
|
impl RangeEvaluator {
|
|
/// Creates an evaluator of `function` for the series of `input` onto `grid`.
|
|
pub fn try_new(
|
|
input: &SeriesSchema,
|
|
grid: Arc<EvalGrid>,
|
|
window: RangeWindow,
|
|
function: RangeFunction,
|
|
) -> Result<Self> {
|
|
let mut window_error = None;
|
|
if let RangeFunction::DoubleExponentialSmoothing { smoothing, trend } = function {
|
|
// Prometheus rejects factors outside (0, 1); NaN passes its check.
|
|
window_error = [("smoothing", smoothing), ("trend", trend)]
|
|
.into_iter()
|
|
.find(|(_, factor)| *factor <= 0.0 || *factor >= 1.0)
|
|
.map(|(name, factor)| {
|
|
format!("invalid {name} factor. Expected: 0 < factor < 1, got: {factor}")
|
|
});
|
|
}
|
|
let fold_offset = fold_offset(grid.spec(), window)?;
|
|
let step_invariant = !matches!(function, RangeFunction::PredictLinear(_));
|
|
if !step_invariant {
|
|
let spec = grid.spec();
|
|
let last =
|
|
i128::from(spec.start_ms) + i128::from(spec.step_ms) * (i128::from(spec.len) - 1);
|
|
for eval_ts in [i128::from(spec.start_ms), last] {
|
|
if i64::try_from(eval_ts + i128::from(fold_offset)).is_err() {
|
|
return Err(DataFusionError::Plan(format!(
|
|
"predict_linear evaluation time {eval_ts} overflows with offset {fold_offset} ms"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
let mut evaluator = Self::with_kernel(
|
|
input,
|
|
grid,
|
|
window,
|
|
function.kernel(fold_offset),
|
|
step_invariant,
|
|
function.indexes_counter_resets(),
|
|
)?;
|
|
evaluator.drops_metric_name = function.drops_metric_name();
|
|
evaluator.window_error = window_error;
|
|
evaluator.function = Some(function);
|
|
// An anchored selector replays one window at every point, so its windows do not advance
|
|
// with the grid and a batch would only repeat the same call.
|
|
if matches!(evaluator.replay, Replay::PerPoint) {
|
|
evaluator.batch = function.batch_kernel();
|
|
}
|
|
Ok(evaluator)
|
|
}
|
|
|
|
/// `step_invariant` tells whether an `@`-anchored result holds at every grid point.
|
|
/// `index_resets` tells whether the kernel corrects counters, which is the only case where
|
|
/// the per-field reset index is worth maintaining.
|
|
pub(crate) fn with_kernel(
|
|
input: &SeriesSchema,
|
|
grid: Arc<EvalGrid>,
|
|
mut window: RangeWindow,
|
|
kernel: WindowKernel,
|
|
step_invariant: bool,
|
|
index_resets: bool,
|
|
) -> Result<Self> {
|
|
if window.range_ms <= 0 {
|
|
return Err(DataFusionError::Plan(format!(
|
|
"range must be positive, got {} ms",
|
|
window.range_ms
|
|
)));
|
|
}
|
|
let replay = match (window.anchor_ms, step_invariant) {
|
|
(None, _) => Replay::PerPoint,
|
|
(Some(_), true) => Replay::Once,
|
|
(Some(_), false) => Replay::EveryPoint,
|
|
};
|
|
window.offset_ms = fold_offset(grid.spec(), window)?;
|
|
let output_schema = Arc::new(SeriesSchema {
|
|
fields: input.fields.clone(),
|
|
eval_grid: Some(grid.clone()),
|
|
});
|
|
Ok(Self {
|
|
grid,
|
|
window,
|
|
replay,
|
|
kernel,
|
|
function: None,
|
|
output: GridSeriesBuilder::try_new(output_schema.clone())?,
|
|
input_grid: input.eval_grid.as_ref().map(|grid| grid.spec()),
|
|
output_schema,
|
|
series: None,
|
|
fields: input
|
|
.fields
|
|
.iter()
|
|
.map(|_| FieldSamples::new(index_resets))
|
|
.collect(),
|
|
row: Vec::with_capacity(input.fields.len()),
|
|
drops_metric_name: false,
|
|
window_error: None,
|
|
#[cfg(test)]
|
|
bulk_ingests: 0,
|
|
batch: None,
|
|
pending: PendingBatch::with_fields(input.fields.len()),
|
|
#[cfg(test)]
|
|
batch_calls: 0,
|
|
})
|
|
}
|
|
|
|
/// Chunks appended without the per-sample loop. Test-only: both paths produce the same
|
|
/// output by construction, so a test that only compared output could not tell whether the
|
|
/// bulk path ran at all.
|
|
#[cfg(test)]
|
|
pub(crate) fn bulk_ingest_count(&self) -> usize {
|
|
self.bulk_ingests
|
|
}
|
|
|
|
/// Batch kernel calls made so far. Test-only: both dispatch shapes produce the same output,
|
|
/// so a test comparing only output could not tell which one ran.
|
|
#[cfg(test)]
|
|
pub(crate) fn batch_call_count(&self) -> usize {
|
|
self.batch_calls
|
|
}
|
|
|
|
/// Schema of the evaluated series: the input fields on the evaluation grid.
|
|
pub fn output_schema(&self) -> &Arc<SeriesSchema> {
|
|
&self.output_schema
|
|
}
|
|
|
|
/// Consumes the next chunk of the current series, or starts a new series.
|
|
///
|
|
/// Returns every position this chunk decided, in one chunk addressed absolutely on the
|
|
/// evaluation grid; `None` when it decided no position holding a value. A decided position
|
|
/// without a result is a row of no chunk, so the positions between two runs of one chunk hold
|
|
/// no value and cost nothing. One push hands out at most one chunk.
|
|
///
|
|
/// A series always closes exactly once: on the chunk holding its last rows, or on an empty
|
|
/// chunk after it when nothing is left. The evaluator must not be reused after an error.
|
|
pub fn push(&mut self, chunk: &RawSeriesChunk) -> Result<Option<EvaluatedChunk>> {
|
|
if self.input_grid.is_some() {
|
|
return Err(DataFusionError::Execution(
|
|
"raw series chunk pushed to an evaluator of evaluated series".into(),
|
|
));
|
|
}
|
|
self.check_chunk(chunk)?;
|
|
self.ingest(chunk.time.as_buffer(), &chunk.fields, true)?;
|
|
let last_ts = chunk.time.as_buffer()[chunk.time.len() - 1];
|
|
if !chunk.is_last {
|
|
// Later chunks only hold samples after `last_ts`.
|
|
self.evaluate(Some(
|
|
i128::from(last_ts) + i128::from(self.window.offset_ms),
|
|
))?;
|
|
return self.emit();
|
|
}
|
|
self.close_series()
|
|
}
|
|
|
|
/// Evaluates one run of an evaluated series, such as the inner result of a subquery: the
|
|
/// value at each grid point is a sample at that time. Stale markers are kept as values,
|
|
/// since the input is computed.
|
|
///
|
|
/// `last` says whether this delivery closes its series. A stream hands over the same logical
|
|
/// series more than once -- a prefix now, its closing chunk later -- and the windows of a later
|
|
/// delivery read the samples of the earlier ones, so the state a series retains lives on across
|
|
/// them: only the `last` call ends it. Handing the deliveries over without saying which is last
|
|
/// would close the series early and lose exactly the samples a window across the boundary
|
|
/// needs.
|
|
///
|
|
/// One delivery may hold several runs of positions, separated by stretches holding no value;
|
|
/// they all reach the caller in one chunk, addressed by the axis. A delivery that holds no
|
|
/// position at all is a legal close when it is `last`, and an error otherwise.
|
|
pub fn push_series(&mut self, series: &Series, last: bool) -> Result<Option<EvaluatedChunk>> {
|
|
let time = &series.time;
|
|
let Some(grid) = time.grid() else {
|
|
return Err(DataFusionError::Execution(
|
|
"evaluated range input must be a grid series".into(),
|
|
));
|
|
};
|
|
if self.input_grid != Some(grid.spec())
|
|
|| series.fields.len() != self.fields.len()
|
|
|| series.fields.iter().any(|field| field.len() != time.len())
|
|
{
|
|
return Err(DataFusionError::Execution(
|
|
"evaluated series does not match the input schema".into(),
|
|
));
|
|
}
|
|
// The input may skip positions: it is ingested by the positions it carries, and the points
|
|
// it skips hold no sample, which is exactly what a hole means.
|
|
let timestamps = time.timestamps();
|
|
let last_ts = timestamps.last().copied();
|
|
if !last && last_ts.is_none() {
|
|
// An empty delivery inside a series decides nothing: it is indistinguishable from a
|
|
// stall, so only the closing `is_last` one is a legal close.
|
|
return Err(DataFusionError::Execution(
|
|
"evaluated range input holds no position before its last run".into(),
|
|
));
|
|
}
|
|
match &mut self.series {
|
|
None => {
|
|
self.output.begin_series();
|
|
self.series = Some(SeriesState {
|
|
primary_key: None,
|
|
metric: series.metric.clone(),
|
|
last_ts,
|
|
cursor: 0,
|
|
closing: false,
|
|
emitted_end: 0,
|
|
});
|
|
}
|
|
Some(state) => {
|
|
// A run continues its series; the earlier runs' samples stay retained for it.
|
|
if state.primary_key.is_some() {
|
|
return Err(DataFusionError::Execution(
|
|
"evaluated series pushed inside a raw series".into(),
|
|
));
|
|
}
|
|
if state.closing {
|
|
return Err(DataFusionError::Execution(
|
|
"evaluated series pushed after it closed".into(),
|
|
));
|
|
}
|
|
if state.metric != series.metric {
|
|
return Err(DataFusionError::Execution(
|
|
"evaluated series changed identity before its last run".into(),
|
|
));
|
|
}
|
|
if let Some(last_ts) = last_ts {
|
|
if let Some(previous) = state.last_ts
|
|
&& timestamps[0] <= previous
|
|
{
|
|
return Err(DataFusionError::Execution(format!(
|
|
"evaluated series runs are not increasing: {} after {previous}",
|
|
timestamps[0]
|
|
)));
|
|
}
|
|
state.last_ts = Some(last_ts);
|
|
}
|
|
}
|
|
}
|
|
if let Some(last_ts) = last_ts {
|
|
self.ingest(×tamps, &series.fields, false)?;
|
|
if last {
|
|
return self.close_series();
|
|
}
|
|
// Later runs only hold samples after `last_ts`, so every point up to it is decided.
|
|
// The samples are shifted onto the evaluation timeline by the selector offset, so the
|
|
// ready point is on that timeline too.
|
|
self.evaluate(Some(
|
|
i128::from(last_ts) + i128::from(self.window.offset_ms),
|
|
))?;
|
|
return self.emit();
|
|
}
|
|
// A closing chunk holding no position: there is nothing to ingest, but the samples the
|
|
// earlier deliveries left retained still decide the windows that follow them, so the close
|
|
// finishes them rather than ending the series on the spot.
|
|
debug_assert!(last, "an empty chunk that is not last is rejected above");
|
|
self.close_series()
|
|
}
|
|
|
|
/// Evaluates the rest of the series of the last input chunk and closes it.
|
|
///
|
|
/// The rows still buffered are handed out here as one chunk, and the chunk that sees nothing
|
|
/// behind it carries `is_last`.
|
|
fn close_series(&mut self) -> Result<Option<EvaluatedChunk>> {
|
|
self.evaluate(None)?;
|
|
self.series
|
|
.as_mut()
|
|
.expect("every path that closes a series started one")
|
|
.closing = true;
|
|
self.emit()
|
|
}
|
|
|
|
/// Hands out the rows buffered so far as one chunk, and closes the series when nothing is left.
|
|
///
|
|
/// The chunk holds every row decided since the last hand-out, in one go and in one [`Series`]:
|
|
/// several runs of positions may sit inside it, separated by stretches holding no value, and
|
|
/// the axis names them. A position decided to hold none costs no row, so it lies between the
|
|
/// runs rather than inside one. `is_last` closes the series, and a series always closes exactly
|
|
/// once: on the chunk that carries its last rows, or on an empty chunk after it when no row is
|
|
/// left.
|
|
fn emit(&mut self) -> Result<Option<EvaluatedChunk>> {
|
|
let Some(state) = self.series.as_ref() else {
|
|
return Ok(None);
|
|
};
|
|
let closing = state.closing;
|
|
let emitted_end = state.emitted_end;
|
|
let mut metric = state.metric.clone();
|
|
if self.drops_metric_name {
|
|
metric.remove_all(&METADATA_LABELS);
|
|
}
|
|
let Some(series) = self.output.take_buffered(metric)? else {
|
|
return if closing {
|
|
self.empty_chunk(emitted_end)
|
|
} else {
|
|
Ok(None)
|
|
};
|
|
};
|
|
// The builder addresses every position it hands out, so the chunk's bounds come from the
|
|
// axis itself: `first_position` is where it starts, and `last_position` where it ends.
|
|
let start = series
|
|
.time
|
|
.first_position()
|
|
.expect("the builder addresses every row on the grid");
|
|
let end = series.time.last_position().map_or(start, |last| last + 1);
|
|
debug_assert!(start >= emitted_end, "a chunk never goes back");
|
|
if closing {
|
|
// The buffer was drained into this chunk, so nothing of the series is left behind it.
|
|
self.series = None;
|
|
EvaluatedChunk::try_new(series, true).map(Some)
|
|
} else {
|
|
self.series
|
|
.as_mut()
|
|
.expect("checked at the entry of emit")
|
|
.emitted_end = end;
|
|
EvaluatedChunk::try_new(series, false).map(Some)
|
|
}
|
|
}
|
|
|
|
/// A closing chunk without values, addressed after the last position handed out.
|
|
fn empty_chunk(&mut self, at: usize) -> Result<Option<EvaluatedChunk>> {
|
|
let state = self.series.as_ref().expect("checked by the caller");
|
|
let mut metric = state.metric.clone();
|
|
if self.drops_metric_name {
|
|
metric.remove_all(&METADATA_LABELS);
|
|
}
|
|
let empty = Samples::from_float(Float64Array::from(Vec::<f64>::new()));
|
|
let fields = SeriesFields::try_from_vec(vec![empty; self.output_schema.fields.len()])
|
|
.map_err(external)?;
|
|
let time = GridRange::try_new(self.grid.clone(), at, 0).map_err(external)?;
|
|
let series = Series::try_new(&self.output_schema, metric, TimeAxis::Grid(time), fields)
|
|
.map_err(external)?;
|
|
self.series = None;
|
|
EvaluatedChunk::try_new(series, true).map(Some)
|
|
}
|
|
|
|
/// Checks that the input did not end inside a series, with rows nobody took.
|
|
pub fn finish(self) -> Result<()> {
|
|
if self.series.is_some() {
|
|
return Err(DataFusionError::Execution(
|
|
"raw series input ended before the last chunk of a series".into(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn check_chunk(&mut self, chunk: &RawSeriesChunk) -> Result<()> {
|
|
let primary_key = Some(chunk.primary_key.as_slice());
|
|
let time = chunk.time.as_buffer();
|
|
if time.is_empty()
|
|
|| chunk.fields.len() != self.fields.len()
|
|
|| chunk.fields.iter().any(|field| field.len() != time.len())
|
|
{
|
|
return Err(DataFusionError::Execution(format!(
|
|
"raw series chunk with {} timestamps and {} fields does not match {} schema fields",
|
|
time.len(),
|
|
chunk.fields.len(),
|
|
self.fields.len()
|
|
)));
|
|
}
|
|
match &mut self.series {
|
|
None => {
|
|
let metric = chunk.metric.clone().ok_or_else(|| {
|
|
DataFusionError::Execution(
|
|
"range evaluation requires materialized series labels".into(),
|
|
)
|
|
})?;
|
|
self.output.begin_series();
|
|
self.series = Some(SeriesState {
|
|
primary_key: Some(chunk.primary_key.clone()),
|
|
metric,
|
|
last_ts: Some(time[time.len() - 1]),
|
|
cursor: 0,
|
|
closing: false,
|
|
emitted_end: 0,
|
|
});
|
|
}
|
|
Some(state) => {
|
|
if state.primary_key.as_ref().map(|key| key.as_slice()) != primary_key
|
|
|| chunk.metric.as_ref() != Some(&state.metric)
|
|
{
|
|
return Err(DataFusionError::Execution(
|
|
"raw series identity changed before its last chunk".into(),
|
|
));
|
|
}
|
|
if let Some(last_ts) = state.last_ts
|
|
&& time[0] <= last_ts
|
|
{
|
|
return Err(DataFusionError::Execution(format!(
|
|
"raw series timestamps are not increasing across chunks: {} after {last_ts}",
|
|
time[0]
|
|
)));
|
|
}
|
|
state.last_ts = Some(time[time.len() - 1]);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Appends the chunk in bulk when every field is an entirely present, unshifted float column.
|
|
///
|
|
/// Returns `false` when any condition of the per-sample path applies, so the caller can run
|
|
/// it. A chunk carrying a stale marker takes that path too: the marker drops the whole row,
|
|
/// and reproducing that here would cost the same scan the loop already makes.
|
|
///
|
|
/// A field mixing floats and native histograms never qualifies: `dense()` reports
|
|
/// [`DenseSamplesRef::Mixed`] for it, keeping its windows on the histogram path where the
|
|
/// mixed-window rules decide the result.
|
|
fn ingest_in_bulk(
|
|
&mut self,
|
|
timestamps: &[i64],
|
|
fields: &SeriesFields,
|
|
drop_stale: bool,
|
|
) -> bool {
|
|
// Lengths are checked by `check_chunk` for raw input and by `push_series` for evaluated
|
|
// input, so only the shape of the columns is decided here.
|
|
if self.window.offset_ms != 0 {
|
|
return false;
|
|
}
|
|
let mut columns = Vec::with_capacity(fields.len());
|
|
for field in fields.iter() {
|
|
let Some(DenseSamplesRef::Float(values)) = field.dense() else {
|
|
return false;
|
|
};
|
|
if values.null_count() != 0 {
|
|
return false;
|
|
}
|
|
columns.push(values);
|
|
}
|
|
// A kept sample's shifted time must fit `i64`; with no offset it is the source timestamp,
|
|
// which `RawTimestamps` already validated as increasing and which the frame widened into
|
|
// this array without narrowing, so there is nothing left to check here.
|
|
if drop_stale
|
|
&& columns.iter().any(|values| {
|
|
values
|
|
.values()
|
|
.iter()
|
|
.any(|value| is_prometheus_stale_nan(*value))
|
|
})
|
|
{
|
|
return false;
|
|
}
|
|
for (field, values) in self.fields.iter_mut().zip(columns) {
|
|
field.push_bulk(timestamps, values.values());
|
|
}
|
|
#[cfg(test)]
|
|
{
|
|
self.bulk_ingests += 1;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Retains the samples of `fields` at `timestamps`. From `raw` input a row holding a stale
|
|
/// marker in any field is dropped; evaluated input keeps it as a value.
|
|
fn ingest(&mut self, timestamps: &[i64], fields: &SeriesFields, raw: bool) -> Result<()> {
|
|
if self.ingest_in_bulk(timestamps, fields, raw) {
|
|
return Ok(());
|
|
}
|
|
let mut samples: Vec<_> = fields.iter().map(|field| field.iter()).collect();
|
|
let mut row = Vec::with_capacity(samples.len());
|
|
for ×tamp in timestamps {
|
|
row.clear();
|
|
let mut stale = false;
|
|
for field in &mut samples {
|
|
let sample = field.next().expect("field length checked by the caller");
|
|
match &sample {
|
|
SampleRef::Missing => {}
|
|
SampleRef::Float(value) => stale |= is_prometheus_stale_nan(*value),
|
|
SampleRef::Histogram(view) => {
|
|
if self.function.is_none() {
|
|
return Err(DataFusionError::NotImplemented(
|
|
"range evaluation of histogram samples".into(),
|
|
));
|
|
}
|
|
stale |= is_prometheus_stale_nan(view.sum);
|
|
}
|
|
}
|
|
row.push(sample);
|
|
}
|
|
if stale && raw {
|
|
continue;
|
|
}
|
|
// Like RangeManipulate, any kept sample whose shifted time leaves i64 fails the series.
|
|
let shifted = timestamp
|
|
.checked_add(self.window.offset_ms)
|
|
.ok_or_else(|| {
|
|
DataFusionError::Execution(format!(
|
|
"sample timestamp {timestamp} overflows with offset {} ms",
|
|
self.window.offset_ms
|
|
))
|
|
})?;
|
|
for (field, sample) in self.fields.iter_mut().zip(&row) {
|
|
match sample {
|
|
SampleRef::Missing => {}
|
|
SampleRef::Float(value) => {
|
|
field.push(shifted, *value);
|
|
}
|
|
SampleRef::Histogram(view) => {
|
|
let mut h = FloatHistogram::try_from_view(view)?;
|
|
// Like Prometheus' `evalSubquery`: an explicit counter reset hint
|
|
// describes the sample's raw neighbours, not its evaluated ones.
|
|
if !raw && h.reset_hint != CounterResetHint::Gauge {
|
|
h.reset_hint = CounterResetHint::Unknown;
|
|
}
|
|
field.histograms.push((shifted, h));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Evaluates grid points up to `ready` on the evaluation timeline, or all remaining points
|
|
/// when `ready` is `None`.
|
|
fn evaluate(&mut self, ready: Option<i128>) -> Result<()> {
|
|
let points_read = self.points_read();
|
|
let state = self.series.as_mut().expect("checked by check_chunk");
|
|
let spec = self.grid.spec();
|
|
let grid = self.grid.timestamps();
|
|
let range = i128::from(self.window.range_ms);
|
|
let mut empty_tail = false;
|
|
while state.cursor < points_read {
|
|
let eval_ts = grid[state.cursor];
|
|
let lower = i128::from(eval_ts) - range;
|
|
let mut earliest = None;
|
|
// Later windows start no earlier, so samples behind this one go even when the point
|
|
// itself is not ready yet.
|
|
for field in &mut self.fields {
|
|
field.head += field.timestamps[field.head..]
|
|
.partition_point(|×tamp| i128::from(timestamp) <= lower);
|
|
field.histogram_head += field.histograms[field.histogram_head..]
|
|
.partition_point(|(timestamp, _)| i128::from(*timestamp) <= lower);
|
|
let first_histogram = field.histograms.get(field.histogram_head);
|
|
for ×tamp in field
|
|
.timestamps
|
|
.get(field.head)
|
|
.into_iter()
|
|
.chain(first_histogram.map(|(timestamp, _)| timestamp))
|
|
{
|
|
earliest = Some(earliest.map_or(timestamp, |min: i64| min.min(timestamp)));
|
|
}
|
|
}
|
|
if ready.is_some_and(|ready| i128::from(eval_ts) > ready) {
|
|
break;
|
|
}
|
|
// No retained sample: every point from here on is decided too. A sample that still
|
|
// has to arrive lies beyond the window of every one of them, so none can gain a value.
|
|
let Some(earliest) = earliest else {
|
|
empty_tail = true;
|
|
break;
|
|
};
|
|
if earliest > eval_ts {
|
|
// Skip the points whose windows end before the earliest sample.
|
|
let step = i128::from(spec.step_ms);
|
|
let steps = (i128::from(earliest) - i128::from(spec.start_ms) + step - 1) / step;
|
|
state.cursor = usize::try_from(steps).map_or(grid.len(), |i| i.min(grid.len()));
|
|
continue;
|
|
}
|
|
if let Some(error) = &self.window_error {
|
|
return Err(DataFusionError::Execution(error.clone()));
|
|
}
|
|
|
|
let mut evaluator = RowEvaluator {
|
|
fields: &self.fields,
|
|
kernel: &mut self.kernel,
|
|
function: self.function,
|
|
range_ms: self.window.range_ms,
|
|
offset_ms: self.window.offset_ms,
|
|
};
|
|
match self.replay {
|
|
Replay::PerPoint => {
|
|
// A window holding a native histogram is decided by the mixed-window rules,
|
|
// which the batch kernel -- reading floats alone -- cannot reproduce. Flush
|
|
// what is pending and decide this point on the per-window path.
|
|
//
|
|
// Histograms are retained in time order, so the first one still ahead of the
|
|
// window head decides: a later one cannot be inside this window if this one
|
|
// is not. The check runs only when a batch kernel is set, so a function
|
|
// without one never pays for it.
|
|
let histogram_window = self.batch.is_some()
|
|
&& self.fields.iter().any(|field| {
|
|
field.histograms.get(field.histogram_head).is_some_and(
|
|
|(timestamp, _)| i128::from(*timestamp) <= i128::from(eval_ts),
|
|
)
|
|
});
|
|
if let Some(kernel) = self.batch.as_mut()
|
|
&& !histogram_window
|
|
{
|
|
accumulate(&mut self.pending, state.cursor, eval_ts, &self.fields);
|
|
if self.pending.len() >= EVALUATION_BATCH {
|
|
flush_batch(
|
|
kernel,
|
|
&mut self.pending,
|
|
&self.fields,
|
|
self.window.range_ms,
|
|
&mut self.row,
|
|
&mut self.output,
|
|
#[cfg(test)]
|
|
&mut self.batch_calls,
|
|
)?;
|
|
}
|
|
state.cursor += 1;
|
|
continue;
|
|
}
|
|
if let Some(kernel) = self.batch.as_mut() {
|
|
flush_batch(
|
|
kernel,
|
|
&mut self.pending,
|
|
&self.fields,
|
|
self.window.range_ms,
|
|
&mut self.row,
|
|
&mut self.output,
|
|
#[cfg(test)]
|
|
&mut self.batch_calls,
|
|
)?;
|
|
}
|
|
evaluator.evaluate(eval_ts, eval_ts, &mut self.row)?;
|
|
self.output.push_samples(state.cursor, &self.row)?;
|
|
}
|
|
Replay::Once => {
|
|
evaluator.evaluate(eval_ts, eval_ts, &mut self.row)?;
|
|
for index in 0..grid.len() {
|
|
self.output.push_samples(index, &self.row)?;
|
|
}
|
|
}
|
|
Replay::EveryPoint => {
|
|
for (index, &point) in grid.iter().enumerate() {
|
|
evaluator.evaluate(eval_ts, point, &mut self.row)?;
|
|
self.output.push_samples(index, &self.row)?;
|
|
}
|
|
}
|
|
}
|
|
state.cursor += 1;
|
|
}
|
|
// The accumulated spans index the retained arrays, which `advance` below rewrites, so the
|
|
// batch has to flush first.
|
|
if let Some(kernel) = self.batch.as_mut() {
|
|
flush_batch(
|
|
kernel,
|
|
&mut self.pending,
|
|
&self.fields,
|
|
self.window.range_ms,
|
|
&mut self.row,
|
|
&mut self.output,
|
|
#[cfg(test)]
|
|
&mut self.batch_calls,
|
|
)?;
|
|
}
|
|
// A point is decided once it has been evaluated, once its window can no longer gain a
|
|
// sample, or -- at the end of the series or of what the input has reached -- once no
|
|
// retained sample reaches it. The last case is what keeps a long empty stretch cheap: the
|
|
// loop stops at the first such point instead of walking the rest of the grid, and none of
|
|
// the points behind it can gain a value, so the cursor skips them.
|
|
if empty_tail && matches!(self.replay, Replay::PerPoint) {
|
|
let mut decided = state.cursor;
|
|
if let Some(ready) = ready {
|
|
while decided < points_read && i128::from(grid[decided]) <= ready {
|
|
decided += 1;
|
|
}
|
|
} else {
|
|
decided = points_read;
|
|
}
|
|
state.cursor = decided;
|
|
}
|
|
for field in &mut self.fields {
|
|
// After the last window, samples are still ingested so that the overflow and
|
|
// histogram checks do not depend on how the series is chunked, but none are kept.
|
|
// An anchored selector can skip past the one point it reads.
|
|
if state.cursor >= points_read {
|
|
field.head = field.timestamps.len();
|
|
field.histogram_head = field.histograms.len();
|
|
}
|
|
field.advance();
|
|
field.histograms.drain(..field.histogram_head);
|
|
field.histogram_head = 0;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The number of leading grid points whose windows are read; an anchored selector reads
|
|
/// only the first.
|
|
fn points_read(&self) -> usize {
|
|
match self.replay {
|
|
Replay::PerPoint => self.grid.timestamps().len(),
|
|
Replay::Once | Replay::EveryPoint => 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Computes the result of every field's window at one grid point.
|
|
struct RowEvaluator<'a> {
|
|
fields: &'a [FieldSamples],
|
|
kernel: &'a mut WindowKernel,
|
|
function: Option<RangeFunction>,
|
|
range_ms: i64,
|
|
offset_ms: i64,
|
|
}
|
|
|
|
impl RowEvaluator<'_> {
|
|
/// Fills `row` with the result over each field's window ending at `window_end`, evaluated
|
|
/// at `eval_ts`.
|
|
fn evaluate(
|
|
&mut self,
|
|
window_end: i64,
|
|
eval_ts: i64,
|
|
row: &mut Vec<Option<GridSample>>,
|
|
) -> Result<()> {
|
|
row.clear();
|
|
for field in self.fields {
|
|
let timestamps = &field.timestamps[field.head..];
|
|
let len = timestamps.partition_point(|×tamp| timestamp <= window_end);
|
|
let floats = (len > 0).then(|| FloatWindow {
|
|
timestamps: ×tamps[..len],
|
|
values: &field.values[field.head..field.head + len],
|
|
counter_resets: field.resets_in(len),
|
|
eval_ts,
|
|
range_ms: self.range_ms,
|
|
});
|
|
let histograms = &field.histograms[field.histogram_head..];
|
|
let histograms = &histograms
|
|
[..histograms.partition_point(|(timestamp, _)| *timestamp <= window_end)];
|
|
row.push(match self.function {
|
|
Some(function) if !histograms.is_empty() => histogram::evaluate(
|
|
function,
|
|
floats,
|
|
histograms,
|
|
eval_ts,
|
|
self.range_ms,
|
|
self.offset_ms,
|
|
&mut *self.kernel,
|
|
)?,
|
|
_ => floats.and_then(|window| (self.kernel)(window).map(GridSample::Float)),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The offset samples are shifted by: the selector offset, or for `@` the offset that places
|
|
/// the anchored window at the first grid point.
|
|
fn fold_offset(grid: GridSpec, window: RangeWindow) -> Result<i64> {
|
|
let Some(anchor) = window.anchor_ms else {
|
|
return Ok(window.offset_ms);
|
|
};
|
|
anchor
|
|
.checked_sub(window.offset_ms)
|
|
.and_then(|end| grid.start_ms.checked_sub(end))
|
|
.ok_or_else(|| {
|
|
DataFusionError::Plan(format!(
|
|
"@ {anchor} ms with offset {} ms is out of range",
|
|
window.offset_ms
|
|
))
|
|
})
|
|
}
|
|
|
|
/// The inner grid of a subquery read by `window` from the outer grid `outer`: the multiples of
|
|
/// `step_ms` after `start - F - range` up to the last window end, where `F` is the fold offset
|
|
/// of `window`; with `@`, the only window end is `start - F`. `None` when no multiple falls in
|
|
/// the windows.
|
|
pub fn subquery_grid(
|
|
outer: GridSpec,
|
|
window: RangeWindow,
|
|
step_ms: i64,
|
|
) -> Result<Option<GridSpec>> {
|
|
if step_ms <= 0 {
|
|
return Err(DataFusionError::Plan(format!(
|
|
"subquery step must be positive, got {step_ms} ms"
|
|
)));
|
|
}
|
|
let fold_offset = i128::from(fold_offset(outer, window)?);
|
|
let step = i128::from(step_ms);
|
|
let start = i128::from(outer.start_ms);
|
|
let last_eval = match window.anchor_ms {
|
|
Some(_) => start,
|
|
None => start + i128::from(outer.step_ms) * (i128::from(outer.len) - 1),
|
|
};
|
|
let first = ((start - fold_offset - i128::from(window.range_ms)).div_euclid(step) + 1) * step;
|
|
let last = last_eval - fold_offset;
|
|
if first > last {
|
|
return Ok(None);
|
|
}
|
|
let len = u64::try_from((last - first) / step + 1)
|
|
.map_err(|_| DataFusionError::Plan("subquery grid has too many points".into()))?;
|
|
let first = i64::try_from(first).map_err(|_| {
|
|
DataFusionError::Plan(format!("subquery grid start {first} ms is out of range"))
|
|
})?;
|
|
GridSpec::try_new(first, step_ms, len)
|
|
.map(Some)
|
|
.map_err(|error| DataFusionError::External(Box::new(error)))
|
|
}
|