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}