Skip to main content

omgbase_surface/
planner.rs

1//! The tier-3 pushdown planner: reduces the row set a query scans by
2//! translating its pushable top-level `where` conjuncts into ONE SQL
3//! statement against the store, then handing the produced rows (plus the
4//! untranslatable residual) back to the in-memory engine to finish. Port of
5//! `packages/core/src/oqx-js/planner.ts` over the [`oqx`] seam
6//! ([`QueryPlanner`], [`Plan`], [`oqx::partition_pushable`],
7//! [`oqx::residual_query`], [`oqx::ROWS_ROOT`]).
8//!
9//! Correctness is guaranteed by the residual fallback — anything
10//! [`crate::translate`] declines stays in-memory — and verified by the
11//! differential gate (planned == in-memory over every corpus-backed query
12//! case and the conformance list). Deliberately conservative: only simple
13//! top-level target scans (no `follow`, no `from E` re-projection) whose
14//! source is a bare `docs|blocks|nodes|links` or `<target>`, with at
15//! least one pushable scalar conjunct, are planned; everything else declines
16//! to a full in-memory run.
17//!
18//! One shape difference from the reference, forced by ownership: the
19//! reference attaches a store context to its plan (`makeStoreContext(…,
20//! rowsRoot)`), but a [`crate::StoreContext`] borrows the store's connection
21//! and `Plan::context` must be `'static`, so the plan carries no context and
22//! the runner ([`mod@crate::query`]) builds the rows-root context itself
23//! ([`crate::StoreContext::with_rows_root`]) — the same six lines
24//! `oqx::PlannedEngine::run` would execute.
25
26use oqx::Consumer;
27use oqx::ast::{Expr, Follow, FollowDestination, OpNode, Query, SelectItem, Subquery, Where};
28use oqx::{Plan, QueryPlanner, Value, partition_pushable, residual_query};
29use rusqlite::Connection;
30use rusqlite::types::Value as SqlValue;
31
32use crate::context::{REVISION_COLS, REVISION_FROM, Target, fetch_rows, tag_rows};
33use crate::translate::{TranslateCtx, translate_predicate};
34
35/// The SQL aliases of the scanned row (`self`) and its owning doc (`doc`).
36fn aliases(t: Target) -> (&'static str, &'static str) {
37    match t {
38        Target::Docs => ("d", "d"),
39        Target::Blocks => ("b", "d"),
40        Target::Nodes => ("n", "d"),
41        Target::Links => ("e", "d"),
42        Target::Revisions => ("r", "d"),
43    }
44}
45
46/// The scan's `FROM`. The `revisions` arms here and below (4.7) are the
47/// context's own scan spelled as clauses, so a caller keyed by target
48/// (`names::present`, a probe) can speak of the kind; the planner itself
49/// never plans a revisions scan ([`compile`]).
50pub(crate) fn from_clause(t: Target) -> &'static str {
51    match t {
52        Target::Docs => "docs d",
53        Target::Blocks => "blocks b JOIN docs d ON d.doc_id = b.doc_id",
54        Target::Nodes => "nodes n JOIN docs d ON d.doc_id = n.doc_id",
55        Target::Links => "edges e JOIN docs d ON d.doc_id = e.src_doc",
56        Target::Revisions => REVISION_FROM,
57    }
58}
59
60/// Row columns + the owning-doc path as `__path` (matches the context's
61/// roots so produced rows are indistinguishable from a full scan's).
62pub(crate) fn columns(t: Target) -> &'static str {
63    match t {
64        Target::Docs => "d.*",
65        Target::Blocks => "b.*, d.path AS __path",
66        Target::Nodes => "n.*, d.path AS __path",
67        Target::Links => "e.*, d.path AS __path",
68        Target::Revisions => REVISION_COLS,
69    }
70}
71
72/// The root order (`spec/surface` §1.1).
73pub(crate) fn order_clause(t: Target) -> &'static str {
74    match t {
75        Target::Docs => "d.path, d.doc_id",
76        Target::Blocks => "d.path, b.block_id",
77        Target::Nodes => "d.path, n.node_id",
78        Target::Links => "d.path, e.edge_id",
79        Target::Revisions => "r.path, r.rev_id",
80    }
81}
82
83/// The liveness guards of the root scan, with the repo id bound. The history
84/// has no liveness: every revision of every document of the repository,
85/// tombstoned ones included (§1.1, 4.7) — the repository guard through the
86/// owning `docs` row, since `revisions` carries no `repo_id`.
87pub(crate) fn guards(t: Target) -> &'static str {
88    match t {
89        Target::Docs => "d.repo_id = ? AND d.deleted_commit IS NULL",
90        Target::Blocks => "b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL",
91        Target::Nodes => "n.repo_id = ? AND d.deleted_commit IS NULL",
92        Target::Links => "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL",
93        Target::Revisions => "d.repo_id = ?",
94    }
95}
96
97/// The joined targets driven FROM the document: the same rows and columns as
98/// [`from_clause`], with the loop order fixed by `CROSS JOIN` so a predicate on
99/// the document (`d.path = ?`) is the outer search and the target's rows are
100/// reached through their `doc_id` / `src_doc` index (the store indexes'
101/// `path` probes). [`guards_by_doc`] goes with it; `Docs` is `from_clause`.
102pub(crate) fn from_by_doc(t: Target) -> &'static str {
103    match t {
104        Target::Docs | Target::Revisions => from_clause(t),
105        Target::Blocks => "docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id",
106        Target::Nodes => "docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id",
107        Target::Links => "docs d CROSS JOIN edges e ON e.src_doc = d.doc_id",
108    }
109}
110
111/// [`guards`] for a [`from_by_doc`] statement: the same tests, with the
112/// document's repo first and the target's repo term behind SQLite's unary `+`
113/// so it stays a filter and never selects a `(repo_id, …)` index over the
114/// `doc_id` one. Two repo-id binds (`Docs`: one, as `guards`).
115pub(crate) fn guards_by_doc(t: Target) -> &'static str {
116    match t {
117        Target::Docs | Target::Revisions => guards(t),
118        Target::Blocks => {
119            "d.repo_id = ? AND +b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL"
120        }
121        Target::Nodes => "d.repo_id = ? AND +n.repo_id = ? AND d.deleted_commit IS NULL",
122        Target::Links => {
123            "d.repo_id = ? AND +e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL"
124        }
125    }
126}
127
128/// The root collection a query scans, if it is a bare
129/// `docs|blocks|nodes|links|revisions` source at the root scope (else `None` —
130/// a nested `^docs` is a block's receiver, never the query's source). The
131/// runner asks too: a bare root scan's rows are store rows by construction.
132/// Five-wide (the reference's `ROOTS` test); [`compile`] plans four — a
133/// `revisions` scan (4.7) always runs in memory.
134pub(crate) fn root_target(source: &Expr) -> Option<Target> {
135    match source {
136        Expr::Ident { name, .. } => Target::parse(name),
137        _ => None,
138    }
139}
140
141/// Whether `name` is a relation of `target`'s rows that is named after a
142/// target (§1.2, `_`-prefixed since 5.0: `_nodes`, `_blocks`, `_revisions` on
143/// docs, `_nodes` on blocks, `_blocks` on nodes). A BARE root name on a row is
144/// never a relation.
145fn is_target_relation_of(target: Target, name: &str) -> bool {
146    matches!(
147        (target, name),
148        (Target::Docs, "_nodes" | "_blocks" | "_revisions")
149            | (Target::Blocks, "_nodes")
150            | (Target::Nodes, "_blocks")
151    )
152}
153
154/// The row a reach-through head reaches (`_doc.x`, `_block.x`, and a link's
155/// `_dst.x`; the self aliases are gone in 5.0).
156fn reach_through(head: &str) -> Option<Target> {
157    match head {
158        "_doc" | "_dst" => Some(Target::Docs),
159        "_block" => Some(Target::Blocks),
160        _ => None,
161    }
162}
163
164// ---- the residual-error decline (surface 1.1 patch, §1) ---------------------------------
165
166/// Whether a residual `where` could raise an OQX eval error the pushed
167/// conjuncts might hide by emptying the scan. In memory every conjunct of the
168/// original `&&` is evaluated for the first row and a throwing one aborts the
169/// run; planned, a pushed conjunct that matches no row means the residual
170/// never runs and the error vanishes. So a residual containing any function
171/// or method call (an unknown function, a bad regex, a wrong arity…), a
172/// nested block with the `single` consumer (more than one row raises), a
173/// `^`-escaped name (no enclosing scope at the top level) sends the whole
174/// query to the in-memory engine unplanned (an engine attribute never
175/// raises; a bare root name read as a property — inside a block at any depth,
176/// since 5.0 the relations are `_blocks`/`_nodes`/`_revisions` — does). Only
177/// comparisons, logical
178/// operators, `in`, `!`, literals, bindings and plain reads keep the push.
179///
180/// The walk is generic over the whole `oqx` AST — every `Where` node, every
181/// nested block (its `from`, `where`, `select`, `order by`, `follow`,
182/// `limit` / `offset`) and every `Expr`. A `^name:` lift in a nested select
183/// counts as a `^`-escaped name. Same decisions as the reference's
184/// `residualMayRaise`.
185fn residual_may_raise(w: &Where, target: Target) -> bool {
186    where_may_raise(w, target, true)
187}
188
189fn where_may_raise(w: &Where, target: Target, root: bool) -> bool {
190    match w {
191        Where::And { parts, .. } | Where::Or { parts, .. } => {
192            parts.iter().any(|p| where_may_raise(p, target, root))
193        }
194        Where::Not { expr, .. } => where_may_raise(expr, target, root),
195        Where::Scalar { expr, .. } => expr_may_raise(expr, target, root),
196        Where::Op(op) => op_may_raise(op, target, root),
197    }
198}
199
200fn op_may_raise(op: &OpNode, target: Target, root: bool) -> bool {
201    op.op == Consumer::Single
202        || expr_may_raise(&op.receiver, target, root)
203        || subquery_may_raise(&op.sub, target)
204}
205
206fn subquery_may_raise(sub: &Subquery, target: Target) -> bool {
207    let inner = |e: &Expr| expr_may_raise(e, target, false);
208    sub.from.iter().any(inner)
209        || sub
210            .r#where
211            .as_ref()
212            .is_some_and(|w| where_may_raise(w, target, false))
213        || sub.select.iter().any(|item| match item {
214            SelectItem::Field { expr, lift, .. } => *lift > 0 || inner(expr),
215            SelectItem::Collect { op, .. } => op_may_raise(op, target, false),
216        })
217        || sub.order_by.iter().flatten().any(|o| inner(&o.expr))
218        || sub
219            .follow
220            .as_ref()
221            .is_some_and(|f| follow_may_raise(f, target))
222        || sub.frontier.as_ref().is_some_and(inner)
223        // A flattened block (oqx 0.25) evaluates each `flatten` expression per row.
224        || sub.flatten.iter().any(|f| inner(&f.expr))
225        // A grouped block (oqx 0.24) evaluates its keys, `having` and its
226        // aggregates (`sum` over a string raises) per group.
227        || sub
228            .group_by
229            .as_ref()
230            .is_some_and(|g| g.keys.iter().any(|k| inner(&k.expr)))
231        || sub
232            .having
233            .as_ref()
234            .is_some_and(|h| where_may_raise(h, target, false))
235        || sub.limit.as_ref().is_some_and(inner)
236        || sub.offset.as_ref().is_some_and(inner)
237}
238
239// A destination is a relation read in the frontier row's scope, or a destination
240// block (whose receiver, body, `frontier` and `depth` are its own — oqx 0.20).
241fn follow_may_raise(f: &Follow, target: Target) -> bool {
242    let inner = |e: &Expr| expr_may_raise(e, target, false);
243    f.destinations.iter().any(|d| match d {
244        FollowDestination::Relation(e) => inner(e),
245        FollowDestination::Block(op) => op_may_raise(op, target, false),
246    })
247}
248
249fn expr_may_raise(e: &Expr, target: Target, root: bool) -> bool {
250    let again = |e: &Expr| expr_may_raise(e, target, root);
251    match e {
252        Expr::Lit { .. } | Expr::Binding { .. } => false,
253        // A bare target name read as a property raises (the root-row rule,
254        // `crate::query`): at the root scope the scan itself (the source) is
255        // the one bare spelling that does not; inside a block it always does.
256        Expr::Ident { name, .. } if Target::parse(name).is_some() => {
257            !root || !is_target_relation_of(target, name)
258        }
259        // An engine attribute or an authored read never raises.
260        Expr::Ident { .. } => false,
261        Expr::Outer { .. } | Expr::Call { .. } => true,
262        // `<head>.<target>` raises unless the head is a reach-through whose row
263        // has that relation.
264        Expr::Member { recv, name, .. } => {
265            (Target::parse(name).is_some()
266                && !matches!(&**recv, Expr::Ident { name: head, .. }
267                        if reach_through(head).is_some_and(|t| is_target_relation_of(t, name))))
268                || again(recv)
269        }
270        Expr::Unary { expr, .. } => again(expr),
271        // `x!` raises when absent; a value-position directive may `single`-fail
272        // or read anything its block reads.
273        Expr::Required { .. } => true,
274        Expr::Op(op) => op_may_raise(op, target, root),
275        // A regex operator (oqx 0.22) is a `matches` call: an invalid pattern
276        // raises, exactly as the call would.
277        Expr::Binary {
278            op, left, right, ..
279        } => op.is_regex() || again(left) || again(right),
280        Expr::Logical { left, right, .. } | Expr::In { left, right, .. } => {
281            again(left) || again(right)
282        }
283        Expr::Range { lo, hi, .. } => {
284            lo.as_deref().is_some_and(again) || hi.as_deref().is_some_and(again)
285        }
286        // A tuple (oqx 0.20) is never pushed down; it raises iff an element does.
287        Expr::Tuple { items, .. } => items.iter().any(again),
288        // A template literal (oqx 0.21) is never pushed down; interpolating a
289        // list or a record is an eval error only the data decides, so with any
290        // interpolation it may raise.
291        Expr::Template { parts, .. } => oqx::walk::template_exprs(parts).next().is_some(),
292        // An aggregate (oqx 0.24) raises on a non-number under `sum`/`avg`,
293        // which only the data decides; a grouped query is never planned anyway
294        // (`compile`).
295        Expr::Aggregate { .. } => true,
296    }
297}
298
299/// A compiled plan before execution: the statement, its params and the
300/// residual query. Pure — what the planner would run, for inspection.
301#[derive(Clone, Debug, PartialEq)]
302pub struct Compiled {
303    pub target: Target,
304    pub sql: String,
305    pub params: Vec<SqlValue>,
306    pub residual: Query,
307}
308
309/// Compile `query` against `repo_id`, or `None` when the shape declines: a
310/// `follow`, a `from E` re-projection, a `group by` (oqx 0.24) or a `flatten`
311/// (oqx 0.25) — a grouped or flattened query is declined whole: its `where`
312/// conjuncts could be pushed (the fan-out and the grouping happen after them,
313/// so the residual would flatten and group the produced rows correctly), but
314/// a pushed conjunct that empties the scan would hide an eval error a
315/// `flatten` expression, an aggregate or `having` raises in memory, so decline
316/// (c) extends to the post-where clauses; SQL `GROUP BY` / `json_each`
317/// pushdown is a later round — a source that is not a root scan, or no
318/// pushable conjunct at all (the engine then does everything).
319#[must_use]
320pub fn compile(query: &Query, params: &[Value], repo_id: &str) -> Option<Compiled> {
321    if query.follow.is_some()
322        || !query.from.is_empty()
323        || query.group_by.is_some()
324        || !query.flatten.is_empty()
325    {
326        return None;
327    }
328    let target = root_target(&query.source)?;
329    // The history (4.7) is never planned: no store index, no pushdown — the
330    // scan runs in memory, as the reference's four-wide `rootTarget` decides.
331    if target == Target::Revisions {
332        return None;
333    }
334    let (self_alias, doc_alias) = aliases(target);
335    let ctx = TranslateCtx {
336        target,
337        self_alias,
338        doc_alias,
339        params,
340    };
341    let (pushed, residual) = partition_pushable(query.r#where.as_ref(), |e| {
342        translate_predicate(e, &ctx).is_some()
343    });
344    if pushed.is_empty() {
345        return None;
346    }
347    // Decline (c): a residual that could raise must not be hidden behind an
348    // emptied scan — the whole query runs in memory.
349    if residual
350        .as_ref()
351        .is_some_and(|w| residual_may_raise(w, target))
352    {
353        return None;
354    }
355    let mut where_sql = guards(target).to_owned();
356    let mut sql_params = vec![SqlValue::Text(repo_id.to_owned())];
357    for e in &pushed {
358        let frag = translate_predicate(e, &ctx).expect("accepted by partition_pushable");
359        where_sql.push_str(" AND (");
360        where_sql.push_str(&frag.sql);
361        where_sql.push(')');
362        sql_params.extend(frag.params);
363    }
364    let sql = format!(
365        "SELECT {} FROM {} WHERE {where_sql} ORDER BY {}",
366        columns(target),
367        from_clause(target),
368        order_clause(target)
369    );
370    Some(Compiled {
371        target,
372        sql,
373        params: sql_params,
374        residual: residual_query(query, residual),
375    })
376}
377
378/// The SQLite planner over one repo of the store.
379pub struct SqlitePlanner<'a> {
380    conn: &'a Connection,
381    repo_id: String,
382}
383
384impl<'a> SqlitePlanner<'a> {
385    #[must_use]
386    pub fn new(conn: &'a Connection, repo_id: &str) -> Self {
387        Self {
388            conn,
389            repo_id: repo_id.to_owned(),
390        }
391    }
392
393    /// Plan `query`: `Ok(None)` when the shape declines, `Ok(Some(plan))`
394    /// with the produced rows (tagged like the context's root rows) and the
395    /// residual, `Err` when the statement itself failed (a store error, not
396    /// a decline — the runner reports it rather than silently rescanning).
397    pub fn try_plan(&self, query: &Query, params: &[Value]) -> rusqlite::Result<Option<Plan>> {
398        let Some(compiled) = compile(query, params, &self.repo_id) else {
399            return Ok(None);
400        };
401        let rows = fetch_rows(self.conn, &compiled.sql, &compiled.params)?;
402        Ok(Some(Plan::new(
403            tag_rows(rows, compiled.target),
404            compiled.residual,
405        )))
406    }
407}
408
409impl QueryPlanner for SqlitePlanner<'_> {
410    /// The seam's shape: a failed statement declines (the engine falls back
411    /// to a full scan). The runner uses [`Self::try_plan`] to surface it.
412    fn plan(&self, query: &Query, params: &[Value]) -> Option<Plan> {
413        self.try_plan(query, params).ok().flatten()
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use oqx::ROWS_ROOT;
421    use oqx::ast::Where;
422
423    fn parse(src: &str) -> Query {
424        oqx::parse_string(src).expect("parses")
425    }
426
427    fn text(s: &str) -> SqlValue {
428        SqlValue::Text(s.to_owned())
429    }
430
431    #[test]
432    fn a_pushable_scan_compiles_to_one_statement_in_root_order() {
433        let c =
434            compile(&parse("from docs where _path == \"index.md\""), &[], "r_1").expect("planned");
435        assert_eq!(c.target, Target::Docs);
436        assert_eq!(
437            c.sql,
438            "SELECT d.* FROM docs d WHERE d.repo_id = ? AND d.deleted_commit IS NULL AND ((('/' || d.path) IS ?)) ORDER BY d.path, d.doc_id"
439        );
440        assert_eq!(c.params, vec![text("r_1"), text("index.md")]);
441        assert_eq!(
442            c.residual.source,
443            Expr::Ident {
444                name: ROWS_ROOT.to_owned(),
445                span: oqx::Span::EMPTY,
446            }
447        );
448        assert_eq!(c.residual.r#where, None);
449    }
450
451    #[test]
452    fn every_target_has_its_join_columns_guards_and_order() {
453        let b = compile(
454            &parse("from blocks where _path.startsWith(\"lab/\")"),
455            &[],
456            "r",
457        )
458        .unwrap();
459        assert_eq!(
460            b.sql,
461            "SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id \
462             WHERE b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL \
463             AND ((substr(('/' || d.path), 1, length(?)) = ?)) ORDER BY d.path, b.block_id"
464        );
465        assert_eq!(b.params, vec![text("r"), text("lab/"), text("lab/")]);
466        let n = compile(
467            &parse("nodes count { where _kind == \"md:task\" }"),
468            &[],
469            "r",
470        )
471        .unwrap();
472        assert_eq!(n.target, Target::Nodes);
473        assert!(n.sql.starts_with(
474            "SELECT n.*, d.path AS __path FROM nodes n JOIN docs d ON d.doc_id = n.doc_id WHERE n.repo_id = ? AND d.deleted_commit IS NULL AND ((n.kind IS ?))"
475        ));
476        assert!(n.sql.ends_with("ORDER BY d.path, n.node_id"));
477        let e = compile(
478            &parse("from links where _predicate == \"references\""),
479            &[],
480            "r",
481        )
482        .unwrap();
483        assert!(e.sql.starts_with(
484            "SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc WHERE e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL AND ((e.predicate IS ?))"
485        ));
486        assert!(e.sql.ends_with("ORDER BY d.path, e.edge_id"));
487    }
488
489    #[test]
490    fn mixed_conjunctions_push_the_translatable_parts_and_keep_the_rest() {
491        let q = parse(
492            "from docs where _path.startsWith(\"processes/\") && _nodes exists { where _kind == \"md:task\" } && layer == \"canon\"",
493        );
494        let c = compile(&q, &[], "r").expect("planned");
495        assert!(
496            c.sql
497                .contains("(substr(('/' || d.path), 1, length(?)) = ?)"),
498            "{}",
499            c.sql
500        );
501        assert!(c.sql.contains("p.key = 'layer'"), "{}", c.sql);
502        // Guards first, then the fragments' params in statement order.
503        assert_eq!(
504            c.params,
505            vec![
506                text("r"),
507                text("processes/"),
508                text("processes/"),
509                text("canon")
510            ]
511        );
512        // One conjunct left → it stands alone, not wrapped in an `and`.
513        assert!(
514            matches!(c.residual.r#where, Some(Where::Op(_))),
515            "{:?}",
516            c.residual.r#where
517        );
518        assert!(c.residual.from.is_empty());
519        assert_eq!(c.residual.select, q.select);
520        assert_eq!(c.residual.consumer, q.consumer);
521    }
522
523    #[test]
524    fn declined_shapes_return_none() {
525        // nothing pushable → let the engine do it all
526        assert!(compile(&parse("from docs"), &[], "r").is_none());
527        assert!(compile(&parse("from docs where era in 800..1680"), &[], "r").is_none());
528        assert!(compile(&parse("from docs where !verified"), &[], "r").is_none());
529        assert!(
530            compile(
531                &parse("from docs where _path == \"a\" || _path == \"b\""),
532                &[],
533                "r"
534            )
535            .is_none()
536        );
537        assert!(
538            compile(
539                &parse("from docs where _nodes exists { where _kind == \"md:task\" }"),
540                &[],
541                "r"
542            )
543            .is_none()
544        );
545        // follow
546        assert!(
547            compile(
548                &parse("from docs where _path == \"a.md\" follow distinct _out_links collect { from _dst }"),
549                &[],
550                "r"
551            )
552            .is_none()
553        );
554        // flatten (oqx 0.25): declined whole, like `group by`
555        assert!(
556            compile(
557                &parse("select _path, t from docs where layer == \"canon\" flatten _tags as t"),
558                &[],
559                "r"
560            )
561            .is_none()
562        );
563        assert!(
564            compile(
565                &parse("from docs where layer == \"canon\" flatten _blocks { where _type == \"task\" } as b"),
566                &[],
567                "r"
568            )
569            .is_none()
570        );
571        // follow with several destinations: relations and a destination block
572        // decline exactly like a single relation (0.14)
573        assert!(
574            compile(
575                &parse("from docs where _path == \"a.md\" follow _out_links collect { from _dst }, _in_links collect { from _doc }"),
576                &[],
577                "r"
578            )
579            .is_none()
580        );
581        assert!(
582            compile(
583                &parse(
584                    "from docs where _path == \"a.md\" follow ^docs collect { where after.contains(^_path) }"
585                ),
586                &[],
587                "r"
588            )
589            .is_none()
590        );
591        // a body-level `frontier` (oqx 0.20) plans like the `follow` it needs
592        assert!(
593            compile(
594                &parse("from docs where _path == \"a.md\" frontier type == \"hub\" follow distinct _out_links collect { from _dst frontier type == \"practitioner\" }"),
595                &[],
596                "r"
597            )
598            .is_none()
599        );
600        // a grouped query is declined whole (oqx 0.24), pushable `where` or not
601        assert!(
602            compile(
603                &parse("select layer, n: count() from docs where _path == \"a.md\" group by layer"),
604                &[],
605                "r"
606            )
607            .is_none()
608        );
609        // not a root scan
610        assert!(compile(&parse("from things where _path == \"a.md\""), &[], "r").is_none());
611        assert!(compile(&parse("from nope where _path == \"a.md\""), &[], "r").is_none());
612        assert!(compile(&parse("from docs._nodes where _kind == \"x\""), &[], "r").is_none());
613        // a `from E` re-projection (an AST-level shape)
614        let mut q = parse("from docs where _path == \"a.md\"");
615        q.from.push(Expr::Ident {
616            name: "nodes".to_owned(),
617            span: oqx::Span::EMPTY,
618        });
619        assert!(compile(&q, &[], "r").is_none());
620    }
621
622    // -- decline (c): a residual that could raise sends the whole query in memory --
623
624    #[test]
625    fn a_residual_that_could_raise_declines_the_whole_query() {
626        let declined = |src: &str| {
627            assert!(
628                compile(&parse(src), &[], "r").is_none(),
629                "should decline: {src}"
630            );
631        };
632        let planned = |src: &str| {
633            assert!(
634                compile(&parse(src), &[], "r").is_some(),
635                "should plan: {src}"
636            );
637        };
638        // an engine attribute is never a raise — `_path` pushes, `_raw`/
639        // `_updated_at`/`_tags` are plain residuals
640        planned("from docs where _path == \"x\" && _path == \"nope.md\"");
641        planned("from docs where _path == \"nope.md\" && !_raw");
642        planned("from docs where _path == \"nope.md\" && _updated_at");
643        planned("from docs where _path == \"nope.md\" && _tags");
644        // `_doc.<attribute>` on any target, at any depth, is a plain read too
645        planned("from blocks where _path == \"x\" && _doc._path == \"y\"");
646        planned("from blocks where _path == \"x\" && _nodes exists { where _doc._path == \"y\" }");
647        // a bare root name inside a block raises (5.0: never a relation) —
648        // the whole query declines; its `_` spelling is the relation
649        declined("from docs where _path == \"x\" && _nodes exists { where blocks exists { } }");
650        planned(
651            "from docs where _path == \"x\" && _nodes exists { where _doc._blocks exists { } }",
652        );
653        // a function or method call
654        declined("from docs where _path.matches(\"[\") && _path == \"nope.md\"");
655        declined("from docs where nope(\"x\") && _path == \"nope.md\"");
656        declined("from docs where _path == \"x\" && size(_tags) > 1");
657        declined(
658            "from docs where _path == \"x\" && _nodes exists { where _name.lower() == \"a\" }",
659        );
660        declined(
661            "from docs where _path == \"x\" && _nodes count { where _kind == \"a\" order by size(_name) } > 1",
662        );
663        // a `^`-escaped name: an outer reference or a lift
664        declined("from docs where _path == \"x\" && ^slug == \"y\"");
665        declined("from docs where _path == \"x\" && _nodes exists { where _name == ^_title }");
666        declined("from docs where _path == \"x\" && _nodes collect { ^first_task: _name }");
667        // a nested block with the `single` consumer
668        declined(
669            "from docs where _path == \"x\" && _blocks exists { select t: _nodes single { where _kind == \"md:task\" } }",
670        );
671        // the pushed side raising is impossible; only the residual matters
672        planned("from docs where _path.startsWith(\"lab/\") && _path == \"x\"");
673        // plain reads, comparisons, `in`, `!`, literals and bindings keep the push
674        planned("from docs where _path == \"x\" && era in 800..1680");
675        planned("from docs where _path == \"x\" && !verified");
676        planned("from docs where _path == \"x\" && (layer == \"a\" || layer == \"b\")");
677        planned("from docs where _path == \"x\" && verified == true");
678        planned("from docs where _path == \"x\" && _nodes exists { where _kind == \"md:task\" }");
679        planned(
680            "from docs where _path == \"x\" && _nodes count { where _kind == \"md:task\" limit 5 } > 1",
681        );
682        // a bare `path` is an authored property on every kind (5.0), and
683        // inside a block the rows are another scope — an ordinary read either way
684        planned("from blocks where _path == \"x\" && !path");
685        planned("from docs where _path == \"x\" && _nodes exists { where path == \"y\" }");
686        planned("from docs where _path == \"x\" && _frontmatter.path == \"y\"");
687        // a key read through a source handle is pushed (3.0), scoped to its
688        // source; a WHOLE handle reference and `entries(…)` stay residual (and
689        // the function call declines the query)
690        let fm = compile(
691            &parse("from docs where _frontmatter.layer == \"canon\""),
692            &[],
693            "r",
694        )
695        .expect("planned");
696        assert!(fm.sql.contains("p.source = 'frontmatter'"), "{}", fm.sql);
697        let data =
698            compile(&parse("from docs where _data.layer == \"canon\""), &[], "r").expect("planned");
699        assert!(!data.sql.contains("p.source"), "{}", data.sql);
700        planned("from blocks where _doc._inline.element == \"fire\"");
701        planned("from docs where _path == \"x\" && _frontmatter == null");
702        declined("from docs where _path == \"x\" && entries(_frontmatter) exists { }");
703    }
704
705    #[test]
706    fn a_top_level_not_or_or_is_the_whole_residual_and_declines() {
707        // `!(…)` and `||` at the top are `Where::Not` / `Where::Or`, never scalar
708        // parts, so nothing is pushed even when their leaves would be.
709        assert!(compile(&parse("from docs where !(_path == \"a\")"), &[], "r").is_none());
710        assert!(
711            compile(
712                &parse("from docs where (_path == \"a\" || _path == \"b\") && layer == \"canon\""),
713                &[],
714                "r"
715            )
716            .is_some()
717        );
718    }
719}