1use 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
35fn 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
46pub(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
60pub(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
72pub(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
83pub(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
97pub(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
111pub(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
128pub(crate) fn root_target(source: &Expr) -> Option<Target> {
135 match source {
136 Expr::Ident { name, .. } => Target::parse(name),
137 _ => None,
138 }
139}
140
141fn 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
154fn 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
164fn 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 || sub.flatten.iter().any(|f| inner(&f.expr))
225 || 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
239fn 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 Expr::Ident { name, .. } if Target::parse(name).is_some() => {
257 !root || !is_target_relation_of(target, name)
258 }
259 Expr::Ident { .. } => false,
261 Expr::Outer { .. } | Expr::Call { .. } => true,
262 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 Expr::Required { .. } => true,
274 Expr::Op(op) => op_may_raise(op, target, root),
275 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 Expr::Tuple { items, .. } => items.iter().any(again),
288 Expr::Template { parts, .. } => oqx::walk::template_exprs(parts).next().is_some(),
292 Expr::Aggregate { .. } => true,
296 }
297}
298
299#[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#[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 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 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
378pub 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 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 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 assert_eq!(
504 c.params,
505 vec![
506 text("r"),
507 text("processes/"),
508 text("processes/"),
509 text("canon")
510 ]
511 );
512 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 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 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 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 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 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 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 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 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 #[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 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 planned("from blocks where _path == \"x\" && _doc._path == \"y\"");
646 planned("from blocks where _path == \"x\" && _nodes exists { where _doc._path == \"y\" }");
647 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 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 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 declined(
669 "from docs where _path == \"x\" && _blocks exists { select t: _nodes single { where _kind == \"md:task\" } }",
670 );
671 planned("from docs where _path.startsWith(\"lab/\") && _path == \"x\"");
673 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 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 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 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}