trustfall_core/interpreter/hints/
mod.rs

1use std::{collections::BTreeMap, fmt::Debug, ops::Bound, sync::Arc};
2
3use self::vertex_info::InternalVertexInfo;
4
5use super::InterpretedQuery;
6use crate::ir::{
7    EdgeKind, EdgeParameters, Eid, FieldValue, IREdge, IRFold, IRQueryComponent, IRVertex, Output,
8    Recursive, Vid,
9};
10
11mod candidates;
12mod dynamic;
13mod filters;
14mod sealed;
15mod vertex_info;
16
17pub use candidates::{CandidateValue, Range};
18pub use dynamic::DynamicallyResolvedValue;
19pub use vertex_info::{RequiredProperty, VertexInfo};
20
21/// Contains overall information about the query being executed, such as its outputs and variables.
22#[non_exhaustive]
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct QueryInfo<'a> {
25    query: &'a InterpretedQuery,
26}
27
28impl<'a> QueryInfo<'a> {
29    fn new(query: &'a InterpretedQuery) -> Self {
30        Self { query }
31    }
32
33    /// All the names and type information of the output data of this query.
34    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
35    #[inline]
36    pub fn outputs(&self) -> &BTreeMap<Arc<str>, Output> {
37        &self.query.indexed_query.outputs
38    }
39
40    /// The variables with which the query was executed.
41    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
42    #[inline]
43    pub fn variables(&self) -> &Arc<BTreeMap<Arc<str>, FieldValue>> {
44        &self.query.arguments
45    }
46}
47
48/// Enables adapter optimizations by showing how a query uses a vertex. Implements [`VertexInfo`].
49#[non_exhaustive]
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ResolveInfo {
52    query: InterpretedQuery,
53    current_vid: Vid,
54    vertex_completed: bool,
55}
56
57/// Enables adapter optimizations by showing how a query uses a particular edge.
58#[non_exhaustive]
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ResolveEdgeInfo {
61    query: InterpretedQuery,
62    current_vid: Vid,
63    target_vid: Vid,
64    crossing_eid: Eid,
65}
66
67impl ResolveInfo {
68    pub(crate) fn new(query: InterpretedQuery, current_vid: Vid, vertex_completed: bool) -> Self {
69        Self { query, current_vid, vertex_completed }
70    }
71
72    pub(crate) fn into_inner(self) -> InterpretedQuery {
73        self.query
74    }
75
76    /// Get information about the overall query being executed.
77    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
78    #[inline]
79    pub fn query(&self) -> QueryInfo<'_> {
80        QueryInfo::new(&self.query)
81    }
82}
83
84impl sealed::__Sealed for ResolveInfo {}
85impl sealed::__Sealed for NeighborInfo {}
86
87impl InternalVertexInfo for ResolveInfo {
88    #[inline]
89    fn query(&self) -> &InterpretedQuery {
90        &self.query
91    }
92
93    #[inline]
94    fn non_binding_filters(&self) -> bool {
95        false // We are *at* the vertex itself. Filters always bind here.
96    }
97
98    #[inline]
99    fn execution_frontier(&self) -> Bound<Vid> {
100        if self.vertex_completed {
101            Bound::Included(self.current_vid)
102        } else {
103            // e.g. during type coercions or in `get_starting_vertices()`
104            Bound::Excluded(self.current_vid)
105        }
106    }
107
108    #[inline]
109    fn current_vertex(&self) -> &IRVertex {
110        &self.current_component().vertices[&self.current_vid]
111    }
112
113    #[inline]
114    fn current_component(&self) -> &IRQueryComponent {
115        // Inside `ResolveInfo`, the starting component and
116        // the current component are one and the same.
117        self.starting_component()
118    }
119
120    #[inline]
121    fn starting_component(&self) -> &IRQueryComponent {
122        &self.query.indexed_query.vids[&self.current_vid]
123    }
124
125    #[inline]
126    fn query_variables(&self) -> &BTreeMap<Arc<str>, FieldValue> {
127        self.query.arguments.as_ref()
128    }
129
130    fn make_non_folded_edge_info(&self, edge: &IREdge) -> EdgeInfo {
131        let neighboring_info = NeighborInfo {
132            query: self.query.clone(),
133            execution_frontier: self.execution_frontier(),
134            starting_vertex: self.current_vid,
135            neighbor_vertex: edge.to_vid,
136            neighbor_path: vec![edge.eid],
137            within_optional_scope: edge.optional,
138            locally_non_binding_filters: check_locally_non_binding_filters_for_edge(edge),
139        };
140        EdgeInfo {
141            eid: edge.eid,
142            parameters: edge.parameters.clone(),
143            optional: edge.optional,
144            recursive: edge.recursive.clone(),
145            folded: FoldState::None,
146            destination: neighboring_info,
147        }
148    }
149
150    fn make_folded_edge_info(&self, fold: &IRFold) -> EdgeInfo {
151        let at_least_one_element_required =
152            filters::fold_requires_at_least_one_element(&self.query.arguments, fold);
153
154        let neighboring_info = NeighborInfo {
155            query: self.query.clone(),
156            execution_frontier: self.execution_frontier(),
157            starting_vertex: self.current_vid,
158            neighbor_vertex: fold.to_vid,
159            neighbor_path: vec![fold.eid],
160            within_optional_scope: !at_least_one_element_required,
161            locally_non_binding_filters: false,
162        };
163        EdgeInfo {
164            eid: fold.eid,
165            parameters: fold.parameters.clone(),
166            optional: false,
167            recursive: None,
168            folded: if at_least_one_element_required {
169                FoldState::FoldedMandatory
170            } else {
171                FoldState::FoldedOptional
172            },
173            destination: neighboring_info,
174        }
175    }
176}
177
178impl ResolveEdgeInfo {
179    pub(crate) fn new(
180        query: InterpretedQuery,
181        current_vid: Vid,
182        target_vid: Vid,
183        crossing_eid: Eid,
184    ) -> Self {
185        Self { query, current_vid, target_vid, crossing_eid }
186    }
187
188    pub(crate) fn into_inner(self) -> InterpretedQuery {
189        self.query
190    }
191
192    /// Get information about the overall query being executed.
193    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
194    #[inline]
195    pub fn query(&self) -> QueryInfo<'_> {
196        QueryInfo::new(&self.query)
197    }
198
199    /// The unique ID of this edge within its query.
200    #[inline]
201    pub fn eid(&self) -> Eid {
202        self.crossing_eid
203    }
204
205    /// The unique ID of the vertex where the edge in this operation begins.
206    #[inline]
207    pub fn origin_vid(&self) -> Vid {
208        self.current_vid
209    }
210
211    /// The unique ID of the vertex to which this edge points.
212    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
213    #[inline]
214    pub fn destination_vid(&self) -> Vid {
215        self.target_vid
216    }
217
218    /// Info about the destination vertex of the edge being expanded where this value was provided.
219    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
220    #[inline]
221    pub fn destination(&self) -> NeighborInfo {
222        self.edge().destination
223    }
224
225    /// Info about the edge being expanded where this value was provided.
226    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
227    #[inline]
228    pub fn edge(&self) -> EdgeInfo {
229        let eid = self.eid();
230        match &self.query.indexed_query.eids[&eid] {
231            EdgeKind::Regular(regular) => {
232                debug_assert_eq!(
233                    self.target_vid, regular.to_vid,
234                    "expected Vid {:?} but got {:?} in edge {regular:?}",
235                    self.target_vid, regular.to_vid
236                );
237                EdgeInfo {
238                    eid,
239                    parameters: regular.parameters.clone(),
240                    optional: regular.optional,
241                    recursive: regular.recursive.clone(),
242                    folded: FoldState::None,
243                    destination: NeighborInfo {
244                        query: self.query.clone(),
245                        execution_frontier: Bound::Excluded(self.target_vid),
246                        starting_vertex: self.origin_vid(),
247                        neighbor_vertex: regular.to_vid,
248                        neighbor_path: vec![eid],
249                        within_optional_scope: regular.optional,
250                        locally_non_binding_filters: check_locally_non_binding_filters_for_edge(
251                            regular,
252                        ),
253                    },
254                }
255            }
256            EdgeKind::Fold(fold) => {
257                debug_assert_eq!(
258                    self.target_vid, fold.to_vid,
259                    "expected Vid {:?} but got {:?} in fold {fold:?}",
260                    self.target_vid, fold.to_vid
261                );
262
263                // In this case, we are *currently* resolving the folded edge.
264                // Its own filters always apply, and we don't need to check whether
265                // the fold is required to have at least one element.
266                let within_optional_scope = false;
267                let folded = FoldState::FoldedMandatory;
268
269                EdgeInfo {
270                    eid,
271                    parameters: fold.parameters.clone(),
272                    optional: false,
273                    recursive: None,
274                    folded,
275                    destination: NeighborInfo {
276                        query: self.query.clone(),
277                        execution_frontier: Bound::Excluded(self.target_vid),
278                        starting_vertex: self.origin_vid(),
279                        neighbor_vertex: fold.to_vid,
280                        neighbor_path: vec![eid],
281                        within_optional_scope,
282                        locally_non_binding_filters: false,
283                    },
284                }
285            }
286        }
287    }
288}
289
290/// Information about an edge that is being resolved as part of a query.
291#[non_exhaustive]
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct EdgeInfo {
294    eid: Eid,
295    parameters: EdgeParameters,
296    optional: bool,
297    recursive: Option<Recursive>,
298    folded: FoldState,
299    destination: NeighborInfo,
300}
301
302impl EdgeInfo {
303    /// The ID that uniquely identifies this edge in its query.
304    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
305    #[inline]
306    pub fn eid(&self) -> Eid {
307        self.eid
308    }
309
310    /// The values with which this edge was parameterized.
311    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
312    #[inline]
313    pub fn parameters(&self) -> &EdgeParameters {
314        &self.parameters
315    }
316
317    /// Info about the vertex to which this edge points.
318    #[allow(dead_code)] // false-positive: dead in the bin target, not dead in the lib
319    #[inline]
320    pub fn destination(&self) -> &NeighborInfo {
321        &self.destination
322    }
323
324    /// Whether this edge is required to exist, or else the computed row will be discarded.
325    #[inline]
326    pub fn is_mandatory(&self) -> bool {
327        self.folded != FoldState::FoldedOptional && !self.optional && self.recursive.is_none()
328    }
329}
330
331/// Information about a neighboring vertex. Implements [`VertexInfo`].
332#[non_exhaustive]
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct NeighborInfo {
335    query: InterpretedQuery,
336    execution_frontier: Bound<Vid>, // how far query execution has progressed
337    starting_vertex: Vid,
338    neighbor_vertex: Vid,
339    neighbor_path: Vec<Eid>,
340
341    // Filtering operations (filters, required edges, etc.) don't bind inside an `@optional` scope,
342    // to ensure that optional-then-filter semantics are upheld and avoid adapter footguns.
343    // They also don't bind inside a `@fold` scope that is not mandated to have >= 1 element,
344    // since such scopes also have optional semantics: no matter what property values are
345    // within such a scope, they cannot enable an earlier edge resolver to early-discard vertices.
346    //
347    // For specific examples, see test cases with names like:
348    // - `optional_with_nested_filter_*`
349    // - `fold_with_nested_filter`
350    // - `fold_with_nested_filter_and_tag`
351    // - `fold_with_count_filter_and_nested_filter`
352    // - `fold_with_count_filter_and_nested_filter_with_tag`
353    // - `fold_with_both_count_and_nested_filter_dependent_on_tag`
354    within_optional_scope: bool,
355
356    // A situation specific to *this vertex in particular* makes filters non-binding.
357    // For example: a `@recurse(depth: 2)` edge.
358    locally_non_binding_filters: bool,
359}
360
361impl InternalVertexInfo for NeighborInfo {
362    #[inline]
363    fn query(&self) -> &InterpretedQuery {
364        &self.query
365    }
366
367    #[inline]
368    fn non_binding_filters(&self) -> bool {
369        self.within_optional_scope || self.locally_non_binding_filters
370    }
371
372    #[inline]
373    fn execution_frontier(&self) -> Bound<Vid> {
374        self.execution_frontier
375    }
376
377    #[inline]
378    fn current_vertex(&self) -> &IRVertex {
379        &self.current_component().vertices[&self.neighbor_vertex]
380    }
381
382    #[inline]
383    fn current_component(&self) -> &IRQueryComponent {
384        &self.query.indexed_query.vids[&self.neighbor_vertex]
385    }
386
387    #[inline]
388    fn starting_component(&self) -> &IRQueryComponent {
389        &self.query.indexed_query.vids[&self.starting_vertex]
390    }
391
392    #[inline]
393    fn query_variables(&self) -> &BTreeMap<Arc<str>, FieldValue> {
394        self.query.arguments.as_ref()
395    }
396
397    fn make_non_folded_edge_info(&self, edge: &IREdge) -> EdgeInfo {
398        let mut neighbor_path = self.neighbor_path.clone();
399        neighbor_path.push(edge.eid);
400
401        let neighboring_info = NeighborInfo {
402            query: self.query.clone(),
403            execution_frontier: self.execution_frontier,
404            starting_vertex: self.starting_vertex,
405            neighbor_vertex: edge.to_vid,
406            neighbor_path,
407            within_optional_scope: self.within_optional_scope,
408            locally_non_binding_filters: check_locally_non_binding_filters_for_edge(edge),
409        };
410        EdgeInfo {
411            eid: edge.eid,
412            parameters: edge.parameters.clone(),
413            optional: edge.optional,
414            recursive: edge.recursive.clone(),
415            folded: FoldState::None,
416            destination: neighboring_info,
417        }
418    }
419
420    fn make_folded_edge_info(&self, fold: &IRFold) -> EdgeInfo {
421        let at_least_one_element_required =
422            filters::fold_requires_at_least_one_element(&self.query.arguments, fold);
423
424        let mut neighbor_path = self.neighbor_path.clone();
425        neighbor_path.push(fold.eid);
426        let neighboring_info = NeighborInfo {
427            query: self.query.clone(),
428            execution_frontier: self.execution_frontier,
429            starting_vertex: self.starting_vertex,
430            neighbor_vertex: fold.to_vid,
431            neighbor_path,
432            within_optional_scope: self.within_optional_scope || !at_least_one_element_required,
433            locally_non_binding_filters: false,
434        };
435        EdgeInfo {
436            eid: fold.eid,
437            parameters: fold.parameters.clone(),
438            optional: false,
439            recursive: None,
440            folded: if at_least_one_element_required {
441                FoldState::FoldedMandatory
442            } else {
443                FoldState::FoldedOptional
444            },
445            destination: neighboring_info,
446        }
447    }
448}
449
450/// For recursive edges to depth 2+, filter operations at the destination
451/// do not affect whether the edge is taken or not.
452///
453/// The query semantics state that recursive edge traversals happen first, then filters are applied.
454/// At depth 1, those filters are applied after only one edge expansion, so filters "count".
455/// With recursions to depth 2+, there are "middle" layers of vertices that don't have to satisfy
456/// the filters (and will be filtered out) but can still have more edge expansions in the recursion.
457fn check_locally_non_binding_filters_for_edge(edge: &IREdge) -> bool {
458    edge.recursive.as_ref().map(|r| r.depth.get() >= 2).unwrap_or(false)
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq)]
462pub(super) enum FoldState {
463    /// The edge is not folded.
464    None,
465
466    /// The edge has `@fold`, but no other clauses that require at least one instance of the edge.
467    FoldedOptional,
468
469    /// The edge has `@fold` plus a clause that requires at least one instance of the edge
470    /// for the result set to not be discarded.
471    ///
472    /// For example: `some_edge @fold @transform(op: "count") @filter(op: ">", value: ["$zero"])`
473    FoldedMandatory,
474}
475
476#[cfg(test)]
477mod tests;