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