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