Skip to content

Commit 65a444d

Browse files
committed
refactor(nodedb-crdt): split validator monolith into focused submodules
The 792-line validator.rs was a growing catch-all. Split into: - core.rs — Validator struct, constructors, accessors - types.rs — ValidationOutcome, Violation, ProposedChange - validate.rs — core validation logic - policy_dispatch.rs — conflict-policy resolution - bitemporal.rs — bitemporal time-window helpers (valid-until sentinels, liveness predicate, window-overlap check) - tests.rs — unit tests (moved from inline) mod.rs re-exports the full public surface unchanged; no behavioral differences in this commit.
1 parent bd05275 commit 65a444d

8 files changed

Lines changed: 904 additions & 792 deletions

File tree

nodedb-crdt/src/validator.rs

Lines changed: 0 additions & 792 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Bitemporal-aware validation helpers.
2+
//!
3+
//! CRDT constraint validation for bitemporal collections must scope to the
4+
//! "current" version of each row — rows whose `_ts_valid_until` is open
5+
//! (MAX or unset). A UNIQUE collision between an old (superseded) version
6+
//! and a new live row is not a violation; two live rows with the same
7+
//! value IS a violation.
8+
9+
/// Reserved column name for the valid-time upper bound.
10+
pub const VALID_UNTIL: &str = "_ts_valid_until";
11+
12+
/// Reserved column name for the valid-time lower bound.
13+
pub const VALID_FROM: &str = "_ts_valid_from";
14+
15+
/// Reserved column name for the system-time stamp.
16+
pub const SYSTEM_TS: &str = "_ts_system";
17+
18+
/// Sentinel value representing "no upper bound" on valid-time.
19+
pub const VALID_UNTIL_OPEN: i64 = i64::MAX;
20+
21+
/// Is a row "currently live" at the given system time?
22+
///
23+
/// A row is live when `valid_until` is open (MAX) or strictly greater
24+
/// than the query time. Rows with `valid_until == now` are considered
25+
/// just-superseded and NOT live.
26+
pub fn is_live(valid_until: i64, now_ms: i64) -> bool {
27+
valid_until == VALID_UNTIL_OPEN || valid_until > now_ms
28+
}
29+
30+
/// Do two valid-time windows overlap?
31+
///
32+
/// Windows are half-open `[from, until)`. Two windows overlap iff each
33+
/// starts strictly before the other ends.
34+
pub fn windows_overlap(a_from: i64, a_until: i64, b_from: i64, b_until: i64) -> bool {
35+
a_from < b_until && b_from < a_until
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use super::*;
41+
42+
#[test]
43+
fn live_with_open_until() {
44+
assert!(is_live(VALID_UNTIL_OPEN, 1_000));
45+
}
46+
47+
#[test]
48+
fn superseded_row_not_live() {
49+
assert!(!is_live(500, 1_000));
50+
assert!(!is_live(1_000, 1_000));
51+
}
52+
53+
#[test]
54+
fn future_valid_until_is_live() {
55+
assert!(is_live(2_000, 1_000));
56+
}
57+
58+
#[test]
59+
fn overlap_disjoint() {
60+
assert!(!windows_overlap(0, 100, 100, 200));
61+
assert!(!windows_overlap(200, 300, 0, 100));
62+
}
63+
64+
#[test]
65+
fn overlap_shared_interval() {
66+
assert!(windows_overlap(0, 150, 100, 200));
67+
assert!(windows_overlap(100, 200, 0, 150));
68+
}
69+
70+
#[test]
71+
fn overlap_contained() {
72+
assert!(windows_overlap(0, 1000, 100, 200));
73+
assert!(windows_overlap(100, 200, 0, 1000));
74+
}
75+
}

nodedb-crdt/src/validator/core.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//! Validator struct, constructors, and accessors.
2+
3+
use std::collections::{HashMap, HashSet};
4+
5+
use crate::constraint::ConstraintSet;
6+
use crate::dead_letter::DeadLetterQueue;
7+
use crate::deferred::DeferredQueue;
8+
use crate::policy::PolicyRegistry;
9+
use crate::signing::DeltaSigner;
10+
11+
/// The constraint validator.
12+
///
13+
/// Validates proposed changes against a set of constraints and the current
14+
/// committed state. Violations are resolved via declarative policies:
15+
/// - AUTO_RESOLVED — policy handles it (e.g., LAST_WRITER_WINS)
16+
/// - DEFERRED — queued for exponential backoff retry (CASCADE_DEFER)
17+
/// - WEBHOOK_REQUIRED — caller must POST to webhook for decision
18+
/// - ESCALATE — route to dead-letter queue (fallback)
19+
pub struct Validator {
20+
pub(super) constraints: ConstraintSet,
21+
pub(super) dlq: DeadLetterQueue,
22+
pub(super) policies: PolicyRegistry,
23+
pub(super) deferred: DeferredQueue,
24+
/// Monotonic suffix counter: (collection, field) -> next suffix number
25+
pub(super) suffix_counter: HashMap<(String, String), u64>,
26+
/// Optional delta signature verifier. When set, signed deltas are
27+
/// verified before constraint validation.
28+
pub(super) delta_verifier: Option<DeltaSigner>,
29+
/// Collections known to be bitemporal. UNIQUE checks for rows in these
30+
/// collections scope to currently-live rows (open `_ts_valid_until`)
31+
/// so superseded versions don't falsely collide with live writes.
32+
pub(super) bitemporal_collections: HashSet<String>,
33+
}
34+
35+
impl Validator {
36+
/// Create a new validator with default (ephemeral) policies.
37+
pub fn new(constraints: ConstraintSet, dlq_capacity: usize) -> Self {
38+
Self::new_with_policies(constraints, dlq_capacity, PolicyRegistry::new(), 1000)
39+
}
40+
41+
/// Create a new validator with custom policies and deferred queue.
42+
pub fn new_with_policies(
43+
constraints: ConstraintSet,
44+
dlq_capacity: usize,
45+
policies: PolicyRegistry,
46+
deferred_capacity: usize,
47+
) -> Self {
48+
Self {
49+
constraints,
50+
dlq: DeadLetterQueue::new(dlq_capacity),
51+
policies,
52+
deferred: DeferredQueue::new(deferred_capacity),
53+
suffix_counter: HashMap::new(),
54+
delta_verifier: None,
55+
bitemporal_collections: HashSet::new(),
56+
}
57+
}
58+
59+
/// Register a collection as bitemporal. UNIQUE constraints for rows in
60+
/// this collection will scope to currently-live rows only.
61+
pub fn mark_bitemporal(&mut self, collection: impl Into<String>) {
62+
self.bitemporal_collections.insert(collection.into());
63+
}
64+
65+
/// Is the given collection registered as bitemporal?
66+
pub fn is_bitemporal(&self, collection: &str) -> bool {
67+
self.bitemporal_collections.contains(collection)
68+
}
69+
70+
/// Access the dead-letter queue.
71+
pub fn dlq(&self) -> &DeadLetterQueue {
72+
&self.dlq
73+
}
74+
75+
/// Mutable access to the DLQ (for dequeue/retry).
76+
pub fn dlq_mut(&mut self) -> &mut DeadLetterQueue {
77+
&mut self.dlq
78+
}
79+
80+
/// Access the policy registry.
81+
pub fn policies(&self) -> &PolicyRegistry {
82+
&self.policies
83+
}
84+
85+
/// Mutable access to the policy registry.
86+
pub fn policies_mut(&mut self) -> &mut PolicyRegistry {
87+
&mut self.policies
88+
}
89+
90+
/// Access the deferred queue.
91+
pub fn deferred(&self) -> &DeferredQueue {
92+
&self.deferred
93+
}
94+
95+
/// Mutable access to the deferred queue.
96+
pub fn deferred_mut(&mut self) -> &mut DeferredQueue {
97+
&mut self.deferred
98+
}
99+
100+
/// Set the delta signature verifier. When set, deltas with non-zero
101+
/// signatures in their CrdtAuthContext will be verified before validation.
102+
pub fn set_delta_verifier(&mut self, verifier: DeltaSigner) {
103+
self.delta_verifier = Some(verifier);
104+
}
105+
106+
/// Access the delta verifier.
107+
pub fn delta_verifier(&self) -> Option<&DeltaSigner> {
108+
self.delta_verifier.as_ref()
109+
}
110+
111+
/// Mutable access to the delta verifier.
112+
pub fn delta_verifier_mut(&mut self) -> Option<&mut DeltaSigner> {
113+
self.delta_verifier.as_mut()
114+
}
115+
}

nodedb-crdt/src/validator/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//! Constraint validator — checks deltas against committed state.
2+
//!
3+
//! This is the core of the CRDT/SQL bridge. When a delta arrives from a peer,
4+
//! the validator checks all applicable constraints against the leader's
5+
//! committed state. If any constraint is violated, the delta is rejected
6+
//! with a compensation hint.
7+
8+
pub mod bitemporal;
9+
mod core;
10+
mod policy_dispatch;
11+
mod types;
12+
mod validate;
13+
14+
#[cfg(test)]
15+
mod tests;
16+
17+
pub use core::Validator;
18+
pub use types::{ProposedChange, ValidationOutcome, Violation};

0 commit comments

Comments
 (0)