Skip to main content

densa_data/
window.rs

1//! Time windows: a table that keeps only its rows within a span of the newest time.
2
3use std::time::Duration;
4
5use arrow_schema::{DataType, Schema, TimeUnit};
6
7use crate::filter::resolve;
8use crate::value::{Kind, Value, ValueFn};
9use crate::view::{Rows, SortColumn, ViewState};
10use crate::DataError;
11
12/// Keeps a [`LiveTable`](crate::LiveTable) to its recent rows: those whose value in a time
13/// column is within a span of the newest. Set it with
14/// [`LiveTable::set_window`](crate::LiveTable::set_window).
15///
16/// The window ends at the largest finite value of the column among the live rows, so it
17/// moves with the data, not with the clock: replayed or delayed data is windowed by its own
18/// times, and nothing expires while no rows arrive. A row expires when the end is more than
19/// the span past its value; rows whose value is null or NaN never do.
20///
21/// ```
22/// use std::sync::Arc;
23/// use std::time::Duration;
24/// use arrow_array::{ArrayRef, Int64Array, RecordBatch, TimestampMillisecondArray};
25/// use densa_data::{LiveTable, Window};
26///
27/// let ticks = |ids: Vec<i64>, times: Vec<i64>| {
28///     RecordBatch::try_from_iter([
29///         ("id", Arc::new(Int64Array::from(ids)) as ArrayRef),
30///         ("time", Arc::new(TimestampMillisecondArray::from(times)) as ArrayRef),
31///     ])
32/// };
33/// let mut table = LiveTable::new(ticks(vec![], vec![])?.schema(), "id")?;
34/// table.set_window(Some(Window::duration("time", Duration::from_secs(60))))?;
35///
36/// table.upsert(ticks(vec![1, 2], vec![0, 30_000])?)?;
37/// let changes = table.upsert(ticks(vec![3], vec![75_000])?)?;
38/// assert_eq!(changes.deleted, 1); // row 1 is 75 s older than the newest row
39/// assert_eq!(table.len(), 2);
40/// # Ok::<(), Box<dyn std::error::Error>>(())
41/// ```
42#[derive(Debug, Clone, PartialEq)]
43pub struct Window {
44    column: String,
45    span: Span,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
49enum Span {
50    Duration(Duration),
51    Value(f64),
52}
53
54impl Window {
55    /// A window of `span` over a column of timestamps, dates, times of day or durations.
56    #[must_use]
57    pub fn duration(column: impl Into<String>, span: Duration) -> Self {
58        Self {
59            column: column.into(),
60            span: Span::Duration(span),
61        }
62    }
63
64    /// A window of `span` over a column of numbers (integers, decimals or floats), in the
65    /// column's own units: `60_000.0` keeps a minute of milliseconds. `span` must be zero or
66    /// more. A span within a billionth of a whole number of an integer or decimal column's
67    /// units counts as that number, so float error (`0.29` hundredths) loses no unit.
68    #[must_use]
69    pub fn span(column: impl Into<String>, span: f64) -> Self {
70        Self {
71            column: column.into(),
72            span: Span::Value(span),
73        }
74    }
75
76    /// The time column's name.
77    pub fn column(&self) -> &str {
78        &self.column
79    }
80}
81
82/// How far below the newest value a row may be, in the column's units: whole units for
83/// integer columns (differences are whole, so the span rounds down without changing which
84/// rows stay), the span itself for floats.
85#[derive(Debug, Clone, Copy, PartialEq)]
86enum Reach {
87    Whole(i128),
88    Float(f64),
89}
90
91/// A window bound to its column, with the table's rows in the column's order.
92#[derive(Debug, Clone)]
93pub(crate) struct WindowState {
94    spec: Window,
95    reach: Reach,
96    /// Every live row, oldest first (nulls, then NaN, last).
97    pub(crate) rows: ViewState,
98    column: usize,
99    read: ValueFn,
100}
101
102impl WindowState {
103    pub(crate) fn compile(spec: &Window, schema: &Schema) -> Result<Self, DataError> {
104        let (column, kind, read) = resolve(schema, &spec.column)?;
105        let mismatch = || DataError::WindowSpan(spec.column.clone());
106        let reach = match (spec.span, time_unit(schema.field(column).data_type())) {
107            (Span::Duration(span), Some(unit)) => {
108                // Whole units, rounding down. Lossless: at most 2^64 seconds of nanoseconds.
109                Reach::Whole((span.as_nanos() / unit).cast_signed())
110            }
111            (Span::Value(span), None) if span >= 0.0 => match kind {
112                Kind::Int | Kind::UInt => Reach::Whole(whole(span)),
113                Kind::Decimal { scale } => Reach::Whole(whole(span * 10f64.powi(scale.into()))),
114                Kind::Float => Reach::Float(span),
115                Kind::Bool | Kind::Str => return Err(mismatch()),
116            },
117            _ => return Err(mismatch()),
118        };
119        Ok(Self {
120            spec: spec.clone(),
121            reach,
122            rows: ViewState::sorted(SortColumn::ascending(column, read)),
123            column,
124            read,
125        })
126    }
127
128    pub(crate) fn spec(&self) -> &Window {
129        &self.spec
130    }
131
132    /// How many of the oldest rows are past the window.
133    pub(crate) fn expired(&self, rows: Rows<'_>) -> usize {
134        let value = |id| {
135            let at = rows.at(id);
136            (self.read)(rows.batches[at.batch].column(self.column).as_ref(), at.row)
137        };
138        // Values below +inf come first (-inf, then numbers); +inf, NaN and nulls after them.
139        // The newest is the last of those. If it is -inf, every row below +inf is -inf, and
140        // `-inf - -inf` is NaN: none is older.
141        let below = self.rows.partition_point(|id| below_infinity(value(id)));
142        let Some(newest) = below.checked_sub(1).and_then(|i| self.rows.get(i)) else {
143            return 0;
144        };
145        let newest = value(newest);
146        self.rows
147            .partition_point(|id| older(value(id), newest, self.reach))
148    }
149}
150
151const NANOS_PER_DAY: u128 = 86_400 * 1_000_000_000;
152
153/// The nanoseconds in one unit of a column of dates, timestamps, times or durations; `None`
154/// for other types.
155fn time_unit(data_type: &DataType) -> Option<u128> {
156    let unit = match data_type {
157        DataType::Timestamp(unit, _)
158        | DataType::Time32(unit)
159        | DataType::Time64(unit)
160        | DataType::Duration(unit) => unit,
161        DataType::Date32 => return Some(NANOS_PER_DAY),
162        DataType::Date64 => &TimeUnit::Millisecond,
163        _ => return None,
164    };
165    Some(match unit {
166        TimeUnit::Second => 1_000_000_000,
167        TimeUnit::Millisecond => 1_000_000,
168        TimeUnit::Microsecond => 1_000,
169        TimeUnit::Nanosecond => 1,
170    })
171}
172
173/// `span` rounded down to a whole number (saturating: a span past `i128` keeps every row).
174/// A whole number a float error away is taken as it: `0.29 * 100.0` is 28.999999999999996.
175fn whole(span: f64) -> i128 {
176    let nearest = span.round();
177    let span = match (span - nearest).abs() <= NEAR * nearest.abs() {
178        true => nearest,
179        false => span.floor(),
180    };
181    // Saturating cast; `span` is not NaN (checked by the caller's `>= 0.0`).
182    span as i128
183}
184
185/// How close, relative to it, a span must be to a whole number to be taken as it.
186const NEAR: f64 = 1e-9;
187
188fn below_infinity(value: Value<'_>) -> bool {
189    match value {
190        Value::Int(_) | Value::UInt(_) | Value::Decimal(_) => true,
191        Value::Float(v) => v < f64::INFINITY,
192        Value::Null | Value::Bool(_) | Value::Str(_) => false,
193    }
194}
195
196/// Whether `value` is more than `reach` below `newest`, the window's end (a value of the same
197/// column below +inf).
198fn older(value: Value<'_>, newest: Value<'_>, reach: Reach) -> bool {
199    let whole = |v: Value<'_>| match v {
200        Value::Int(v) => Some(i128::from(v)),
201        Value::UInt(v) => Some(i128::from(v)),
202        Value::Decimal(v) => Some(v),
203        _ => None,
204    };
205    match (reach, value, newest) {
206        (Reach::Float(reach), Value::Float(value), Value::Float(newest)) => newest - value > reach,
207        (Reach::Whole(reach), value, newest) => match (whole(value), whole(newest)) {
208            // Saturating: two decimals far apart are as far apart as can be.
209            (Some(value), Some(newest)) => newest.saturating_sub(value) > reach,
210            _ => false,
211        },
212        _ => false,
213    }
214}