Skip to main content

uni_query/query/rewrite/rules/
temporal.rs

1/// Temporal function rewrite rules
2///
3/// This module implements rewrite rules for temporal functions, transforming
4/// them into equivalent predicate expressions that can be pushed down to storage.
5use crate::query::rewrite::context::RewriteContext;
6use crate::query::rewrite::error::RewriteError;
7use crate::query::rewrite::rule::{ArgConstraints, Arity, RewriteRule};
8use uni_cypher::ast::{BinaryOp, CypherLiteral, Expr};
9
10/// Helper function to extract a string literal from an expression
11fn extract_string_literal(expr: &Expr) -> Result<String, RewriteError> {
12    match expr {
13        Expr::Literal(CypherLiteral::String(s)) => Ok(s.clone()),
14        _ => Err(RewriteError::TransformError {
15            message: "Expected string literal".to_string(),
16        }),
17    }
18}
19
20/// Build a property access expression: entity.property_name
21fn property(entity: Expr, property_name: String) -> Expr {
22    Expr::Property(Box::new(entity), property_name)
23}
24
25/// Build: entity.end_prop IS NULL OR entity.end_prop > timestamp
26///
27/// This implements half-open interval semantics: [start, end) where end is exclusive.
28/// For an entity to be valid at a timestamp, we need: start <= timestamp < end
29fn ongoing_or_after(entity: Expr, end_prop: String, timestamp: Expr) -> Expr {
30    Expr::BinaryOp {
31        left: Box::new(Expr::IsNull(Box::new(property(
32            entity.clone(),
33            end_prop.clone(),
34        )))),
35        op: BinaryOp::Or,
36        right: Box::new(Expr::BinaryOp {
37            left: Box::new(property(entity, end_prop)),
38            op: BinaryOp::Gt,
39            right: Box::new(timestamp),
40        }),
41    }
42}
43
44/// Rewrite rule for uni.temporal.validAt
45///
46/// Transforms: uni.temporal.validAt(e, 'start', 'end', ts)
47/// Into: e.start <= ts AND (e.end IS NULL OR e.end > ts)
48///
49/// This implements half-open interval semantics: [start, end) where:
50/// - start is inclusive (<=)
51/// - end is exclusive (>)
52/// - null end means "ongoing" (no end date)
53pub struct ValidAtRule;
54
55impl RewriteRule for ValidAtRule {
56    fn function_name(&self) -> &str {
57        "uni.temporal.validAt"
58    }
59
60    fn validate_args(&self, args: &[Expr]) -> Result<(), RewriteError> {
61        let constraints = ArgConstraints {
62            arity: Arity::Exact(4),
63            literal_args: vec![1, 2], // Property names must be literals
64            entity_arg: Some(0),      // First arg is entity
65        };
66        constraints.validate(args)
67    }
68
69    fn rewrite(&self, args: Vec<Expr>, _ctx: &RewriteContext) -> Result<Expr, RewriteError> {
70        let entity = args[0].clone();
71        let start_prop = extract_string_literal(&args[1])?;
72        let end_prop = extract_string_literal(&args[2])?;
73        let timestamp = args[3].clone();
74
75        // Build: e.start <= ts AND (e.end IS NULL OR e.end > ts)
76        Ok(Expr::BinaryOp {
77            left: Box::new(Expr::BinaryOp {
78                left: Box::new(property(entity.clone(), start_prop)),
79                op: BinaryOp::LtEq,
80                right: Box::new(timestamp.clone()),
81            }),
82            op: BinaryOp::And,
83            right: Box::new(ongoing_or_after(entity, end_prop, timestamp)),
84        })
85    }
86}
87
88/// Rewrite rule for uni.temporal.overlaps
89///
90/// Transforms: uni.temporal.overlaps(e, 'start', 'end', range_start, range_end)
91/// Into: e.start <= range_end AND (e.end IS NULL OR e.end > range_start)
92///
93/// Uses half-open interval semantics: entity range [start, end) overlaps with
94/// query range [range_start, range_end) when start < range_end AND end > range_start.
95pub struct OverlapsRule;
96
97impl RewriteRule for OverlapsRule {
98    fn function_name(&self) -> &str {
99        "uni.temporal.overlaps"
100    }
101
102    fn validate_args(&self, args: &[Expr]) -> Result<(), RewriteError> {
103        let constraints = ArgConstraints {
104            arity: Arity::Exact(5),
105            literal_args: vec![1, 2], // Property names must be literals
106            entity_arg: Some(0),      // First arg is entity
107        };
108        constraints.validate(args)
109    }
110
111    fn rewrite(&self, args: Vec<Expr>, _ctx: &RewriteContext) -> Result<Expr, RewriteError> {
112        let entity = args[0].clone();
113        let start_prop = extract_string_literal(&args[1])?;
114        let end_prop = extract_string_literal(&args[2])?;
115        let range_start = args[3].clone();
116        let range_end = args[4].clone();
117
118        // Build: e.start <= range_end AND (e.end IS NULL OR e.end > range_start)
119        Ok(Expr::BinaryOp {
120            left: Box::new(Expr::BinaryOp {
121                left: Box::new(property(entity.clone(), start_prop)),
122                op: BinaryOp::LtEq,
123                right: Box::new(range_end),
124            }),
125            op: BinaryOp::And,
126            right: Box::new(ongoing_or_after(entity, end_prop, range_start)),
127        })
128    }
129}
130
131/// Rewrite rule for uni.temporal.precedes
132///
133/// Transforms: uni.temporal.precedes(e, 'end', ts)
134/// Into: e.end < ts
135///
136/// This checks if the entity's end time is before the given timestamp.
137/// Note: This returns NULL if e.end is NULL (ongoing periods don't precede).
138pub struct PrecedesRule;
139
140impl RewriteRule for PrecedesRule {
141    fn function_name(&self) -> &str {
142        "uni.temporal.precedes"
143    }
144
145    fn validate_args(&self, args: &[Expr]) -> Result<(), RewriteError> {
146        let constraints = ArgConstraints {
147            arity: Arity::Exact(3),
148            literal_args: vec![1], // Property name must be literal
149            entity_arg: Some(0),   // First arg is entity
150        };
151        constraints.validate(args)
152    }
153
154    fn rewrite(&self, args: Vec<Expr>, _ctx: &RewriteContext) -> Result<Expr, RewriteError> {
155        let entity = args[0].clone();
156        let end_prop = extract_string_literal(&args[1])?;
157        let timestamp = args[2].clone();
158
159        // Build: e.end < ts
160        Ok(Expr::BinaryOp {
161            left: Box::new(property(entity, end_prop)),
162            op: BinaryOp::Lt,
163            right: Box::new(timestamp),
164        })
165    }
166}
167
168/// Rewrite rule for uni.temporal.succeeds
169///
170/// Transforms: uni.temporal.succeeds(e, 'start', ts)
171/// Into: e.start > ts
172///
173/// This checks if the entity's start time is after the given timestamp.
174pub struct SucceedsRule;
175
176impl RewriteRule for SucceedsRule {
177    fn function_name(&self) -> &str {
178        "uni.temporal.succeeds"
179    }
180
181    fn validate_args(&self, args: &[Expr]) -> Result<(), RewriteError> {
182        let constraints = ArgConstraints {
183            arity: Arity::Exact(3),
184            literal_args: vec![1], // Property name must be literal
185            entity_arg: Some(0),   // First arg is entity
186        };
187        constraints.validate(args)
188    }
189
190    fn rewrite(&self, args: Vec<Expr>, _ctx: &RewriteContext) -> Result<Expr, RewriteError> {
191        let entity = args[0].clone();
192        let start_prop = extract_string_literal(&args[1])?;
193        let timestamp = args[2].clone();
194
195        // Build: e.start > ts
196        Ok(Expr::BinaryOp {
197            left: Box::new(property(entity, start_prop)),
198            op: BinaryOp::Gt,
199            right: Box::new(timestamp),
200        })
201    }
202}
203
204/// Rewrite rule for uni.temporal.isOngoing
205///
206/// Transforms: uni.temporal.isOngoing(e, 'end')
207/// Into: e.end IS NULL
208///
209/// This checks if the entity is currently ongoing (no end date).
210pub struct IsOngoingRule;
211
212impl RewriteRule for IsOngoingRule {
213    fn function_name(&self) -> &str {
214        "uni.temporal.isOngoing"
215    }
216
217    fn validate_args(&self, args: &[Expr]) -> Result<(), RewriteError> {
218        let constraints = ArgConstraints {
219            arity: Arity::Exact(2),
220            literal_args: vec![1], // Property name must be literal
221            entity_arg: Some(0),   // First arg is entity
222        };
223        constraints.validate(args)
224    }
225
226    fn rewrite(&self, args: Vec<Expr>, _ctx: &RewriteContext) -> Result<Expr, RewriteError> {
227        let entity = args[0].clone();
228        let end_prop = extract_string_literal(&args[1])?;
229
230        // Build: e.end IS NULL
231        Ok(Expr::IsNull(Box::new(property(entity, end_prop))))
232    }
233}
234
235/// Rewrite rule for uni.temporal.hasClosed
236///
237/// Transforms: uni.temporal.hasClosed(e, 'end')
238/// Into: e.end IS NOT NULL
239///
240/// This checks if the entity has ended (has an end date).
241pub struct HasClosedRule;
242
243impl RewriteRule for HasClosedRule {
244    fn function_name(&self) -> &str {
245        "uni.temporal.hasClosed"
246    }
247
248    fn validate_args(&self, args: &[Expr]) -> Result<(), RewriteError> {
249        let constraints = ArgConstraints {
250            arity: Arity::Exact(2),
251            literal_args: vec![1], // Property name must be literal
252            entity_arg: Some(0),   // First arg is entity
253        };
254        constraints.validate(args)
255    }
256
257    fn rewrite(&self, args: Vec<Expr>, _ctx: &RewriteContext) -> Result<Expr, RewriteError> {
258        let entity = args[0].clone();
259        let end_prop = extract_string_literal(&args[1])?;
260
261        // Build: e.end IS NOT NULL
262        Ok(Expr::IsNotNull(Box::new(property(entity, end_prop))))
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    fn test_entity() -> Expr {
271        Expr::Variable("e".into())
272    }
273
274    fn test_timestamp() -> Expr {
275        Expr::Variable("ts".into())
276    }
277
278    #[test]
279    fn test_valid_at_validation() {
280        let rule = ValidAtRule;
281
282        // Valid arguments
283        let valid_args = vec![
284            test_entity(),
285            Expr::Literal(CypherLiteral::String("start".into())),
286            Expr::Literal(CypherLiteral::String("end".into())),
287            test_timestamp(),
288        ];
289        assert!(rule.validate_args(&valid_args).is_ok());
290
291        // Wrong arity
292        let wrong_arity = vec![test_entity()];
293        assert!(rule.validate_args(&wrong_arity).is_err());
294
295        // Non-literal property name
296        let non_literal = vec![
297            test_entity(),
298            Expr::Variable("prop".into()), // Should be literal
299            Expr::Literal(CypherLiteral::String("end".into())),
300            test_timestamp(),
301        ];
302        assert!(rule.validate_args(&non_literal).is_err());
303    }
304
305    #[test]
306    fn test_valid_at_rewrite() {
307        let rule = ValidAtRule;
308        let ctx = RewriteContext::default();
309
310        let args = vec![
311            test_entity(),
312            Expr::Literal(CypherLiteral::String("start".into())),
313            Expr::Literal(CypherLiteral::String("end".into())),
314            test_timestamp(),
315        ];
316
317        let result = rule.rewrite(args, &ctx).unwrap();
318
319        // Should be an AND expression
320        assert!(matches!(
321            result,
322            Expr::BinaryOp {
323                op: BinaryOp::And,
324                ..
325            }
326        ));
327    }
328
329    #[test]
330    fn test_overlaps_rewrite() {
331        let rule = OverlapsRule;
332        let ctx = RewriteContext::default();
333
334        let args = vec![
335            test_entity(),
336            Expr::Literal(CypherLiteral::String("start".into())),
337            Expr::Literal(CypherLiteral::String("end".into())),
338            Expr::Variable("rs".into()),
339            Expr::Variable("re".into()),
340        ];
341
342        let result = rule.rewrite(args, &ctx).unwrap();
343
344        // Should be an AND expression
345        assert!(matches!(
346            result,
347            Expr::BinaryOp {
348                op: BinaryOp::And,
349                ..
350            }
351        ));
352    }
353
354    #[test]
355    fn test_precedes_rewrite() {
356        let rule = PrecedesRule;
357        let ctx = RewriteContext::default();
358
359        let args = vec![
360            test_entity(),
361            Expr::Literal(CypherLiteral::String("end".into())),
362            test_timestamp(),
363        ];
364
365        let result = rule.rewrite(args, &ctx).unwrap();
366
367        // Should be a less-than expression
368        assert!(matches!(
369            result,
370            Expr::BinaryOp {
371                op: BinaryOp::Lt,
372                ..
373            }
374        ));
375    }
376
377    #[test]
378    fn test_succeeds_rewrite() {
379        let rule = SucceedsRule;
380        let ctx = RewriteContext::default();
381
382        let args = vec![
383            test_entity(),
384            Expr::Literal(CypherLiteral::String("start".into())),
385            test_timestamp(),
386        ];
387
388        let result = rule.rewrite(args, &ctx).unwrap();
389
390        // Should be a greater-than expression
391        assert!(matches!(
392            result,
393            Expr::BinaryOp {
394                op: BinaryOp::Gt,
395                ..
396            }
397        ));
398    }
399
400    #[test]
401    fn test_is_ongoing_rewrite() {
402        let rule = IsOngoingRule;
403        let ctx = RewriteContext::default();
404
405        let args = vec![
406            test_entity(),
407            Expr::Literal(CypherLiteral::String("end".into())),
408        ];
409
410        let result = rule.rewrite(args, &ctx).unwrap();
411
412        // Should be an IS NULL expression
413        assert!(matches!(result, Expr::IsNull(_)));
414    }
415
416    #[test]
417    fn test_has_closed_rewrite() {
418        let rule = HasClosedRule;
419        let ctx = RewriteContext::default();
420
421        let args = vec![
422            test_entity(),
423            Expr::Literal(CypherLiteral::String("end".into())),
424        ];
425
426        let result = rule.rewrite(args, &ctx).unwrap();
427
428        // Should be an IS NOT NULL expression
429        assert!(matches!(result, Expr::IsNotNull(_)));
430    }
431}