1use crate::ast::{CompareOp, Expr, FieldRef};
18use reddb_types::types::Value;
19
20pub struct GeoWithinPredicate<'a> {
22 pub column: &'a str,
24 pub vertices: Vec<(f64, f64)>,
26}
27
28pub fn literal_polygon_vertices(args: &[Expr]) -> Option<Vec<(f64, f64)>> {
34 if args.len() < 6 || !args.len().is_multiple_of(2) {
35 return None;
36 }
37 let mut vertices = Vec::with_capacity(args.len() / 2);
38 for pair in args.chunks_exact(2) {
39 vertices.push((literal_f64(&pair[0])?, literal_f64(&pair[1])?));
40 }
41 Some(vertices)
42}
43
44pub fn geo_within_call(expr: &Expr) -> Option<GeoWithinPredicate<'_>> {
49 let Expr::FunctionCall { name, args, .. } = expr else {
50 return None;
51 };
52 if !name.eq_ignore_ascii_case("GEO_WITHIN") {
53 return None;
54 }
55 let [Expr::Column { field, .. }, polygon] = args.as_slice() else {
56 return None;
57 };
58 let FieldRef::TableColumn { column, .. } = field else {
59 return None;
60 };
61 let Expr::FunctionCall {
62 name: polygon_name,
63 args: polygon_args,
64 ..
65 } = polygon
66 else {
67 return None;
68 };
69 if !polygon_name.eq_ignore_ascii_case("POLYGON") {
70 return None;
71 }
72 Some(GeoWithinPredicate {
73 column: column.as_str(),
74 vertices: literal_polygon_vertices(polygon_args)?,
75 })
76}
77
78pub fn geo_within_truth_test<'a>(
86 lhs: &'a Expr,
87 op: CompareOp,
88 rhs: &Expr,
89) -> Option<GeoWithinPredicate<'a>> {
90 if !matches!(op, CompareOp::Eq) {
91 return None;
92 }
93 if !matches!(
94 rhs,
95 Expr::Literal {
96 value: Value::Boolean(true),
97 ..
98 }
99 ) {
100 return None;
101 }
102 geo_within_call(lhs)
103}
104
105fn literal_f64(expr: &Expr) -> Option<f64> {
106 match expr {
107 Expr::Literal {
108 value: Value::Float(value),
109 ..
110 } => Some(*value),
111 Expr::Literal {
112 value: Value::Integer(value),
113 ..
114 } => Some(*value as f64),
115 Expr::Literal {
116 value: Value::UnsignedInteger(value),
117 ..
118 } => Some(*value as f64),
119 _ => None,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::ast::Span;
127
128 fn lit(value: f64) -> Expr {
129 Expr::Literal {
130 value: Value::Float(value),
131 span: Span::synthetic(),
132 }
133 }
134
135 fn polygon(coords: &[f64]) -> Expr {
136 Expr::FunctionCall {
137 name: "POLYGON".to_string(),
138 args: coords.iter().copied().map(lit).collect(),
139 span: Span::synthetic(),
140 }
141 }
142
143 fn geo_within(column: &str, polygon: Expr) -> Expr {
144 Expr::FunctionCall {
145 name: "GEO_WITHIN".to_string(),
146 args: vec![
147 Expr::Column {
148 field: FieldRef::TableColumn {
149 table: String::new(),
150 column: column.to_string(),
151 },
152 span: Span::synthetic(),
153 },
154 polygon,
155 ],
156 span: Span::synthetic(),
157 }
158 }
159
160 #[test]
161 fn recognises_constant_polygon_call() {
162 let call = geo_within("loc", polygon(&[0.0, 0.0, 1.0, 0.0, 1.0, 1.0]));
163 let predicate = geo_within_call(&call).expect("constant polygon is recognised");
164 assert_eq!(predicate.column, "loc");
165 assert_eq!(predicate.vertices, [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]);
166 }
167
168 #[test]
169 fn rejects_non_constant_polygon() {
170 let mut args: Vec<Expr> = [0.0, 0.0, 1.0, 0.0, 1.0].iter().copied().map(lit).collect();
171 args.push(Expr::Column {
172 field: FieldRef::TableColumn {
173 table: String::new(),
174 column: "lon".to_string(),
175 },
176 span: Span::synthetic(),
177 });
178 let call = geo_within(
179 "loc",
180 Expr::FunctionCall {
181 name: "POLYGON".to_string(),
182 args,
183 span: Span::synthetic(),
184 },
185 );
186 assert!(geo_within_call(&call).is_none());
187 }
188
189 #[test]
190 fn rejects_degenerate_vertex_count() {
191 assert!(literal_polygon_vertices(&[lit(0.0), lit(0.0), lit(1.0), lit(1.0)]).is_none());
192 assert!(literal_polygon_vertices(&[lit(0.0), lit(0.0), lit(1.0)]).is_none());
193 }
194
195 #[test]
196 fn truth_test_only_matches_equals_true() {
197 let call = geo_within("loc", polygon(&[0.0, 0.0, 1.0, 0.0, 1.0, 1.0]));
198 let yes = Expr::Literal {
199 value: Value::Boolean(true),
200 span: Span::synthetic(),
201 };
202 let no = Expr::Literal {
203 value: Value::Boolean(false),
204 span: Span::synthetic(),
205 };
206 assert!(geo_within_truth_test(&call, CompareOp::Eq, &yes).is_some());
207 assert!(geo_within_truth_test(&call, CompareOp::Eq, &no).is_none());
208 assert!(geo_within_truth_test(&call, CompareOp::Ne, &yes).is_none());
209 }
210}