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}