laminar_sql/datafusion/window_udf/
mod.rs1use std::hash::{Hash, Hasher};
24use std::ops::RangeInclusive;
25use std::sync::Arc;
26
27use arrow::datatypes::{DataType, TimeUnit};
28use arrow_array::{ArrayRef, TimestampMicrosecondArray, TimestampMillisecondArray};
29use datafusion_common::{DataFusionError, Result, ScalarValue};
30use datafusion_expr::{
31 ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature, Volatility,
32};
33use laminar_core::time::cast_to_millis_array;
34
35macro_rules! window_udf {
43 (
44 $(#[$attr:meta])*
45 $type:ident, $sql_name:literal, $arity:expr, |$args:ident| $body:expr
46 ) => {
47 $(#[$attr])*
48 #[derive(Debug)]
49 pub struct $type {
50 signature: Signature,
51 }
52
53 impl $type {
54 #[must_use]
56 pub fn new() -> Self {
57 Self { signature: variadic_signature($arity) }
58 }
59 }
60
61 impl Default for $type {
62 fn default() -> Self { Self::new() }
63 }
64
65 impl PartialEq for $type {
66 fn eq(&self, _: &Self) -> bool { true }
67 }
68
69 impl Eq for $type {}
70
71 impl Hash for $type {
72 fn hash<H: Hasher>(&self, state: &mut H) { $sql_name.hash(state); }
73 }
74
75 impl ScalarUDFImpl for $type {
76 fn as_any(&self) -> &dyn std::any::Any {
77 self
78 }
79
80 fn name(&self) -> &'static str { $sql_name }
81 fn signature(&self) -> &Signature { &self.signature }
82 fn return_type(&self, _: &[DataType]) -> Result<DataType> {
83 Ok(DataType::Timestamp(TimeUnit::Microsecond, None))
84 }
85 fn invoke_with_args(&self, sfa: ScalarFunctionArgs) -> Result<ColumnarValue> {
86 let $args = sfa.args;
87 check_arity(&$args, $arity, $sql_name)?;
88 $body
89 }
90 }
91 };
92}
93
94window_udf!(
97 TumbleWindowStart,
100 "tumble",
101 2..=3,
102 |args| {
103 let interval_ms = positive_interval(&args[1], "tumble", "interval")?;
104 let offset_ms = optional_offset(&args, 2)?;
105 into_us_columnar(&args[0], |ts| tumble_start_ms(ts, interval_ms, offset_ms))
106 }
107);
108
109window_udf!(
110 TumbleWindowEnd,
113 "tumble_end",
114 2..=3,
115 |args| {
116 let interval_ms = positive_interval(&args[1], "tumble_end", "interval")?;
117 let offset_ms = optional_offset(&args, 2)?;
118 into_us_columnar(&args[0], |ts| {
119 tumble_start_ms(ts, interval_ms, offset_ms).saturating_add(interval_ms)
120 })
121 }
122);
123
124window_udf!(
125 HopWindowStart,
129 "hop",
130 3..=4,
131 |args| {
132 let slide_ms = positive_interval(&args[1], "hop", "slide")?;
133 let size_ms = positive_interval(&args[2], "hop", "size")?;
134 let offset_ms = optional_offset(&args, 3)?;
135 into_us_columnar(&args[0], |ts| {
136 hop_start_ms(ts, slide_ms, size_ms, offset_ms)
137 })
138 }
139);
140
141window_udf!(
142 HopWindowEnd,
145 "hop_end",
146 3..=4,
147 |args| {
148 let slide_ms = positive_interval(&args[1], "hop_end", "slide")?;
149 let size_ms = positive_interval(&args[2], "hop_end", "size")?;
150 let offset_ms = optional_offset(&args, 3)?;
151 into_us_columnar(&args[0], |ts| {
152 hop_start_ms(ts, slide_ms, size_ms, offset_ms).saturating_add(size_ms)
153 })
154 }
155);
156
157window_udf!(
158 SessionWindowStart,
163 "session",
164 2..=2,
165 |args| into_us_columnar(&args[0], |ts| ts)
166);
167
168window_udf!(
169 CumulateWindowStart,
173 "cumulate",
174 3..=3,
175 |args| {
176 let (_step_ms, size_ms) = cumulate_intervals(&args, "cumulate")?;
177 into_us_columnar(&args[0], |ts| tumble_start_ms(ts, size_ms, 0))
178 }
179);
180
181window_udf!(
182 CumulateWindowEnd,
185 "cumulate_end",
186 3..=3,
187 |args| {
188 let (_step_ms, size_ms) = cumulate_intervals(&args, "cumulate_end")?;
189 into_us_columnar(&args[0], |ts| {
190 tumble_start_ms(ts, size_ms, 0).saturating_add(size_ms)
191 })
192 }
193);
194
195fn tumble_start_ms(ts: i64, interval_ms: i64, offset_ms: i64) -> i64 {
201 let adj = ts - offset_ms;
202 (adj - adj.rem_euclid(interval_ms)) + offset_ms
203}
204
205fn hop_start_ms(ts: i64, slide_ms: i64, size_ms: i64, offset_ms: i64) -> i64 {
208 let adj = ts - size_ms + slide_ms - offset_ms;
209 (adj - adj.rem_euclid(slide_ms)) + offset_ms
210}
211
212fn variadic_signature(arity: RangeInclusive<usize>) -> Signature {
217 let mut arities: Vec<TypeSignature> = arity.map(TypeSignature::Any).collect();
218 let inner = if arities.len() == 1 {
219 arities.pop().unwrap()
220 } else {
221 TypeSignature::OneOf(arities)
222 };
223 Signature::new(inner, Volatility::Immutable)
224}
225
226fn check_arity(args: &[ColumnarValue], arity: RangeInclusive<usize>, fn_name: &str) -> Result<()> {
227 if arity.contains(&args.len()) {
228 return Ok(());
229 }
230 let expected = if arity.start() == arity.end() {
231 format!("exactly {}", arity.start())
232 } else {
233 format!("{}-{}", arity.start(), arity.end())
234 };
235 Err(DataFusionError::Plan(format!(
236 "{}() requires {} arguments, got {}",
237 fn_name,
238 expected,
239 args.len()
240 )))
241}
242
243fn positive_interval(value: &ColumnarValue, fn_name: &str, arg_name: &str) -> Result<i64> {
244 let ms = extract_interval_ms(value)?;
245 if ms <= 0 {
246 return Err(DataFusionError::Plan(format!(
247 "{fn_name}() {arg_name} must be positive"
248 )));
249 }
250 Ok(ms)
251}
252
253fn optional_offset(args: &[ColumnarValue], idx: usize) -> Result<i64> {
254 match args.get(idx) {
255 Some(value) => extract_interval_ms(value),
256 None => Ok(0),
257 }
258}
259
260fn cumulate_intervals(args: &[ColumnarValue], fn_name: &str) -> Result<(i64, i64)> {
261 let step_ms = positive_interval(&args[1], fn_name, "step")?;
262 let size_ms = positive_interval(&args[2], fn_name, "size")?;
263 if step_ms > size_ms {
264 return Err(DataFusionError::Plan(format!(
265 "{fn_name}() step must not exceed size"
266 )));
267 }
268 if size_ms % step_ms != 0 {
269 return Err(DataFusionError::Plan(format!(
270 "{fn_name}() size must be evenly divisible by step"
271 )));
272 }
273 Ok((step_ms, size_ms))
274}
275
276fn extract_interval_ms(value: &ColumnarValue) -> Result<i64> {
279 match value {
280 ColumnarValue::Scalar(scalar) => scalar_interval_to_ms(scalar),
281 ColumnarValue::Array(_) => Err(DataFusionError::NotImplemented(
282 "Array interval arguments not supported for window functions".to_string(),
283 )),
284 }
285}
286
287fn scalar_interval_to_ms(scalar: &ScalarValue) -> Result<i64> {
288 match scalar {
289 ScalarValue::IntervalDayTime(Some(v)) => {
290 Ok(i64::from(v.days) * 86_400_000 + i64::from(v.milliseconds))
291 }
292 ScalarValue::IntervalMonthDayNano(Some(v)) => {
293 if v.months != 0 {
294 return Err(DataFusionError::NotImplemented(
295 "Month-based intervals not supported for window functions \
296 (use days/hours/minutes/seconds)"
297 .to_string(),
298 ));
299 }
300 Ok(i64::from(v.days) * 86_400_000 + v.nanoseconds / 1_000_000)
301 }
302 ScalarValue::IntervalYearMonth(_) => Err(DataFusionError::NotImplemented(
303 "Year-month intervals not supported for window functions".to_string(),
304 )),
305 ScalarValue::Int64(Some(ms)) => Ok(*ms),
306 _ => Err(DataFusionError::Plan(format!(
307 "Expected interval argument for window function, got: {scalar:?}"
308 ))),
309 }
310}
311
312fn into_us_columnar(
318 value: &ColumnarValue,
319 transform: impl Fn(i64) -> i64,
320) -> Result<ColumnarValue> {
321 match value {
322 ColumnarValue::Array(array) => {
323 let input = to_millis_array(array)?;
324 let result: TimestampMicrosecondArray = input
325 .iter()
326 .map(|opt| opt.map(|ms| transform(ms).saturating_mul(1000)))
327 .collect();
328 Ok(ColumnarValue::Array(Arc::new(result)))
329 }
330 ColumnarValue::Scalar(scalar) => {
331 let result =
332 scalar_to_timestamp_ms(scalar)?.map(|ms| transform(ms).saturating_mul(1000));
333 Ok(ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(
334 result, None,
335 )))
336 }
337 }
338}
339
340fn to_millis_array(array: &ArrayRef) -> Result<TimestampMillisecondArray> {
341 cast_to_millis_array(array.as_ref()).map_err(|e| DataFusionError::Plan(e.to_string()))
342}
343
344fn scalar_to_timestamp_ms(scalar: &ScalarValue) -> Result<Option<i64>> {
345 match scalar {
346 ScalarValue::TimestampMillisecond(v, _) | ScalarValue::Int64(v) => Ok(*v),
347 ScalarValue::TimestampMicrosecond(v, _) => Ok(v.map(|v| v / 1_000)),
348 ScalarValue::TimestampNanosecond(v, _) => Ok(v.map(|v| v / 1_000_000)),
349 ScalarValue::TimestampSecond(v, _) => Ok(v.map(|v| v * 1_000)),
350 _ => Err(DataFusionError::Plan(format!(
351 "Expected timestamp argument for window function, got: {scalar:?}"
352 ))),
353 }
354}
355
356#[cfg(test)]
357mod tests;