Skip to main content

corium_client/
query.rs

1//! A typesafe, builder-patterned Datalog query value.
2//!
3//! Every type here is a plain immutable value that lowers to the boundary
4//! [`Edn`] the corium query engine parses. Nothing is stringly-typed: a
5//! [`Query`] is assembled from [`Var`]s, [`Term`]s, and [`Clause`]s and
6//! rendered with [`Query::to_edn`], so a malformed query is a type error, not
7//! a parse error at execution time.
8//!
9//! ```
10//! use corium_client::query::{Query, data, var, attr, gte, lit};
11//!
12//! // [:find ?name ?age
13//! //  :in $ ?min
14//! //  :where [?e :person/name ?name]
15//! //         [?e :person/age ?age]
16//! //         [(>= ?age ?min)]]
17//! let q = Query::find([var("name"), var("age")])
18//!     .in_scalar(var("min"))
19//!     .where_(data(var("e"), attr("person/name"), var("name")))
20//!     .and(data(var("e"), attr("person/age"), var("age")))
21//!     .and(gte(var("age"), var("min")))
22//!     .to_edn();
23//! ```
24
25use corium_query::edn::Edn;
26
27use crate::pull::Pull;
28use crate::value::IntoEdn;
29
30/// A logic variable such as `?e`. Constructed with or without the leading
31/// `?`: `var("e")` and `var("?e")` are equal.
32#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
33pub struct Var(String);
34
35impl Var {
36    /// Builds a variable, adding the leading `?` if the caller omits it.
37    #[must_use]
38    pub fn new(name: impl AsRef<str>) -> Self {
39        let name = name.as_ref();
40        if name.starts_with('?') {
41            Self(name.to_owned())
42        } else {
43            Self(format!("?{name}"))
44        }
45    }
46
47    /// The variable text including the leading `?`.
48    #[must_use]
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52
53    fn to_edn(&self) -> Edn {
54        Edn::Symbol(self.0.clone())
55    }
56}
57
58/// Shorthand for [`Var::new`].
59#[must_use]
60pub fn var(name: impl AsRef<str>) -> Var {
61    Var::new(name)
62}
63
64/// An attribute keyword constant, e.g. `attr("person/name")` for
65/// `:person/name`. Returns an [`Edn`] usable in any [`Term`] position.
66#[must_use]
67pub fn attr(name: &str) -> Edn {
68    Edn::keyword(name)
69}
70
71/// Alias for [`attr`]: a keyword constant from `"ns/name"` text.
72#[must_use]
73pub fn kw(name: &str) -> Edn {
74    Edn::keyword(name)
75}
76
77/// A constant term from any [`IntoEdn`] scalar, e.g. `lit(42)` or
78/// `lit("hello")`.
79#[must_use]
80pub fn lit(value: impl IntoEdn) -> Term {
81    Term::Const(value.into_edn())
82}
83
84/// The blank term `_`.
85#[must_use]
86pub fn blank() -> Term {
87    Term::Blank
88}
89
90/// A term in a pattern, predicate, function, or rule position.
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub enum Term {
93    /// A logic variable.
94    Var(Var),
95    /// The blank `_`.
96    Blank,
97    /// A constant, resolved against the database at execution time.
98    Const(Edn),
99}
100
101impl Term {
102    fn to_edn(&self) -> Edn {
103        match self {
104            Self::Var(variable) => variable.to_edn(),
105            Self::Blank => Edn::symbol("_"),
106            Self::Const(form) => form.clone(),
107        }
108    }
109}
110
111impl From<Var> for Term {
112    fn from(value: Var) -> Self {
113        Self::Var(value)
114    }
115}
116
117impl From<Edn> for Term {
118    fn from(value: Edn) -> Self {
119        Self::Const(value)
120    }
121}
122
123/// A `:find` element: a variable, a pull expression, or an aggregate call.
124#[derive(Clone, Debug, Eq, PartialEq)]
125pub enum FindElem {
126    /// A plain variable `?x`.
127    Var(Var),
128    /// A pull expression `(pull ?e pattern)`.
129    Pull(Var, Pull),
130    /// An aggregate call such as `(count ?x)` or `(min 3 ?x)`.
131    Aggregate {
132        /// Operator name (`count`, `sum`, `avg`, `min`, `max`, ...).
133        op: String,
134        /// Optional leading constant argument, as in `(min 3 ?x)`.
135        n: Option<i64>,
136        /// The aggregated variable.
137        var: Var,
138    },
139}
140
141impl FindElem {
142    fn to_edn(&self) -> Edn {
143        match self {
144            Self::Var(variable) => variable.to_edn(),
145            Self::Pull(entity, pattern) => {
146                Edn::List(vec![Edn::symbol("pull"), entity.to_edn(), pattern.to_edn()])
147            }
148            Self::Aggregate { op, n, var } => {
149                let mut items = vec![Edn::symbol(op)];
150                if let Some(n) = n {
151                    items.push(Edn::Long(*n));
152                }
153                items.push(var.to_edn());
154                Edn::List(items)
155            }
156        }
157    }
158}
159
160impl From<Var> for FindElem {
161    fn from(value: Var) -> Self {
162        Self::Var(value)
163    }
164}
165
166/// A pull expression find element `(pull ?e pattern)`.
167#[must_use]
168pub fn pull_expr(entity: Var, pattern: Pull) -> FindElem {
169    FindElem::Pull(entity, pattern)
170}
171
172macro_rules! aggregate_fn {
173    ($(#[$meta:meta])* $name:ident => $op:literal) => {
174        $(#[$meta])*
175        #[must_use]
176        pub fn $name(var: Var) -> FindElem {
177            FindElem::Aggregate { op: $op.to_owned(), n: None, var }
178        }
179    };
180}
181
182aggregate_fn!(/// The `(count ?x)` aggregate.
183    count => "count");
184aggregate_fn!(/// The `(count-distinct ?x)` aggregate.
185    count_distinct => "count-distinct");
186aggregate_fn!(/// The `(sum ?x)` aggregate.
187    sum => "sum");
188aggregate_fn!(/// The `(avg ?x)` aggregate.
189    avg => "avg");
190aggregate_fn!(/// The `(min ?x)` aggregate.
191    min => "min");
192aggregate_fn!(/// The `(max ?x)` aggregate.
193    max => "max");
194
195/// A bounded aggregate such as `(min 3 ?x)` or `(max 5 ?x)`.
196#[must_use]
197pub fn agg_n(op: &str, n: i64, var: Var) -> FindElem {
198    FindElem::Aggregate {
199        op: op.to_owned(),
200        n: Some(n),
201        var,
202    }
203}
204
205/// The shape of the `:find` clause, which fixes the result shape.
206#[derive(Clone, Debug, Eq, PartialEq)]
207enum Find {
208    /// Relation of tuples: `:find ?a ?b`.
209    Rel(Vec<FindElem>),
210    /// Collection: `:find [?x ...]`.
211    Coll(FindElem),
212    /// Single tuple: `:find [?a ?b]`.
213    Tuple(Vec<FindElem>),
214    /// Scalar: `:find ?x .`.
215    Scalar(FindElem),
216}
217
218impl Find {
219    fn to_find_items(&self) -> Vec<Edn> {
220        match self {
221            Self::Rel(elems) => elems.iter().map(FindElem::to_edn).collect(),
222            Self::Coll(elem) => {
223                vec![Edn::Vector(vec![elem.to_edn(), Edn::symbol("...")])]
224            }
225            Self::Tuple(elems) => {
226                vec![Edn::Vector(elems.iter().map(FindElem::to_edn).collect())]
227            }
228            Self::Scalar(elem) => vec![elem.to_edn(), Edn::symbol(".")],
229        }
230    }
231}
232
233/// A function-clause output binding.
234#[derive(Clone, Debug, Eq, PartialEq)]
235pub enum Binding {
236    /// `?x`.
237    Scalar(Var),
238    /// `[?x ?y]`.
239    Tuple(Vec<Var>),
240    /// `[?x ...]`.
241    Coll(Var),
242    /// `[[?x ?y]]`.
243    Rel(Vec<Var>),
244}
245
246impl Binding {
247    fn to_edn(&self) -> Edn {
248        match self {
249            Self::Scalar(var) => var.to_edn(),
250            Self::Tuple(vars) => Edn::Vector(vars.iter().map(Var::to_edn).collect()),
251            Self::Coll(var) => Edn::Vector(vec![var.to_edn(), Edn::symbol("...")]),
252            Self::Rel(vars) => {
253                Edn::Vector(vec![Edn::Vector(vars.iter().map(Var::to_edn).collect())])
254            }
255        }
256    }
257}
258
259/// A `:where` clause.
260#[derive(Clone, Debug, Eq, PartialEq)]
261pub enum Clause {
262    /// A data pattern `[src? e a v tx? added?]`.
263    Pattern(Pattern),
264    /// A predicate `[(name args...)]`.
265    Predicate {
266        /// Predicate name (`>`, `<`, a rule predicate, ...).
267        name: String,
268        /// Argument terms.
269        args: Vec<Term>,
270    },
271    /// A function `[(name args...) binding]`.
272    Function {
273        /// Function name.
274        name: String,
275        /// Argument terms.
276        args: Vec<Term>,
277        /// Output binding.
278        binding: Binding,
279    },
280    /// `(not clauses...)` or `(not-join [vars] clauses...)`.
281    Not {
282        /// Join variables for `not-join`; `None` for plain `not`.
283        vars: Option<Vec<Var>>,
284        /// Negated clauses.
285        clauses: Vec<Clause>,
286    },
287    /// `(or branches...)` or `(or-join [vars] branches...)`.
288    Or {
289        /// Join variables for `or-join`; `None` for plain `or`.
290        vars: Option<Vec<Var>>,
291        /// Alternative clause groups; each inner vector is an `and` branch.
292        branches: Vec<Vec<Clause>>,
293    },
294    /// A rule invocation `(rule-name args...)`.
295    Rule {
296        /// Rule name.
297        name: String,
298        /// Argument terms.
299        args: Vec<Term>,
300    },
301}
302
303impl Clause {
304    fn to_edn(&self) -> Edn {
305        match self {
306            Self::Pattern(pattern) => pattern.to_edn(),
307            Self::Predicate { name, args } => Edn::Vector(vec![call_form(name, args)]),
308            Self::Function {
309                name,
310                args,
311                binding,
312            } => Edn::Vector(vec![call_form(name, args), binding.to_edn()]),
313            Self::Not { vars, clauses } => {
314                let mut items = Vec::new();
315                match vars {
316                    Some(vars) => {
317                        items.push(Edn::symbol("not-join"));
318                        items.push(Edn::Vector(vars.iter().map(Var::to_edn).collect()));
319                    }
320                    None => items.push(Edn::symbol("not")),
321                }
322                items.extend(clauses.iter().map(Clause::to_edn));
323                Edn::List(items)
324            }
325            Self::Or { vars, branches } => {
326                let mut items = Vec::new();
327                match vars {
328                    Some(vars) => {
329                        items.push(Edn::symbol("or-join"));
330                        items.push(Edn::Vector(vars.iter().map(Var::to_edn).collect()));
331                    }
332                    None => items.push(Edn::symbol("or")),
333                }
334                for branch in branches {
335                    if branch.len() == 1 {
336                        items.push(branch[0].to_edn());
337                    } else {
338                        let mut and = vec![Edn::symbol("and")];
339                        and.extend(branch.iter().map(Clause::to_edn));
340                        items.push(Edn::List(and));
341                    }
342                }
343                Edn::List(items)
344            }
345            Self::Rule { name, args } => {
346                let mut items = vec![Edn::symbol(name)];
347                items.extend(args.iter().map(Term::to_edn));
348                Edn::List(items)
349            }
350        }
351    }
352}
353
354fn call_form(name: &str, args: &[Term]) -> Edn {
355    let mut items = vec![Edn::symbol(name)];
356    items.extend(args.iter().map(Term::to_edn));
357    Edn::List(items)
358}
359
360/// A data pattern, built by [`data`] and refined with [`Pattern::src`],
361/// [`Pattern::tx`], and [`Pattern::added`].
362#[derive(Clone, Debug, Eq, PartialEq)]
363pub struct Pattern {
364    src: Option<String>,
365    e: Term,
366    a: Term,
367    v: Term,
368    tx: Option<Term>,
369    added: Option<Term>,
370}
371
372impl Pattern {
373    /// Reads the pattern from a non-default database source, e.g. `"$hist"`.
374    #[must_use]
375    pub fn src(mut self, src: impl Into<String>) -> Self {
376        self.src = Some(src.into());
377        self
378    }
379
380    /// Binds the transaction position.
381    #[must_use]
382    pub fn tx(mut self, tx: impl Into<Term>) -> Self {
383        self.tx = Some(tx.into());
384        self
385    }
386
387    /// Binds the assertion-flag position (`?added` on history views).
388    #[must_use]
389    pub fn added(mut self, added: impl Into<Term>) -> Self {
390        self.added = Some(added.into());
391        self
392    }
393
394    fn to_edn(&self) -> Edn {
395        let mut items = Vec::with_capacity(6);
396        if let Some(src) = &self.src {
397            items.push(Edn::symbol(src));
398        }
399        items.push(self.e.to_edn());
400        items.push(self.a.to_edn());
401        items.push(self.v.to_edn());
402        // tx/added are trailing positions; emit `added` only when `tx` is set
403        // so positions stay aligned.
404        if self.tx.is_some() || self.added.is_some() {
405            items.push(self.tx.as_ref().map_or(Edn::symbol("_"), Term::to_edn));
406        }
407        if let Some(added) = &self.added {
408            items.push(added.to_edn());
409        }
410        Edn::Vector(items)
411    }
412}
413
414impl From<Pattern> for Clause {
415    fn from(value: Pattern) -> Self {
416        Self::Pattern(value)
417    }
418}
419
420/// A data pattern `[?e :attr ?v]`. Refine it with [`Pattern::src`],
421/// [`Pattern::tx`], or [`Pattern::added`] before adding it to a query.
422#[must_use]
423pub fn data(e: impl Into<Term>, a: impl Into<Term>, v: impl Into<Term>) -> Pattern {
424    Pattern {
425        src: None,
426        e: e.into(),
427        a: a.into(),
428        v: v.into(),
429        tx: None,
430        added: None,
431    }
432}
433
434/// A predicate clause `[(name args...)]`.
435#[must_use]
436pub fn pred(name: impl Into<String>, args: Vec<Term>) -> Clause {
437    Clause::Predicate {
438        name: name.into(),
439        args,
440    }
441}
442
443macro_rules! comparison_fn {
444    ($(#[$meta:meta])* $name:ident => $op:literal) => {
445        $(#[$meta])*
446        #[must_use]
447        pub fn $name(left: impl Into<Term>, right: impl Into<Term>) -> Clause {
448            Clause::Predicate { name: $op.to_owned(), args: vec![left.into(), right.into()] }
449        }
450    };
451}
452
453comparison_fn!(/// The predicate `[(> a b)]`.
454    gt => ">");
455comparison_fn!(/// The predicate `[(< a b)]`.
456    lt => "<");
457comparison_fn!(/// The predicate `[(>= a b)]`.
458    gte => ">=");
459comparison_fn!(/// The predicate `[(<= a b)]`.
460    lte => "<=");
461comparison_fn!(/// The predicate `[(= a b)]`.
462    eq => "=");
463comparison_fn!(/// The predicate `[(not= a b)]`.
464    neq => "not=");
465
466/// A function call, built by [`call`] and completed with [`Call::bind`].
467#[derive(Clone, Debug, Eq, PartialEq)]
468pub struct Call {
469    name: String,
470    args: Vec<Term>,
471}
472
473impl Call {
474    /// Binds the call's output, producing a function clause
475    /// `[(name args...) binding]`.
476    #[must_use]
477    pub fn bind(self, binding: Binding) -> Clause {
478        Clause::Function {
479            name: self.name,
480            args: self.args,
481            binding,
482        }
483    }
484
485    /// Binds the call's output to a single scalar variable.
486    #[must_use]
487    pub fn bind_scalar(self, var: Var) -> Clause {
488        self.bind(Binding::Scalar(var))
489    }
490}
491
492/// A function call `(name args...)`, completed with [`Call::bind`].
493#[must_use]
494pub fn call(name: impl Into<String>, args: Vec<Term>) -> Call {
495    Call {
496        name: name.into(),
497        args,
498    }
499}
500
501fn collect_clauses<C: Into<Clause>>(clauses: impl IntoIterator<Item = C>) -> Vec<Clause> {
502    clauses.into_iter().map(Into::into).collect()
503}
504
505fn collect_branches<B, C>(branches: impl IntoIterator<Item = B>) -> Vec<Vec<Clause>>
506where
507    B: IntoIterator<Item = C>,
508    C: Into<Clause>,
509{
510    branches.into_iter().map(collect_clauses).collect()
511}
512
513/// A `(not clauses...)` clause.
514#[must_use]
515pub fn not<C: Into<Clause>>(clauses: impl IntoIterator<Item = C>) -> Clause {
516    Clause::Not {
517        vars: None,
518        clauses: collect_clauses(clauses),
519    }
520}
521
522/// A `(not-join [vars] clauses...)` clause.
523#[must_use]
524pub fn not_join<C: Into<Clause>>(vars: Vec<Var>, clauses: impl IntoIterator<Item = C>) -> Clause {
525    Clause::Not {
526        vars: Some(vars),
527        clauses: collect_clauses(clauses),
528    }
529}
530
531/// An `(or branches...)` clause. Each branch is a group of clauses joined by
532/// an implicit `and`.
533#[must_use]
534pub fn or<B, C>(branches: impl IntoIterator<Item = B>) -> Clause
535where
536    B: IntoIterator<Item = C>,
537    C: Into<Clause>,
538{
539    Clause::Or {
540        vars: None,
541        branches: collect_branches(branches),
542    }
543}
544
545/// An `(or-join [vars] branches...)` clause.
546#[must_use]
547pub fn or_join<B, C>(vars: Vec<Var>, branches: impl IntoIterator<Item = B>) -> Clause
548where
549    B: IntoIterator<Item = C>,
550    C: Into<Clause>,
551{
552    Clause::Or {
553        vars: Some(vars),
554        branches: collect_branches(branches),
555    }
556}
557
558/// A rule invocation `(rule-name args...)`.
559#[must_use]
560pub fn rule(name: impl Into<String>, args: Vec<Term>) -> Clause {
561    Clause::Rule {
562        name: name.into(),
563        args,
564    }
565}
566
567/// A `:in` binding after the implicit default database `$`.
568#[derive(Clone, Debug, Eq, PartialEq)]
569enum InSpec {
570    Db(String),
571    Rules,
572    Scalar(Var),
573    Tuple(Vec<Var>),
574    Coll(Var),
575    Rel(Vec<Var>),
576}
577
578impl InSpec {
579    fn to_edn(&self) -> Edn {
580        match self {
581            Self::Db(name) => Edn::symbol(name),
582            Self::Rules => Edn::symbol("%"),
583            Self::Scalar(var) => var.to_edn(),
584            Self::Tuple(vars) => Edn::Vector(vars.iter().map(Var::to_edn).collect()),
585            Self::Coll(var) => Edn::Vector(vec![var.to_edn(), Edn::symbol("...")]),
586            Self::Rel(vars) => {
587                Edn::Vector(vec![Edn::Vector(vars.iter().map(Var::to_edn).collect())])
588            }
589        }
590    }
591}
592
593/// An immutable Datalog query value.
594///
595/// Start from one of the `find*` constructors, chain `:in` and `:where`
596/// builders, and render with [`Query::to_edn`]. The query shape
597/// (relation/collection/tuple/scalar) is fixed by which constructor is used
598/// and determines the shape of the [`crate::QueryResult`].
599#[derive(Clone, Debug, Eq, PartialEq)]
600pub struct Query {
601    find: Find,
602    with: Vec<Var>,
603    inputs: Vec<InSpec>,
604    wheres: Vec<Clause>,
605}
606
607impl Query {
608    fn from_find(find: Find) -> Self {
609        Self {
610            find,
611            with: Vec::new(),
612            inputs: Vec::new(),
613            wheres: Vec::new(),
614        }
615    }
616
617    /// A relation query `:find ?a ?b` yielding a set of tuples.
618    #[must_use]
619    pub fn find<E: Into<FindElem>>(elems: impl IntoIterator<Item = E>) -> Self {
620        Self::from_find(Find::Rel(elems.into_iter().map(Into::into).collect()))
621    }
622
623    /// A relation query from an explicit list of find elements. Use this when
624    /// mixing element kinds, e.g. a variable and a pull expression.
625    #[must_use]
626    pub fn find_rel(elems: Vec<FindElem>) -> Self {
627        Self::from_find(Find::Rel(elems))
628    }
629
630    /// A collection query `:find [?x ...]` yielding a flat list of values.
631    #[must_use]
632    pub fn find_coll(elem: impl Into<FindElem>) -> Self {
633        Self::from_find(Find::Coll(elem.into()))
634    }
635
636    /// A tuple query `:find [?a ?b]` yielding a single tuple.
637    #[must_use]
638    pub fn find_tuple<E: Into<FindElem>>(elems: impl IntoIterator<Item = E>) -> Self {
639        Self::from_find(Find::Tuple(elems.into_iter().map(Into::into).collect()))
640    }
641
642    /// A scalar query `:find ?x .` yielding a single value.
643    #[must_use]
644    pub fn find_scalar(elem: impl Into<FindElem>) -> Self {
645        Self::from_find(Find::Scalar(elem.into()))
646    }
647
648    /// Adds `:with` grouping variables (kept out of `:find` but preserved for
649    /// aggregate cardinality).
650    #[must_use]
651    pub fn with(mut self, vars: impl IntoIterator<Item = Var>) -> Self {
652        self.with.extend(vars);
653        self
654    }
655
656    /// Declares an additional database source input such as `$hist`, bound
657    /// positionally after the default `$`.
658    #[must_use]
659    pub fn in_db(mut self, name: impl Into<String>) -> Self {
660        self.inputs.push(InSpec::Db(name.into()));
661        self
662    }
663
664    /// Declares a rule-set input `%`.
665    #[must_use]
666    pub fn in_rules(mut self) -> Self {
667        self.inputs.push(InSpec::Rules);
668        self
669    }
670
671    /// Declares a scalar input `?x`.
672    #[must_use]
673    pub fn in_scalar(mut self, var: Var) -> Self {
674        self.inputs.push(InSpec::Scalar(var));
675        self
676    }
677
678    /// Declares a tuple input `[?x ?y]`.
679    #[must_use]
680    pub fn in_tuple(mut self, vars: Vec<Var>) -> Self {
681        self.inputs.push(InSpec::Tuple(vars));
682        self
683    }
684
685    /// Declares a collection input `[?x ...]`.
686    #[must_use]
687    pub fn in_coll(mut self, var: Var) -> Self {
688        self.inputs.push(InSpec::Coll(var));
689        self
690    }
691
692    /// Declares a relation input `[[?x ?y]]`.
693    #[must_use]
694    pub fn in_rel(mut self, vars: Vec<Var>) -> Self {
695        self.inputs.push(InSpec::Rel(vars));
696        self
697    }
698
699    /// Adds the first `:where` clause (or another one; identical to
700    /// [`Query::and`]).
701    #[must_use]
702    pub fn where_(mut self, clause: impl Into<Clause>) -> Self {
703        self.wheres.push(clause.into());
704        self
705    }
706
707    /// Adds another `:where` clause.
708    #[must_use]
709    pub fn and(mut self, clause: impl Into<Clause>) -> Self {
710        self.wheres.push(clause.into());
711        self
712    }
713
714    /// Whether this query declares any non-database `:in` inputs (rules,
715    /// scalars, tuples, collections, or relations), which the caller must
716    /// supply as arguments.
717    #[must_use]
718    pub fn has_inputs(&self) -> bool {
719        self.inputs
720            .iter()
721            .any(|spec| !matches!(spec, InSpec::Db(_)))
722    }
723
724    /// Renders the query to its boundary [`Edn`] map form.
725    #[must_use]
726    pub fn to_edn(&self) -> Edn {
727        let mut pairs = Vec::with_capacity(4);
728        pairs.push((Edn::keyword("find"), Edn::Vector(self.find.to_find_items())));
729        if !self.with.is_empty() {
730            pairs.push((
731                Edn::keyword("with"),
732                Edn::Vector(self.with.iter().map(Var::to_edn).collect()),
733            ));
734        }
735        if !self.inputs.is_empty() {
736            let mut ins = vec![Edn::symbol("$")];
737            ins.extend(self.inputs.iter().map(InSpec::to_edn));
738            pairs.push((Edn::keyword("in"), Edn::Vector(ins)));
739        }
740        pairs.push((
741            Edn::keyword("where"),
742            Edn::Vector(self.wheres.iter().map(Clause::to_edn).collect()),
743        ));
744        Edn::Map(pairs)
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use corium_query::ast::parse_query;
752
753    /// Every builder output must parse cleanly in the engine.
754    fn assert_parses(query: &Query) {
755        let edn = query.to_edn();
756        parse_query(&edn).unwrap_or_else(|error| panic!("query did not parse: {error}\n{edn}"));
757    }
758
759    #[test]
760    fn relation_with_predicate_and_input() {
761        let query = Query::find([var("name"), var("age")])
762            .in_scalar(var("min"))
763            .where_(data(var("e"), attr("person/name"), var("name")))
764            .and(data(var("e"), attr("person/age"), var("age")))
765            .and(gte(var("age"), var("min")));
766        assert_parses(&query);
767        assert_eq!(
768            query.to_edn().to_string(),
769            "{:find [?name ?age], :in [$ ?min], :where [[?e :person/name ?name] \
770             [?e :person/age ?age] [(>= ?age ?min)]]}"
771        );
772    }
773
774    #[test]
775    fn scalar_coll_tuple_shapes_parse() {
776        assert_parses(&Query::find_scalar(var("e")).where_(data(
777            var("e"),
778            attr("db/ident"),
779            blank(),
780        )));
781        assert_parses(&Query::find_coll(var("e")).where_(data(
782            var("e"),
783            attr("db/ident"),
784            blank(),
785        )));
786        assert_parses(&Query::find_tuple([var("e"), var("a")]).where_(data(
787            var("e"),
788            attr("db/ident"),
789            var("a"),
790        )));
791    }
792
793    #[test]
794    fn aggregate_and_function_clauses_parse() {
795        assert_parses(
796            &Query::find_rel(vec![min(var("age")), max(var("age"))]).where_(data(
797                var("e"),
798                attr("person/age"),
799                var("age"),
800            )),
801        );
802        let with_count = Query::find_rel(vec![FindElem::Var(var("name")), count(var("e"))])
803            .where_(data(var("e"), attr("person/name"), var("name")));
804        assert_parses(&with_count);
805        assert_parses(
806            &Query::find([var("total")])
807                .where_(data(var("e"), attr("order/subtotal"), var("sub")))
808                .and(data(var("e"), attr("order/tax"), var("tax")))
809                .and(
810                    call("+", vec![var("sub").into(), var("tax").into()]).bind_scalar(var("total")),
811                ),
812        );
813    }
814
815    #[test]
816    fn not_or_and_rules_parse() {
817        assert_parses(
818            &Query::find([var("e")])
819                .where_(data(var("e"), attr("person/name"), blank()))
820                .and(not(vec![data(
821                    var("e"),
822                    attr("person/deceased"),
823                    lit(true),
824                )])),
825        );
826        assert_parses(&Query::find([var("e")]).where_(or(vec![
827            vec![data(var("e"), attr("person/name"), lit("Alice"))],
828            vec![data(var("e"), attr("person/name"), lit("Bob"))],
829        ])));
830        assert_parses(
831            &Query::find([var("e")])
832                .in_rules()
833                .where_(rule("ancestor", vec![var("e").into(), var("x").into()])),
834        );
835    }
836}