Skip to main content

corium_query/
lib.rs

1//! The Corium query engine: EDN Datalog, Pull, the entity API, and direct
2//! index access, executing on the peer against immutable [`corium_db::Db`]
3//! values (see `docs/design/query-engine.md`).
4
5pub mod aggregate;
6pub mod ast;
7pub mod boundary;
8pub mod builtins;
9pub mod cache;
10pub mod edn;
11pub mod entity;
12pub mod exec;
13pub mod plan;
14pub mod pull;
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use corium_core::{Keyword, Value};
19use corium_db::Db;
20use thiserror::Error;
21
22use crate::aggregate::AggOut;
23use crate::ast::{FindElem, FindSpec, InSpec, Query, Var, parse_query, parse_rules};
24use crate::boundary::{edn_to_value, value_to_edn};
25use crate::edn::{Edn, EdnError};
26use crate::exec::{ExecCtx, Frame, to_entity};
27
28pub use crate::cache::QueryCache;
29pub use crate::entity::Entity;
30pub use crate::pull::{pull, pull_many};
31
32/// Query failure.
33#[derive(Debug, Error, Eq, PartialEq)]
34pub enum QueryError {
35    /// Malformed query, rules, or pull pattern.
36    #[error("query parse error: {0}")]
37    Parse(String),
38    /// Boundary EDN failed to read.
39    #[error(transparent)]
40    Edn(#[from] EdnError),
41    /// An ident has no entity in the database.
42    #[error("unknown ident {0}")]
43    UnknownIdent(Keyword),
44    /// A `$…` source has no bound database.
45    #[error("unknown database source {0}")]
46    UnknownSource(String),
47    /// A variable was consumed before anything bound it.
48    #[error("unbound variable {0}")]
49    Unbound(String),
50    /// Wrong number of arguments.
51    #[error("arity error: {0}")]
52    Arity(String),
53    /// Operand types not supported by an operation.
54    #[error("type error: {0}")]
55    Type(String),
56    /// Feature or name outside the v1 native set.
57    #[error("unsupported: {0}")]
58    Unsupported(String),
59    /// The execution fuel budget was exhausted.
60    #[error("query fuel exhausted")]
61    FuelExhausted,
62}
63
64/// One query input, positionally matching the query's `:in` specification.
65#[derive(Clone, Debug)]
66pub enum QInput<'a> {
67    /// A database value for a `$…` source.
68    Db(&'a Db),
69    /// An EDN argument (scalar, tuple, collection, relation, or rule set).
70    Edn(Edn),
71}
72
73/// Execution telemetry.
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
75pub struct ExecReport {
76    /// Datoms touched by index scans.
77    pub datoms_scanned: usize,
78}
79
80/// Resolution hook for predicate/function clause names outside the native
81/// set: given the name and evaluated arguments, returns `Some(result)` when
82/// the extern resolves the call, `None` to fall through to the canonical
83/// unsupported-name error. This is the seam the sandboxed cljrs host wires
84/// into (`docs/design/query-engine.md`, resolution step 2).
85pub type ExternCall = std::sync::Arc<
86    dyn Fn(&str, &[Value]) -> Option<Result<builtins::CallResult, QueryError>> + Send + Sync,
87>;
88
89/// Options bounding a query execution.
90#[derive(Clone, Default)]
91pub struct ExecOptions {
92    /// Maximum datoms the execution may touch (fuel).
93    pub fuel: Option<u64>,
94    /// Optional resolver for non-native call clause names.
95    pub extern_call: Option<ExternCall>,
96}
97
98impl std::fmt::Debug for ExecOptions {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.debug_struct("ExecOptions")
101            .field("fuel", &self.fuel)
102            .field("extern_call", &self.extern_call.is_some())
103            .finish()
104    }
105}
106
107/// Runs a query given as an EDN form.
108///
109/// # Errors
110/// Returns [`QueryError`] for malformed queries and execution failures.
111pub fn q(form: &Edn, inputs: &[QInput<'_>]) -> Result<Edn, QueryError> {
112    let query = parse_query(form)?;
113    run(&query, inputs, ExecOptions::default()).map(|(result, _)| result)
114}
115
116/// Runs a query given as EDN text.
117///
118/// # Errors
119/// Returns [`QueryError`] for unreadable text, malformed queries, and
120/// execution failures.
121pub fn q_str(text: &str, inputs: &[QInput<'_>]) -> Result<Edn, QueryError> {
122    q(&edn::read_one(text)?, inputs)
123}
124
125/// Runs a parsed query with options, returning the result and telemetry.
126///
127/// # Errors
128/// Returns [`QueryError`] for execution failures.
129#[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
130pub fn run(
131    query: &Query,
132    inputs: &[QInput<'_>],
133    options: ExecOptions,
134) -> Result<(Edn, ExecReport), QueryError> {
135    if inputs.len() != query.inputs.len() {
136        return Err(QueryError::Arity(format!(
137            "query takes {} inputs, got {}",
138            query.inputs.len(),
139            inputs.len()
140        )));
141    }
142
143    // Bind inputs: databases, rules, and initial frames.
144    let mut dbs: BTreeMap<String, &Db> = BTreeMap::new();
145    let mut rules = Vec::new();
146    for (spec, input) in query.inputs.iter().zip(inputs) {
147        if let (InSpec::Db(name), QInput::Db(db)) = (spec, input) {
148            dbs.insert(name.clone(), db);
149        }
150    }
151    let default_db = dbs
152        .get(ast::DEFAULT_SRC)
153        .copied()
154        .or_else(|| dbs.values().next().copied());
155    let mut frames: Vec<Frame> = vec![Frame::new()];
156    for (spec, input) in query.inputs.iter().zip(inputs) {
157        match (spec, input) {
158            (InSpec::Db(_), QInput::Db(_)) => {}
159            (InSpec::Rules, QInput::Edn(form)) => rules = parse_rules(form)?,
160            (InSpec::Scalar(var), QInput::Edn(form)) => {
161                let value = input_value(default_db, form)?;
162                for frame in &mut frames {
163                    frame.insert(var.clone(), value.clone());
164                }
165            }
166            (InSpec::Tuple(vars), QInput::Edn(form)) => {
167                let items = form.as_seq().ok_or_else(|| {
168                    QueryError::Type(format!("tuple input requires a vector, got {form}"))
169                })?;
170                if items.len() != vars.len() {
171                    return Err(QueryError::Arity("tuple input arity mismatch".into()));
172                }
173                for (var, item) in vars.iter().zip(items) {
174                    let value = input_value(default_db, item)?;
175                    for frame in &mut frames {
176                        frame.insert(var.clone(), value.clone());
177                    }
178                }
179            }
180            (InSpec::Coll(var), QInput::Edn(form)) => {
181                let items = form.as_seq().ok_or_else(|| {
182                    QueryError::Type(format!("collection input requires a vector, got {form}"))
183                })?;
184                let values = items
185                    .iter()
186                    .map(|item| input_value(default_db, item))
187                    .collect::<Result<Vec<_>, _>>()?;
188                frames = cross_bind(&frames, |frame, out| {
189                    for value in &values {
190                        let mut next = frame.clone();
191                        next.insert(var.clone(), value.clone());
192                        out.push(next);
193                    }
194                });
195            }
196            (InSpec::Rel(vars), QInput::Edn(form)) => {
197                let tuples = form.as_seq().ok_or_else(|| {
198                    QueryError::Type(format!("relation input requires a vector, got {form}"))
199                })?;
200                let mut converted: Vec<Vec<Value>> = Vec::new();
201                for tuple in tuples {
202                    let items = tuple
203                        .as_seq()
204                        .ok_or_else(|| QueryError::Type("relation input requires tuples".into()))?;
205                    if items.len() != vars.len() {
206                        return Err(QueryError::Arity("relation input arity mismatch".into()));
207                    }
208                    converted.push(
209                        items
210                            .iter()
211                            .map(|item| input_value(default_db, item))
212                            .collect::<Result<_, _>>()?,
213                    );
214                }
215                frames = cross_bind(&frames, |frame, out| {
216                    for tuple in &converted {
217                        let mut next = frame.clone();
218                        for (var, value) in vars.iter().zip(tuple) {
219                            next.insert(var.clone(), value.clone());
220                        }
221                        out.push(next);
222                    }
223                });
224            }
225            (spec, input) => {
226                return Err(QueryError::Type(format!(
227                    "input does not fit :in specification {spec:?}: {input:?}"
228                )));
229            }
230        }
231    }
232
233    let mut ctx = ExecCtx::new(dbs, rules);
234    if let Some(fuel) = options.fuel {
235        ctx.set_fuel(fuel);
236    }
237    if let Some(extern_call) = options.extern_call.clone() {
238        ctx.set_extern_call(extern_call);
239    }
240    let frames = exec::eval_clauses(&ctx, &query.wheres, frames, &mut Vec::new())?;
241
242    // Project to find + with variables with set semantics.
243    let elems = query.find.elems();
244    let mut proj_vars: Vec<Var> = elems.iter().map(|elem| elem.var().clone()).collect();
245    proj_vars.extend(query.with.iter().cloned());
246    let mut rows: BTreeSet<Vec<Value>> = BTreeSet::new();
247    for frame in frames {
248        let row = proj_vars
249            .iter()
250            .map(|var| {
251                frame
252                    .get(var)
253                    .cloned()
254                    .ok_or_else(|| QueryError::Unbound(var.clone()))
255            })
256            .collect::<Result<Vec<_>, _>>()?;
257        rows.insert(row);
258    }
259
260    // Aggregate or project.
261    let has_aggregate = elems
262        .iter()
263        .any(|elem| matches!(elem, FindElem::Aggregate(_)));
264    let out_rows: Vec<Vec<Cell>> = if has_aggregate {
265        let group_positions: Vec<usize> = elems
266            .iter()
267            .enumerate()
268            .filter(|(_, elem)| !matches!(elem, FindElem::Aggregate(_)))
269            .map(|(index, _)| index)
270            .collect();
271        let mut groups: BTreeMap<Vec<Value>, Vec<Vec<Value>>> = BTreeMap::new();
272        for row in rows {
273            let key = group_positions
274                .iter()
275                .map(|&index| row[index].clone())
276                .collect();
277            groups.entry(key).or_default().push(row);
278        }
279        let mut out = Vec::new();
280        for group in groups.values() {
281            let mut cells = Vec::with_capacity(elems.len());
282            for (index, elem) in elems.iter().enumerate() {
283                match elem {
284                    FindElem::Aggregate(agg) => {
285                        let values: Vec<Value> =
286                            group.iter().map(|row| row[index].clone()).collect();
287                        cells.push(Cell::Agg(aggregate::apply(&agg.op, agg.n, &values)?));
288                    }
289                    FindElem::Var(_) | FindElem::Pull(_, _) => {
290                        cells.push(Cell::Val(group[0][index].clone(), (*elem).clone()));
291                    }
292                }
293            }
294            out.push(cells);
295        }
296        out
297    } else {
298        rows.into_iter()
299            .map(|row| {
300                elems
301                    .iter()
302                    .enumerate()
303                    .map(|(index, elem)| Cell::Val(row[index].clone(), (*elem).clone()))
304                    .collect()
305            })
306            .collect()
307    };
308
309    // Convert to EDN, applying pull expressions.
310    let mut edn_rows: Vec<Vec<Edn>> = Vec::with_capacity(out_rows.len());
311    for row in out_rows {
312        let mut out = Vec::with_capacity(row.len());
313        for cell in row {
314            out.push(cell_to_edn(default_db, cell)?);
315        }
316        edn_rows.push(out);
317    }
318    edn_rows.sort();
319
320    let result = match &query.find {
321        FindSpec::Rel(_) => Edn::Vector(edn_rows.into_iter().map(Edn::Vector).collect()),
322        FindSpec::Coll(_) => Edn::Vector(
323            edn_rows
324                .into_iter()
325                .filter_map(|row| row.into_iter().next())
326                .collect(),
327        ),
328        FindSpec::Tuple(_) => edn_rows.into_iter().next().map_or(Edn::Nil, Edn::Vector),
329        FindSpec::Scalar(_) => edn_rows
330            .into_iter()
331            .next()
332            .and_then(|row| row.into_iter().next())
333            .unwrap_or(Edn::Nil),
334    };
335    Ok((
336        result,
337        ExecReport {
338            datoms_scanned: ctx.scanned(),
339        },
340    ))
341}
342
343enum Cell {
344    Val(Value, FindElem),
345    Agg(AggOut),
346}
347
348fn cell_to_edn(default_db: Option<&Db>, cell: Cell) -> Result<Edn, QueryError> {
349    let db = default_db.ok_or_else(|| QueryError::UnknownSource(ast::DEFAULT_SRC.into()))?;
350    match cell {
351        Cell::Val(value, FindElem::Pull(_, pattern)) => {
352            let eid = to_entity(&value).ok_or_else(|| {
353                QueryError::Type("pull requires an entity-valued variable".into())
354            })?;
355            pull::pull(db, &pattern, eid)
356        }
357        Cell::Val(value, _) | Cell::Agg(AggOut::Value(value)) => Ok(value_to_edn(db, &value)),
358        Cell::Agg(AggOut::Coll(values)) => Ok(Edn::Vector(
359            values.iter().map(|value| value_to_edn(db, value)).collect(),
360        )),
361        Cell::Agg(AggOut::Set(values)) => {
362            let mut items: Vec<Edn> = values.iter().map(|value| value_to_edn(db, value)).collect();
363            items.sort();
364            items.dedup();
365            Ok(Edn::Set(items))
366        }
367    }
368}
369
370fn cross_bind(frames: &[Frame], mut expand: impl FnMut(&Frame, &mut Vec<Frame>)) -> Vec<Frame> {
371    let mut out = Vec::new();
372    for frame in frames {
373        expand(frame, &mut out);
374    }
375    out
376}
377
378/// Converts an input argument to a value, supporting scalars, tagged
379/// entities/instants/uuids, and lookup refs.
380fn input_value(db: Option<&Db>, form: &Edn) -> Result<Value, QueryError> {
381    if let Some(value) = edn_to_value(db, form) {
382        return Ok(value);
383    }
384    // Lookup ref `[attr value]` resolved against the default database.
385    if let (Some(db), Edn::Vector(items)) = (db, form) {
386        if let [attr_form, value_form] = items.as_slice() {
387            if let Some(attr_kw) = attr_form.as_keyword() {
388                let attr = db
389                    .idents()
390                    .entid(attr_kw)
391                    .ok_or_else(|| QueryError::UnknownIdent(attr_kw.clone()))?;
392                let value = edn_to_value(Some(db), value_form).ok_or_else(|| {
393                    QueryError::Type(format!("bad lookup ref value {value_form}"))
394                })?;
395                let value = db.schema().get(attr).map_or(value.clone(), |meta| {
396                    exec::coerce_for_type(value, meta.value_type)
397                });
398                return db
399                    .lookup(attr, &value)
400                    .map(Value::Ref)
401                    .ok_or_else(|| QueryError::Type(format!("lookup ref {form} did not resolve")));
402            }
403        }
404    }
405    Err(QueryError::Type(format!("cannot convert input {form}")))
406}