Skip to main content

vaultdb_core/
query.rs

1//! Public AST types for vault queries.
2//!
3//! Frontmatter predicates and link-graph predicates are first-class siblings
4//! in the same enum (`Expr`), reflecting vaultdb's dual-structure thesis: a
5//! markdown vault is *both* a relational table (frontmatter) and a graph
6//! (wikilinks), and the query language treats both equally.
7
8use std::str::FromStr;
9
10use crate::error::{Result, VaultdbError};
11use crate::record::Value;
12
13/// A composable filter expression. The AST root for vault queries.
14#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
15#[non_exhaustive]
16pub enum Expr {
17    /// A frontmatter or virtual-field predicate.
18    Predicate(Predicate),
19    /// All sub-expressions must hold.
20    And(Vec<Expr>),
21    /// At least one sub-expression must hold.
22    Or(Vec<Expr>),
23    /// Negation of a sub-expression.
24    Not(Box<Expr>),
25    /// Records that link out to a target matching the inner predicate.
26    LinksTo(LinkPredicate),
27    /// Records linked from anything matching the inner predicate.
28    LinkedFrom(LinkPredicate),
29}
30
31/// A leaf predicate over a record's frontmatter or virtual fields.
32#[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/// A scalar comparison operator (used by `Predicate::Compare`).
69#[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/// A predicate over the link graph: either a literal target, or a query into
80/// records satisfying a sub-expression. The `Where` variant is what makes
81/// joins-via-links possible (e.g., "give me all notes that link to anything
82/// tagged `topic/ai`").
83#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
84#[non_exhaustive]
85pub enum LinkPredicate {
86    Target(String),
87    Where(Box<Expr>),
88}
89
90/// A complete query: the root expression, optional projection, sort, limit,
91/// and the folder to scan.
92#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
93pub struct Query {
94    pub folder: String,
95    pub filter: Option<Expr>,
96    /// `None` means "select all fields".
97    pub select: Option<Vec<String>>,
98    pub sort: Option<SortKey>,
99    pub limit: Option<usize>,
100    pub recursive: bool,
101}
102
103/// A sort key: which field to sort by, ascending or descending.
104#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
105pub struct SortKey {
106    pub field: String,
107    pub descending: bool,
108}
109
110impl Expr {
111    /// Parse a where-DSL string into an `Expr`. Convenience wrapper over
112    /// `<Expr as FromStr>::from_str`, so library users have an obvious
113    /// discoverable entry point.
114    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        // The where-DSL parser lives in `crate::dsl` (pest-driven).
124        // It produces a public `Expr` directly. See `where_dsl.pest`
125        // for the grammar; precedence is SQL-conventional (AND tighter
126        // than OR).
127        crate::dsl::parse(input)
128    }
129}
130
131// Operator overloads for ergonomic programmatic construction.
132//
133// `a & b`, `a | b`, and `!a` build the corresponding AST nodes. Chains
134// of the same operator are flattened (`a & b & c` produces a single
135// three-element `And`, not nested two-element ones), so the resulting
136// expression mirrors what a hand-written `Expr::And(vec![...])` would.
137
138impl 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            // Double negation cancels.
185            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        // Round-trip
237        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        // Untagged enum representation
267        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        // & binds tighter than |, so `a | b & c` is `a | (b & c)`.
314        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!(Value::from(2_500_000_000_i64), Value::Integer(2_500_000_000));
328        assert_eq!(Value::from(3.14_f64), Value::Float(3.14));
329        assert_eq!(Value::from(true), Value::Bool(true));
330        assert_eq!(Value::from("hi"), Value::String("hi".into()));
331        assert_eq!(Value::from(String::from("hi")), Value::String("hi".into()));
332        assert_eq!(
333            Value::from(vec!["a", "b"]),
334            Value::List(vec![Value::String("a".into()), Value::String("b".into())])
335        );
336    }
337}