1use std::str::FromStr;
9
10use crate::error::{Result, VaultdbError};
11use crate::record::Value;
12
13#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
15#[non_exhaustive]
16pub enum Expr {
17 Predicate(Predicate),
19 And(Vec<Expr>),
21 Or(Vec<Expr>),
23 Not(Box<Expr>),
25 LinksTo(LinkPredicate),
27 LinkedFrom(LinkPredicate),
29}
30
31#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
33#[non_exhaustive]
34pub enum Predicate {
35 Equals {
36 field: String,
37 value: Value,
38 },
39 Contains {
40 field: String,
41 value: Value,
42 },
43 Compare {
44 field: String,
45 op: CompareOp,
46 value: Value,
47 },
48 Matches {
49 field: String,
50 regex: String,
51 },
52 StartsWith {
53 field: String,
54 value: String,
55 },
56 EndsWith {
57 field: String,
58 value: String,
59 },
60 Exists {
61 field: String,
62 },
63 Missing {
64 field: String,
65 },
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
70#[non_exhaustive]
71pub enum CompareOp {
72 Lt,
73 Le,
74 Gt,
75 Ge,
76 Ne,
77}
78
79#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
84#[non_exhaustive]
85pub enum LinkPredicate {
86 Target(String),
87 Where(Box<Expr>),
88}
89
90#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
93pub struct Query {
94 pub folder: String,
95 pub filter: Option<Expr>,
96 pub select: Option<Vec<String>>,
98 pub sort: Option<SortKey>,
99 pub limit: Option<usize>,
100 pub recursive: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
105pub struct SortKey {
106 pub field: String,
107 pub descending: bool,
108}
109
110impl Expr {
111 pub fn parse(input: &str) -> Result<Self> {
115 input.parse()
116 }
117}
118
119impl FromStr for Expr {
120 type Err = VaultdbError;
121
122 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
123 crate::dsl::parse(input)
128 }
129}
130
131impl std::ops::BitAnd for Expr {
139 type Output = Expr;
140 fn bitand(self, rhs: Expr) -> Expr {
141 match (self, rhs) {
142 (Expr::And(mut a), Expr::And(b)) => {
143 a.extend(b);
144 Expr::And(a)
145 }
146 (Expr::And(mut a), other) => {
147 a.push(other);
148 Expr::And(a)
149 }
150 (other, Expr::And(mut b)) => {
151 b.insert(0, other);
152 Expr::And(b)
153 }
154 (a, b) => Expr::And(vec![a, b]),
155 }
156 }
157}
158
159impl std::ops::BitOr for Expr {
160 type Output = Expr;
161 fn bitor(self, rhs: Expr) -> Expr {
162 match (self, rhs) {
163 (Expr::Or(mut a), Expr::Or(b)) => {
164 a.extend(b);
165 Expr::Or(a)
166 }
167 (Expr::Or(mut a), other) => {
168 a.push(other);
169 Expr::Or(a)
170 }
171 (other, Expr::Or(mut b)) => {
172 b.insert(0, other);
173 Expr::Or(b)
174 }
175 (a, b) => Expr::Or(vec![a, b]),
176 }
177 }
178}
179
180impl std::ops::Not for Expr {
181 type Output = Expr;
182 fn not(self) -> Expr {
183 match self {
184 Expr::Not(inner) => *inner,
186 other => Expr::Not(Box::new(other)),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn parse_simple_equals() {
197 let e: Expr = "status = active".parse().unwrap();
198 match e {
199 Expr::Predicate(Predicate::Equals { field, value }) => {
200 assert_eq!(field, "status");
201 assert_eq!(value, Value::String("active".into()));
202 }
203 other => panic!("expected Equals, got {:?}", other),
204 }
205 }
206
207 #[test]
208 fn parse_exists() {
209 let e: Expr = "title exists".parse().unwrap();
210 match e {
211 Expr::Predicate(Predicate::Exists { field }) => assert_eq!(field, "title"),
212 other => panic!("expected Exists, got {:?}", other),
213 }
214 }
215
216 #[test]
217 fn parse_compare_gt() {
218 let e: Expr = "year > 2020".parse().unwrap();
219 match e {
220 Expr::Predicate(Predicate::Compare { field, op, value }) => {
221 assert_eq!(field, "year");
222 assert_eq!(op, CompareOp::Gt);
223 assert_eq!(value, Value::Integer(2020));
224 }
225 other => panic!("expected Compare, got {:?}", other),
226 }
227 }
228
229 #[test]
230 fn expr_serializes_via_serde() {
231 let e = Expr::Predicate(Predicate::Equals {
232 field: "k".into(),
233 value: Value::String("v".into()),
234 });
235 let json = serde_json::to_string(&e).unwrap();
236 let back: Expr = serde_json::from_str(&json).unwrap();
238 assert_eq!(e, back);
239 }
240
241 #[test]
242 fn query_struct_construction() {
243 let q = Query {
244 folder: "notes".into(),
245 filter: Some(Expr::Predicate(Predicate::Exists {
246 field: "title".into(),
247 })),
248 select: Some(vec!["_name".into(), "title".into()]),
249 sort: Some(SortKey {
250 field: "_modified".into(),
251 descending: true,
252 }),
253 limit: Some(10),
254 recursive: false,
255 };
256 assert_eq!(q.folder, "notes");
257 assert!(q.filter.is_some());
258 assert_eq!(q.limit, Some(10));
259 }
260
261 #[test]
262 fn link_predicate_target() {
263 let lp = LinkPredicate::Target("Foo".into());
264 let e = Expr::LinksTo(lp);
265 let json = serde_json::to_string(&e).unwrap();
266 assert!(json.contains("Foo"));
268 }
269
270 fn p_eq(field: &str, v: Value) -> Expr {
271 Expr::Predicate(Predicate::Equals {
272 field: field.into(),
273 value: v,
274 })
275 }
276
277 #[test]
278 fn bitand_flattens_chains() {
279 let a = p_eq("a", Value::Integer(1));
280 let b = p_eq("b", Value::Integer(2));
281 let c = p_eq("c", Value::Integer(3));
282 let combined = a & b & c;
283 match combined {
284 Expr::And(parts) => assert_eq!(parts.len(), 3),
285 other => panic!("expected flattened And, got {:?}", other),
286 }
287 }
288
289 #[test]
290 fn bitor_flattens_chains() {
291 let a = p_eq("a", Value::Integer(1));
292 let b = p_eq("b", Value::Integer(2));
293 let c = p_eq("c", Value::Integer(3));
294 let combined = a | b | c;
295 match combined {
296 Expr::Or(parts) => assert_eq!(parts.len(), 3),
297 other => panic!("expected flattened Or, got {:?}", other),
298 }
299 }
300
301 #[test]
302 fn not_double_negation_cancels() {
303 let a = p_eq("a", Value::Integer(1));
304 let twice = !!a.clone();
305 assert_eq!(a, twice);
306 }
307
308 #[test]
309 fn mixed_operators_respect_precedence() {
310 let a = p_eq("a", Value::Integer(1));
311 let b = p_eq("b", Value::Integer(2));
312 let c = p_eq("c", Value::Integer(3));
313 let combined = a.clone() | b.clone() & c.clone();
315 match combined {
316 Expr::Or(parts) if parts.len() == 2 => {
317 assert_eq!(parts[0], a);
318 assert!(matches!(&parts[1], Expr::And(inner) if inner.len() == 2));
319 }
320 other => panic!("expected Or-of-(a, And(b,c)), got {:?}", other),
321 }
322 }
323
324 #[test]
325 fn value_from_primitives() {
326 assert_eq!(Value::from(42_i32), Value::Integer(42));
327 assert_eq!(
328 Value::from(2_500_000_000_i64),
329 Value::Integer(2_500_000_000)
330 );
331 assert_eq!(Value::from(1.5_f64), Value::Float(1.5));
332 assert_eq!(Value::from(true), Value::Bool(true));
333 assert_eq!(Value::from("hi"), Value::String("hi".into()));
334 assert_eq!(Value::from(String::from("hi")), Value::String("hi".into()));
335 assert_eq!(
336 Value::from(vec!["a", "b"]),
337 Value::List(vec![Value::String("a".into()), Value::String("b".into())])
338 );
339 }
340}