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