Skip to main content

corium_query/
pull.rs

1//! The Pull API: declarative hierarchical selection from an entity.
2//!
3//! Supports the full v1 grammar: attribute specs, `*`, reverse refs
4//! (`:ns/_name`), nested maps, `:as`/`:limit`/`:default` options, bounded
5//! and unbounded recursion (`{:friend 6}` / `'...'`), and component
6//! auto-recursion. Results are EDN maps keyed by attribute ident.
7
8use std::collections::BTreeSet;
9
10use corium_core::{AttrId, EntityId, IndexOrder, Keyword, Value, ValueType};
11use corium_db::{Db, key_prefix};
12
13use crate::QueryError;
14use crate::boundary::value_to_edn;
15use crate::edn::Edn;
16
17/// Datomic's default limit on cardinality-many results.
18const DEFAULT_LIMIT: usize = 1000;
19
20#[derive(Clone, Debug)]
21enum PullAttr {
22    Forward(AttrId, Keyword),
23    Reverse(AttrId, Keyword),
24}
25
26#[derive(Clone, Debug)]
27enum SubSelect {
28    None,
29    Pattern(Box<PullPattern>),
30    Recur(Option<usize>),
31}
32
33/// Cardinality-many result limit: unset (Datomic's default of 1000),
34/// explicitly unlimited (`:limit nil`), or a bound.
35#[derive(Clone, Copy, Debug)]
36enum Limit {
37    Default,
38    Unlimited,
39    At(usize),
40}
41
42impl Limit {
43    fn bound(self) -> Option<usize> {
44        match self {
45            Self::Default => Some(DEFAULT_LIMIT),
46            Self::Unlimited => None,
47            Self::At(bound) => Some(bound),
48        }
49    }
50}
51
52#[derive(Clone, Debug)]
53struct PullSpec {
54    attr: PullAttr,
55    as_key: Option<Edn>,
56    limit: Limit,
57    default: Option<Edn>,
58    sub: SubSelect,
59}
60
61#[derive(Clone, Debug, Default)]
62struct PullPattern {
63    wildcard: bool,
64    db_id: bool,
65    specs: Vec<PullSpec>,
66}
67
68fn parse_error(message: impl Into<String>) -> QueryError {
69    QueryError::Parse(message.into())
70}
71
72fn resolve_attr(db: &Db, keyword: &Keyword) -> Result<PullAttr, QueryError> {
73    if let Some(reverse_name) = keyword.name.strip_prefix('_') {
74        let forward = Keyword::new(keyword.namespace.as_deref(), reverse_name);
75        let attr = db
76            .idents()
77            .entid(&forward)
78            .ok_or_else(|| QueryError::UnknownIdent(forward.clone()))?;
79        return Ok(PullAttr::Reverse(attr, keyword.clone()));
80    }
81    let attr = db
82        .idents()
83        .entid(keyword)
84        .ok_or_else(|| QueryError::UnknownIdent(keyword.clone()))?;
85    Ok(PullAttr::Forward(attr, keyword.clone()))
86}
87
88fn parse_pattern(db: &Db, form: &Edn) -> Result<PullPattern, QueryError> {
89    let items = form
90        .as_seq()
91        .ok_or_else(|| parse_error("pull pattern must be a vector"))?;
92    let mut pattern = PullPattern::default();
93    for item in items {
94        match item {
95            Edn::Symbol(sym) if sym == "*" => pattern.wildcard = true,
96            Edn::Keyword(k) if k.namespace.as_deref() == Some("db") && k.name == "id" => {
97                pattern.db_id = true;
98            }
99            Edn::Keyword(k) => pattern.specs.push(PullSpec {
100                attr: resolve_attr(db, k)?,
101                as_key: None,
102                limit: Limit::Default,
103                default: None,
104                sub: SubSelect::None,
105            }),
106            Edn::Vector(_) => pattern
107                .specs
108                .push(parse_attr_spec(db, item, SubSelect::None)?),
109            Edn::Map(pairs) => {
110                for (key, sub_form) in pairs {
111                    let sub = match sub_form {
112                        Edn::Long(depth) => SubSelect::Recur(Some(
113                            usize::try_from(*depth)
114                                .map_err(|_| parse_error("bad recursion depth"))?,
115                        )),
116                        Edn::Symbol(sym) if sym == "..." => SubSelect::Recur(None),
117                        _ => SubSelect::Pattern(Box::new(parse_pattern(db, sub_form)?)),
118                    };
119                    pattern.specs.push(parse_attr_spec(db, key, sub)?);
120                }
121            }
122            _ => return Err(parse_error(format!("bad pull spec {item}"))),
123        }
124    }
125    Ok(pattern)
126}
127
128/// Parses a keyword or `[attr opts…]` attribute spec.
129fn parse_attr_spec(db: &Db, form: &Edn, sub: SubSelect) -> Result<PullSpec, QueryError> {
130    match form {
131        Edn::Keyword(k) => Ok(PullSpec {
132            attr: resolve_attr(db, k)?,
133            as_key: None,
134            limit: Limit::Default,
135            default: None,
136            sub,
137        }),
138        Edn::Vector(items) => {
139            let (attr_form, opts) = items
140                .split_first()
141                .ok_or_else(|| parse_error("empty attribute spec"))?;
142            let Edn::Keyword(k) = attr_form else {
143                return Err(parse_error(format!("bad attribute spec {form}")));
144            };
145            let mut spec = PullSpec {
146                attr: resolve_attr(db, k)?,
147                as_key: None,
148                limit: Limit::Default,
149                default: None,
150                sub,
151            };
152            let mut opts = opts.iter();
153            while let Some(opt) = opts.next() {
154                let value = opts
155                    .next()
156                    .ok_or_else(|| parse_error("attribute option requires a value"))?;
157                match opt.as_keyword().map(|k| k.name.as_str()) {
158                    Some("as") => spec.as_key = Some(value.clone()),
159                    Some("limit") => {
160                        spec.limit = match value {
161                            Edn::Nil => Limit::Unlimited,
162                            Edn::Long(n) => Limit::At(
163                                usize::try_from(*n).map_err(|_| parse_error("bad :limit value"))?,
164                            ),
165                            _ => return Err(parse_error("bad :limit value")),
166                        };
167                    }
168                    Some("default") => spec.default = Some(value.clone()),
169                    _ => return Err(parse_error(format!("unknown attribute option {opt}"))),
170                }
171            }
172            Ok(spec)
173        }
174        _ => Err(parse_error(format!("bad attribute spec {form}"))),
175    }
176}
177
178/// Pulls `pattern` for one entity, producing an EDN map (or `nil` when the
179/// entity has no matching datoms).
180///
181/// # Errors
182/// Returns [`QueryError`] for malformed patterns or unknown attribute idents.
183pub fn pull(db: &Db, pattern: &Edn, eid: EntityId) -> Result<Edn, QueryError> {
184    let parsed = parse_pattern(db, pattern)?;
185    // The root is on the recursion path: a cycle back to it stops.
186    let mut path = BTreeSet::from([eid]);
187    pull_entity(db, &parsed, eid, &mut path)
188}
189
190/// Pulls `pattern` for each entity, preserving order.
191///
192/// # Errors
193/// Returns [`QueryError`] for malformed patterns or unknown attribute idents.
194pub fn pull_many(db: &Db, pattern: &Edn, eids: &[EntityId]) -> Result<Edn, QueryError> {
195    let parsed = parse_pattern(db, pattern)?;
196    let results = eids
197        .iter()
198        .map(|eid| {
199            let mut path = BTreeSet::from([*eid]);
200            pull_entity(db, &parsed, *eid, &mut path)
201        })
202        .collect::<Result<Vec<_>, _>>()?;
203    Ok(Edn::Vector(results))
204}
205
206fn entity_datoms(db: &Db, eid: EntityId) -> Vec<(AttrId, Value)> {
207    let prefix = key_prefix(IndexOrder::Eavt, Some(eid), None, None);
208    db.datoms_prefix(IndexOrder::Eavt, &prefix)
209        .map(|datom| (datom.a, datom.v.clone()))
210        .collect()
211}
212
213fn reverse_refs(db: &Db, attr: AttrId, eid: EntityId) -> Vec<EntityId> {
214    let value = Value::Ref(eid);
215    let prefix = key_prefix(IndexOrder::Vaet, None, Some(attr), Some(&value));
216    db.datoms_prefix(IndexOrder::Vaet, &prefix)
217        .map(|datom| datom.e)
218        .collect()
219}
220
221fn pull_entity(
222    db: &Db,
223    pattern: &PullPattern,
224    eid: EntityId,
225    path: &mut BTreeSet<EntityId>,
226) -> Result<Edn, QueryError> {
227    let own = entity_datoms(db, eid);
228    let mut pairs: Vec<(Edn, Edn)> = Vec::new();
229    if pattern.db_id || pattern.wildcard {
230        pairs.push((
231            Edn::keyword("db/id"),
232            Edn::Long(i64::try_from(eid.raw()).unwrap_or(i64::MAX)),
233        ));
234    }
235    if pattern.wildcard {
236        let explicit: BTreeSet<AttrId> = pattern
237            .specs
238            .iter()
239            .filter_map(|spec| match &spec.attr {
240                PullAttr::Forward(attr, _) => Some(*attr),
241                PullAttr::Reverse(_, _) => None,
242            })
243            .collect();
244        let mut attrs: Vec<AttrId> = own.iter().map(|(a, _)| *a).collect();
245        attrs.dedup();
246        for attr in attrs {
247            if explicit.contains(&attr) {
248                continue;
249            }
250            let Some(ident) = db.idents().ident(attr) else {
251                continue;
252            };
253            let spec = PullSpec {
254                attr: PullAttr::Forward(attr, ident.clone()),
255                as_key: None,
256                limit: Limit::Default,
257                default: None,
258                // Wildcard recursively pulls component entities.
259                sub: SubSelect::None,
260            };
261            if let Some((key, value)) = pull_spec(db, &spec, pattern, eid, &own, path)? {
262                pairs.push((key, value));
263            }
264        }
265    }
266    for spec in &pattern.specs {
267        if let Some((key, value)) = pull_spec(db, spec, pattern, eid, &own, path)? {
268            pairs.push((key, value));
269        }
270    }
271    if pairs.is_empty() {
272        return Ok(Edn::Nil);
273    }
274    pairs.sort_by(|left, right| left.0.cmp(&right.0));
275    pairs.dedup_by(|left, right| left.0 == right.0);
276    Ok(Edn::Map(pairs))
277}
278
279#[allow(clippy::too_many_lines)]
280fn pull_spec(
281    db: &Db,
282    spec: &PullSpec,
283    enclosing: &PullPattern,
284    eid: EntityId,
285    own: &[(AttrId, Value)],
286    path: &mut BTreeSet<EntityId>,
287) -> Result<Option<(Edn, Edn)>, QueryError> {
288    match &spec.attr {
289        PullAttr::Forward(attr, ident) => {
290            let meta = db.schema().get(*attr);
291            let is_ref = meta.is_some_and(|m| m.value_type == ValueType::Ref);
292            let is_component = meta.is_some_and(|m| m.is_component);
293            let many = meta.is_none_or(|m| m.cardinality == corium_core::Cardinality::Many);
294            let limit = spec.limit.bound();
295            let mut values: Vec<Value> = own
296                .iter()
297                .filter(|(a, _)| a == attr)
298                .map(|(_, v)| v.clone())
299                .collect();
300            if let Some(limit) = limit {
301                values.truncate(limit);
302            }
303            if values.is_empty() {
304                let default = spec.default.clone().map(|form| {
305                    (
306                        spec.as_key
307                            .clone()
308                            .unwrap_or_else(|| Edn::Keyword(ident.clone())),
309                        form,
310                    )
311                });
312                return Ok(default);
313            }
314            let render =
315                |value: &Value, path: &mut BTreeSet<EntityId>| -> Result<Edn, QueryError> {
316                    if is_ref {
317                        if let Value::Ref(child) = value {
318                            return render_ref(db, spec, enclosing, *child, is_component, path);
319                        }
320                    }
321                    Ok(value_to_edn(db, value))
322                };
323            let rendered = if many {
324                let items = values
325                    .iter()
326                    .map(|value| render(value, path))
327                    .collect::<Result<Vec<_>, _>>()?;
328                Edn::Vector(items)
329            } else {
330                render(&values[0], path)?
331            };
332            let key = spec
333                .as_key
334                .clone()
335                .unwrap_or_else(|| Edn::Keyword(ident.clone()));
336            Ok(Some((key, rendered)))
337        }
338        PullAttr::Reverse(attr, reverse_ident) => {
339            let is_component = db.schema().get(*attr).is_some_and(|m| m.is_component);
340            let parents = reverse_refs(db, *attr, eid);
341            if parents.is_empty() {
342                let default = spec.default.clone().map(|form| {
343                    (
344                        spec.as_key
345                            .clone()
346                            .unwrap_or_else(|| Edn::Keyword(reverse_ident.clone())),
347                        form,
348                    )
349                });
350                return Ok(default);
351            }
352            let limit = spec.limit.bound();
353            let mut parents = parents;
354            if let Some(limit) = limit {
355                parents.truncate(limit);
356            }
357            let render = |parent: EntityId, path: &mut BTreeSet<EntityId>| match &spec.sub {
358                SubSelect::None => Ok(Edn::Map(vec![(
359                    Edn::keyword("db/id"),
360                    Edn::Long(i64::try_from(parent.raw()).unwrap_or(i64::MAX)),
361                )])),
362                SubSelect::Pattern(sub) => pull_entity(db, sub, parent, path),
363                SubSelect::Recur(_) => Err(QueryError::Parse(
364                    "recursion is not supported on reverse refs".into(),
365                )),
366            };
367            // A component's parent is unique: reverse component refs are scalar.
368            let rendered = if is_component {
369                render(parents[0], path)?
370            } else {
371                Edn::Vector(
372                    parents
373                        .into_iter()
374                        .map(|parent| render(parent, path))
375                        .collect::<Result<Vec<_>, _>>()?,
376                )
377            };
378            let key = spec
379                .as_key
380                .clone()
381                .unwrap_or_else(|| Edn::Keyword(reverse_ident.clone()));
382            Ok(Some((key, rendered)))
383        }
384    }
385}
386
387fn render_ref(
388    db: &Db,
389    spec: &PullSpec,
390    enclosing: &PullPattern,
391    child: EntityId,
392    is_component: bool,
393    path: &mut BTreeSet<EntityId>,
394) -> Result<Edn, QueryError> {
395    let db_id_map = |child: EntityId| {
396        Edn::Map(vec![(
397            Edn::keyword("db/id"),
398            Edn::Long(i64::try_from(child.raw()).unwrap_or(i64::MAX)),
399        )])
400    };
401    let spec_attr = |candidate: &PullSpec| match (&candidate.attr, &spec.attr) {
402        (PullAttr::Forward(a, _), PullAttr::Forward(b, _))
403        | (PullAttr::Reverse(a, _), PullAttr::Reverse(b, _)) => a == b,
404        _ => false,
405    };
406    match &spec.sub {
407        SubSelect::Pattern(sub) => {
408            if !path.insert(child) {
409                return Ok(db_id_map(child));
410            }
411            let result = pull_entity(db, sub, child, path);
412            path.remove(&child);
413            result
414        }
415        SubSelect::Recur(depth) => {
416            if depth == &Some(0) || !path.insert(child) {
417                return Ok(db_id_map(child));
418            }
419            // Recursion re-applies the enclosing pattern, with this spec's
420            // remaining depth decremented.
421            let mut sub = enclosing.clone();
422            for candidate in &mut sub.specs {
423                if spec_attr(candidate) {
424                    if let SubSelect::Recur(d) = &candidate.sub {
425                        candidate.sub = SubSelect::Recur(d.map(|d| d.saturating_sub(1)));
426                    }
427                }
428            }
429            let result = pull_entity(db, &sub, child, path);
430            path.remove(&child);
431            result
432        }
433        SubSelect::None => {
434            if is_component {
435                if !path.insert(child) {
436                    return Ok(db_id_map(child));
437                }
438                // Component auto-recursion: pull the whole component entity.
439                let wildcard = PullPattern {
440                    wildcard: true,
441                    db_id: true,
442                    specs: Vec::new(),
443                };
444                let result = pull_entity(db, &wildcard, child, path);
445                path.remove(&child);
446                result
447            } else {
448                Ok(db_id_map(child))
449            }
450        }
451    }
452}