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 && let Value::Ref(child) = value {
317                        return render_ref(db, spec, enclosing, *child, is_component, path);
318                    }
319                    Ok(value_to_edn(db, value))
320                };
321            let rendered = if many {
322                let items = values
323                    .iter()
324                    .map(|value| render(value, path))
325                    .collect::<Result<Vec<_>, _>>()?;
326                Edn::Vector(items)
327            } else {
328                render(&values[0], path)?
329            };
330            let key = spec
331                .as_key
332                .clone()
333                .unwrap_or_else(|| Edn::Keyword(ident.clone()));
334            Ok(Some((key, rendered)))
335        }
336        PullAttr::Reverse(attr, reverse_ident) => {
337            let is_component = db.schema().get(*attr).is_some_and(|m| m.is_component);
338            let parents = reverse_refs(db, *attr, eid);
339            if parents.is_empty() {
340                let default = spec.default.clone().map(|form| {
341                    (
342                        spec.as_key
343                            .clone()
344                            .unwrap_or_else(|| Edn::Keyword(reverse_ident.clone())),
345                        form,
346                    )
347                });
348                return Ok(default);
349            }
350            let limit = spec.limit.bound();
351            let mut parents = parents;
352            if let Some(limit) = limit {
353                parents.truncate(limit);
354            }
355            let render = |parent: EntityId, path: &mut BTreeSet<EntityId>| match &spec.sub {
356                SubSelect::None => Ok(Edn::Map(vec![(
357                    Edn::keyword("db/id"),
358                    Edn::Long(i64::try_from(parent.raw()).unwrap_or(i64::MAX)),
359                )])),
360                SubSelect::Pattern(sub) => pull_entity(db, sub, parent, path),
361                SubSelect::Recur(_) => Err(QueryError::Parse(
362                    "recursion is not supported on reverse refs".into(),
363                )),
364            };
365            // A component's parent is unique: reverse component refs are scalar.
366            let rendered = if is_component {
367                render(parents[0], path)?
368            } else {
369                Edn::Vector(
370                    parents
371                        .into_iter()
372                        .map(|parent| render(parent, path))
373                        .collect::<Result<Vec<_>, _>>()?,
374                )
375            };
376            let key = spec
377                .as_key
378                .clone()
379                .unwrap_or_else(|| Edn::Keyword(reverse_ident.clone()));
380            Ok(Some((key, rendered)))
381        }
382    }
383}
384
385fn render_ref(
386    db: &Db,
387    spec: &PullSpec,
388    enclosing: &PullPattern,
389    child: EntityId,
390    is_component: bool,
391    path: &mut BTreeSet<EntityId>,
392) -> Result<Edn, QueryError> {
393    let db_id_map = |child: EntityId| {
394        Edn::Map(vec![(
395            Edn::keyword("db/id"),
396            Edn::Long(i64::try_from(child.raw()).unwrap_or(i64::MAX)),
397        )])
398    };
399    let spec_attr = |candidate: &PullSpec| match (&candidate.attr, &spec.attr) {
400        (PullAttr::Forward(a, _), PullAttr::Forward(b, _))
401        | (PullAttr::Reverse(a, _), PullAttr::Reverse(b, _)) => a == b,
402        _ => false,
403    };
404    match &spec.sub {
405        SubSelect::Pattern(sub) => {
406            if !path.insert(child) {
407                return Ok(db_id_map(child));
408            }
409            let result = pull_entity(db, sub, child, path);
410            path.remove(&child);
411            result
412        }
413        SubSelect::Recur(depth) => {
414            if depth == &Some(0) || !path.insert(child) {
415                return Ok(db_id_map(child));
416            }
417            // Recursion re-applies the enclosing pattern, with this spec's
418            // remaining depth decremented.
419            let mut sub = enclosing.clone();
420            for candidate in &mut sub.specs {
421                if spec_attr(candidate)
422                    && let SubSelect::Recur(d) = &candidate.sub
423                {
424                    candidate.sub = SubSelect::Recur(d.map(|d| d.saturating_sub(1)));
425                }
426            }
427            let result = pull_entity(db, &sub, child, path);
428            path.remove(&child);
429            result
430        }
431        SubSelect::None => {
432            if is_component {
433                if !path.insert(child) {
434                    return Ok(db_id_map(child));
435                }
436                // Component auto-recursion: pull the whole component entity.
437                let wildcard = PullPattern {
438                    wildcard: true,
439                    db_id: true,
440                    specs: Vec::new(),
441                };
442                let result = pull_entity(db, &wildcard, child, path);
443                path.remove(&child);
444                result
445            } else {
446                Ok(db_id_map(child))
447            }
448        }
449    }
450}