Skip to main content

sqry_db/planner/
ir.rs

1//! Query plan intermediate representation.
2//!
3//! The IR is **snapshot-independent**: no `StringId`, `NodeId`, or other
4//! interner-bound handles appear anywhere. This means a single plan can be
5//! compiled once and evaluated against any compatible graph snapshot, and two
6//! plans constructed independently but with identical semantics hash to the
7//! same value — a property the fuser in [`super::fuse`] relies on.
8//!
9//! # Types in this module
10//!
11//! | Type | Role |
12//! |------|------|
13//! | [`QueryPlan`] | Top-level wrapper produced by [`super::compile::QueryBuilder`] (DB10). |
14//! | [`PlanNode`] | Recursive operator tree. Every node produces a node-set. |
15//! | [`Direction`] | Edge traversal direction (outgoing/incoming/both). |
16//! | [`SetOperation`] | Pairwise set algebra (union/intersect/difference). |
17//! | [`Predicate`] | Filter condition — existence checks, relation predicates, boolean combinators. |
18//! | [`PredicateValue`] | Right-hand side of a relation predicate: pattern, regex, or nested subquery. |
19//! | [`StringPattern`] | Name-matching pattern with [`MatchMode`] discriminator. |
20//! | [`PathPattern`] | File-path glob matching, always glob-based. |
21//! | [`RegexPattern`] | Regex source + compilation flags. |
22//! | [`MatchMode`] | Exact / Glob / Prefix / Suffix / Contains modes for [`StringPattern`]. |
23//!
24//! # Design Notes
25//!
26//! ## Edge kind filtering
27//!
28//! [`PlanNode::EdgeTraversal`] carries `edge_kind: Option<EdgeKind>`, matching
29//! the spec (§3). Because [`EdgeKind`] contains metadata fields that are
30//! irrelevant to traversal (e.g., `Calls { argument_count, is_async }`), the
31//! [`QueryBuilder`] is expected to construct [`EdgeKind`] values with zeroed
32//! metadata (e.g., `EdgeKind::Calls { argument_count: 0, is_async: false }`)
33//! so that plan hashing remains stable for the fuser. The executor matches
34//! by `std::mem::discriminant`, matching the pattern used by
35//! [`crate::queries::ReachabilityQuery`].
36//!
37//! ## Scope filtering
38//!
39//! [`Predicate::InScope`] re-uses [`ScopeKind`] from the binding plane
40//! (`sqry-core::graph::unified::bind::scope::arena::ScopeKind`). The existing
41//! enum is already `Copy`, `Hash`, `Eq`, `Serialize`, `Deserialize`, so no
42//! wrapper type is introduced.
43//!
44//! ## Subqueries
45//!
46//! [`PredicateValue::Subquery`] is boxed to break the `Predicate`→`PlanNode`
47//! recursion cycle. Subqueries are evaluated on-demand by the executor,
48//! and their results are cached through [`crate::QueryDb::get`].
49//!
50//! ## Serialization
51//!
52//! Every IR type derives `Serialize` and `Deserialize`. This is required so
53//! that plans can be fingerprinted for the fuser, embedded in structured log
54//! output, and (in DB22) persisted in `.sqry/graph/derived.sqry` as part of
55//! hot-cache keys.
56//!
57//! [`EdgeKind`]: sqry_core::graph::unified::edge::kind::EdgeKind
58//! [`ScopeKind`]: sqry_core::graph::unified::bind::scope::arena::ScopeKind
59//! [`QueryBuilder`]: super::compile::QueryBuilder
60
61use serde::{Deserialize, Serialize};
62
63use sqry_core::graph::unified::bind::scope::arena::ScopeKind;
64use sqry_core::graph::unified::edge::kind::EdgeKind;
65use sqry_core::graph::unified::node::kind::NodeKind;
66use sqry_core::schema::Visibility;
67
68/// Top-level query plan produced by the compiler.
69///
70/// A plan is a rooted tree of [`PlanNode`] operators. The executor walks the
71/// tree from the root, evaluating each node's output into a node-set and
72/// feeding it forward through [`PlanNode::Chain`] and [`PlanNode::SetOp`]
73/// combinators.
74///
75/// The thin wrapper exists so that:
76///
77/// - the fuser in [`super::fuse`] can operate on a list of `QueryPlan`s
78///   without confusion about which `PlanNode` is the root,
79/// - plan-level metadata can be added later (e.g., execution hints, limits)
80///   without touching every [`PlanNode`] variant.
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub struct QueryPlan {
83    /// The root operator of the plan tree.
84    pub root: PlanNode,
85}
86
87impl QueryPlan {
88    /// Creates a new plan wrapping the given root operator.
89    #[must_use]
90    pub fn new(root: PlanNode) -> Self {
91        Self { root }
92    }
93
94    /// Returns a reference to the plan's root operator.
95    #[inline]
96    #[must_use]
97    pub fn root(&self) -> &PlanNode {
98        &self.root
99    }
100
101    /// Returns the total number of [`PlanNode`] operators in the tree,
102    /// counting every node exactly once.
103    ///
104    /// Useful for plan-complexity metrics, fusion heuristics, and tests.
105    #[must_use]
106    pub fn operator_count(&self) -> usize {
107        self.root.operator_count()
108    }
109}
110
111/// Operator tree for a structural query.
112///
113/// Every variant produces a node-set. Consumers treat the resulting
114/// `Vec<NodeId>` as a sorted, deduplicated sequence.
115///
116/// # Variant semantics
117///
118/// - [`PlanNode::NodeScan`] scans the graph and emits every node matching the
119///   supplied filters. This is the only variant without an input set.
120/// - [`PlanNode::EdgeTraversal`] requires an input set (supplied by the
121///   enclosing [`PlanNode::Chain`]) and follows edges from those nodes.
122/// - [`PlanNode::Filter`] narrows its input set by evaluating the predicate.
123/// - [`PlanNode::SetOp`] evaluates two sub-plans independently and combines
124///   the results. Neither side inherits an input set from an enclosing chain.
125/// - [`PlanNode::Chain`] threads the output of each step into the next step's
126///   input. The first step must be context-free (a `NodeScan` or `SetOp`).
127#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum PlanNode {
130    /// Scan all nodes matching the given filters.
131    ///
132    /// Each filter is an optional narrowing condition; `None` means
133    /// "accept any value". `kind = None`, `visibility = None`,
134    /// `name_pattern = None` yields a full node scan.
135    NodeScan {
136        /// Optional kind filter (Function, Method, Class, ...).
137        kind: Option<NodeKind>,
138        /// Optional visibility filter (Public, Private).
139        visibility: Option<Visibility>,
140        /// Optional symbol-name pattern filter.
141        name_pattern: Option<StringPattern>,
142    },
143    /// Follow edges outward from the input set.
144    ///
145    /// `edge_kind = None` matches any edge kind. `max_depth = 1` performs a
146    /// single hop; values greater than one perform a bounded BFS.
147    EdgeTraversal {
148        /// Direction in which to follow edges.
149        direction: Direction,
150        /// Optional edge-kind filter. See module docs for how the executor
151        /// compares variants by discriminant.
152        edge_kind: Option<EdgeKind>,
153        /// Maximum hops to follow. Must be `>= 1` to produce any output.
154        max_depth: u32,
155    },
156    /// Narrow the input set with a predicate.
157    Filter {
158        /// The predicate to evaluate for each input node.
159        predicate: Predicate,
160    },
161    /// Combine two independently-evaluated sub-plans with a set operation.
162    SetOp {
163        /// The set operation: Union, Intersect, or Difference.
164        op: SetOperation,
165        /// Left-hand operand sub-plan.
166        left: Box<PlanNode>,
167        /// Right-hand operand sub-plan.
168        right: Box<PlanNode>,
169    },
170    /// Pipeline a sequence of steps, threading each step's output into the
171    /// next step's input.
172    ///
173    /// The first step must be context-free (a [`PlanNode::NodeScan`] or
174    /// [`PlanNode::SetOp`]); subsequent steps are applied to the running set.
175    Chain {
176        /// Steps to evaluate in order. An empty vec yields the empty set.
177        steps: Vec<PlanNode>,
178    },
179}
180
181impl PlanNode {
182    /// Returns the total number of operators in this subtree.
183    ///
184    /// Counts this node plus all descendants through
185    /// [`PlanNode::SetOp::left`], [`PlanNode::SetOp::right`], and
186    /// [`PlanNode::Chain::steps`]. [`PlanNode::Filter`]'s predicate tree
187    /// is *not* counted here — predicate trees have their own complexity.
188    #[must_use]
189    pub fn operator_count(&self) -> usize {
190        match self {
191            PlanNode::NodeScan { .. }
192            | PlanNode::EdgeTraversal { .. }
193            | PlanNode::Filter { .. } => 1,
194            PlanNode::SetOp { left, right, .. } => {
195                1 + left.operator_count() + right.operator_count()
196            }
197            PlanNode::Chain { steps } => {
198                1 + steps.iter().map(PlanNode::operator_count).sum::<usize>()
199            }
200        }
201    }
202
203    /// Returns `true` if this node has no input dependency (can be evaluated
204    /// in isolation).
205    ///
206    /// Used by the compiler to validate [`PlanNode::Chain::steps`] — the
207    /// first step must be context-free.
208    #[must_use]
209    pub fn is_context_free(&self) -> bool {
210        matches!(self, PlanNode::NodeScan { .. } | PlanNode::SetOp { .. })
211    }
212}
213
214/// Direction of an edge traversal relative to the input node set.
215///
216/// # Semantics
217///
218/// - [`Direction::Forward`] — follow outgoing edges (`source -> target`).
219///   `callees:X` is a forward traversal of `Calls` edges from `X`.
220/// - [`Direction::Reverse`] — follow incoming edges (`target -> source`).
221///   `callers:X` is a reverse traversal of `Calls` edges into `X`.
222/// - [`Direction::Both`] — follow edges in both directions. Useful for
223///   symmetric relations and for impact analysis that should include both
224///   callers and callees.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum Direction {
228    /// Follow outgoing edges (`source -> target`).
229    Forward,
230    /// Follow incoming edges (`target -> source`).
231    Reverse,
232    /// Follow edges in both directions.
233    Both,
234}
235
236impl Direction {
237    /// Returns the inverse direction.
238    ///
239    /// `Forward <-> Reverse`, `Both` is its own inverse.
240    #[must_use]
241    pub const fn invert(self) -> Self {
242        match self {
243            Direction::Forward => Direction::Reverse,
244            Direction::Reverse => Direction::Forward,
245            Direction::Both => Direction::Both,
246        }
247    }
248}
249
250/// Set algebra on two node-set results.
251///
252/// All three operators are commutative in the output set, but order is
253/// preserved in the IR so that the executor can stream the smaller side
254/// first in the fuser pass (DB11).
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum SetOperation {
258    /// Union: `left ∪ right`.
259    Union,
260    /// Intersection: `left ∩ right`.
261    Intersect,
262    /// Difference: `left \ right` (elements in left but not right).
263    Difference,
264}
265
266/// Filter condition applied to a node-set.
267///
268/// Predicates fall into four groups:
269///
270/// 1. **Existence checks**: [`Predicate::HasCaller`], [`Predicate::HasCallee`],
271///    [`Predicate::IsUnused`]. Cheap — no value or subquery traversal needed.
272/// 2. **Value-bearing relation predicates**: [`Predicate::Callers`] through
273///    [`Predicate::Implements`]. These accept a [`PredicateValue`] on the
274///    right-hand side — a literal pattern, a regex, or a nested subquery.
275///    They map 1:1 to the six relation handlers in
276///    `sqry-core::query::executor::graph_eval` (`match_callers`,
277///    `match_callees`, `match_imports`, `match_exports`, `match_references`,
278///    `match_implements`) and their `_subquery` variants.
279/// 3. **Attribute filters**: [`Predicate::InFile`], [`Predicate::InScope`],
280///    [`Predicate::MatchesName`].
281/// 4. **Boolean combinators**: [`Predicate::And`], [`Predicate::Or`],
282///    [`Predicate::Not`].
283///
284/// # Semantic alignment with `graph_eval`
285///
286/// The six relation variants exactly mirror the value-bearing operators in
287/// `sqry-core::query::types::Value`:
288///
289/// | This IR | `graph_eval` handler | Text syntax |
290/// |---------|----------------------|-------------|
291/// | `Callers(v)`     | `match_callers` / `match_callers_subquery`       | `callers:v`    |
292/// | `Callees(v)`     | `match_callees` / `match_callees_subquery`       | `callees:v`    |
293/// | `Imports(v)`     | `match_imports` / `match_imports_subquery`       | `imports:v`    |
294/// | `Exports(v)`     | `match_exports` / `match_exports_subquery`       | `exports:v`    |
295/// | `References(v)`  | `match_references` / `match_references_subquery` | `references:v` (supports `~=` regex) |
296/// | `Implements(v)`  | `match_implements` / `match_implements_subquery` | `impl:v` / `implements:v` |
297///
298/// The text frontend in DB13 must accept both `impl:` and `implements:`
299/// aliases for [`Predicate::Implements`] (spec §M8).
300#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
301#[serde(rename_all = "snake_case")]
302pub enum Predicate {
303    // --- Existence checks ---
304    /// True iff the node has at least one incoming `Calls` edge.
305    HasCaller,
306    /// True iff the node has at least one outgoing `Calls` edge.
307    HasCallee,
308    /// True iff the node is not reachable from any entry point.
309    IsUnused,
310
311    // --- Value-bearing relation predicates ---
312    /// `callers:<value>`: node's callers match the pattern / subquery.
313    Callers(PredicateValue),
314    /// `callees:<value>`: node's callees match the pattern / subquery.
315    Callees(PredicateValue),
316    /// `imports:<value>`: node's imports match the pattern / subquery.
317    Imports(PredicateValue),
318    /// `exports:<value>`: node's exports match the pattern / subquery.
319    Exports(PredicateValue),
320    /// `references:<value>` (also supports `~=` regex via
321    /// [`PredicateValue::Regex`]).
322    References(PredicateValue),
323    /// `impl:<value>` / `implements:<value>`: matches nodes implementing
324    /// the referenced trait / interface.
325    Implements(PredicateValue),
326
327    // --- Attribute filters ---
328    /// `in:<path-glob>`: true iff the node's file path matches the glob.
329    InFile(PathPattern),
330    /// `scope:<kind>`: true iff the node's enclosing scope kind matches.
331    InScope(ScopeKind),
332    /// `name:<pattern>`: true iff the node's name matches the pattern.
333    MatchesName(StringPattern),
334    /// `returns:<TypeName>`: true iff the node (a function or method) has at
335    /// least one outgoing
336    /// [`EdgeKind::TypeOf { context: Some(TypeOfContext::Return), .. }`][crate::queries]
337    /// edge whose target node's interned name matches `TypeName` exactly.
338    ///
339    /// # Semantics
340    ///
341    /// The match is evaluated **edge-based, not signature-text-based**: the
342    /// executor walks `TypeOf` edges with `TypeOfContext::Return` from the
343    /// candidate node, resolves the target node's primary name through the
344    /// snapshot string interner, and compares with byte-exact equality
345    /// (case-sensitive). Substring, glob, and regex variants are deliberately
346    /// out of scope for this predicate; a future `returns~:` token (regex
347    /// form) will lower to a separate IR variant rather than overload this
348    /// one — the same shape used today for [`Predicate::References`] vs the
349    /// `references ~= /…/` regex form.
350    ///
351    /// # Why a bare `String` instead of [`StringPattern`]?
352    ///
353    /// [`StringPattern`] auto-detects glob meta-characters and promotes to
354    /// [`MatchMode::Glob`]; modelling the predicate value with `String`
355    /// keeps the contract narrow — `returns:Foo*` is a parse error in
356    /// users' minds, not a hidden glob expansion. The single-mode shape
357    /// also keeps cache keys monomorphic which simplifies any future
358    /// derived-query backing for this predicate.
359    Returns(String),
360
361    // --- Boolean combinators ---
362    /// Logical AND over a list of predicates. Empty list is vacuously true.
363    And(Vec<Predicate>),
364    /// Logical OR over a list of predicates. Empty list is vacuously false.
365    Or(Vec<Predicate>),
366    /// Logical NOT of a single predicate.
367    Not(Box<Predicate>),
368}
369
370impl Predicate {
371    /// Returns `true` if this predicate (or any nested predicate through
372    /// boolean combinators) references a [`PredicateValue::Subquery`].
373    ///
374    /// The executor uses this hint to decide whether to allocate subquery
375    /// evaluation scratch space up front.
376    #[must_use]
377    pub fn has_subquery(&self) -> bool {
378        match self {
379            Predicate::HasCaller
380            | Predicate::HasCallee
381            | Predicate::IsUnused
382            | Predicate::InFile(_)
383            | Predicate::InScope(_)
384            | Predicate::MatchesName(_)
385            | Predicate::Returns(_) => false,
386
387            Predicate::Callers(v)
388            | Predicate::Callees(v)
389            | Predicate::Imports(v)
390            | Predicate::Exports(v)
391            | Predicate::References(v)
392            | Predicate::Implements(v) => v.is_subquery(),
393
394            Predicate::And(list) | Predicate::Or(list) => list.iter().any(Predicate::has_subquery),
395            Predicate::Not(inner) => inner.has_subquery(),
396        }
397    }
398}
399
400/// Right-hand side of a relation predicate.
401///
402/// Mirrors `sqry-core::query::types::Value`'s String / Regex / Subquery
403/// arms that actually flow into the six relation handlers. The IR variant
404/// is deliberately narrower than the general-purpose [`Value`] — numeric
405/// and boolean values never appear on the right of a relation predicate,
406/// so they are omitted here.
407///
408/// [`Value`]: sqry_core::query::types::Value
409#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
410#[serde(rename_all = "snake_case")]
411pub enum PredicateValue {
412    /// Literal string pattern — exact match, glob, prefix, suffix, or
413    /// contains. Maps to `Operator::Equal` in `graph_eval`.
414    Pattern(StringPattern),
415    /// Regex pattern. Maps to `Operator::Regex` in `graph_eval`.
416    ///
417    /// Only [`Predicate::References`] uses this in the current text
418    /// syntax (`references ~= /foo.*/i`), but any relation predicate
419    /// may accept it at the IR level.
420    Regex(RegexPattern),
421    /// Nested subquery: `callers:(kind:function AND async:true)`.
422    /// Evaluated as a distinct [`PlanNode`] before the outer relation
423    /// predicate runs.
424    Subquery(Box<PlanNode>),
425}
426
427impl PredicateValue {
428    /// Returns `true` if this value is a [`PredicateValue::Subquery`].
429    #[must_use]
430    pub const fn is_subquery(&self) -> bool {
431        matches!(self, PredicateValue::Subquery(_))
432    }
433
434    /// Returns the inner [`PlanNode`] if this value is a subquery.
435    #[must_use]
436    pub fn as_subquery(&self) -> Option<&PlanNode> {
437        match self {
438            PredicateValue::Subquery(plan) => Some(plan),
439            _ => None,
440        }
441    }
442}
443
444/// Symbol-name matching pattern.
445///
446/// The [`MatchMode`] discriminator selects between exact, glob, prefix,
447/// suffix, and contains semantics. The raw pattern is kept as a `String`
448/// so the executor can compile it lazily.
449///
450/// # Why not `Arc<str>`?
451///
452/// The IR is a data description — compact and easy to clone. Patterns are
453/// small (typically under 64 bytes), and cloning the IR is rare (once per
454/// plan submission). The `Arc<str>` savings do not justify the added
455/// complexity at this layer.
456#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
457pub struct StringPattern {
458    /// The raw pattern string.
459    pub raw: String,
460    /// How the pattern should be interpreted by the executor.
461    pub mode: MatchMode,
462    /// Whether matching is case-insensitive. Defaults to `false` (case
463    /// sensitive) to match `graph_eval` semantics.
464    #[serde(default)]
465    pub case_insensitive: bool,
466}
467
468impl StringPattern {
469    /// Creates an exact-match pattern (case-sensitive).
470    #[must_use]
471    pub fn exact(raw: impl Into<String>) -> Self {
472        Self {
473            raw: raw.into(),
474            mode: MatchMode::Exact,
475            case_insensitive: false,
476        }
477    }
478
479    /// Creates a glob pattern (case-sensitive).
480    #[must_use]
481    pub fn glob(raw: impl Into<String>) -> Self {
482        Self {
483            raw: raw.into(),
484            mode: MatchMode::Glob,
485            case_insensitive: false,
486        }
487    }
488
489    /// Creates a prefix-match pattern (case-sensitive).
490    #[must_use]
491    pub fn prefix(raw: impl Into<String>) -> Self {
492        Self {
493            raw: raw.into(),
494            mode: MatchMode::Prefix,
495            case_insensitive: false,
496        }
497    }
498
499    /// Creates a suffix-match pattern (case-sensitive).
500    #[must_use]
501    pub fn suffix(raw: impl Into<String>) -> Self {
502        Self {
503            raw: raw.into(),
504            mode: MatchMode::Suffix,
505            case_insensitive: false,
506        }
507    }
508
509    /// Creates a substring-contains pattern (case-sensitive).
510    #[must_use]
511    pub fn contains(raw: impl Into<String>) -> Self {
512        Self {
513            raw: raw.into(),
514            mode: MatchMode::Contains,
515            case_insensitive: false,
516        }
517    }
518
519    /// Returns a case-insensitive copy of this pattern.
520    #[must_use]
521    pub fn case_insensitive(mut self) -> Self {
522        self.case_insensitive = true;
523        self
524    }
525}
526
527/// Match semantics for a [`StringPattern`].
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
529#[serde(rename_all = "snake_case")]
530pub enum MatchMode {
531    /// Exact string equality.
532    Exact,
533    /// Shell-style glob matching (`*`, `?`, `[abc]`).
534    Glob,
535    /// Prefix match (`str.starts_with(raw)`).
536    Prefix,
537    /// Suffix match (`str.ends_with(raw)`).
538    Suffix,
539    /// Substring match (`str.contains(raw)`).
540    Contains,
541}
542
543/// File-path matching pattern. Always interpreted as a shell-style glob,
544/// matching `graph_eval`'s `Operator::Equal` semantics on path fields.
545///
546/// The raw glob is kept as a `String`; the executor compiles the glob
547/// lazily using `globset` or the project's existing glob helpers.
548#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
549pub struct PathPattern {
550    /// The raw glob string, e.g. `src/**/*.rs`.
551    pub glob: String,
552}
553
554impl PathPattern {
555    /// Creates a new path glob pattern.
556    #[must_use]
557    pub fn new(glob: impl Into<String>) -> Self {
558        Self { glob: glob.into() }
559    }
560
561    /// Returns the raw glob string.
562    #[inline]
563    #[must_use]
564    pub fn as_str(&self) -> &str {
565        &self.glob
566    }
567}
568
569impl<S: Into<String>> From<S> for PathPattern {
570    fn from(s: S) -> Self {
571        Self::new(s)
572    }
573}
574
575/// Regex pattern with compilation flags.
576///
577/// Mirrors `sqry-core::query::types::RegexValue` so that `graph_eval`
578/// `Operator::Regex` semantics round-trip losslessly through the IR.
579/// Compilation is deferred to the executor — a failed regex compile is
580/// reported as a query error, not a plan-construction error.
581#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
582pub struct RegexPattern {
583    /// The regex source string.
584    pub pattern: String,
585    /// Flags that influence compilation.
586    #[serde(default)]
587    pub flags: RegexFlags,
588}
589
590impl RegexPattern {
591    /// Creates a new regex pattern with default flags.
592    #[must_use]
593    pub fn new(pattern: impl Into<String>) -> Self {
594        Self {
595            pattern: pattern.into(),
596            flags: RegexFlags::default(),
597        }
598    }
599
600    /// Creates a new regex pattern with the given flags.
601    #[must_use]
602    pub fn with_flags(pattern: impl Into<String>, flags: RegexFlags) -> Self {
603        Self {
604            pattern: pattern.into(),
605            flags,
606        }
607    }
608}
609
610/// Regex compilation flags, mirroring
611/// `sqry-core::query::types::RegexFlags`.
612///
613/// # Serialization
614///
615/// All three fields default to `false`; the `#[serde(default)]` attribute
616/// keeps the JSON encoding compact when the flags are at their defaults.
617#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
618pub struct RegexFlags {
619    /// Case-insensitive matching (regex flag `i`).
620    #[serde(default)]
621    pub case_insensitive: bool,
622    /// Multiline mode: `^` and `$` match line boundaries (regex flag `m`).
623    #[serde(default)]
624    pub multiline: bool,
625    /// Dot matches newlines (regex flag `s`).
626    #[serde(default)]
627    pub dot_all: bool,
628}
629
630// ============================================================================
631// Inline unit tests
632// ============================================================================
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn query_plan_wraps_root() {
640        let root = PlanNode::NodeScan {
641            kind: Some(NodeKind::Function),
642            visibility: None,
643            name_pattern: None,
644        };
645        let plan = QueryPlan::new(root.clone());
646        assert_eq!(plan.root(), &root);
647    }
648
649    #[test]
650    fn operator_count_single_node() {
651        let scan = PlanNode::NodeScan {
652            kind: None,
653            visibility: None,
654            name_pattern: None,
655        };
656        assert_eq!(scan.operator_count(), 1);
657    }
658
659    #[test]
660    fn operator_count_nested_setop_and_chain() {
661        let scan = PlanNode::NodeScan {
662            kind: None,
663            visibility: None,
664            name_pattern: None,
665        };
666        let traverse = PlanNode::EdgeTraversal {
667            direction: Direction::Forward,
668            edge_kind: None,
669            max_depth: 1,
670        };
671        let set = PlanNode::SetOp {
672            op: SetOperation::Union,
673            left: Box::new(scan.clone()),
674            right: Box::new(scan.clone()),
675        };
676        let chain = PlanNode::Chain {
677            steps: vec![set.clone(), traverse],
678        };
679        // chain(1) + setop(1) + scan(1) + scan(1) + traverse(1) = 5
680        assert_eq!(chain.operator_count(), 5);
681        assert_eq!(set.operator_count(), 3);
682    }
683
684    #[test]
685    fn context_free_variants() {
686        let scan = PlanNode::NodeScan {
687            kind: None,
688            visibility: None,
689            name_pattern: None,
690        };
691        let setop = PlanNode::SetOp {
692            op: SetOperation::Intersect,
693            left: Box::new(scan.clone()),
694            right: Box::new(scan.clone()),
695        };
696        let traverse = PlanNode::EdgeTraversal {
697            direction: Direction::Reverse,
698            edge_kind: None,
699            max_depth: 1,
700        };
701        let filter = PlanNode::Filter {
702            predicate: Predicate::HasCaller,
703        };
704
705        assert!(scan.is_context_free());
706        assert!(setop.is_context_free());
707        assert!(!traverse.is_context_free());
708        assert!(!filter.is_context_free());
709    }
710
711    #[test]
712    fn direction_invert_is_involution() {
713        assert_eq!(Direction::Forward.invert(), Direction::Reverse);
714        assert_eq!(Direction::Reverse.invert(), Direction::Forward);
715        assert_eq!(Direction::Both.invert(), Direction::Both);
716        for d in [Direction::Forward, Direction::Reverse, Direction::Both] {
717            assert_eq!(d.invert().invert(), d);
718        }
719    }
720
721    #[test]
722    fn predicate_has_subquery_detects_nested_calls() {
723        let leaf = Predicate::HasCaller;
724        assert!(!leaf.has_subquery());
725
726        let attr = Predicate::InFile(PathPattern::new("src/api/**"));
727        assert!(!attr.has_subquery());
728
729        let sub = Predicate::Callers(PredicateValue::Subquery(Box::new(PlanNode::NodeScan {
730            kind: Some(NodeKind::Method),
731            visibility: None,
732            name_pattern: None,
733        })));
734        assert!(sub.has_subquery());
735
736        let pattern = Predicate::Callers(PredicateValue::Pattern(StringPattern::exact("foo")));
737        assert!(!pattern.has_subquery());
738
739        let nested_in_and = Predicate::And(vec![leaf.clone(), sub.clone()]);
740        assert!(nested_in_and.has_subquery());
741
742        let nested_in_not = Predicate::Not(Box::new(sub));
743        assert!(nested_in_not.has_subquery());
744
745        let and_no_sub = Predicate::And(vec![leaf.clone(), attr.clone()]);
746        assert!(!and_no_sub.has_subquery());
747    }
748
749    #[test]
750    fn predicate_value_is_subquery() {
751        let plan = PlanNode::NodeScan {
752            kind: None,
753            visibility: None,
754            name_pattern: None,
755        };
756        let sub = PredicateValue::Subquery(Box::new(plan.clone()));
757        let pat = PredicateValue::Pattern(StringPattern::exact("foo"));
758        let re = PredicateValue::Regex(RegexPattern::new("^foo$"));
759
760        assert!(sub.is_subquery());
761        assert!(!pat.is_subquery());
762        assert!(!re.is_subquery());
763
764        assert_eq!(sub.as_subquery(), Some(&plan));
765        assert_eq!(pat.as_subquery(), None);
766        assert_eq!(re.as_subquery(), None);
767    }
768
769    #[test]
770    fn string_pattern_builders_preserve_raw() {
771        let raw = "parse_*";
772        assert_eq!(StringPattern::exact(raw).raw, raw);
773        assert_eq!(StringPattern::glob(raw).mode, MatchMode::Glob);
774        assert_eq!(StringPattern::prefix(raw).mode, MatchMode::Prefix);
775        assert_eq!(StringPattern::suffix(raw).mode, MatchMode::Suffix);
776        assert_eq!(StringPattern::contains(raw).mode, MatchMode::Contains);
777    }
778
779    #[test]
780    fn string_pattern_case_insensitive_toggle() {
781        let p = StringPattern::exact("Foo").case_insensitive();
782        assert!(p.case_insensitive);
783    }
784
785    #[test]
786    fn path_pattern_from_str_and_as_str() {
787        let p: PathPattern = "src/**/*.rs".into();
788        assert_eq!(p.as_str(), "src/**/*.rs");
789
790        let p2 = PathPattern::new(String::from("docs/**"));
791        assert_eq!(p2.as_str(), "docs/**");
792    }
793
794    #[test]
795    fn regex_pattern_default_flags_are_false() {
796        let r = RegexPattern::new("^foo$");
797        assert_eq!(r.pattern, "^foo$");
798        assert!(!r.flags.case_insensitive);
799        assert!(!r.flags.multiline);
800        assert!(!r.flags.dot_all);
801    }
802
803    #[test]
804    fn regex_pattern_with_flags() {
805        let flags = RegexFlags {
806            case_insensitive: true,
807            multiline: true,
808            dot_all: false,
809        };
810        let r = RegexPattern::with_flags("foo", flags);
811        assert!(r.flags.case_insensitive);
812        assert!(r.flags.multiline);
813        assert!(!r.flags.dot_all);
814    }
815}