Skip to main content

triblespace_core/
query.rs

1//! Query facilities for matching tribles by declaring patterns of constraints.
2//! Build queries with the [`find!`](crate::prelude::find) macro which binds variables and
3//! combines constraint expressions:
4//!
5//! ```
6//! # use triblespace_core::prelude::*;
7//! # use triblespace_core::prelude::valueschemas::ShortString;
8//! let results = find!((x: Value<ShortString>), x.is("foo".to_value())).collect::<Vec<_>>();
9//! ```
10//!
11//! Variables are converted via [`TryFromValue`](crate::value::TryFromValue). By default,
12//! conversion failures silently skip the row (filter semantics). Append `?` to a variable
13//! to receive `Result<T, E>` instead, letting the caller handle errors explicitly.
14//!
15//! For a tour of the language see the "Query Language" chapter in the book.
16//! Conceptual background on schemas and join strategy appears in the
17//! "Query Engine" and "Atreides Join" chapters.
18/// [`ConstantConstraint`](constantconstraint::ConstantConstraint) — pins a variable to a single value.
19pub mod constantconstraint;
20/// [`EqualityConstraint`](equalityconstraint::EqualityConstraint) — constrains two variables to have the same value.
21pub mod equalityconstraint;
22/// [`KeysConstraint`](hashmapconstraint::KeysConstraint) — constrains a variable to HashMap keys.
23pub mod hashmapconstraint;
24/// [`SetConstraint`](hashsetconstraint::SetConstraint) — constrains a variable to HashSet members.
25pub mod hashsetconstraint;
26/// [`IgnoreConstraint`](ignore::IgnoreConstraint) — hides variables from the outer query.
27pub mod ignore;
28/// [`IntersectionConstraint`](intersectionconstraint::IntersectionConstraint) — logical AND.
29pub mod intersectionconstraint;
30/// [`PatchValueConstraint`](patchconstraint::PatchValueConstraint) and [`PatchIdConstraint`](patchconstraint::PatchIdConstraint) — constrains variables to PATCH entries.
31pub mod patchconstraint;
32/// [`RegularPathConstraint`](regularpathconstraint::RegularPathConstraint) — regular path expressions over graphs.
33pub mod regularpathconstraint;
34/// [`UnionConstraint`](unionconstraint::UnionConstraint) — logical OR.
35pub mod unionconstraint;
36mod variableset;
37
38use std::cmp::Reverse;
39use std::fmt;
40use std::iter::FromIterator;
41use std::marker::PhantomData;
42
43use arrayvec::ArrayVec;
44use constantconstraint::*;
45/// Re-export of [`IgnoreConstraint`](ignore::IgnoreConstraint).
46pub use ignore::IgnoreConstraint;
47
48use crate::value::schemas::genid::GenId;
49use crate::value::RawValue;
50use crate::value::Value;
51use crate::value::ValueSchema;
52
53/// Re-export of [`PathOp`](regularpathconstraint::PathOp).
54pub use regularpathconstraint::PathOp;
55/// Re-export of [`RegularPathConstraint`](regularpathconstraint::RegularPathConstraint).
56pub use regularpathconstraint::RegularPathConstraint;
57/// Re-export of [`VariableSet`](variableset::VariableSet).
58pub use variableset::VariableSet;
59
60/// Types storing tribles can implement this trait to expose them to queries.
61/// The trait provides a method to create a constraint for a given trible pattern.
62pub trait TriblePattern {
63    /// The type of the constraint created by the pattern method.
64    type PatternConstraint<'a>: Constraint<'a>
65    where
66        Self: 'a;
67
68    /// Create a constraint for a given trible pattern.
69    /// The method takes three variables, one for each part of the trible.
70    /// The schemas of the entities and attributes are always [GenId], while the value
71    /// schema can be any type implementing [ValueSchema] and is specified as a type parameter.
72    ///
73    /// This method is usually not called directly, but rather through typed query language
74    /// macros like [pattern!][crate::namespace].
75    fn pattern<'a, V: ValueSchema>(
76        &'a self,
77        e: Variable<GenId>,
78        a: Variable<GenId>,
79        v: Variable<V>,
80    ) -> Self::PatternConstraint<'a>;
81}
82
83/// Low-level identifier for a variable in a query.
84pub type VariableId = usize;
85
86/// Context for creating variables in a query.
87/// The context keeps track of the next index to assign to a variable.
88/// This allows for the creation of new anonymous variables in higher-level query languages.
89#[derive(Debug)]
90pub struct VariableContext {
91    /// The index that will be assigned to the next variable.
92    pub next_index: VariableId,
93}
94
95impl Default for VariableContext {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl VariableContext {
102    /// Create a new variable context.
103    /// The context starts with an index of 0.
104    pub fn new() -> Self {
105        VariableContext { next_index: 0 }
106    }
107
108    /// Create a new variable.
109    /// The variable is assigned the next available index.
110    ///
111    /// Panics if the number of variables exceeds 128.
112    ///
113    /// This method is usually not called directly, but rather through typed query language
114    /// macros like [find!][crate::query].
115    pub fn next_variable<T: ValueSchema>(&mut self) -> Variable<T> {
116        assert!(
117            self.next_index < 128,
118            "currently queries support at most 128 variables"
119        );
120        let v = Variable::new(self.next_index);
121        self.next_index += 1;
122        v
123    }
124}
125
126/// A placeholder for unknowns in a query.
127/// Within the query engine each variable is identified by an integer,
128/// which can be accessed via the `index` property.
129/// Variables also have an associated type which is used to parse the [Value]s
130/// found by the query engine.
131#[derive(Debug)]
132pub struct Variable<T: ValueSchema> {
133    /// The integer index identifying this variable in the [`Binding`].
134    pub index: VariableId,
135    typed: PhantomData<T>,
136}
137
138impl<T: ValueSchema> Copy for Variable<T> {}
139
140impl<T: ValueSchema> Clone for Variable<T> {
141    fn clone(&self) -> Self {
142        *self
143    }
144}
145
146impl<T: ValueSchema> Variable<T> {
147    /// Creates a variable with the given index.
148    pub fn new(index: VariableId) -> Self {
149        Variable {
150            index,
151            typed: PhantomData,
152        }
153    }
154
155    /// Extracts the bound value for this variable from `binding`.
156    ///
157    /// # Panics
158    ///
159    /// Panics if the variable has not been bound.
160    pub fn extract(self, binding: &Binding) -> &Value<T> {
161        let raw = binding.get(self.index).unwrap_or_else(|| {
162            panic!(
163                "query variable (idx {}) was never bound; ensure it appears in a constraint or remove it from the projection",
164                self.index
165            )
166        });
167        Value::as_transmute_raw(raw)
168    }
169}
170
171/// Collections can implement this trait so that they can be used in queries.
172/// The returned constraint will filter the values assigned to the variable
173/// to only those that are contained in the collection.
174pub trait ContainsConstraint<'a, T: ValueSchema> {
175    /// The concrete constraint type produced by [`has`](ContainsConstraint::has).
176    type Constraint: Constraint<'a>;
177
178    /// Create a constraint that filters the values assigned to the variable
179    /// to only those that are contained in the collection.
180    ///
181    /// The returned constraint will usually perform a conversion between the
182    /// concrete rust type stored in the collection a [Value] of the appropriate schema
183    /// type for the variable.
184    fn has(self, v: Variable<T>) -> Self::Constraint;
185}
186
187impl<T: ValueSchema> Variable<T> {
188    /// Create a constraint so that only a specific value can be assigned to the variable.
189    pub fn is(self, constant: Value<T>) -> ConstantConstraint {
190        ConstantConstraint::new(self, constant)
191    }
192}
193
194/// The binding keeps track of the values assigned to variables in a query.
195/// It maps variables to values - by their index - via a simple array,
196/// and keeps track of which variables are bound.
197/// It is used to store intermediate results and to pass information
198/// between different constraints.
199/// The binding is mutable, as it is modified by the query engine.
200/// It is not thread-safe and should not be shared between threads.
201/// The binding is a simple data structure that is cheap to clone.
202/// It is not intended to be used as a long-term storage for query results.
203#[derive(Clone, Debug)]
204pub struct Binding {
205    /// Bitset tracking which variables have been assigned a value.
206    pub bound: VariableSet,
207    values: [RawValue; 128],
208}
209
210impl Binding {
211    /// Binds `variable` to `value`.
212    pub fn set(&mut self, variable: VariableId, value: &RawValue) {
213        self.values[variable] = *value;
214        self.bound.set(variable);
215    }
216
217    /// Unset a variable in the binding.
218    /// This is used to backtrack in the query engine.
219    pub fn unset(&mut self, variable: VariableId) {
220        self.bound.unset(variable);
221    }
222
223    /// Check if a variable is bound in the binding.
224    pub fn get(&self, variable: VariableId) -> Option<&RawValue> {
225        if self.bound.is_set(variable) {
226            Some(&self.values[variable])
227        } else {
228            None
229        }
230    }
231}
232
233impl Default for Binding {
234    fn default() -> Self {
235        Self {
236            bound: VariableSet::new_empty(),
237            values: [[0; 32]; 128],
238        }
239    }
240}
241
242/// The cooperative protocol that every query participant implements.
243///
244/// A constraint restricts the values that can be assigned to query variables.
245/// The query engine does not plan joins in advance; instead it consults
246/// constraints directly during a depth-first search over partial bindings.
247/// Each constraint reports which variables it touches, estimates how many
248/// candidates remain, enumerates concrete values on demand, and signals
249/// whether its requirements are still satisfiable. This protocol is the
250/// sole interface between the engine and the data — whether that data lives
251/// in a [`TribleSet`](crate::trible::TribleSet), a [`HashMap`](std::collections::HashMap),
252/// or a custom application predicate.
253///
254/// # The protocol
255///
256/// The engine drives the search by calling five methods in a fixed rhythm:
257///
258/// | Method | Role | Called when |
259/// |--------|------|------------|
260/// | [`variables`](Constraint::variables) | Declares which variables the constraint touches. | Once, at query start. |
261/// | [`estimate`](Constraint::estimate) | Predicts the candidate count for a variable. | Before each binding decision. |
262/// | [`propose`](Constraint::propose) | Enumerates candidate values for a variable. | On the most selective constraint. |
263/// | [`confirm`](Constraint::confirm) | Filters candidates proposed by another constraint. | On all remaining constraints. |
264/// | [`satisfied`](Constraint::satisfied) | Checks whether fully-bound sub-constraints still hold. | Before propose/confirm in composite constraints. |
265///
266/// [`influence`](Constraint::influence) completes the picture by telling the
267/// engine which estimates to refresh when a variable is bound or unbound.
268///
269/// # Statelessness
270///
271/// Constraints are stateless: every method receives the current [`Binding`]
272/// as a parameter rather than maintaining internal bookkeeping. This lets
273/// the engine backtrack freely by unsetting variables in the binding
274/// without notifying the constraints.
275///
276/// # Composability
277///
278/// Constraints combine via [`IntersectionConstraint`](crate::query::intersectionconstraint::IntersectionConstraint)
279/// (logical AND — built by [`and!`](crate::and)) and
280/// [`UnionConstraint`](crate::query::unionconstraint::UnionConstraint)
281/// (logical OR — built by [`or!`](crate::or)). Because every constraint
282/// speaks the same protocol, heterogeneous data sources mix freely in a
283/// single query.
284///
285/// # Implementing a custom constraint
286///
287/// A new constraint only needs to implement [`variables`](Constraint::variables),
288/// [`estimate`](Constraint::estimate), [`propose`](Constraint::propose), and
289/// [`confirm`](Constraint::confirm). Override [`satisfied`](Constraint::satisfied)
290/// when the constraint can detect unsatisfiability before the engine asks
291/// about individual variables (e.g. a fully-bound triple lookup that found
292/// no match). Override [`influence`](Constraint::influence) when binding one
293/// variable changes the estimates for a non-obvious set of others.
294pub trait Constraint<'a> {
295    /// Returns the set of variables this constraint touches.
296    ///
297    /// Called once at query start. The engine uses this to build influence
298    /// graphs and to determine which constraints participate when a
299    /// particular variable is being bound.
300    fn variables(&self) -> VariableSet;
301
302    /// Estimates the number of candidate values for `variable` given the
303    /// current partial `binding`.
304    ///
305    /// Returns `None` when `variable` is not constrained by this constraint.
306    /// The estimate need not be exact — it guides variable ordering, not
307    /// correctness. Tighter estimates lead to better search pruning; see the
308    /// [Atreides join](crate) family for how different estimate fidelities
309    /// affect performance.
310    fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize>;
311
312    /// Enumerates candidate values for `variable` into `proposals`.
313    ///
314    /// Called on the constraint with the lowest estimate for the variable
315    /// being bound. Values are appended to `proposals`; the engine may
316    /// already have values in the vector from a previous round.
317    ///
318    /// Does nothing when `variable` is not constrained by this constraint.
319    fn propose(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>);
320
321    /// Filters `proposals` to remove values for `variable` that violate
322    /// this constraint.
323    ///
324    /// Called on every constraint *except* the one that proposed, in order
325    /// of increasing estimate. Implementations remove entries from
326    /// `proposals` that are inconsistent with the current `binding`.
327    ///
328    /// Does nothing when `variable` is not constrained by this constraint.
329    fn confirm(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>);
330
331    /// Returns whether this constraint is consistent with the current
332    /// `binding`.
333    ///
334    /// The default implementation returns `true`. Override this when the
335    /// constraint can cheaply detect that no solution exists — for example,
336    /// a [`TribleSetConstraint`](crate::trible::tribleset::triblesetconstraint::TribleSetConstraint)
337    /// whose entity, attribute, and value are all bound but the triple is
338    /// absent from the dataset.
339    ///
340    /// Composite constraints propagate this check to their children:
341    /// [`IntersectionConstraint`](crate::query::intersectionconstraint::IntersectionConstraint)
342    /// requires *all* children to be satisfied, while
343    /// [`UnionConstraint`](crate::query::unionconstraint::UnionConstraint)
344    /// requires *at least one*. The union uses this to skip dead variants
345    /// in propose and confirm, preventing values from a satisfied variant
346    /// from leaking through a dead one.
347    fn satisfied(&self, _binding: &Binding) -> bool {
348        true
349    }
350
351    /// Returns the set of variables whose estimates may change when
352    /// `variable` is bound or unbound.
353    ///
354    /// The default includes every variable this constraint touches except
355    /// `variable` itself. Returns an empty set when `variable` is not part
356    /// of this constraint.
357    fn influence(&self, variable: VariableId) -> VariableSet {
358        let mut vars = self.variables();
359        if vars.is_set(variable) {
360            vars.unset(variable);
361            vars
362        } else {
363            VariableSet::new_empty()
364        }
365    }
366}
367
368impl<'a, T: Constraint<'a> + ?Sized> Constraint<'a> for Box<T> {
369    fn variables(&self) -> VariableSet {
370        let inner: &T = self;
371        inner.variables()
372    }
373
374    fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
375        let inner: &T = self;
376        inner.estimate(variable, binding)
377    }
378
379    fn propose(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
380        let inner: &T = self;
381        inner.propose(variable, binding, proposals)
382    }
383
384    fn confirm(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
385        let inner: &T = self;
386        inner.confirm(variable, binding, proposals)
387    }
388
389    fn satisfied(&self, binding: &Binding) -> bool {
390        let inner: &T = self;
391        inner.satisfied(binding)
392    }
393
394    fn influence(&self, variable: VariableId) -> VariableSet {
395        let inner: &T = self;
396        inner.influence(variable)
397    }
398}
399
400impl<'a, T: Constraint<'a> + ?Sized> Constraint<'static> for std::sync::Arc<T> {
401    fn variables(&self) -> VariableSet {
402        let inner: &T = self;
403        inner.variables()
404    }
405
406    fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
407        let inner: &T = self;
408        inner.estimate(variable, binding)
409    }
410
411    fn propose(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
412        let inner: &T = self;
413        inner.propose(variable, binding, proposals)
414    }
415
416    fn confirm(&self, variable: VariableId, binding: &Binding, proposal: &mut Vec<RawValue>) {
417        let inner: &T = self;
418        inner.confirm(variable, binding, proposal)
419    }
420
421    fn satisfied(&self, binding: &Binding) -> bool {
422        let inner: &T = self;
423        inner.satisfied(binding)
424    }
425
426    fn influence(&self, variable: VariableId) -> VariableSet {
427        let inner: &T = self;
428        inner.influence(variable)
429    }
430}
431
432/// A query is an iterator over the results of a query.
433/// It takes a constraint and a post-processing function as input,
434/// and returns the results of the query as a stream of values.
435/// The query engine uses a depth-first search to find solutions to the query,
436/// proposing values for the variables and backtracking when it reaches a dead end.
437/// The query engine is designed to be simple and efficient, providing low, consistent,
438/// and predictable latency, skew resistance, and no required (or possible) tuning.
439/// The query engine is designed to be used in combination with the [Constraint] trait,
440/// which provides a simple and flexible way to implement constraints that can be used
441/// to filter the results of a query.
442///
443/// This struct is usually not created directly, but rather through the `find!` macro,
444/// which provides a convenient way to declare variables and concrete types for them.
445/// And which sets up the nessecairy context for higher-level query languages
446/// like the one provided by the [crate::namespace] module.
447pub struct Query<C, P: Fn(&Binding) -> Option<R>, R> {
448    constraint: C,
449    postprocessing: P,
450    mode: Search,
451    binding: Binding,
452    influences: [VariableSet; 128],
453    estimates: [usize; 128],
454    touched_variables: VariableSet,
455    stack: ArrayVec<VariableId, 128>,
456    unbound: ArrayVec<VariableId, 128>,
457    values: ArrayVec<Option<Vec<RawValue>>, 128>,
458}
459
460impl<'a, C: Constraint<'a>, P: Fn(&Binding) -> Option<R>, R> Query<C, P, R> {
461    /// Create a new query.
462    /// The query takes a constraint and a post-processing function as input,
463    /// and returns the results of the query as a stream of values.
464    /// The post-processing function returns `Option<R>`: returning `None`
465    /// skips the current binding and continues the search.
466    ///
467    /// This method is usually not called directly, but rather through the [find!] macro,
468    pub fn new(constraint: C, postprocessing: P) -> Self {
469        let variables = constraint.variables();
470        let influences = std::array::from_fn(|v| {
471            if variables.is_set(v) {
472                constraint.influence(v)
473            } else {
474                VariableSet::new_empty()
475            }
476        });
477        let binding = Binding::default();
478        let estimates = std::array::from_fn(|v| {
479            if variables.is_set(v) {
480                constraint
481                    .estimate(v, &binding)
482                    .expect("unconstrained variable in query")
483            } else {
484                usize::MAX
485            }
486        });
487        let mut unbound = ArrayVec::from_iter(variables);
488        unbound.sort_unstable_by_key(|v| {
489            (
490                Reverse(
491                    estimates[*v]
492                        .checked_ilog2()
493                        .map(|magnitude| magnitude + 1)
494                        .unwrap_or(0),
495                ),
496                influences[*v].count(),
497            )
498        });
499
500        Query {
501            constraint,
502            postprocessing,
503            mode: Search::NextVariable,
504            binding,
505            influences,
506            estimates,
507            touched_variables: VariableSet::new_empty(),
508            stack: ArrayVec::new(),
509            unbound,
510            values: ArrayVec::from([const { None }; 128]),
511        }
512    }
513}
514
515/// The search mode of the query engine.
516/// The query engine uses a depth-first search to find solutions to the query,
517/// proposing values for the variables and backtracking when it reaches a dead end.
518/// The search mode is used to keep track of the current state of the search.
519/// The search mode can be one of the following:
520/// - `NextVariable` - The query engine is looking for the next variable to assign a value to.
521/// - `NextValue` - The query engine is looking for the next value to assign to a variable.
522/// - `Backtrack` - The query engine is backtracking to try a different value for a variable.
523/// - `Done` - The query engine has finished the search and there are no more results.
524#[derive(Copy, Clone, Debug)]
525enum Search {
526    NextVariable,
527    NextValue,
528    Backtrack,
529    Done,
530}
531
532impl<'a, C: Constraint<'a>, P: Fn(&Binding) -> Option<R>, R> Iterator for Query<C, P, R> {
533    type Item = R;
534
535    fn next(&mut self) -> Option<Self::Item> {
536        loop {
537            match &self.mode {
538                Search::NextVariable => {
539                    self.mode = Search::NextValue;
540                    if self.unbound.is_empty() {
541                        if let Some(result) = (self.postprocessing)(&self.binding) {
542                            return Some(result);
543                        }
544                        // Post-processing rejected this binding; continue
545                        // searching (mode is already NextValue).
546                        continue;
547                    }
548
549                    let mut stale_estimates = VariableSet::new_empty();
550
551                    while let Some(variable) = self.touched_variables.drain_next_ascending() {
552                        stale_estimates = stale_estimates.union(self.influences[variable]);
553                    }
554
555                    // We remove the bound variables from the stale estimates,
556                    // as already bound variables cannot be influenced by the unbound ones.
557                    stale_estimates = stale_estimates.subtract(self.binding.bound);
558
559                    if !stale_estimates.is_empty() {
560                        while let Some(v) = stale_estimates.drain_next_ascending() {
561                            self.estimates[v] = self
562                                .constraint
563                                .estimate(v, &self.binding)
564                                .expect("unconstrained variable in query");
565                        }
566
567                        self.unbound.sort_unstable_by_key(|v| {
568                            (
569                                Reverse(
570                                    self.estimates[*v]
571                                        .checked_ilog2()
572                                        .map(|magnitude| magnitude + 1)
573                                        .unwrap_or(0),
574                                ),
575                                self.influences[*v].count(),
576                            )
577                        });
578                    }
579
580                    let variable = self.unbound.pop().expect("non-empty unbound");
581                    let estimate = self.estimates[variable];
582
583                    self.stack.push(variable);
584                    let values = self.values[variable].get_or_insert(Vec::new());
585                    values.clear();
586                    values.reserve_exact(estimate.saturating_sub(values.capacity()));
587                    self.constraint.propose(variable, &self.binding, values);
588                }
589                Search::NextValue => {
590                    if let Some(&variable) = self.stack.last() {
591                        if let Some(assignment) = self.values[variable]
592                            .as_mut()
593                            .expect("values should be initialized")
594                            .pop()
595                        {
596                            self.binding.set(variable, &assignment);
597                            self.touched_variables.set(variable);
598                            self.mode = Search::NextVariable;
599                        } else {
600                            self.mode = Search::Backtrack;
601                        }
602                    } else {
603                        self.mode = Search::Done;
604                        return None;
605                    }
606                }
607                Search::Backtrack => {
608                    if let Some(variable) = self.stack.pop() {
609                        self.binding.unset(variable);
610                        // Note that we did not update estiamtes for the unbound variables
611                        // as we are backtracking, so the estimates are still valid.
612                        // Since we choose this variable before, we know that it would
613                        // still go last in the unbound list.
614                        self.unbound.push(variable);
615
616                        // However, we need to update the touched variables,
617                        // as we are backtracking and the variable is no longer bound.
618                        // We're essentially restoring the estimate of the touched variables
619                        // to the state before we bound this variable.
620                        self.touched_variables.set(variable);
621                        self.mode = Search::NextValue;
622                    } else {
623                        self.mode = Search::Done;
624                        return None;
625                    }
626                }
627                Search::Done => {
628                    return None;
629                }
630            }
631        }
632    }
633}
634
635impl<'a, C: Constraint<'a>, P: Fn(&Binding) -> Option<R>, R> fmt::Debug for Query<C, P, R> {
636    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637        f.debug_struct("Query")
638            .field("constraint", &std::any::type_name::<C>())
639            .field("mode", &self.mode)
640            .field("binding", &self.binding)
641            .field("stack", &self.stack)
642            .field("unbound", &self.unbound)
643            .finish()
644    }
645}
646
647/// Iterate over query results, converting each variable via
648/// [`TryFromValue`](crate::value::TryFromValue).
649///
650/// The macro takes two arguments: a tuple of variables with optional type
651/// annotations, and a constraint expression. It injects a `__local_find_context!`
652/// macro that provides the variable context to nested query macros like
653/// [`pattern!`](crate::namespace), [`temp!`], and [`ignore!`].
654///
655/// # Variable syntax
656///
657/// | Syntax | Meaning |
658/// |--------|---------|
659/// | `name` | inferred type, filter on conversion failure |
660/// | `name: Type` | explicit type, filter on conversion failure |
661/// | `name?` | inferred type, yield `Result<T, E>` (no filter) |
662/// | `name: Type?` | explicit type, yield `Result<T, E>` (no filter) |
663///
664/// **Filter semantics (default):** when a variable's conversion fails the
665/// entire row is silently skipped — like a constraint that doesn't match.
666/// For types whose `TryFromValue::Error = Infallible` the error branch is
667/// dead code, so no rows can ever be accidentally filtered.
668///
669/// **`?` pass-through:** appending `?` to a variable makes it yield
670/// `Result<T, E>` directly. Both `Ok` and `Err` values pass through with
671/// no filtering, matching Rust's `?` semantics of "bubble the error to the
672/// caller."
673///
674/// # Examples
675///
676/// ```
677/// # use triblespace_core::prelude::*;
678/// # use triblespace_core::prelude::valueschemas::ShortString;
679/// // Filter semantics — rows where conversion fails are skipped:
680/// let results = find!((x: Value<ShortString>), x.is("foo".to_value())).collect::<Vec<_>>();
681/// ```
682#[macro_export]
683macro_rules! find {
684    ($($tokens:tt)*) => {
685        {
686            let mut ctx = $crate::query::VariableContext::new();
687
688            macro_rules! __local_find_context {
689                () => { &mut ctx }
690            }
691
692            $crate::macros::__find_impl!($crate, ctx, $($tokens)*)
693        }
694    };
695}
696/// Re-export of the [`find!`] macro.
697pub use find;
698
699#[macro_export]
700macro_rules! exists {
701    (($($vars:tt)*), $Constraint:expr) => {
702        $crate::query::find!(($($vars)*), $Constraint).next().is_some()
703    };
704}
705/// Re-export of the [`exists!`] macro.
706pub use exists;
707
708#[macro_export]
709macro_rules! temp {
710    (($Var:ident), $body:expr) => {{
711        let $Var = __local_find_context!().next_variable();
712        $body
713    }};
714    (($Var:ident,), $body:expr) => {
715        $crate::temp!(($Var), $body)
716    };
717    (($Var:ident, $($rest:ident),+ $(,)?), $body:expr) => {{
718        $crate::temp!(
719            ($Var),
720            $crate::temp!(($($rest),+), $body)
721        )
722    }};
723}
724/// Re-export of the [`temp!`] macro.
725pub use temp;
726
727#[cfg(test)]
728mod tests {
729    use valueschemas::ShortString;
730
731    use crate::ignore;
732    use crate::prelude::valueschemas::*;
733    use crate::prelude::*;
734
735    use crate::examples::literature;
736
737    use fake::faker::lorem::en::Sentence;
738    use fake::faker::lorem::en::Words;
739    use fake::faker::name::raw::*;
740    use fake::locales::*;
741    use fake::Fake;
742
743    use std::collections::HashSet;
744
745    use super::*;
746
747    pub mod knights {
748        use crate::prelude::*;
749
750        attributes! {
751            "8143F46E812E88C4544E7094080EC523" as loves: valueschemas::GenId;
752            "D6E0F2A6E5214E1330565B4D4138E55C" as name: valueschemas::ShortString;
753        }
754    }
755
756    mod social {
757        use crate::prelude::*;
758
759        attributes! {
760            "A19EC1D9DD534BA9896223A457A6B9C9" as name: valueschemas::ShortString;
761            "C21DE0AA5BA3446AB886C9640BA60244" as friend: valueschemas::GenId;
762        }
763    }
764
765    #[test]
766    fn and_set() {
767        let mut books = HashSet::<String>::new();
768        let mut movies = HashSet::<Value<ShortString>>::new();
769
770        books.insert("LOTR".to_string());
771        books.insert("Dragonrider".to_string());
772        books.insert("Highlander".to_string());
773
774        movies.insert("LOTR".to_value());
775        movies.insert("Highlander".to_value());
776
777        let inter: Vec<_> =
778            find!((a: Value<ShortString>), and!(books.has(a), movies.has(a))).collect();
779
780        assert_eq!(inter.len(), 2);
781
782        let cross: Vec<_> =
783            find!((a: Value<ShortString>, b: Value<ShortString>), and!(books.has(a), movies.has(b))).collect();
784
785        assert_eq!(cross.len(), 6);
786
787        let one: Vec<_> = find!((a: Value<ShortString>),
788            and!(books.has(a), a.is(ShortString::value_from("LOTR")))
789        )
790        .collect();
791
792        assert_eq!(one.len(), 1);
793    }
794
795    #[test]
796    fn pattern() {
797        let mut kb = TribleSet::new();
798        (0..1000).for_each(|_| {
799            let author = fucid();
800            let book = fucid();
801            kb += entity! { &author @
802               literature::firstname: FirstName(EN).fake::<String>(),
803               literature::lastname: LastName(EN).fake::<String>(),
804            };
805            kb += entity! { &book @
806               literature::author: &author,
807               literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
808               literature::quote: Sentence(5..25).fake::<String>().to_blob().get_handle()
809            };
810        });
811
812        let author = fucid();
813        let book = fucid();
814        kb += entity! { &author @
815           literature::firstname: "Frank",
816           literature::lastname: "Herbert",
817        };
818        kb += entity! { &book @
819           literature::author: &author,
820           literature::title: "Dune",
821           literature::quote: "I must not fear. Fear is the \
822                   mind-killer. Fear is the little-death that brings total \
823                   obliteration. I will face my fear. I will permit it to \
824                   pass over me and through me. And when it has gone past I \
825                   will turn the inner eye to see its path. Where the fear \
826                   has gone there will be nothing. Only I will remain.".to_blob().get_handle()
827        };
828
829        (0..100).for_each(|_| {
830            let author = fucid();
831            let book = fucid();
832            kb += entity! { &author @
833               literature::firstname: "Fake",
834               literature::lastname: "Herbert",
835            };
836            kb += entity! { &book @
837               literature::author: &author,
838               literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
839               literature::quote: Sentence(5..25).fake::<String>().to_blob().get_handle()
840            };
841        });
842
843        let r: Vec<_> = find!(
844        (author: Value<_>, book: Value<_>, title: Value<_>, quote: Value<_>),
845        pattern!(&kb, [
846        {?author @
847            literature::firstname: "Frank",
848            literature::lastname: "Herbert"},
849        {?book @
850          literature::author: ?author,
851          literature::title: ?title,
852          literature::quote: ?quote
853        }]))
854        .collect();
855
856        assert_eq!(1, r.len())
857    }
858
859    #[test]
860    fn constant() {
861        let r: Vec<_> = find! {
862            (string: Value<_>, number: Value<_>),
863            and!(
864                string.is(ShortString::value_from("Hello World!")),
865                number.is(I256BE::value_from(42))
866            )
867        }.collect();
868
869        assert_eq!(1, r.len())
870    }
871
872    #[test]
873    fn exists_true() {
874        assert!(exists!((a: Value<_>), a.is(I256BE::value_from(42))));
875    }
876
877    #[test]
878    fn exists_false() {
879        assert!(!exists!(
880            (a: Value<_>),
881            and!(a.is(I256BE::value_from(1)), a.is(I256BE::value_from(2)))
882        ));
883    }
884
885    #[test]
886    fn temp_variables_span_patterns() {
887        use social::*;
888
889        let mut kb = TribleSet::new();
890        let alice = fucid();
891        let bob = fucid();
892
893        kb += entity! { &alice @ name: "Alice", friend: &bob };
894        kb += entity! { &bob @ name: "Bob" };
895
896        let matches: Vec<_> = find!(
897            (person_name: Value<_>),
898            temp!((mutual_friend),
899                and!(
900                    pattern!(&kb, [{ _?person @ name: ?person_name, friend: ?mutual_friend }]),
901                    pattern!(&kb, [{ ?mutual_friend @ name: "Bob" }])
902                )
903            )
904        )
905        .collect();
906
907        assert_eq!(matches.len(), 1);
908        assert_eq!(matches[0].0.try_from_value::<&str>().unwrap(), "Alice");
909    }
910
911    #[test]
912    fn ignore_skips_variables() {
913        let results: Vec<_> = find!(
914            (x: Value<_>),
915            ignore!((y), and!(x.is(I256BE::value_from(1)), y.is(I256BE::value_from(2))))
916        )
917        .collect();
918
919        assert_eq!(results.len(), 1);
920        assert_eq!(results[0].0, I256BE::value_from(1));
921    }
922
923    #[test]
924    fn estimate_override_debug_order() {
925        use std::cell::RefCell;
926        use std::rc::Rc;
927
928        let mut ctx = VariableContext::new();
929        let a = ctx.next_variable::<ShortString>();
930        let b = ctx.next_variable::<ShortString>();
931
932        let base = and!(
933            a.is(ShortString::value_from("A")),
934            b.is(ShortString::value_from("B"))
935        );
936
937        let mut wrapper = crate::debug::query::EstimateOverrideConstraint::new(base);
938        wrapper.set_estimate(a.index, 10);
939        wrapper.set_estimate(b.index, 1);
940
941        let record = Rc::new(RefCell::new(Vec::new()));
942        let debug = crate::debug::query::DebugConstraint::new(wrapper, Rc::clone(&record));
943
944        let q: Query<_, _, _> = Query::new(debug, |_| Some(()));
945        let r: Vec<_> = q.collect();
946        assert_eq!(1, r.len());
947        assert_eq!(&*record.borrow(), &[b.index, a.index]);
948    }
949}