evidentsource_core/domain/
constraints.rs

1//! Append conditions for transactional consistency (DCB specification).
2//!
3//! This module provides types for expressing conditions on batch transactions,
4//! ensuring optimistic concurrency control following the Dynamic Consistency
5//! Boundary (DCB) specification.
6//!
7//! See: <https://dcb.events/specification/>
8
9use std::cmp::{max, min};
10
11use super::error::ConstraintError;
12use super::revision::{Range, Revision};
13use super::selectors::EventSelector;
14
15/// An append condition for a batch transaction (DCB spec: "Append Condition").
16///
17/// Append conditions ensure that events matching a selector have revision numbers
18/// within specified bounds at the time of transaction.
19///
20/// See: <https://dcb.events/specification/>
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub enum AppendCondition {
23    /// Events matching selector must have revision >= min.
24    Min(EventSelector, Revision),
25    /// Events matching selector must have revision <= max.
26    Max(EventSelector, Revision),
27    /// Events matching selector must have revision within range.
28    Range(EventSelector, Range),
29}
30
31impl AppendCondition {
32    /// Create a MIN condition (events must have revision >= min).
33    ///
34    /// Use this when you want to ensure events exist (min=1) or have been
35    /// updated to at least a certain revision.
36    pub fn min(selector: EventSelector, revision: Revision) -> Self {
37        AppendCondition::Min(selector, revision)
38    }
39
40    /// Create a MAX condition (events must have revision <= max).
41    ///
42    /// Use this when you want to ensure events don't exist yet (max=0) or
43    /// haven't changed beyond a known revision (optimistic concurrency).
44    pub fn max(selector: EventSelector, revision: Revision) -> Self {
45        AppendCondition::Max(selector, revision)
46    }
47
48    /// Create a RANGE condition (events must have revision within range).
49    pub fn range(selector: EventSelector, range: Range) -> Self {
50        AppendCondition::Range(selector, range)
51    }
52
53    /// Create a condition that fails if any events match the selector.
54    ///
55    /// This is the core DCB pattern for ensuring uniqueness. It's equivalent
56    /// to `max(selector, 0)`.
57    ///
58    /// See: <https://dcb.events/specification/>
59    ///
60    /// # Example
61    ///
62    /// ```
63    /// use evidentsource_core::domain::{AppendCondition, EventSelector};
64    ///
65    /// let selector = EventSelector::stream_equals("my-stream").unwrap();
66    /// let condition = AppendCondition::fail_if_events_match(selector);
67    /// ```
68    pub fn fail_if_events_match(selector: EventSelector) -> Self {
69        AppendCondition::Max(selector, 0)
70    }
71
72    /// Require that no events matching the selector exist.
73    ///
74    /// This is a semantic alias for `max(selector, 0)`. Use this when creating
75    /// new entities to ensure uniqueness.
76    ///
77    /// # Example
78    ///
79    /// ```
80    /// use evidentsource_core::domain::{AppendCondition, EventSelector};
81    ///
82    /// let selector = EventSelector::stream_equals("my-stream").unwrap();
83    /// let condition = AppendCondition::must_not_exist(selector);
84    /// ```
85    pub fn must_not_exist(selector: EventSelector) -> Self {
86        AppendCondition::Max(selector, 0)
87    }
88
89    /// Require that at least one event matching the selector exists.
90    ///
91    /// This is a semantic alias for `min(selector, 1)`. Use this when updating
92    /// existing entities to ensure they exist.
93    ///
94    /// # Example
95    ///
96    /// ```
97    /// use evidentsource_core::domain::{AppendCondition, EventSelector};
98    ///
99    /// let selector = EventSelector::stream_equals("my-stream").unwrap();
100    /// let condition = AppendCondition::must_exist(selector);
101    /// ```
102    pub fn must_exist(selector: EventSelector) -> Self {
103        AppendCondition::Min(selector, 1)
104    }
105
106    /// Require events matching the selector to be exactly at a specific revision.
107    ///
108    /// Use this for strict revision checks where you need to ensure the exact
109    /// state hasn't changed.
110    ///
111    /// # Errors
112    ///
113    /// Returns `ConstraintError::InvalidRange` if the revision is invalid for
114    /// creating a range (this should never happen in practice).
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// use evidentsource_core::domain::{AppendCondition, EventSelector};
120    ///
121    /// let selector = EventSelector::stream_equals("my-stream").unwrap();
122    /// let condition = AppendCondition::at_revision(selector, 42).unwrap();
123    /// ```
124    pub fn at_revision(
125        selector: EventSelector,
126        revision: Revision,
127    ) -> Result<Self, ConstraintError> {
128        Range::new(revision, revision)
129            .map(|range| AppendCondition::Range(selector, range))
130            .map_err(|_| ConstraintError::InvalidRange {
131                min: revision,
132                max: revision,
133            })
134    }
135
136    /// Require events matching the selector haven't changed since a known revision.
137    ///
138    /// This is a semantic alias for `max(selector, revision)`. Use this for
139    /// optimistic concurrency control where you want to ensure no updates
140    /// have occurred since you last read the data.
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use evidentsource_core::domain::{AppendCondition, EventSelector};
146    ///
147    /// let selector = EventSelector::stream_equals("my-stream").unwrap();
148    /// // Ensure the stream hasn't been modified since revision 42
149    /// let condition = AppendCondition::unchanged_since(selector, 42);
150    /// ```
151    pub fn unchanged_since(selector: EventSelector, last_seen_revision: Revision) -> Self {
152        AppendCondition::Max(selector, last_seen_revision)
153    }
154
155    /// Get the selector for this condition.
156    pub fn selector(&self) -> &EventSelector {
157        match self {
158            AppendCondition::Min(selector, _)
159            | AppendCondition::Max(selector, _)
160            | AppendCondition::Range(selector, _) => selector,
161        }
162    }
163
164    /// Get the minimum revision bound, if any.
165    pub fn min_revision(&self) -> Option<Revision> {
166        match self {
167            AppendCondition::Min(_, min) => Some(*min),
168            AppendCondition::Range(_, range) => Some(range.min()),
169            AppendCondition::Max(_, _) => None,
170        }
171    }
172
173    /// Get the maximum revision bound, if any.
174    pub fn max_revision(&self) -> Option<Revision> {
175        match self {
176            AppendCondition::Max(_, max) => Some(*max),
177            AppendCondition::Range(_, range) => Some(range.max()),
178            AppendCondition::Min(_, _) => None,
179        }
180    }
181
182    /// Combine two conditions into one.
183    ///
184    /// Both conditions must have the same selector. The resulting condition
185    /// is the union of the bounds (widest range that satisfies both).
186    ///
187    /// # Errors
188    ///
189    /// Returns `ConstraintError::IncompatibleSelectors` if the conditions have
190    /// different selectors.
191    ///
192    /// Returns `ConstraintError::Conflict` if the combined bounds are invalid
193    /// (e.g., min > max).
194    pub fn combine(&self, rhs: &Self) -> Result<Self, ConstraintError> {
195        let lselector = self.selector();
196        let rselector = rhs.selector();
197
198        if lselector != rselector {
199            return Err(ConstraintError::IncompatibleSelectors);
200        }
201
202        let selector = lselector.clone();
203
204        match (self, rhs) {
205            // Min + Min -> take the smaller min (widens the range)
206            (AppendCondition::Min(_, lmin), AppendCondition::Min(_, rmin)) => {
207                Ok(AppendCondition::Min(selector, min(*lmin, *rmin)))
208            }
209
210            // Max + Max -> take the larger max (widens the range)
211            (AppendCondition::Max(_, lmax), AppendCondition::Max(_, rmax)) => {
212                Ok(AppendCondition::Max(selector, max(*lmax, *rmax)))
213            }
214
215            // Min + Max -> create range
216            (AppendCondition::Min(_, min_val), AppendCondition::Max(_, max_val))
217            | (AppendCondition::Max(_, max_val), AppendCondition::Min(_, min_val)) => {
218                Range::new(*min_val, *max_val)
219                    .map(|range| AppendCondition::Range(selector, range))
220                    .map_err(|_| ConstraintError::Conflict {
221                        left: format!("{:?}", self),
222                        right: format!("{:?}", rhs),
223                    })
224            }
225
226            // Range + Min -> extend range min
227            (AppendCondition::Range(_, range), AppendCondition::Min(_, rmin))
228            | (AppendCondition::Min(_, rmin), AppendCondition::Range(_, range)) => {
229                Range::new(min(range.min(), *rmin), range.max())
230                    .map(|new_range| AppendCondition::Range(selector, new_range))
231                    .map_err(|_| ConstraintError::Conflict {
232                        left: format!("{:?}", self),
233                        right: format!("{:?}", rhs),
234                    })
235            }
236
237            // Range + Max -> extend range max
238            (AppendCondition::Range(_, range), AppendCondition::Max(_, rmax))
239            | (AppendCondition::Max(_, rmax), AppendCondition::Range(_, range)) => {
240                Range::new(range.min(), max(range.max(), *rmax))
241                    .map(|new_range| AppendCondition::Range(selector, new_range))
242                    .map_err(|_| ConstraintError::Conflict {
243                        left: format!("{:?}", self),
244                        right: format!("{:?}", rhs),
245                    })
246            }
247
248            // Range + Range -> union of ranges
249            (AppendCondition::Range(_, lrange), AppendCondition::Range(_, rrange)) => Range::new(
250                min(lrange.min(), rrange.min()),
251                max(lrange.max(), rrange.max()),
252            )
253            .map(|new_range| AppendCondition::Range(selector, new_range))
254            .map_err(|_| ConstraintError::Conflict {
255                left: format!("{:?}", self),
256                right: format!("{:?}", rhs),
257            }),
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    fn make_selector(name: &str) -> EventSelector {
267        EventSelector::event_type_equals(name).unwrap()
268    }
269
270    #[test]
271    fn test_convenience_constructors() {
272        let selector = make_selector("test");
273
274        let min_c = AppendCondition::min(selector.clone(), 5);
275        assert!(matches!(min_c, AppendCondition::Min(_, 5)));
276
277        let max_c = AppendCondition::max(selector.clone(), 10);
278        assert!(matches!(max_c, AppendCondition::Max(_, 10)));
279
280        let range = Range::new(5, 10).unwrap();
281        let range_c = AppendCondition::range(selector.clone(), range);
282        assert!(matches!(range_c, AppendCondition::Range(_, _)));
283    }
284
285    #[test]
286    fn test_combine_min_min() {
287        let selector = make_selector("test");
288        let c1 = AppendCondition::min(selector.clone(), 10);
289        let c2 = AppendCondition::min(selector.clone(), 5);
290
291        let combined = c1.combine(&c2).unwrap();
292        assert!(matches!(combined, AppendCondition::Min(_, 5)));
293    }
294
295    #[test]
296    fn test_combine_max_max() {
297        let selector = make_selector("test");
298        let c1 = AppendCondition::max(selector.clone(), 10);
299        let c2 = AppendCondition::max(selector.clone(), 15);
300
301        let combined = c1.combine(&c2).unwrap();
302        assert!(matches!(combined, AppendCondition::Max(_, 15)));
303    }
304
305    #[test]
306    fn test_combine_min_max_valid() {
307        let selector = make_selector("test");
308        let c1 = AppendCondition::min(selector.clone(), 5);
309        let c2 = AppendCondition::max(selector.clone(), 10);
310
311        let combined = c1.combine(&c2).unwrap();
312        match combined {
313            AppendCondition::Range(_, range) => {
314                assert_eq!(range.min(), 5);
315                assert_eq!(range.max(), 10);
316            }
317            _ => panic!("Expected Range"),
318        }
319    }
320
321    #[test]
322    fn test_combine_min_max_conflict() {
323        let selector = make_selector("test");
324        let c1 = AppendCondition::min(selector.clone(), 10);
325        let c2 = AppendCondition::max(selector.clone(), 5);
326
327        let result = c1.combine(&c2);
328        assert!(matches!(result, Err(ConstraintError::Conflict { .. })));
329    }
330
331    #[test]
332    fn test_combine_different_selectors() {
333        let selector1 = make_selector("type1");
334        let selector2 = make_selector("type2");
335        let c1 = AppendCondition::min(selector1, 5);
336        let c2 = AppendCondition::min(selector2, 5);
337
338        let result = c1.combine(&c2);
339        assert!(matches!(
340            result,
341            Err(ConstraintError::IncompatibleSelectors)
342        ));
343    }
344
345    #[test]
346    fn test_combine_range_min() {
347        let selector = make_selector("test");
348        let range = Range::new(5, 10).unwrap();
349        let c1 = AppendCondition::range(selector.clone(), range);
350        let c2 = AppendCondition::min(selector.clone(), 3);
351
352        let combined = c1.combine(&c2).unwrap();
353        match combined {
354            AppendCondition::Range(_, range) => {
355                assert_eq!(range.min(), 3);
356                assert_eq!(range.max(), 10);
357            }
358            _ => panic!("Expected Range"),
359        }
360    }
361
362    #[test]
363    fn test_selector_accessor() {
364        let selector = make_selector("test");
365        let condition = AppendCondition::min(selector.clone(), 5);
366        assert_eq!(condition.selector(), &selector);
367    }
368
369    #[test]
370    fn test_fail_if_events_match() {
371        let selector = make_selector("test");
372        let condition = AppendCondition::fail_if_events_match(selector.clone());
373
374        // fail_if_events_match is equivalent to max(selector, 0)
375        assert!(matches!(condition, AppendCondition::Max(_, 0)));
376        assert_eq!(condition.max_revision(), Some(0));
377    }
378
379    #[test]
380    fn test_must_not_exist() {
381        let selector = make_selector("test");
382        let condition = AppendCondition::must_not_exist(selector.clone());
383
384        // must_not_exist is equivalent to max(selector, 0)
385        assert!(matches!(condition, AppendCondition::Max(_, 0)));
386        assert_eq!(condition.max_revision(), Some(0));
387    }
388
389    #[test]
390    fn test_must_exist() {
391        let selector = make_selector("test");
392        let condition = AppendCondition::must_exist(selector.clone());
393
394        // must_exist is equivalent to min(selector, 1)
395        assert!(matches!(condition, AppendCondition::Min(_, 1)));
396        assert_eq!(condition.min_revision(), Some(1));
397    }
398
399    #[test]
400    fn test_at_revision() {
401        let selector = make_selector("test");
402        let condition = AppendCondition::at_revision(selector.clone(), 42).unwrap();
403
404        // at_revision creates a range [rev, rev]
405        match condition {
406            AppendCondition::Range(_, range) => {
407                assert_eq!(range.min(), 42);
408                assert_eq!(range.max(), 42);
409            }
410            _ => panic!("Expected Range"),
411        }
412    }
413
414    #[test]
415    fn test_unchanged_since() {
416        let selector = make_selector("test");
417        let condition = AppendCondition::unchanged_since(selector.clone(), 100);
418
419        // unchanged_since is equivalent to max(selector, revision)
420        assert!(matches!(condition, AppendCondition::Max(_, 100)));
421        assert_eq!(condition.max_revision(), Some(100));
422    }
423}