use std::str::FromStr;
use crate::error::{Result, VaultdbError};
use crate::record::Value;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum Expr {
Predicate(Predicate),
And(Vec<Expr>),
Or(Vec<Expr>),
Not(Box<Expr>),
LinksTo(LinkPredicate),
LinkedFrom(LinkPredicate),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum Predicate {
Equals {
field: String,
value: Value,
},
Contains {
field: String,
value: Value,
},
Compare {
field: String,
op: CompareOp,
value: Value,
},
Matches {
field: String,
regex: String,
},
StartsWith {
field: String,
value: String,
},
EndsWith {
field: String,
value: String,
},
Exists {
field: String,
},
Missing {
field: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum CompareOp {
Lt,
Le,
Gt,
Ge,
Ne,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum LinkPredicate {
Target(String),
Where(Box<Expr>),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Query {
pub folder: String,
pub filter: Option<Expr>,
pub select: Option<Vec<String>>,
pub sort: Option<SortKey>,
pub limit: Option<usize>,
pub recursive: bool,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SortKey {
pub field: String,
pub descending: bool,
}
impl Expr {
pub fn parse(input: &str) -> Result<Self> {
input.parse()
}
}
impl FromStr for Expr {
type Err = VaultdbError;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
crate::dsl::parse(input)
}
}
impl std::ops::BitAnd for Expr {
type Output = Expr;
fn bitand(self, rhs: Expr) -> Expr {
match (self, rhs) {
(Expr::And(mut a), Expr::And(b)) => {
a.extend(b);
Expr::And(a)
}
(Expr::And(mut a), other) => {
a.push(other);
Expr::And(a)
}
(other, Expr::And(mut b)) => {
b.insert(0, other);
Expr::And(b)
}
(a, b) => Expr::And(vec![a, b]),
}
}
}
impl std::ops::BitOr for Expr {
type Output = Expr;
fn bitor(self, rhs: Expr) -> Expr {
match (self, rhs) {
(Expr::Or(mut a), Expr::Or(b)) => {
a.extend(b);
Expr::Or(a)
}
(Expr::Or(mut a), other) => {
a.push(other);
Expr::Or(a)
}
(other, Expr::Or(mut b)) => {
b.insert(0, other);
Expr::Or(b)
}
(a, b) => Expr::Or(vec![a, b]),
}
}
}
impl std::ops::Not for Expr {
type Output = Expr;
fn not(self) -> Expr {
match self {
Expr::Not(inner) => *inner,
other => Expr::Not(Box::new(other)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_equals() {
let e: Expr = "status = active".parse().unwrap();
match e {
Expr::Predicate(Predicate::Equals { field, value }) => {
assert_eq!(field, "status");
assert_eq!(value, Value::String("active".into()));
}
other => panic!("expected Equals, got {:?}", other),
}
}
#[test]
fn parse_exists() {
let e: Expr = "title exists".parse().unwrap();
match e {
Expr::Predicate(Predicate::Exists { field }) => assert_eq!(field, "title"),
other => panic!("expected Exists, got {:?}", other),
}
}
#[test]
fn parse_compare_gt() {
let e: Expr = "year > 2020".parse().unwrap();
match e {
Expr::Predicate(Predicate::Compare { field, op, value }) => {
assert_eq!(field, "year");
assert_eq!(op, CompareOp::Gt);
assert_eq!(value, Value::Integer(2020));
}
other => panic!("expected Compare, got {:?}", other),
}
}
#[test]
fn expr_serializes_via_serde() {
let e = Expr::Predicate(Predicate::Equals {
field: "k".into(),
value: Value::String("v".into()),
});
let json = serde_json::to_string(&e).unwrap();
let back: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn query_struct_construction() {
let q = Query {
folder: "notes".into(),
filter: Some(Expr::Predicate(Predicate::Exists {
field: "title".into(),
})),
select: Some(vec!["_name".into(), "title".into()]),
sort: Some(SortKey {
field: "_modified".into(),
descending: true,
}),
limit: Some(10),
recursive: false,
};
assert_eq!(q.folder, "notes");
assert!(q.filter.is_some());
assert_eq!(q.limit, Some(10));
}
#[test]
fn link_predicate_target() {
let lp = LinkPredicate::Target("Foo".into());
let e = Expr::LinksTo(lp);
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("Foo"));
}
fn p_eq(field: &str, v: Value) -> Expr {
Expr::Predicate(Predicate::Equals {
field: field.into(),
value: v,
})
}
#[test]
fn bitand_flattens_chains() {
let a = p_eq("a", Value::Integer(1));
let b = p_eq("b", Value::Integer(2));
let c = p_eq("c", Value::Integer(3));
let combined = a & b & c;
match combined {
Expr::And(parts) => assert_eq!(parts.len(), 3),
other => panic!("expected flattened And, got {:?}", other),
}
}
#[test]
fn bitor_flattens_chains() {
let a = p_eq("a", Value::Integer(1));
let b = p_eq("b", Value::Integer(2));
let c = p_eq("c", Value::Integer(3));
let combined = a | b | c;
match combined {
Expr::Or(parts) => assert_eq!(parts.len(), 3),
other => panic!("expected flattened Or, got {:?}", other),
}
}
#[test]
fn not_double_negation_cancels() {
let a = p_eq("a", Value::Integer(1));
let twice = !!a.clone();
assert_eq!(a, twice);
}
#[test]
fn mixed_operators_respect_precedence() {
let a = p_eq("a", Value::Integer(1));
let b = p_eq("b", Value::Integer(2));
let c = p_eq("c", Value::Integer(3));
let combined = a.clone() | b.clone() & c.clone();
match combined {
Expr::Or(parts) if parts.len() == 2 => {
assert_eq!(parts[0], a);
assert!(matches!(&parts[1], Expr::And(inner) if inner.len() == 2));
}
other => panic!("expected Or-of-(a, And(b,c)), got {:?}", other),
}
}
#[test]
fn value_from_primitives() {
assert_eq!(Value::from(42_i32), Value::Integer(42));
assert_eq!(
Value::from(2_500_000_000_i64),
Value::Integer(2_500_000_000)
);
assert_eq!(Value::from(1.5_f64), Value::Float(1.5));
assert_eq!(Value::from(true), Value::Bool(true));
assert_eq!(Value::from("hi"), Value::String("hi".into()));
assert_eq!(Value::from(String::from("hi")), Value::String("hi".into()));
assert_eq!(
Value::from(vec!["a", "b"]),
Value::List(vec![Value::String("a".into()), Value::String("b".into())])
);
}
}