uni_query/query/rewrite/rules/
temporal.rs1use 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
10fn 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
20fn property(entity: Expr, property_name: String) -> Expr {
22 Expr::Property(Box::new(entity), property_name)
23}
24
25fn 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
44pub 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], entity_arg: Some(0), };
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 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
88pub 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], entity_arg: Some(0), };
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 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
131pub 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], entity_arg: Some(0), };
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 Ok(Expr::BinaryOp {
161 left: Box::new(property(entity, end_prop)),
162 op: BinaryOp::Lt,
163 right: Box::new(timestamp),
164 })
165 }
166}
167
168pub 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], entity_arg: Some(0), };
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 Ok(Expr::BinaryOp {
197 left: Box::new(property(entity, start_prop)),
198 op: BinaryOp::Gt,
199 right: Box::new(timestamp),
200 })
201 }
202}
203
204pub 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], entity_arg: Some(0), };
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 Ok(Expr::IsNull(Box::new(property(entity, end_prop))))
232 }
233}
234
235pub 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], entity_arg: Some(0), };
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 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 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 let wrong_arity = vec![test_entity()];
293 assert!(rule.validate_args(&wrong_arity).is_err());
294
295 let non_literal = vec![
297 test_entity(),
298 Expr::Variable("prop".into()), 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 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 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 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 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 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 assert!(matches!(result, Expr::IsNotNull(_)));
430 }
431}