laminar_sql/datafusion/
watermark_udf.rs1use std::hash::{Hash, Hasher};
11use std::sync::atomic::{AtomicI64, Ordering};
12use std::sync::Arc;
13
14use arrow::datatypes::{DataType, TimeUnit};
15use datafusion_common::{Result, ScalarValue};
16use datafusion_expr::{
17 ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature, Volatility,
18};
19
20pub const NO_WATERMARK: i64 = -1;
23
24#[derive(Debug)]
33pub struct WatermarkUdf {
34 signature: Signature,
35 watermark_ms: Arc<AtomicI64>,
36}
37
38impl WatermarkUdf {
39 pub fn new(watermark_ms: Arc<AtomicI64>) -> Self {
46 Self {
47 signature: Signature::new(TypeSignature::Nullary, Volatility::Volatile),
48 watermark_ms,
49 }
50 }
51
52 #[must_use]
54 pub fn unset() -> Self {
55 Self::new(Arc::new(AtomicI64::new(NO_WATERMARK)))
56 }
57
58 #[must_use]
60 pub fn watermark_ref(&self) -> &Arc<AtomicI64> {
61 &self.watermark_ms
62 }
63}
64
65impl PartialEq for WatermarkUdf {
66 fn eq(&self, _other: &Self) -> bool {
67 true }
69}
70
71impl Eq for WatermarkUdf {}
72
73impl Hash for WatermarkUdf {
74 fn hash<H: Hasher>(&self, state: &mut H) {
75 "watermark".hash(state);
76 }
77}
78
79impl ScalarUDFImpl for WatermarkUdf {
80 fn as_any(&self) -> &dyn std::any::Any {
81 self
82 }
83
84 fn name(&self) -> &'static str {
85 "watermark"
86 }
87
88 fn signature(&self) -> &Signature {
89 &self.signature
90 }
91
92 fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
93 Ok(DataType::Timestamp(TimeUnit::Millisecond, None))
94 }
95
96 fn invoke_with_args(&self, _args: ScalarFunctionArgs) -> Result<ColumnarValue> {
97 let wm = self.watermark_ms.load(Ordering::Relaxed);
98 if wm < 0 {
99 Ok(ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(
100 None, None,
101 )))
102 } else {
103 Ok(ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(
104 Some(wm),
105 None,
106 )))
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use arrow::datatypes::TimeUnit;
115 use arrow_schema::Field;
116 use datafusion_common::config::ConfigOptions;
117 use datafusion_expr::ScalarUDF;
118
119 fn make_args() -> ScalarFunctionArgs {
120 ScalarFunctionArgs {
121 args: vec![],
122 arg_fields: vec![],
123 number_rows: 1,
124 return_field: Arc::new(Field::new(
125 "output",
126 DataType::Timestamp(TimeUnit::Millisecond, None),
127 true,
128 )),
129 config_options: Arc::new(ConfigOptions::default()),
130 }
131 }
132
133 #[test]
134 fn test_watermark_default_null() {
135 let udf = WatermarkUdf::unset();
136 let result = udf.invoke_with_args(make_args()).unwrap();
137 match result {
138 ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(None, _)) => {}
139 other => panic!("Expected NULL when no watermark set, got: {other:?}"),
140 }
141 }
142
143 #[test]
144 fn test_watermark_fixed_value() {
145 let wm = Arc::new(AtomicI64::new(1_000_000));
146 let udf = WatermarkUdf::new(Arc::clone(&wm));
147
148 let result = udf.invoke_with_args(make_args()).unwrap();
149 match result {
150 ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(Some(v), _)) => {
151 assert_eq!(v, 1_000_000);
152 }
153 other => panic!("Expected TimestampMillisecond(1_000_000), got: {other:?}"),
154 }
155 }
156
157 #[test]
158 fn test_watermark_atomic_update() {
159 let wm = Arc::new(AtomicI64::new(NO_WATERMARK));
160 let udf = WatermarkUdf::new(Arc::clone(&wm));
161
162 let result = udf.invoke_with_args(make_args()).unwrap();
164 assert!(matches!(
165 result,
166 ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(None, _))
167 ));
168
169 wm.store(500_000, Ordering::Relaxed);
171 let result = udf.invoke_with_args(make_args()).unwrap();
172 match result {
173 ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(Some(v), _)) => {
174 assert_eq!(v, 500_000);
175 }
176 other => panic!("Expected updated watermark, got: {other:?}"),
177 }
178 }
179
180 #[test]
181 fn test_watermark_registration() {
182 let udf = ScalarUDF::new_from_impl(WatermarkUdf::unset());
183 assert_eq!(udf.name(), "watermark");
184 }
185}