trustfall_core/interpreter/hints/
vertex_info.rs

1use std::borrow::Cow;
2use std::collections::HashSet;
3use std::{
4    collections::BTreeMap,
5    ops::{Bound, RangeBounds},
6    sync::Arc,
7};
8
9use crate::{
10    interpreter::InterpretedQuery,
11    ir::{
12        Argument, FieldRef, FieldValue, IREdge, IRFold, IRQueryComponent, IRVertex, LocalField,
13        Operation, Vid,
14    },
15};
16
17use super::{dynamic::DynamicallyResolvedValue, CandidateValue, EdgeInfo, Range};
18
19/// Represents a required property of a specific vertex
20#[non_exhaustive]
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct RequiredProperty {
23    pub name: Arc<str>,
24}
25
26impl RequiredProperty {
27    pub fn new(name: Arc<str>) -> Self {
28        Self { name }
29    }
30}
31
32/// Information about what the currently-executing query needs at a specific vertex.
33#[cfg_attr(docsrs, doc(notable_trait))]
34pub trait VertexInfo: super::sealed::__Sealed {
35    /// The unique ID of the vertex this [`VertexInfo`] describes.
36    fn vid(&self) -> Vid;
37
38    /// The type coercion (`... on SomeType`) applied by the query at this vertex, if any.
39    fn coerced_to_type(&self) -> Option<&Arc<str>>;
40
41    /// Return all properties required for the current vertex, including: output, filtered, and
42    /// tagged properties. It's guaranteed that each property will only show once in the iterator,
43    /// so even if a property has been used as a filter and output, it will only show once.
44    ///
45    /// There is no guaranteed order.
46    ///
47    /// This can be especially useful for adapters doing network calls. For example, if the adapter
48    /// is using a relational database, it can retrieve the name of all properties and
49    /// only request those columns from the table.
50    fn required_properties(&self) -> Box<dyn Iterator<Item = RequiredProperty> + '_>;
51
52    /// Check whether the query demands this vertex property to have specific values:
53    /// a single value, or one of a set or range of values. The candidate values
54    /// are known *statically*: up-front, without executing any of the query.
55    ///
56    /// For example, filtering a property based on a query variable (e.g.
57    /// `@filter(op: "=", value: ["$expected"])`) means the filtered property will
58    /// need to match the value of the `expected` query variable. This variable's value is known
59    /// up-front at the beginning of query execution, so the filtered property has
60    /// a statically-required value.
61    ///
62    /// In contrast, filters relying the value of a `@tag` do not produce
63    /// statically-required values, since the `@tag` value must be computed at runtime.
64    /// For this case, see the [`VertexInfo::dynamically_required_property()`] method.
65    fn statically_required_property(&self, name: &str) -> Option<CandidateValue<FieldValue>>;
66
67    /// Check whether the query demands this vertex property to have specific values:
68    /// a single value, or one of a set or range of values. The candidate values
69    /// are only known *dynamically* i.e. require some of the query
70    /// to have already been executed at the point when this method is called.
71    ///
72    /// For example, filtering a property with `@filter(op: "=", value: ["%expected"])`
73    /// means the property must have a value equal to the value of an earlier property
74    /// whose value is tagged like `@tag(name: "expected")`. If the vertex containing
75    /// the tagged property has already been resolved in this query, this method will offer
76    /// to produce candidate values based on that tag's value.
77    ///
78    /// If *only* static information and no dynamic information is known about a property's value,
79    /// this method will return `None` in order to avoid unnecessary cloning.
80    /// The [`VertexInfo::statically_required_property()`] method can be used to retrieve
81    /// the statically-known information about the property's value.
82    ///
83    /// If *both* static and dynamic information is known about a property's value, all information
84    /// will be merged automatically and presented via the output of this method.
85    fn dynamically_required_property(&self, name: &str) -> Option<DynamicallyResolvedValue<'_>>;
86
87    /// Returns info for the first not-yet-resolved edge by the given name that is *mandatory*:
88    /// this vertex must contain the edge, or its result set will be discarded.
89    ///
90    /// Edges marked `@optional`, `@fold`, or `@recurse` are not mandatory:
91    /// - `@optional` edges that don't exist produce `null` outputs.
92    /// - `@fold` edges that don't exist produce empty aggregations.
93    /// - `@recurse` always starts at depth 0 (i.e. returning the *current* vertex),
94    ///   so the edge is not required to exist.
95    fn first_mandatory_edge(&self, name: &str) -> Option<EdgeInfo>;
96
97    /// Returns info for the first not-yet-resolved edge by the given name.
98    ///
99    /// Just a convenience wrapper over [`VertexInfo::edges_with_name()`].
100    fn first_edge(&self, name: &str) -> Option<EdgeInfo>;
101
102    /// Returns an iterator of all not-yet-resolved edges by that name originating from this vertex.
103    ///
104    /// This is the building block of [`VertexInfo::first_edge()`].
105    /// When possible, prefer using that method as it will lead to more readable code.
106    fn edges_with_name<'a>(&'a self, name: &'a str) -> Box<dyn Iterator<Item = EdgeInfo> + 'a>;
107
108    /// Returns an iterator of all not-yet-resolved edges by that name that are *mandatory*:
109    /// this vertex must contain the edge, or its result set will be discarded.
110    ///
111    /// This is the building block of [`VertexInfo::first_mandatory_edge()`].
112    /// When possible, prefer using that method as it will lead to more readable code.
113    fn mandatory_edges_with_name<'a>(
114        &'a self,
115        name: &'a str,
116    ) -> Box<dyn Iterator<Item = EdgeInfo> + 'a>;
117}
118
119pub(super) trait InternalVertexInfo: super::sealed::__Sealed {
120    fn query(&self) -> &InterpretedQuery;
121
122    fn non_binding_filters(&self) -> bool;
123
124    /// How far query execution has progressed thus far:
125    /// - `Bound::Included` means that data from that [`Vid`] is available, and
126    /// - `Bound::Excluded` means that data from that [`Vid`] is not yet available.
127    ///
128    /// Data from vertices with [`Vid`] values smaller than the given number is always available.
129    fn execution_frontier(&self) -> Bound<Vid>;
130
131    /// The vertex that this [`InternalVertexInfo`] represents.
132    fn current_vertex(&self) -> &IRVertex;
133
134    /// The component where the vertex represented by this [`InternalVertexInfo`] is found.
135    fn current_component(&self) -> &IRQueryComponent;
136
137    /// The component where resolution is happening,
138    /// i.e. where the traversal through the optimization hints began.
139    fn starting_component(&self) -> &IRQueryComponent;
140
141    fn query_variables(&self) -> &BTreeMap<Arc<str>, FieldValue>;
142
143    fn make_non_folded_edge_info(&self, edge: &IREdge) -> EdgeInfo;
144
145    fn make_folded_edge_info(&self, fold: &IRFold) -> EdgeInfo;
146}
147
148impl<T: InternalVertexInfo + super::sealed::__Sealed> VertexInfo for T {
149    fn vid(&self) -> Vid {
150        self.current_vertex().vid
151    }
152
153    fn coerced_to_type(&self) -> Option<&Arc<str>> {
154        let vertex = self.current_vertex();
155        if vertex.coerced_from_type.is_some() {
156            Some(&vertex.type_name)
157        } else {
158            None
159        }
160    }
161
162    fn required_properties(&self) -> Box<dyn Iterator<Item = RequiredProperty> + '_> {
163        let current_component = self.current_component();
164
165        let current_vertex = self.current_vertex();
166
167        let properties = current_component
168            .outputs
169            .values()
170            .filter(|c| c.vertex_id == current_vertex.vid)
171            .map(|c| RequiredProperty::new(c.field_name.clone()));
172
173        let properties = properties.chain(
174            current_vertex
175                .filters
176                .iter()
177                .map(|f| RequiredProperty::new(f.left().field_name.clone())),
178        );
179
180        let properties = properties.chain(current_component.vertices.values().flat_map(|v| {
181            v.filters
182                .iter()
183                .filter_map(|f| match f.right() {
184                    Some(Argument::Tag(FieldRef::ContextField(ctx))) => {
185                        if current_vertex.vid == ctx.vertex_id {
186                            Some(ctx.field_name.clone())
187                        } else {
188                            None
189                        }
190                    }
191                    _ => None,
192                })
193                .map(RequiredProperty::new)
194        }));
195
196        let mut seen_property = HashSet::new();
197        Box::new(properties.filter(move |r| seen_property.insert(r.name.clone())))
198    }
199
200    fn statically_required_property(&self, property: &str) -> Option<CandidateValue<FieldValue>> {
201        if self.non_binding_filters() {
202            // This `VertexInfo` is in a place where the filters applied to fields
203            // don't actually constrain their value in the usual way that lends itself
204            // to optimization.
205            //
206            // For example, we may be looking at the data of a vertex produced by a `@recurse`,
207            // where the *final* vertices produced by the recursion must satisfy the filters, but
208            // intermediate layers of the recursion do not: non-matching ones will get filtered out,
209            // but only after the edge recurses to their own neighbors as well.
210            return None;
211        }
212
213        let query_variables = self.query_variables();
214
215        // We only care about filtering operations that are both:
216        // - on the requested property of this vertex, and
217        // - statically-resolvable, i.e. do not depend on tagged arguments
218        let mut relevant_filters = filters_on_local_property(self.current_vertex(), property)
219            .filter(|op| {
220                // Either there's no "right-hand side" in the operator (as in "is_not_null"),
221                // or the right-hand side is a variable.
222                matches!(op.right(), None | Some(Argument::Variable(..)))
223            })
224            .peekable();
225
226        // Early-return in case there are no filters that apply here.
227        let field = relevant_filters.peek()?.left();
228
229        let candidate =
230            compute_statically_known_candidate(field, relevant_filters, query_variables)
231                .map(|x| x.into_owned());
232        debug_assert!(
233            // Ensure we never return a range variant with a completely unrestricted range.
234            candidate.clone().unwrap_or(CandidateValue::All) != CandidateValue::Range(Range::full()),
235            "caught returning a range variant with a completely unrestricted range; it should have been CandidateValue::All instead"
236        );
237
238        candidate
239    }
240
241    fn dynamically_required_property(
242        &self,
243        property: &str,
244    ) -> Option<DynamicallyResolvedValue<'_>> {
245        if self.non_binding_filters() {
246            // This `VertexInfo` is in a place where the filters applied to fields
247            // don't actually constrain their value in the usual way that lends itself
248            // to optimization.
249            //
250            // For example, we may be looking at the data of a vertex produced by a `@recurse`,
251            // where the *final* vertices produced by the recursion must satisfy the filters, but
252            // intermediate layers of the recursion do not: non-matching ones will get filtered out,
253            // but only after the edge recurses to their own neighbors as well.
254            return None;
255        }
256
257        // We only care about filtering operations that are all of the following:
258        // - on the requested property of this vertex;
259        // - dynamically-resolvable, i.e. depend on tagged arguments,
260        // - the used tagged argument is from a vertex that has already been computed
261        //   at the time this call was made, and
262        // - use a supported filtering operation using those tagged arguments.
263        let resolved_range = (Bound::Unbounded, self.execution_frontier());
264        let relevant_filters: Vec<_> = filters_on_local_property(self.current_vertex(), property)
265            .filter(|op| {
266                matches!(
267                    op,
268                    Operation::Equals(..)
269                        | Operation::NotEquals(..)
270                        | Operation::LessThan(..)
271                        | Operation::LessThanOrEqual(..)
272                        | Operation::GreaterThan(..)
273                        | Operation::GreaterThanOrEqual(..)
274                        | Operation::OneOf(..)
275                ) && match op.right() {
276                    Some(Argument::Tag(FieldRef::ContextField(ctx))) => {
277                        // Ensure the vertex holding the @tag has already been computed.
278                        resolved_range.contains(&ctx.vertex_id)
279                    }
280                    Some(Argument::Tag(FieldRef::FoldSpecificField(fsf))) => {
281                        // Ensure the fold holding the @tag has already been computed.
282                        resolved_range.contains(&fsf.fold_root_vid)
283                    }
284                    _ => false,
285                }
286            })
287            .collect();
288
289        // Early-return in case there are no filters that apply here.
290        let first_filter = relevant_filters.first()?;
291
292        let initial_candidate = self.statically_required_property(property).unwrap_or_else(|| {
293            if first_filter.left().field_type.nullable() {
294                CandidateValue::All
295            } else {
296                CandidateValue::Range(Range::full_non_null())
297            }
298        });
299
300        // Right now, this API only supports materializing the constraint from a single tag.
301        // Choose which @filter to choose as the one providing the value.
302        //
303        // In order of priority, we'll choose:
304        // - an `=` filter
305        // - a `one_of` filter
306        // - a `< / <= / > / >=` filter
307        // - a `!=` filter,
308        // breaking ties based on which filter was specified first.
309        let filter_to_use = {
310            relevant_filters.iter().find(|op| matches!(op, Operation::Equals(..))).unwrap_or_else(
311                || {
312                    relevant_filters
313                        .iter()
314                        .find(|op| matches!(op, Operation::OneOf(..)))
315                        .unwrap_or_else(|| {
316                            relevant_filters
317                                .iter()
318                                .find(|op| {
319                                    matches!(
320                                        op,
321                                        Operation::LessThan(..)
322                                            | Operation::LessThanOrEqual(..)
323                                            | Operation::GreaterThan(..)
324                                            | Operation::GreaterThanOrEqual(..)
325                                    )
326                                })
327                                .unwrap_or(first_filter)
328                        })
329                },
330            )
331        };
332
333        let field = filter_to_use
334            .right()
335            .expect("filter did not have an operand")
336            .as_tag()
337            .expect("operand was not a tag");
338        let bare_operation = filter_to_use
339            .try_map(|_| Ok::<(), ()>(()), |_| Ok(()))
340            .expect("removing operands failed");
341        Some(DynamicallyResolvedValue::new(
342            self.query().clone(),
343            self.starting_component(),
344            field,
345            bare_operation,
346            initial_candidate,
347        ))
348    }
349
350    fn edges_with_name<'a>(&'a self, name: &'a str) -> Box<dyn Iterator<Item = EdgeInfo> + 'a> {
351        let component = self.current_component();
352        let current_vid = self.current_vertex().vid;
353
354        let non_folded_edges = component
355            .edges
356            .values()
357            .filter(move |edge| edge.from_vid == current_vid && edge.edge_name.as_ref() == name)
358            .map(|edge| self.make_non_folded_edge_info(edge.as_ref()));
359        let folded_edges = component
360            .folds
361            .values()
362            .filter(move |fold| fold.from_vid == current_vid && fold.edge_name.as_ref() == name)
363            .map(|fold| self.make_folded_edge_info(fold.as_ref()));
364
365        Box::new(non_folded_edges.chain(folded_edges))
366    }
367
368    fn mandatory_edges_with_name<'a>(
369        &'a self,
370        name: &'a str,
371    ) -> Box<dyn Iterator<Item = EdgeInfo> + 'a> {
372        if self.non_binding_filters() {
373            Box::new(std::iter::empty())
374        } else {
375            Box::new(self.edges_with_name(name).filter(EdgeInfo::is_mandatory))
376        }
377    }
378
379    fn first_mandatory_edge(&self, name: &str) -> Option<EdgeInfo> {
380        self.mandatory_edges_with_name(name).next()
381    }
382
383    fn first_edge(&self, name: &str) -> Option<EdgeInfo> {
384        self.edges_with_name(name).next()
385    }
386}
387
388fn filters_on_local_property<'a: 'b, 'b>(
389    vertex: &'a IRVertex,
390    property_name: &'b str,
391) -> impl Iterator<Item = &'a Operation<LocalField, Argument>> + 'b {
392    vertex.filters.iter().filter(move |op| op.left().field_name.as_ref() == property_name)
393}
394
395fn compute_statically_known_candidate<'a, 'b>(
396    field: &'a LocalField,
397    relevant_filters: impl Iterator<Item = &'a Operation<LocalField, Argument>>,
398    query_variables: &'b BTreeMap<Arc<str>, FieldValue>,
399) -> Option<CandidateValue<Cow<'b, FieldValue>>> {
400    let is_subject_field_nullable = field.field_type.nullable();
401    super::filters::candidate_from_statically_evaluated_filters(
402        relevant_filters,
403        query_variables,
404        is_subject_field_nullable,
405    )
406}
407
408#[cfg(test)]
409mod tests {
410    use std::{ops::Bound, sync::Arc};
411
412    use crate::{
413        interpreter::hints::{
414            vertex_info::compute_statically_known_candidate, CandidateValue, Range,
415        },
416        ir::{Argument, FieldValue, LocalField, Operation, Type, VariableRef},
417    };
418
419    #[test]
420    fn exclude_not_equals_candidates() {
421        let first: Arc<str> = Arc::from("first");
422        let second: Arc<str> = Arc::from("second");
423        let third: Arc<str> = Arc::from("third");
424        let null: Arc<str> = Arc::from("null");
425        let list: Arc<str> = Arc::from("my_list");
426        let longer_list: Arc<str> = Arc::from("longer_list");
427        let nullable_int_type = Type::parse("Int").unwrap();
428        let int_type = Type::parse("Int!").unwrap();
429        let list_int_type = Type::parse("[Int!]!").unwrap();
430
431        let first_var = Argument::Variable(VariableRef {
432            variable_name: first.clone(),
433            variable_type: int_type.clone(),
434        });
435        let second_var = Argument::Variable(VariableRef {
436            variable_name: second.clone(),
437            variable_type: int_type.clone(),
438        });
439        let null_var = Argument::Variable(VariableRef {
440            variable_name: null.clone(),
441            variable_type: nullable_int_type.clone(),
442        });
443        let list_var = Argument::Variable(VariableRef {
444            variable_name: list.clone(),
445            variable_type: list_int_type.clone(),
446        });
447        let longer_list_var = Argument::Variable(VariableRef {
448            variable_name: longer_list.clone(),
449            variable_type: list_int_type.clone(),
450        });
451
452        let local_field =
453            LocalField { field_name: Arc::from("my_field"), field_type: nullable_int_type.clone() };
454
455        let variables = btreemap! {
456            first => FieldValue::Int64(1),
457            second => FieldValue::Int64(2),
458            third => FieldValue::Int64(3),
459            null => FieldValue::Null,
460            list => FieldValue::List(Arc::new([FieldValue::Int64(1), FieldValue::Int64(2)])),
461            longer_list => FieldValue::List(Arc::new([FieldValue::Int64(1), FieldValue::Int64(2), FieldValue::Int64(3)])),
462        };
463
464        let test_data = [
465            // Both `= 1` and `!= 1` are impossible to satisfy simultaneously.
466            (
467                vec![
468                    Operation::NotEquals(local_field.clone(), first_var.clone()),
469                    Operation::Equals(local_field.clone(), first_var.clone()),
470                ],
471                Some(CandidateValue::Impossible),
472            ),
473            // `= 2` and `!= 1` means the value must be 2.
474            (
475                vec![
476                    Operation::NotEquals(local_field.clone(), first_var.clone()),
477                    Operation::Equals(local_field.clone(), second_var.clone()),
478                ],
479                Some(CandidateValue::Single(&variables["second"])),
480            ),
481            //
482            // `one_of [1, 2]` and `!= 1` allows only `2`.
483            (
484                vec![
485                    Operation::OneOf(local_field.clone(), list_var.clone()),
486                    Operation::NotEquals(local_field.clone(), first_var.clone()),
487                ],
488                Some(CandidateValue::Single(&variables["second"])),
489            ),
490            //
491            // `one_of [1, 2, 3]` and `not_one_of [1, 2]` allows only `3`.
492            (
493                vec![
494                    Operation::OneOf(local_field.clone(), longer_list_var.clone()),
495                    Operation::NotOneOf(local_field.clone(), list_var.clone()),
496                ],
497                Some(CandidateValue::Single(&variables["third"])),
498            ),
499            //
500            // `>= 2` and `not_one_of [1, 2]` produces the exclusive > 2 range
501            (
502                vec![
503                    Operation::GreaterThanOrEqual(local_field.clone(), second_var.clone()),
504                    Operation::NotOneOf(local_field.clone(), list_var.clone()),
505                ],
506                Some(CandidateValue::Range(Range::with_start(
507                    Bound::Excluded(&variables["second"]),
508                    true,
509                ))),
510            ),
511            //
512            // `>= 2` and `is_not_null` and `not_one_of [1, 2]` produces the exclusive non-null > 2 range
513            (
514                vec![
515                    Operation::GreaterThanOrEqual(local_field.clone(), second_var.clone()),
516                    Operation::NotOneOf(local_field.clone(), list_var.clone()),
517                    Operation::IsNotNull(local_field.clone()),
518                ],
519                Some(CandidateValue::Range(Range::with_start(
520                    Bound::Excluded(&variables["second"]),
521                    false,
522                ))),
523            ),
524            //
525            // `> 2` and `is_not_null` produces the exclusive non-null > 2 range
526            (
527                vec![
528                    Operation::GreaterThan(local_field.clone(), second_var.clone()),
529                    Operation::IsNotNull(local_field.clone()),
530                ],
531                Some(CandidateValue::Range(Range::with_start(
532                    Bound::Excluded(&variables["second"]),
533                    false,
534                ))),
535            ),
536            //
537            // `<= 2` and `!= 2` and `is_not_null` produces the exclusive non-null < 2 range
538            (
539                vec![
540                    Operation::LessThanOrEqual(local_field.clone(), second_var.clone()),
541                    Operation::NotEquals(local_field.clone(), second_var.clone()),
542                    Operation::IsNotNull(local_field.clone()),
543                ],
544                Some(CandidateValue::Range(Range::with_end(
545                    Bound::Excluded(&variables["second"]),
546                    false,
547                ))),
548            ),
549            //
550            // `< 2` and `is_not_null` produces the exclusive non-null < 2 range
551            (
552                vec![
553                    Operation::LessThan(local_field.clone(), second_var.clone()),
554                    Operation::IsNotNull(local_field.clone()),
555                ],
556                Some(CandidateValue::Range(Range::with_end(
557                    Bound::Excluded(&variables["second"]),
558                    false,
559                ))),
560            ),
561            //
562            // `is_not_null` by itself only eliminates null
563            (
564                vec![Operation::IsNotNull(local_field.clone())],
565                Some(CandidateValue::Range(Range::full_non_null())),
566            ),
567            //
568            // `!= null` also elminates null
569            (
570                vec![Operation::NotEquals(local_field.clone(), null_var.clone())],
571                Some(CandidateValue::Range(Range::full_non_null())),
572            ),
573            //
574            // `!= 1` by itself doesn't produce any candidates
575            (vec![Operation::NotEquals(local_field.clone(), first_var.clone())], None),
576            //
577            // `not_one_of [1, 2]` by itself doesn't produce any candidates
578            (vec![Operation::NotEquals(local_field.clone(), list_var.clone())], None),
579        ];
580
581        for (filters, expected_output) in test_data {
582            assert_eq!(
583                expected_output,
584                compute_statically_known_candidate(&local_field, filters.iter(), &variables)
585                    .as_ref()
586                    .map(|x| x.as_deref()),
587                "with {filters:?}",
588            );
589        }
590
591        // Explicitly drop these values, so clippy stops complaining about unneccessary clones earlier.
592        drop((
593            first_var,
594            second_var,
595            null_var,
596            list_var,
597            longer_list_var,
598            local_field,
599            int_type,
600            nullable_int_type,
601            list_int_type,
602        ));
603    }
604
605    #[test]
606    fn use_schema_to_exclude_null_from_range() {
607        let first: Arc<str> = Arc::from("first");
608        let int_type = Type::parse("Int!").unwrap();
609
610        let first_var = Argument::Variable(VariableRef {
611            variable_name: first.clone(),
612            variable_type: int_type.clone(),
613        });
614
615        let local_field =
616            LocalField { field_name: Arc::from("my_field"), field_type: int_type.clone() };
617
618        let variables = btreemap! {
619            first => FieldValue::Int64(1),
620        };
621
622        let test_data = [
623            // The local field is non-nullable.
624            // When we apply a range bound on the field, the range must be non-nullable too.
625            (
626                vec![Operation::GreaterThanOrEqual(local_field.clone(), first_var.clone())],
627                Some(CandidateValue::Range(Range::with_start(
628                    Bound::Included(&variables["first"]),
629                    false,
630                ))),
631            ),
632            (
633                vec![Operation::GreaterThan(local_field.clone(), first_var.clone())],
634                Some(CandidateValue::Range(Range::with_start(
635                    Bound::Excluded(&variables["first"]),
636                    false,
637                ))),
638            ),
639            (
640                vec![Operation::LessThan(local_field.clone(), first_var.clone())],
641                Some(CandidateValue::Range(Range::with_end(
642                    Bound::Excluded(&variables["first"]),
643                    false,
644                ))),
645            ),
646            (
647                vec![Operation::LessThanOrEqual(local_field.clone(), first_var.clone())],
648                Some(CandidateValue::Range(Range::with_end(
649                    Bound::Included(&variables["first"]),
650                    false,
651                ))),
652            ),
653        ];
654
655        for (filters, expected_output) in test_data {
656            assert_eq!(
657                expected_output,
658                compute_statically_known_candidate(&local_field, filters.iter(), &variables)
659                    .as_ref()
660                    .map(|x| x.as_deref()),
661                "with {filters:?}",
662            );
663        }
664
665        // Explicitly drop these values, so clippy stops complaining about unneccessary clones earlier.
666        drop((first_var, local_field, int_type));
667    }
668}