Skip to main content

tephra_types/
query.rs

1//! Query model.
2//!
3//! Pure data, no I/O and no match logic (the match predicate lives in the engine, where
4//! it decodes an event). A [`Query`] describes which events a decision depends on, and the
5//! same query goes into the [`AppendCondition`] that guards the append. That reuse is what
6//! makes the consistency boundary dynamic: it covers exactly the events the decision read,
7//! nothing more.
8//!
9//! # Semantics
10//!
11//! A query is a set of [`QueryItem`]s OR'd together, plus a separate [`Query::All`]
12//! variant that matches everything. Within an item:
13//!
14//! - the event **type** must match *one* of the listed types (an empty type list
15//!   matches *any* type), and
16//! - the event **tags** must contain *all* of the item's tags.
17//!
18//! So the shape is **OR across items, AND within an item's tags**. An item with no
19//! tags constrains only on type; an empty [`Query::Items`] set matches nothing (an OR
20//! over zero alternatives).
21
22use crate::name::{EventType, Tags};
23use crate::position::Position;
24
25/// One alternative in a [`Query`]: a type constraint AND a tag constraint.
26///
27/// An event matches the item when its type is one of [`types`](Self::types) (or the
28/// list is empty, matching any type) and its tags are a superset of
29/// [`tags`](Self::tags).
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
31pub struct QueryItem {
32    /// The types this item accepts. Empty means "any type". Not sorted: the list is
33    /// tiny (types are low cardinality) so membership is a linear scan.
34    pub types: Vec<EventType>,
35    /// Tags the event must contain *all* of. Sorted and duplicate-free by construction.
36    pub tags: Tags,
37}
38
39impl QueryItem {
40    /// An item constraining on both a set of types and a set of tags.
41    pub fn new(types: Vec<EventType>, tags: Tags) -> Self {
42        QueryItem { types, tags }
43    }
44
45    /// An item constraining only on type (matches any tags).
46    pub fn of_types(types: Vec<EventType>) -> Self {
47        QueryItem {
48            types,
49            tags: Tags::empty(),
50        }
51    }
52
53    /// An item constraining only on tags (matches any type).
54    pub fn with_tags(tags: Tags) -> Self {
55        QueryItem {
56            types: Vec::new(),
57            tags,
58        }
59    }
60}
61
62/// A query: a set of [`QueryItem`]s OR'd together, or the catch-all [`Query::All`].
63///
64/// [`All`](Query::All) is a distinct variant rather than "an item with no
65/// constraints" so the read and condition paths can recognise a full scan and bypass
66/// the index entirely. An empty [`Items`](Query::Items) set is the opposite: it matches
67/// nothing.
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum Query {
70    /// Matches every event. Full scans and broad projection catch-up use this and skip
71    /// the index.
72    All,
73    /// Matches an event if *any* contained item matches (logical OR). Empty matches
74    /// nothing.
75    Items(Vec<QueryItem>),
76}
77
78impl Query {
79    /// The catch-all query, matching every event.
80    pub fn all() -> Self {
81        Query::All
82    }
83
84    /// A query over a set of items, OR'd together.
85    pub fn items(items: impl Into<Vec<QueryItem>>) -> Self {
86        Query::Items(items.into())
87    }
88
89    /// A query with a single item.
90    pub fn item(item: QueryItem) -> Self {
91        Query::Items(vec![item])
92    }
93}
94
95/// The guard on an `append` call.
96///
97/// The store ignores everything at or before [`after`](Self::after) and rejects the
98/// append if anything matching [`fail_if_events_match`](Self::fail_if_events_match)
99/// landed since. `after` is the highest position the client observed while building
100/// its decision model, which may be higher than the last matching event's position.
101///
102/// Positions are 1-based, so [`after`](Self::after) `= Position::ZERO` (the default)
103/// means "consider the whole log": the spec's "omit `after`" case, i.e. fail if *any*
104/// event matches (the uniqueness-guard pattern).
105///
106/// This type only *holds* the condition. Evaluating it (position filtering plus the
107/// match predicate over the durable suffix) is the engine's job.
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct AppendCondition {
110    /// Reject the append if any event after [`after`](Self::after) matches this query.
111    pub fail_if_events_match: Query,
112    /// Ignore everything at or before this position. `Position::ZERO` means the whole
113    /// log.
114    pub after: Position,
115}
116
117impl AppendCondition {
118    /// A condition checking the whole log (`after = Position::ZERO`): fail if any event
119    /// matches.
120    pub fn new(fail_if_events_match: Query) -> Self {
121        AppendCondition {
122            fail_if_events_match,
123            after: Position::ZERO,
124        }
125    }
126
127    /// Sets the exclusive lower bound: only events strictly after `after` are checked.
128    pub fn after(mut self, after: Position) -> Self {
129        self.after = after;
130        self
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn item_constructors() {
140        assert!(QueryItem::of_types(Vec::new()).tags.is_empty());
141        assert!(QueryItem::with_tags(Tags::empty()).types.is_empty());
142        assert_eq!(
143            QueryItem::default(),
144            QueryItem::new(Vec::new(), Tags::empty())
145        );
146    }
147
148    #[test]
149    fn query_constructors() {
150        assert_eq!(Query::all(), Query::All);
151        assert_eq!(Query::items(Vec::new()), Query::Items(Vec::new()));
152        assert_eq!(
153            Query::item(QueryItem::default()),
154            Query::Items(vec![QueryItem::default()])
155        );
156    }
157
158    #[test]
159    fn condition_defaults_to_position_zero() {
160        // Omitting `after` means "check the whole log", which under 1-based positions
161        // is `after: 0`.
162        let cond = AppendCondition::new(Query::all());
163        assert_eq!(cond.after, Position::ZERO);
164        assert_eq!(cond.fail_if_events_match, Query::All);
165    }
166
167    #[test]
168    fn condition_after_sets_bound() {
169        let cond = AppendCondition::new(Query::all()).after(Position::new(42));
170        assert_eq!(cond.after, Position::new(42));
171    }
172}