1use std::{
2 cmp::Ordering as StdOrdering,
3 collections::{BTreeMap, BTreeSet, HashMap, hash_map::Entry},
4 convert::TryInto,
5 fmt, fs, io,
6 path::{Path, PathBuf},
7 str::{self, FromStr},
8 thread,
9 time::{Duration, Instant},
10};
11
12use chrono::{DateTime, Utc};
13use parking_lot::{Mutex, MutexGuard};
14use rocksdb::{
15 DBIterator, DBWithThreadMode, Direction, IteratorMode, MultiThreaded, Options, WriteBatch,
16};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19
20use json_patch::Patch;
21use metrics::{counter, histogram};
22use tracing::warn;
23
24#[cfg(unix)]
25use std::os::fd::AsRawFd;
26
27use super::{
28 encryption::{self, Encryptor},
29 error::{EventError, Result},
30 merkle::{compute_merkle_root, empty_root},
31 schema::MAX_EVENT_NOTE_LENGTH,
32 validation::MAX_EVENT_TYPE_LENGTH,
33};
34use crate::{
35 filter::FilterExpr,
36 snowflake::{MAX_WORKER_ID, SnowflakeGenerator, SnowflakeId},
37};
38
39const SEP: u8 = 0x1F;
40const PREFIX_EVENT: &str = "evt";
41const PREFIX_EVENT_ARCHIVED: &str = "evt-arch";
42const PREFIX_META: &str = "meta";
43const PREFIX_STATE: &str = "state";
44const PREFIX_META_ARCHIVED: &str = "meta-arch";
45const PREFIX_STATE_ARCHIVED: &str = "state-arch";
46const PREFIX_SNAPSHOT: &str = "snapshot";
47const PREFIX_SNAPSHOT_ID: &str = "snapidx";
48const PREFIX_META_CREATED_INDEX: &str = "meta-created";
49const PREFIX_META_UPDATED_INDEX: &str = "meta-updated";
50const PREFIX_META_CREATED_INDEX_ARCHIVED: &str = "meta-created-arch";
51const PREFIX_META_UPDATED_INDEX_ARCHIVED: &str = "meta-updated-arch";
52const PREFIX_REF_SOURCE: &str = "refsrc";
53const PREFIX_REF_TARGET: &str = "reftgt";
54const LOCK_RETRY_ATTEMPTS: usize = 5;
55const LOCK_RETRY_BACKOFF: Duration = Duration::from_millis(100);
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct EventRecord {
59 pub aggregate_type: String,
60 pub aggregate_id: String,
61 pub event_type: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub event_type_raw: Option<String>,
64 pub payload: Value,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub extensions: Option<Value>,
67 pub metadata: EventMetadata,
68 pub version: u64,
69 pub hash: String,
70 pub merkle_root: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct EventMetadata {
75 pub event_id: SnowflakeId,
76 pub created_at: DateTime<Utc>,
77 pub issued_by: Option<ActorClaims>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub note: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ActorClaims {
84 pub group: String,
85 pub user: String,
86}
87
88fn ensure_event_type_lengths(event_type: &str, event_type_raw: Option<&String>) -> Result<()> {
89 if event_type.len() > MAX_EVENT_TYPE_LENGTH {
90 return Err(EventError::InvalidSchema(format!(
91 "event_type cannot exceed {} characters",
92 MAX_EVENT_TYPE_LENGTH
93 )));
94 }
95 if let Some(raw) = event_type_raw {
96 if raw.len() > MAX_EVENT_TYPE_LENGTH {
97 return Err(EventError::InvalidSchema(format!(
98 "event_type_raw cannot exceed {} characters",
99 MAX_EVENT_TYPE_LENGTH
100 )));
101 }
102 }
103 Ok(())
104}
105
106impl EventRecord {
107 pub fn to_output_value(&self) -> serde_json::Result<Value> {
110 let mut value = serde_json::to_value(self)?;
111 let Some(object) = value.as_object_mut() else {
112 return Ok(value);
113 };
114
115 object.remove("event_type_raw");
116
117 if let Some(extensions) = object.remove("extensions") {
118 let metadata_entry = object
119 .entry("metadata")
120 .or_insert_with(|| Value::Object(serde_json::Map::new()));
121 if let Some(meta) = metadata_entry.as_object_mut() {
122 if let Some(ext_map) = extensions.as_object() {
123 for (key, value) in ext_map {
124 meta.insert(key.clone(), value.clone());
125 }
126 } else {
127 meta.insert("@extensions".to_string(), extensions);
128 }
129 } else if let Some(ext_map) = extensions.as_object() {
130 object.insert("metadata".into(), Value::Object(ext_map.clone()));
131 } else {
132 let mut meta = serde_json::Map::new();
133 meta.insert("@extensions".into(), extensions);
134 object.insert("metadata".into(), Value::Object(meta));
135 }
136 }
137
138 Ok(value)
139 }
140}
141
142fn normalize_extension_key(key: &str) -> String {
143 let mut result = String::new();
144 let mut chars = key.chars().peekable();
145 if let Some(first) = chars.next() {
146 if first == '@' {
147 result.push('@');
148 } else {
149 result.push(first.to_ascii_lowercase());
150 }
151 }
152
153 let mut last_char = result.chars().last().unwrap_or('@');
154 for ch in chars {
155 if ch.is_ascii_uppercase() && last_char != '@' && last_char != '_' {
156 result.push('_');
157 }
158 result.push(
159 if ch.is_ascii_alphabetic() {
160 ch.to_ascii_lowercase()
161 } else {
162 ch
163 },
164 );
165 last_char = ch;
166 }
167
168 result
169}
170
171impl<'a> Transaction<'a> {
172 pub fn append(&mut self, input: AppendEvent) -> Result<EventRecord> {
173 if let Some(note) = input.note.as_ref() {
174 if note.chars().count() > MAX_EVENT_NOTE_LENGTH {
175 return Err(EventError::InvalidSchema(format!(
176 "event note cannot exceed {} characters",
177 MAX_EVENT_NOTE_LENGTH
178 )));
179 }
180 }
181 ensure_event_type_lengths(&input.event_type, input.event_type_raw.as_ref())?;
182 let tenant = input.tenant.clone();
183 let references = input.reference_targets.clone();
184 let key = AggregateKey::new(input.aggregate_type.clone(), input.aggregate_id.clone());
185 let event_id = self.store.next_event_id();
186 let (meta, state) = self.ensure_cache(&key)?;
187 if meta.archived {
188 return Err(EventError::AggregateArchived);
189 }
190 let record = apply_event(meta, state, input, event_id);
191 let stored_record = self.store.encode_record(&record)?;
192 let event_value = serde_json::to_vec(&stored_record)?;
193
194 self.pending_events.push(PendingEvent {
195 key: event_key(&record.aggregate_type, &record.aggregate_id, record.version),
196 value: event_value,
197 });
198 self.pending_records.push(record.clone());
199
200 self.pending_refs.push(PendingRefUpdate {
201 tenant,
202 aggregate_type: record.aggregate_type.clone(),
203 aggregate_id: record.aggregate_id.clone(),
204 references,
205 });
206 Ok(record)
207 }
208
209 pub fn commit(mut self) -> Result<Vec<EventRecord>> {
210 if self.pending_events.is_empty()
211 && self.meta_cache.is_empty()
212 && self.state_cache.is_empty()
213 {
214 return Ok(Vec::new());
215 }
216
217 let pending_events = std::mem::take(&mut self.pending_events);
218 let meta_cache = std::mem::take(&mut self.meta_cache);
219 let state_cache = std::mem::take(&mut self.state_cache);
220 let records = std::mem::take(&mut self.pending_records);
221
222 let mut batch = WriteBatch::default();
223
224 for pending in pending_events {
225 batch_put(&mut batch, pending.key, pending.value)?;
226 }
227
228 for meta in meta_cache.into_values() {
229 let previous = self
230 .store
231 .load_meta(&meta.aggregate_type, &meta.aggregate_id)?;
232 self.store.sync_timestamp_indexes(
233 &mut batch,
234 AggregateIndex::Active,
235 previous.as_ref(),
236 Some(&meta),
237 );
238 let key = meta_key(&meta.aggregate_type, &meta.aggregate_id);
239 batch_put(&mut batch, key, serde_json::to_vec(&meta)?)?;
240 }
241
242 for (key, state) in state_cache {
243 let (aggregate_type, aggregate_id) = key.parts();
244 let state_bytes = self.store.encode_state_map(&state)?;
245 batch_put(
246 &mut batch,
247 state_key(aggregate_type, aggregate_id),
248 state_bytes,
249 )?;
250 }
251
252 for pending in self.pending_refs {
253 update_reference_index_inner(
254 &self.store.db,
255 &mut batch,
256 &pending.tenant,
257 &pending.aggregate_type,
258 &pending.aggregate_id,
259 &pending.references,
260 )?;
261 }
262
263 self.store.write_batch(batch)?;
264 Ok(records)
265 }
266
267 fn ensure_cache(
268 &mut self,
269 key: &AggregateKey,
270 ) -> Result<(&mut AggregateMeta, &mut BTreeMap<String, String>)> {
271 if !self.meta_cache.contains_key(key) {
272 let (aggregate_type, aggregate_id) = key.parts();
273 let meta = self
274 .store
275 .load_meta(aggregate_type, aggregate_id)?
276 .unwrap_or_else(|| {
277 AggregateMeta::new(aggregate_type.to_string(), aggregate_id.to_string())
278 });
279 self.meta_cache.insert(key.clone(), meta);
280 }
281
282 if !self.state_cache.contains_key(key) {
283 let (aggregate_type, aggregate_id) = key.parts();
284 let state = self.store.load_state_map(aggregate_type, aggregate_id)?;
285 self.state_cache.insert(key.clone(), state);
286 }
287
288 let meta = self
289 .meta_cache
290 .get_mut(key)
291 .expect("meta cache missing after insertion");
292 let state = self
293 .state_cache
294 .get_mut(key)
295 .expect("state cache missing after insertion");
296 Ok((meta, state))
297 }
298}
299
300fn metadata_to_extensions_map(metadata: &Value) -> BTreeMap<String, Value> {
301 match metadata.as_object() {
302 Some(map) => map
303 .iter()
304 .map(|(key, value)| (normalize_extension_key(key), value.clone()))
305 .collect(),
306 None => {
307 let mut fallback = BTreeMap::new();
308 fallback.insert("@extensions".to_string(), metadata.clone());
309 fallback
310 }
311 }
312}
313
314fn apply_event(
315 meta: &mut AggregateMeta,
316 state: &mut BTreeMap<String, String>,
317 input: AppendEvent,
318 event_id: SnowflakeId,
319) -> EventRecord {
320 let AppendEvent {
321 aggregate_type,
322 aggregate_id,
323 event_type,
324 event_type_raw,
325 payload,
326 metadata,
327 issued_by,
328 note,
329 ..
330 } = input;
331
332 debug_assert_eq!(&meta.aggregate_type, &aggregate_type);
333 debug_assert_eq!(&meta.aggregate_id, &aggregate_id);
334
335 let event_time = Utc::now();
336 let version = meta.version + 1;
337 meta.record_write(event_time);
338 let payload_map = payload_to_map(&payload);
339 let hash = hash_event(
340 &aggregate_type,
341 &aggregate_id,
342 version,
343 &event_type,
344 &payload_map,
345 );
346
347 meta.event_hashes.push(hash.clone());
348 meta.version = version;
349 meta.merkle_root = compute_merkle_root(&meta.event_hashes);
350
351 for (key, value) in payload_map {
352 state.insert(key, value);
353 }
354
355 if let Some(metadata_value) = metadata.as_ref() {
356 let incoming = metadata_to_extensions_map(metadata_value);
357 for (key, value) in incoming {
358 meta.extensions.insert(key, value);
359 }
360 }
361
362 EventRecord {
363 aggregate_type,
364 aggregate_id,
365 event_type,
366 event_type_raw,
367 payload,
368 extensions: metadata,
369 metadata: EventMetadata {
370 event_id,
371 created_at: event_time,
372 issued_by,
373 note,
374 },
375 version,
376 hash,
377 merkle_root: meta.merkle_root.clone(),
378 }
379}
380
381fn batch_put(batch: &mut WriteBatch, key: Vec<u8>, value: Vec<u8>) -> Result<()> {
382 batch.put(key, value);
383 Ok(())
384}
385
386fn decode_pointer_segment(segment: &str) -> String {
387 let mut result = String::with_capacity(segment.len());
388 let mut chars = segment.chars();
389 while let Some(ch) = chars.next() {
390 if ch == '~' {
391 if let Some(next) = chars.next() {
392 match next {
393 '0' => result.push('~'),
394 '1' => result.push('/'),
395 other => {
396 result.push('~');
397 result.push(other);
398 }
399 }
400 } else {
401 result.push('~');
402 }
403 } else {
404 result.push(ch);
405 }
406 }
407 result
408}
409
410fn top_level_key_from_path(path: &str) -> std::result::Result<String, String> {
411 if !path.starts_with('/') {
412 return Err(format!("patch path '{}' must start with '/'", path));
413 }
414 let trimmed = &path[1..];
415 if trimmed.is_empty() {
416 return Err("patch operations targeting the document root are not supported".into());
417 }
418 let segment = trimmed
419 .split('/')
420 .next()
421 .expect("split returns at least one segment");
422 Ok(decode_pointer_segment(segment))
423}
424
425fn collect_top_level_keys(patch: &Value) -> std::result::Result<BTreeSet<String>, String> {
426 let array = patch
427 .as_array()
428 .ok_or_else(|| "patch must be an array".to_string())?;
429 let mut keys = BTreeSet::new();
430 for entry in array {
431 let op = entry
432 .get("op")
433 .and_then(Value::as_str)
434 .ok_or_else(|| "patch entry is missing 'op'".to_string())?;
435 if op != "add" && op != "replace" && op != "remove" {
436 return Err(format!("unsupported patch operation '{}'", op));
437 }
438 let path = entry
439 .get("path")
440 .and_then(Value::as_str)
441 .ok_or_else(|| "patch entry is missing 'path'".to_string())?;
442 let key = top_level_key_from_path(path)?;
443 keys.insert(key);
444 }
445 Ok(keys)
446}
447
448fn parse_state_value(raw: &str) -> Value {
449 if let Ok(parsed) = serde_json::from_str(raw) {
450 return parsed;
451 }
452 Value::String(raw.to_string())
453}
454
455fn state_map_to_value(map: &BTreeMap<String, String>) -> Value {
456 let mut object = serde_json::Map::new();
457 for (key, value) in map {
458 object.insert(key.clone(), parse_state_value(value));
459 }
460 Value::Object(object)
461}
462
463pub fn select_state_field(map: &BTreeMap<String, String>, path: &str) -> Option<Value> {
464 if path.is_empty() {
465 return None;
466 }
467
468 let mut segments = path.split('.');
469 let first = segments.next()?;
470 if first.is_empty() {
471 return None;
472 }
473
474 let mut current = match map.get(first) {
475 Some(raw) => parse_state_value(raw),
476 None => return None,
477 };
478
479 for segment in segments {
480 if segment.is_empty() {
481 return None;
482 }
483 current = match ¤t {
484 Value::Object(object) => object.get(segment)?.clone(),
485 Value::Array(array) => {
486 let index = segment.parse::<usize>().ok()?;
487 array.get(index)?.clone()
488 }
489 _ => return None,
490 };
491 }
492
493 Some(current)
494}
495
496#[derive(Debug, Clone)]
497pub struct AppendEvent {
498 pub aggregate_type: String,
499 pub aggregate_id: String,
500 pub event_type: String,
501 pub event_type_raw: Option<String>,
502 pub payload: Value,
503 pub metadata: Option<Value>,
504 pub issued_by: Option<ActorClaims>,
505 pub note: Option<String>,
506 pub tenant: String,
508 pub reference_targets: Vec<(String, String)>,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct AggregateState {
514 pub aggregate_type: String,
515 pub aggregate_id: String,
516 pub version: u64,
517 pub state: BTreeMap<String, String>,
518 #[serde(default, skip_serializing_if = "BTreeMap::is_empty", flatten)]
519 pub extensions: BTreeMap<String, Value>,
520 pub merkle_root: String,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub created_at: Option<DateTime<Utc>>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub updated_at: Option<DateTime<Utc>>,
525 #[serde(default, skip_serializing_if = "is_false")]
526 pub archived: bool,
527}
528
529#[derive(Debug, Clone)]
530struct AggregateSummary {
531 aggregate_type: String,
532 aggregate_id: String,
533 version: u64,
534 merkle_root: String,
535 archived: bool,
536 created_at: Option<DateTime<Utc>>,
537 updated_at: Option<DateTime<Utc>>,
538 extensions: BTreeMap<String, Value>,
539}
540
541#[derive(Debug, Clone, Copy, Default, Serialize)]
542pub struct StoreCounts {
543 pub active_aggregates: usize,
544 pub archived_aggregates: usize,
545 pub active_events: u64,
546 pub archived_events: u64,
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum CursorIndex {
551 Active,
552 Archived,
553}
554
555impl CursorIndex {
556 fn code(self) -> char {
557 match self {
558 CursorIndex::Active => 'a',
559 CursorIndex::Archived => 'r',
560 }
561 }
562
563 fn from_str_tag(tag: &str) -> Result<Self> {
564 match tag {
565 "a" | "active" => Ok(CursorIndex::Active),
566 "r" | "archived" => Ok(CursorIndex::Archived),
567 other => Err(EventError::InvalidCursor(format!(
568 "unknown cursor segment '{other}'"
569 ))),
570 }
571 }
572}
573
574impl fmt::Display for CursorIndex {
575 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576 match self {
577 CursorIndex::Active => write!(f, "active"),
578 CursorIndex::Archived => write!(f, "archived"),
579 }
580 }
581}
582
583#[derive(Debug, Clone, PartialEq, Eq)]
584enum AggregateCursorKind {
585 Natural,
586 Timestamp {
587 kind: TimestampIndexKind,
588 descending: bool,
589 timestamp_ms: i64,
590 },
591}
592
593#[derive(Debug, Clone, PartialEq, Eq)]
594pub struct AggregateCursor {
595 index: CursorIndex,
596 aggregate_type: String,
597 aggregate_id: String,
598 kind: AggregateCursorKind,
599}
600
601impl AggregateCursor {
602 pub fn new(
603 index: CursorIndex,
604 aggregate_type: impl Into<String>,
605 aggregate_id: impl Into<String>,
606 ) -> Self {
607 Self {
608 index,
609 aggregate_type: aggregate_type.into(),
610 aggregate_id: aggregate_id.into(),
611 kind: AggregateCursorKind::Natural,
612 }
613 }
614
615 pub fn new_timestamp(
616 index: CursorIndex,
617 aggregate_type: impl Into<String>,
618 aggregate_id: impl Into<String>,
619 kind: TimestampIndexKind,
620 descending: bool,
621 timestamp_ms: i64,
622 ) -> Self {
623 Self {
624 index,
625 aggregate_type: aggregate_type.into(),
626 aggregate_id: aggregate_id.into(),
627 kind: AggregateCursorKind::Timestamp {
628 kind,
629 descending,
630 timestamp_ms,
631 },
632 }
633 }
634
635 pub fn index(&self) -> CursorIndex {
636 self.index
637 }
638
639 pub fn aggregate_type(&self) -> &str {
640 &self.aggregate_type
641 }
642
643 pub fn aggregate_id(&self) -> &str {
644 &self.aggregate_id
645 }
646
647 pub fn encode(&self) -> String {
648 match &self.kind {
649 AggregateCursorKind::Natural => format!(
650 "{}:{}:{}",
651 self.index.code(),
652 self.aggregate_type,
653 self.aggregate_id
654 ),
655 AggregateCursorKind::Timestamp {
656 kind,
657 descending,
658 timestamp_ms,
659 } => format!(
660 "ts:{}:{}:{}:{}:{}:{}",
661 kind.as_str(),
662 if *descending { "desc" } else { "asc" },
663 self.index.code(),
664 timestamp_ms,
665 self.aggregate_type,
666 self.aggregate_id
667 ),
668 }
669 }
670
671 pub fn is_timestamp(&self) -> bool {
672 matches!(self.kind, AggregateCursorKind::Timestamp { .. })
673 }
674
675 pub fn timestamp_components(&self) -> Option<(TimestampIndexKind, bool, i64)> {
676 match self.kind {
677 AggregateCursorKind::Timestamp {
678 kind,
679 descending,
680 timestamp_ms,
681 } => Some((kind, descending, timestamp_ms)),
682 _ => None,
683 }
684 }
685
686 pub fn as_timestamp_key(
687 &self,
688 ) -> Option<(TimestampIndexKind, bool, i64, CursorIndex, &str, &str)> {
689 match self.kind {
690 AggregateCursorKind::Timestamp {
691 kind,
692 descending,
693 timestamp_ms,
694 } => Some((
695 kind,
696 descending,
697 timestamp_ms,
698 self.index,
699 self.aggregate_type(),
700 self.aggregate_id(),
701 )),
702 _ => None,
703 }
704 }
705}
706
707impl fmt::Display for AggregateCursor {
708 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
709 write!(f, "{}", self.encode())
710 }
711}
712
713impl std::str::FromStr for AggregateCursor {
714 type Err = EventError;
715
716 fn from_str(value: &str) -> Result<Self> {
717 let trimmed = value.trim();
718 if trimmed.is_empty() {
719 return Err(EventError::InvalidCursor("cursor cannot be empty".into()));
720 }
721 if trimmed.starts_with("ts:") {
722 let mut segments = trimmed.split(':');
723 segments.next(); let kind_str = segments
725 .next()
726 .ok_or_else(|| EventError::InvalidCursor("timestamp cursor missing kind".into()))?;
727 let kind = TimestampIndexKind::from_str_token(kind_str).ok_or_else(|| {
728 EventError::InvalidCursor(format!("unknown timestamp cursor kind '{kind_str}'"))
729 })?;
730 let order = segments.next().ok_or_else(|| {
731 EventError::InvalidCursor("timestamp cursor missing order".into())
732 })?;
733 let descending = match order {
734 "asc" => false,
735 "desc" => true,
736 _ => {
737 return Err(EventError::InvalidCursor(format!(
738 "timestamp cursor order must be 'asc' or 'desc', found '{order}'"
739 )));
740 }
741 };
742 let index_tag = segments.next().ok_or_else(|| {
743 EventError::InvalidCursor("timestamp cursor missing scope".into())
744 })?;
745 let index = CursorIndex::from_str_tag(index_tag)?;
746 let ts_str = segments.next().ok_or_else(|| {
747 EventError::InvalidCursor("timestamp cursor missing timestamp".into())
748 })?;
749 let timestamp_ms = ts_str.parse::<i64>().map_err(|_| {
750 EventError::InvalidCursor(format!(
751 "timestamp cursor contains invalid timestamp '{ts_str}'"
752 ))
753 })?;
754 let aggregate_type = segments.next().ok_or_else(|| {
755 EventError::InvalidCursor("timestamp cursor missing aggregate_type".into())
756 })?;
757 if aggregate_type.is_empty() {
758 return Err(EventError::InvalidCursor(
759 "timestamp cursor aggregate_type cannot be empty".into(),
760 ));
761 }
762 let aggregate_id = segments.next().ok_or_else(|| {
763 EventError::InvalidCursor("timestamp cursor missing aggregate_id".into())
764 })?;
765 if aggregate_id.is_empty() {
766 return Err(EventError::InvalidCursor(
767 "timestamp cursor aggregate_id cannot be empty".into(),
768 ));
769 }
770 if segments.next().is_some() {
771 return Err(EventError::InvalidCursor(
772 "timestamp cursor has too many segments".into(),
773 ));
774 }
775 return Ok(Self::new_timestamp(
776 index,
777 aggregate_type,
778 aggregate_id,
779 kind,
780 descending,
781 timestamp_ms,
782 ));
783 }
784 let mut segments = trimmed.split(':');
785 let Some(index_tag) = segments.next() else {
786 return Err(EventError::InvalidCursor("cursor is incomplete".into()));
787 };
788 let index = CursorIndex::from_str_tag(index_tag)?;
789 let aggregate_type = segments
790 .next()
791 .ok_or_else(|| EventError::InvalidCursor("cursor missing aggregate_type".into()))?;
792 if aggregate_type.is_empty() {
793 return Err(EventError::InvalidCursor(
794 "cursor aggregate_type cannot be empty".into(),
795 ));
796 }
797 let aggregate_id = segments
798 .next()
799 .ok_or_else(|| EventError::InvalidCursor("cursor missing aggregate_id".into()))?;
800 if aggregate_id.is_empty() {
801 return Err(EventError::InvalidCursor(
802 "cursor aggregate_id cannot be empty".into(),
803 ));
804 }
805 if segments.next().is_some() {
806 return Err(EventError::InvalidCursor(
807 "cursor has too many segments".into(),
808 ));
809 }
810
811 Ok(Self::new(index, aggregate_type, aggregate_id))
812 }
813}
814
815pub fn ensure_timestamp_cursor(
816 cursor: &AggregateCursor,
817 kind: TimestampIndexKind,
818 descending: bool,
819 scope: AggregateQueryScope,
820) -> Result<()> {
821 let Some((cursor_kind, cursor_desc, _, cursor_index, _, _)) = cursor.as_timestamp_key() else {
822 return Err(EventError::InvalidCursor(
823 "timestamp cursors must use the 'ts:' prefix (ts:<field>:<order>:...)".into(),
824 ));
825 };
826 if cursor_kind != kind {
827 return Err(EventError::InvalidCursor(format!(
828 "timestamp cursor targets '{}' but sort is '{}'",
829 cursor_kind.as_str(),
830 kind.as_str()
831 )));
832 }
833 if cursor_desc != descending {
834 return Err(EventError::InvalidCursor(
835 "timestamp cursor order does not match the requested sort direction".into(),
836 ));
837 }
838 match scope {
839 AggregateQueryScope::ActiveOnly => {
840 if cursor_index != CursorIndex::Active {
841 return Err(EventError::InvalidCursor(
842 "timestamp cursor must reference active aggregates in this scope".into(),
843 ));
844 }
845 }
846 AggregateQueryScope::ArchivedOnly => {
847 if cursor_index != CursorIndex::Archived {
848 return Err(EventError::InvalidCursor(
849 "timestamp cursor must reference archived aggregates in this scope".into(),
850 ));
851 }
852 }
853 AggregateQueryScope::IncludeArchived => {}
854 }
855 Ok(())
856}
857
858#[derive(Debug, Clone, PartialEq, Eq)]
859pub struct EventCursor {
860 index: CursorIndex,
861 aggregate_type: String,
862 aggregate_id: String,
863 version: u64,
864}
865
866impl EventCursor {
867 pub fn new(
868 index: CursorIndex,
869 aggregate_type: impl Into<String>,
870 aggregate_id: impl Into<String>,
871 version: u64,
872 ) -> Self {
873 Self {
874 index,
875 aggregate_type: aggregate_type.into(),
876 aggregate_id: aggregate_id.into(),
877 version,
878 }
879 }
880
881 pub fn index(&self) -> CursorIndex {
882 self.index
883 }
884
885 pub fn aggregate_type(&self) -> &str {
886 &self.aggregate_type
887 }
888
889 pub fn aggregate_id(&self) -> &str {
890 &self.aggregate_id
891 }
892
893 pub fn version(&self) -> u64 {
894 self.version
895 }
896
897 pub fn encode(&self) -> String {
898 format!(
899 "{}:{}:{}:{}",
900 self.index.code(),
901 self.aggregate_type,
902 self.aggregate_id,
903 self.version
904 )
905 }
906}
907
908impl fmt::Display for EventCursor {
909 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910 write!(
911 f,
912 "{}:{}:{}:{}",
913 self.index.code(),
914 self.aggregate_type,
915 self.aggregate_id,
916 self.version
917 )
918 }
919}
920
921impl std::str::FromStr for EventCursor {
922 type Err = EventError;
923
924 fn from_str(value: &str) -> Result<Self> {
925 let trimmed = value.trim();
926 if trimmed.is_empty() {
927 return Err(EventError::InvalidCursor("cursor cannot be empty".into()));
928 }
929 let mut segments = trimmed.split(':');
930 let Some(index_tag) = segments.next() else {
931 return Err(EventError::InvalidCursor("cursor is incomplete".into()));
932 };
933 let index = CursorIndex::from_str_tag(index_tag)?;
934 let aggregate_type = segments
935 .next()
936 .ok_or_else(|| EventError::InvalidCursor("cursor missing aggregate_type".into()))?;
937 if aggregate_type.is_empty() {
938 return Err(EventError::InvalidCursor(
939 "cursor aggregate_type cannot be empty".into(),
940 ));
941 }
942 let aggregate_id = segments
943 .next()
944 .ok_or_else(|| EventError::InvalidCursor("cursor missing aggregate_id".into()))?;
945 if aggregate_id.is_empty() {
946 return Err(EventError::InvalidCursor(
947 "cursor aggregate_id cannot be empty".into(),
948 ));
949 }
950 let version_str = segments
951 .next()
952 .ok_or_else(|| EventError::InvalidCursor("cursor missing version".into()))?;
953 if segments.next().is_some() {
954 return Err(EventError::InvalidCursor(
955 "cursor has too many segments".into(),
956 ));
957 }
958 let version = version_str.parse::<u64>().map_err(|_| {
959 EventError::InvalidCursor(format!("cursor version '{}' is not a number", version_str))
960 })?;
961
962 Ok(Self::new(index, aggregate_type, aggregate_id, version))
963 }
964}
965
966impl StoreCounts {
967 pub fn total_aggregates(&self) -> usize {
968 self.active_aggregates + self.archived_aggregates
969 }
970
971 pub fn total_events(&self) -> u64 {
972 self.active_events + self.archived_events
973 }
974}
975
976#[derive(Debug, Clone, Copy, PartialEq, Eq)]
977pub enum AggregateSortField {
978 AggregateType,
979 AggregateId,
980 Archived,
981 CreatedAt,
982 UpdatedAt,
983}
984
985#[derive(Debug, Clone, Copy, PartialEq, Eq)]
986pub struct AggregateSort {
987 pub field: AggregateSortField,
988 pub descending: bool,
989}
990
991#[derive(Debug, Clone, Copy, PartialEq, Eq)]
992pub enum AggregateQueryScope {
993 ActiveOnly,
994 ArchivedOnly,
995 IncludeArchived,
996}
997
998#[derive(Debug, Clone, Copy, PartialEq, Eq)]
999pub enum EventSortField {
1000 AggregateType,
1001 AggregateId,
1002 EventType,
1003 Version,
1004 EventId,
1005}
1006
1007#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1008pub struct EventSort {
1009 pub field: EventSortField,
1010 pub descending: bool,
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1014pub enum EventQueryScope<'a> {
1015 All,
1016 AggregateType(&'a str),
1017 Aggregate {
1018 aggregate_type: &'a str,
1019 aggregate_id: &'a str,
1020 },
1021}
1022
1023#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1024pub enum EventArchiveScope {
1025 ActiveOnly,
1026 ArchivedOnly,
1027 IncludeArchived,
1028}
1029
1030#[derive(Clone, Copy, PartialEq, Eq)]
1031enum AggregateIndex {
1032 Active,
1033 Archived,
1034}
1035
1036#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1037pub enum TimestampIndexKind {
1038 CreatedAt,
1039 UpdatedAt,
1040}
1041
1042impl From<AggregateIndex> for CursorIndex {
1043 fn from(value: AggregateIndex) -> Self {
1044 match value {
1045 AggregateIndex::Active => CursorIndex::Active,
1046 AggregateIndex::Archived => CursorIndex::Archived,
1047 }
1048 }
1049}
1050
1051impl From<CursorIndex> for AggregateIndex {
1052 fn from(value: CursorIndex) -> Self {
1053 match value {
1054 CursorIndex::Active => AggregateIndex::Active,
1055 CursorIndex::Archived => AggregateIndex::Archived,
1056 }
1057 }
1058}
1059
1060impl AggregateSortField {
1061 pub fn as_str(self) -> &'static str {
1062 match self {
1063 AggregateSortField::AggregateType => "aggregate_type",
1064 AggregateSortField::AggregateId => "aggregate_id",
1065 AggregateSortField::Archived => "archived",
1066 AggregateSortField::CreatedAt => "created_at",
1067 AggregateSortField::UpdatedAt => "updated_at",
1068 }
1069 }
1070
1071 pub fn compare(self, lhs: &AggregateState, rhs: &AggregateState) -> StdOrdering {
1072 match self {
1073 AggregateSortField::AggregateType => lhs.aggregate_type.cmp(&rhs.aggregate_type),
1074 AggregateSortField::AggregateId => lhs.aggregate_id.cmp(&rhs.aggregate_id),
1075 AggregateSortField::Archived => lhs.archived.cmp(&rhs.archived),
1076 AggregateSortField::CreatedAt => lhs.created_at.cmp(&rhs.created_at),
1077 AggregateSortField::UpdatedAt => lhs.updated_at.cmp(&rhs.updated_at),
1078 }
1079 }
1080}
1081
1082impl AggregateSort {
1083 pub fn compare(self, lhs: &AggregateState, rhs: &AggregateState) -> StdOrdering {
1084 let ordering = self.field.compare(lhs, rhs);
1085 if self.descending {
1086 ordering.reverse()
1087 } else {
1088 ordering
1089 }
1090 }
1091
1092 pub fn parse_directives(raw: &str) -> std::result::Result<Vec<Self>, String> {
1093 let trimmed = raw.trim();
1094 if trimmed.is_empty() {
1095 return Err("sort specification cannot be empty".to_string());
1096 }
1097
1098 let mut directives = Vec::new();
1099 for segment in trimmed.split(',') {
1100 let spec = segment.trim();
1101 if spec.is_empty() {
1102 return Err("sort segments cannot be empty".to_string());
1103 }
1104 directives.push(Self::parse_single(spec)?);
1105 }
1106
1107 Ok(directives)
1108 }
1109
1110 fn parse_single(spec: &str) -> std::result::Result<Self, String> {
1111 let mut parts = spec.split(':');
1112 let field_str = parts
1113 .next()
1114 .ok_or_else(|| "missing sort field".to_string())?
1115 .trim();
1116
1117 if field_str.is_empty() {
1118 return Err("sort field cannot be empty".to_string());
1119 }
1120
1121 let field = field_str.parse::<AggregateSortField>()?;
1122 let descending = match parts.next() {
1123 Some(order) => match order.trim().to_ascii_lowercase().as_str() {
1124 "asc" => false,
1125 "desc" => true,
1126 other => {
1127 return Err(format!(
1128 "invalid sort order '{other}' (expected 'asc' or 'desc')"
1129 ));
1130 }
1131 },
1132 None => false,
1133 };
1134
1135 if parts.next().is_some() {
1136 return Err("sort specification contains too many ':' separators".to_string());
1137 }
1138
1139 Ok(Self { field, descending })
1140 }
1141}
1142
1143impl EventSortField {
1144 pub fn as_str(self) -> &'static str {
1145 match self {
1146 EventSortField::AggregateType => "aggregate_type",
1147 EventSortField::AggregateId => "aggregate_id",
1148 EventSortField::EventType => "event_type",
1149 EventSortField::Version => "version",
1150 EventSortField::EventId => "event_id",
1151 }
1152 }
1153
1154 pub fn compare(self, lhs: &EventRecord, rhs: &EventRecord) -> StdOrdering {
1155 match self {
1156 EventSortField::AggregateType => lhs.aggregate_type.cmp(&rhs.aggregate_type),
1157 EventSortField::AggregateId => lhs.aggregate_id.cmp(&rhs.aggregate_id),
1158 EventSortField::EventType => lhs.event_type.cmp(&rhs.event_type),
1159 EventSortField::Version => lhs.version.cmp(&rhs.version),
1160 EventSortField::EventId => lhs.metadata.event_id.cmp(&rhs.metadata.event_id),
1161 }
1162 }
1163}
1164
1165impl EventSort {
1166 pub fn compare(self, lhs: &EventRecord, rhs: &EventRecord) -> StdOrdering {
1167 let ordering = self.field.compare(lhs, rhs);
1168 if self.descending {
1169 ordering.reverse()
1170 } else {
1171 ordering
1172 }
1173 }
1174}
1175
1176impl std::str::FromStr for AggregateSortField {
1177 type Err = String;
1178
1179 fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
1180 let normalized = value.trim().to_ascii_lowercase();
1181 match normalized.as_str() {
1182 "aggregate_type" | "aggregatetype" => Ok(AggregateSortField::AggregateType),
1183 "aggregate_id" | "aggregateid" => Ok(AggregateSortField::AggregateId),
1184 "archived" => Ok(AggregateSortField::Archived),
1185 "created_at" | "createdat" => Ok(AggregateSortField::CreatedAt),
1186 "updated_at" | "updatedat" => Ok(AggregateSortField::UpdatedAt),
1187 _ => Err(format!("unsupported aggregate sort field '{value}'")),
1188 }
1189 }
1190}
1191
1192impl std::str::FromStr for EventSortField {
1193 type Err = String;
1194
1195 fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
1196 let normalized = value.trim().to_ascii_lowercase();
1197 match normalized.as_str() {
1198 "aggregate_type" | "aggregatetype" => Ok(EventSortField::AggregateType),
1199 "aggregate_id" | "aggregateid" => Ok(EventSortField::AggregateId),
1200 "event_type" | "eventtype" => Ok(EventSortField::EventType),
1201 "version" => Ok(EventSortField::Version),
1202 "event_id" | "eventid" => Ok(EventSortField::EventId),
1203 _ => Err(format!("unsupported event sort field '{value}'")),
1204 }
1205 }
1206}
1207
1208impl AggregateIndex {
1209 fn meta_prefix(self) -> &'static str {
1210 match self {
1211 AggregateIndex::Active => PREFIX_META,
1212 AggregateIndex::Archived => PREFIX_META_ARCHIVED,
1213 }
1214 }
1215
1216 fn state_prefix(self) -> &'static str {
1217 match self {
1218 AggregateIndex::Active => PREFIX_STATE,
1219 AggregateIndex::Archived => PREFIX_STATE_ARCHIVED,
1220 }
1221 }
1222
1223 fn event_prefix(self) -> &'static str {
1224 match self {
1225 AggregateIndex::Active => PREFIX_EVENT,
1226 AggregateIndex::Archived => PREFIX_EVENT_ARCHIVED,
1227 }
1228 }
1229}
1230
1231impl TimestampIndexKind {
1232 fn prefix(self, index: AggregateIndex) -> &'static str {
1233 match (self, index) {
1234 (TimestampIndexKind::CreatedAt, AggregateIndex::Active) => PREFIX_META_CREATED_INDEX,
1235 (TimestampIndexKind::CreatedAt, AggregateIndex::Archived) => {
1236 PREFIX_META_CREATED_INDEX_ARCHIVED
1237 }
1238 (TimestampIndexKind::UpdatedAt, AggregateIndex::Active) => PREFIX_META_UPDATED_INDEX,
1239 (TimestampIndexKind::UpdatedAt, AggregateIndex::Archived) => {
1240 PREFIX_META_UPDATED_INDEX_ARCHIVED
1241 }
1242 }
1243 }
1244
1245 pub fn as_str(self) -> &'static str {
1246 match self {
1247 TimestampIndexKind::CreatedAt => "created_at",
1248 TimestampIndexKind::UpdatedAt => "updated_at",
1249 }
1250 }
1251
1252 fn from_str_token(value: &str) -> Option<Self> {
1253 match value {
1254 "created_at" | "created" => Some(TimestampIndexKind::CreatedAt),
1255 "updated_at" | "updated" => Some(TimestampIndexKind::UpdatedAt),
1256 _ => None,
1257 }
1258 }
1259}
1260
1261#[derive(Debug)]
1262struct TimestampIndexEntry {
1263 aggregate_type: String,
1264 aggregate_id: String,
1265 timestamp_ms: i64,
1266}
1267
1268struct TimestampIndexIterator<'a> {
1269 iter: DBIterator<'a>,
1270 prefix: Vec<u8>,
1271 kind: TimestampIndexKind,
1272 index: AggregateIndex,
1273 finished: bool,
1274 had_error: bool,
1275 start_time: Instant,
1276}
1277
1278impl<'a> TimestampIndexIterator<'a> {
1279 fn new(
1280 store: &'a EventStore,
1281 kind: TimestampIndexKind,
1282 index: AggregateIndex,
1283 descending: bool,
1284 ) -> Self {
1285 let prefix = timestamp_index_prefix(kind, index);
1286 let mut start_key = prefix.clone();
1287 let direction = if descending {
1288 start_key.push(u8::MAX);
1289 Direction::Reverse
1290 } else {
1291 Direction::Forward
1292 };
1293 let iter = store
1294 .db
1295 .iterator(IteratorMode::From(start_key.as_slice(), direction));
1296 Self {
1297 iter,
1298 prefix,
1299 kind,
1300 index,
1301 finished: false,
1302 had_error: false,
1303 start_time: Instant::now(),
1304 }
1305 }
1306
1307 fn next_entry(&mut self) -> Option<TimestampIndexEntry> {
1308 if self.finished {
1309 return None;
1310 }
1311 while let Some(item) = self.iter.next() {
1312 let (key, _) = match item {
1313 Ok(pair) => pair,
1314 Err(_) => {
1315 self.had_error = true;
1316 continue;
1317 }
1318 };
1319 if !key.starts_with(self.prefix.as_slice()) {
1320 self.finished = true;
1321 break;
1322 }
1323 if let Some(entry) = parse_timestamp_index_entry(self.prefix.as_slice(), &key) {
1324 return Some(entry);
1325 }
1326 }
1327 None
1328 }
1329
1330 fn mark_error(&mut self) {
1331 self.had_error = true;
1332 }
1333}
1334
1335impl Drop for TimestampIndexIterator<'_> {
1336 fn drop(&mut self) {
1337 let duration = self.start_time.elapsed().as_secs_f64();
1338 let status = if self.had_error { "err" } else { "ok" };
1339 record_store_op(
1340 timestamp_index_metric(self.kind, self.index),
1341 status,
1342 duration,
1343 );
1344 }
1345}
1346
1347fn compare_aggregate_sort_keys(
1348 lhs: &AggregateState,
1349 rhs: &AggregateState,
1350 keys: &[AggregateSort],
1351) -> StdOrdering {
1352 for key in keys {
1353 let ordering = key.compare(lhs, rhs);
1354 if ordering != StdOrdering::Equal {
1355 return ordering;
1356 }
1357 }
1358 StdOrdering::Equal
1359}
1360
1361fn compare_event_sort_keys(
1362 lhs: &EventRecord,
1363 rhs: &EventRecord,
1364 keys: &[EventSort],
1365) -> StdOrdering {
1366 for key in keys {
1367 let ordering = key.compare(lhs, rhs);
1368 if ordering != StdOrdering::Equal {
1369 return ordering;
1370 }
1371 }
1372 StdOrdering::Equal
1373}
1374
1375fn aggregate_sort_matches_key_order(keys: &[AggregateSort]) -> bool {
1376 if keys.is_empty() {
1377 return true;
1378 }
1379 const NATURAL_ORDER: [AggregateSortField; 2] = [
1380 AggregateSortField::AggregateType,
1381 AggregateSortField::AggregateId,
1382 ];
1383 keys.iter().enumerate().all(|(idx, key)| {
1384 !key.descending
1385 && NATURAL_ORDER
1386 .get(idx)
1387 .map(|field| key.field == *field)
1388 .unwrap_or(false)
1389 })
1390}
1391
1392pub fn timestamp_sort_hint(keys: &[AggregateSort]) -> Option<(TimestampIndexKind, bool)> {
1393 if keys.len() != 1 {
1394 return None;
1395 }
1396 let directive = keys[0];
1397 let kind = match directive.field {
1398 AggregateSortField::CreatedAt => TimestampIndexKind::CreatedAt,
1399 AggregateSortField::UpdatedAt => TimestampIndexKind::UpdatedAt,
1400 _ => return None,
1401 };
1402 Some((kind, directive.descending))
1403}
1404
1405fn event_sort_matches_key_order(keys: &[EventSort]) -> bool {
1406 if keys.is_empty() {
1407 return true;
1408 }
1409 const NATURAL_ORDER: [EventSortField; 3] = [
1410 EventSortField::AggregateType,
1411 EventSortField::AggregateId,
1412 EventSortField::Version,
1413 ];
1414 keys.iter().enumerate().all(|(idx, key)| {
1415 !key.descending
1416 && NATURAL_ORDER
1417 .get(idx)
1418 .map(|field| key.field == *field)
1419 .unwrap_or(false)
1420 })
1421}
1422
1423fn is_false(value: &bool) -> bool {
1424 !*value
1425}
1426
1427#[derive(Debug, Clone, Serialize, Deserialize)]
1428struct AggregateMeta {
1429 aggregate_type: String,
1430 aggregate_id: String,
1431 version: u64,
1432 event_hashes: Vec<String>,
1433 merkle_root: String,
1434 #[serde(default)]
1435 archived: bool,
1436 #[serde(default)]
1437 archived_at: Option<DateTime<Utc>>,
1438 #[serde(default)]
1439 archive_comment: Option<String>,
1440 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1441 extensions: BTreeMap<String, Value>,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 created_at: Option<DateTime<Utc>>,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 updated_at: Option<DateTime<Utc>>,
1446}
1447
1448impl AggregateMeta {
1449 fn new(aggregate_type: String, aggregate_id: String) -> Self {
1450 Self {
1451 aggregate_type,
1452 aggregate_id,
1453 version: 0,
1454 event_hashes: Vec::new(),
1455 merkle_root: empty_root(),
1456 archived: false,
1457 archived_at: None,
1458 archive_comment: None,
1459 extensions: BTreeMap::new(),
1460 created_at: None,
1461 updated_at: None,
1462 }
1463 }
1464
1465 fn record_write(&mut self, event_time: DateTime<Utc>) {
1466 if self.version == 0 && self.created_at.is_none() {
1467 self.created_at = Some(event_time);
1468 }
1469 self.updated_at = Some(event_time);
1470 }
1471}
1472
1473#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1474struct AggregateKey {
1475 aggregate_type: String,
1476 aggregate_id: String,
1477}
1478
1479impl AggregateKey {
1480 fn new(aggregate_type: impl Into<String>, aggregate_id: impl Into<String>) -> Self {
1481 Self {
1482 aggregate_type: aggregate_type.into(),
1483 aggregate_id: aggregate_id.into(),
1484 }
1485 }
1486
1487 fn parts(&self) -> (&str, &str) {
1488 (&self.aggregate_type, &self.aggregate_id)
1489 }
1490}
1491
1492struct PendingEvent {
1493 key: Vec<u8>,
1494 value: Vec<u8>,
1495}
1496
1497struct PendingRefUpdate {
1498 tenant: String,
1499 aggregate_type: String,
1500 aggregate_id: String,
1501 references: Vec<(String, String)>,
1502}
1503
1504#[derive(Debug, Clone, Serialize, Deserialize)]
1505struct StoredReferencePath {
1506 target: String,
1507 path: String,
1508}
1509
1510#[derive(Debug, Clone, Serialize, Deserialize)]
1511struct StoredReferrer {
1512 aggregate_type: String,
1513 aggregate_id: String,
1514 path: String,
1515}
1516
1517pub struct EventStore {
1518 db: DBWithThreadMode<MultiThreaded>,
1519 write_lock: Mutex<()>,
1520 read_only: bool,
1521 encryptor: Option<Encryptor>,
1522 id_generator: Mutex<SnowflakeGenerator>,
1523}
1524
1525pub struct Transaction<'a> {
1526 store: &'a EventStore,
1527 _guard: MutexGuard<'a, ()>,
1528 pending_events: Vec<PendingEvent>,
1529 pending_records: Vec<EventRecord>,
1530 pending_refs: Vec<PendingRefUpdate>,
1531 meta_cache: BTreeMap<AggregateKey, AggregateMeta>,
1532 state_cache: BTreeMap<AggregateKey, BTreeMap<String, String>>,
1533}
1534
1535#[derive(Debug, Clone, Serialize, Deserialize)]
1536pub struct SnapshotRecord {
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub snapshot_id: Option<SnowflakeId>,
1539 pub aggregate_type: String,
1540 pub aggregate_id: String,
1541 pub version: u64,
1542 pub state: BTreeMap<String, String>,
1543 pub merkle_root: String,
1544 pub created_at: DateTime<Utc>,
1545 pub comment: Option<String>,
1546}
1547
1548#[derive(Debug, Clone)]
1549pub struct AggregatePositionEntry {
1550 pub aggregate_type: String,
1551 pub aggregate_id: String,
1552 pub version: u64,
1553}
1554
1555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1556enum LockStatus {
1557 NotLocked,
1558 LockedBy(Option<u32>),
1559 Unknown,
1560}
1561
1562fn open_db_with_lock_handling(
1563 path: &Path,
1564 options: &Options,
1565 read_only: bool,
1566) -> Result<DBWithThreadMode<MultiThreaded>> {
1567 let lock_path = path.join("LOCK");
1568 let mut last_error = None;
1569
1570 for attempt in 0..=LOCK_RETRY_ATTEMPTS {
1571 match try_open_db(path, options, read_only) {
1572 Ok(db) => return Ok(db),
1573 Err(err) => {
1574 let message = err.to_string();
1575 last_error = Some(message.clone());
1576 if !is_lock_error_message(&message) {
1577 return Err(EventError::Storage(message));
1578 }
1579
1580 let status = lock_status(&lock_path)?;
1581 match status {
1582 LockStatus::NotLocked => {
1583 cleanup_stale_lock(&lock_path);
1584 }
1585 LockStatus::LockedBy(pid_opt) => {
1586 if pid_opt.map_or(false, pid_running) {
1587 if attempt == LOCK_RETRY_ATTEMPTS {
1588 return Err(EventError::Storage(store_in_use_message(
1589 path, pid_opt,
1590 )));
1591 }
1592 } else {
1593 cleanup_stale_lock(&lock_path);
1594 }
1595 }
1596 LockStatus::Unknown => {
1597 if attempt == LOCK_RETRY_ATTEMPTS {
1598 return Err(EventError::Storage(store_in_use_message(path, None)));
1599 }
1600 }
1601 }
1602 }
1603 }
1604
1605 if attempt < LOCK_RETRY_ATTEMPTS {
1606 thread::sleep(LOCK_RETRY_BACKOFF);
1607 }
1608 }
1609
1610 Err(EventError::Storage(
1611 last_error.unwrap_or_else(|| "failed to open event store".into()),
1612 ))
1613}
1614
1615fn try_open_db(
1616 path: &Path,
1617 options: &Options,
1618 read_only: bool,
1619) -> std::result::Result<DBWithThreadMode<MultiThreaded>, rocksdb::Error> {
1620 if read_only {
1621 DBWithThreadMode::<MultiThreaded>::open_for_read_only(options, path, false)
1622 } else {
1623 DBWithThreadMode::<MultiThreaded>::open(options, path)
1624 }
1625}
1626
1627fn lock_status(lock_path: &Path) -> io::Result<LockStatus> {
1628 if !lock_path.exists() {
1629 return Ok(LockStatus::NotLocked);
1630 }
1631
1632 #[cfg(unix)]
1633 {
1634 let file = std::fs::File::open(lock_path)?;
1635 let mut flock = libc::flock {
1636 l_type: libc::F_WRLCK as libc::c_short,
1637 l_whence: libc::SEEK_SET as libc::c_short,
1638 l_start: 0,
1639 l_len: 0,
1640 l_pid: 0,
1641 };
1642 let status = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETLK, &mut flock) };
1643 if status == -1 {
1644 return Ok(LockStatus::Unknown);
1645 }
1646 if flock.l_type == libc::F_UNLCK as libc::c_short {
1647 return Ok(LockStatus::NotLocked);
1648 }
1649 return Ok(LockStatus::LockedBy(Some(flock.l_pid as u32)));
1650 }
1651
1652 #[cfg(not(unix))]
1653 {
1654 let _ = lock_path;
1655 Ok(LockStatus::Unknown)
1656 }
1657}
1658
1659fn pid_running(pid: u32) -> bool {
1660 #[cfg(unix)]
1661 {
1662 let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
1663 if result == 0 {
1664 return true;
1665 }
1666 if let Some(errno) = io::Error::last_os_error().raw_os_error() {
1667 return errno != libc::ESRCH;
1668 }
1669 return false;
1670 }
1671
1672 #[cfg(windows)]
1673 {
1674 use windows_sys::Win32::{
1675 Foundation::{CloseHandle, STILL_ACTIVE},
1676 System::Threading::{
1677 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
1678 },
1679 };
1680
1681 unsafe {
1682 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
1683 if handle == 0 {
1684 return false;
1685 }
1686 let mut exit_code = 0;
1687 let success = GetExitCodeProcess(handle, &mut exit_code);
1688 let _ = CloseHandle(handle);
1689 success != 0 && exit_code == STILL_ACTIVE as u32
1690 }
1691 }
1692
1693 #[cfg(not(any(unix, windows)))]
1694 {
1695 let _ = pid;
1696 true
1697 }
1698}
1699
1700fn cleanup_stale_lock(lock_path: &Path) {
1701 if let Err(err) = fs::remove_file(lock_path) {
1702 if err.kind() != io::ErrorKind::NotFound {
1703 warn!(
1704 "failed to remove stale lock file {}: {}",
1705 lock_path.display(),
1706 err
1707 );
1708 }
1709 } else {
1710 warn!(
1711 "removed stale event store lock file at {}",
1712 lock_path.display()
1713 );
1714 }
1715}
1716
1717fn store_in_use_message(path: &Path, pid: Option<u32>) -> String {
1718 match pid {
1719 Some(pid) => format!(
1720 "event store lock at {} is held by process {pid}; stop the running dbx instance and retry",
1721 path.display()
1722 ),
1723 None => format!(
1724 "event store at {} is currently in use; stop the running dbx instance and retry",
1725 path.display()
1726 ),
1727 }
1728}
1729
1730pub fn is_lock_error_message(message: &str) -> bool {
1731 let lower = message.to_ascii_lowercase();
1732 lower.contains("lock file")
1733 || lower.contains("resource temporarily unavailable")
1734 || lower.contains("lock is held")
1735 || lower.contains("is held by process")
1736 || lower.contains("lock hold")
1737 || lower.contains("currently in use")
1738 || lower.contains("no locks available")
1739}
1740
1741impl EventStore {
1742 pub fn open(path: PathBuf, encryptor: Option<Encryptor>, worker_id: u16) -> Result<Self> {
1743 let mut options = Options::default();
1744 options.create_if_missing(true);
1745 configure_db_options(&mut options, false);
1746 let db = open_db_with_lock_handling(&path, &options, false)?;
1747
1748 if worker_id > MAX_WORKER_ID {
1749 return Err(EventError::Config(format!(
1750 "snowflake worker id {} exceeds maximum {}",
1751 worker_id, MAX_WORKER_ID
1752 )));
1753 }
1754
1755 Ok(Self {
1756 db,
1757 write_lock: Mutex::new(()),
1758 read_only: false,
1759 encryptor,
1760 id_generator: Mutex::new(SnowflakeGenerator::new(worker_id)),
1761 })
1762 }
1763
1764 pub fn open_read_only(path: PathBuf, encryptor: Option<Encryptor>) -> Result<Self> {
1765 let mut options = Options::default();
1766 options.create_if_missing(false);
1767 configure_db_options(&mut options, true);
1768 let db = open_db_with_lock_handling(&path, &options, true)?;
1769
1770 Ok(Self {
1771 db,
1772 write_lock: Mutex::new(()),
1773 read_only: true,
1774 encryptor,
1775 id_generator: Mutex::new(SnowflakeGenerator::new(0)),
1776 })
1777 }
1778
1779 pub fn storage_usage_bytes(&self) -> Result<u64> {
1780 let sst = self.rocksdb_property_bytes("rocksdb.total-sst-files-size")?;
1781 let mem = self.rocksdb_property_bytes("rocksdb.size-all-mem-tables")?;
1782 let wal = self.rocksdb_property_bytes("rocksdb.live-wal-file-size")?;
1783 Ok(sst.saturating_add(mem).saturating_add(wal))
1784 }
1785
1786 fn rocksdb_property_bytes(&self, name: &str) -> Result<u64> {
1787 match self.db.property_int_value(name) {
1788 Ok(Some(value)) => Ok(value),
1789 Ok(None) => Ok(0),
1790 Err(err) => Err(EventError::Storage(format!(
1791 "failed to read RocksDB property '{name}': {err}"
1792 ))),
1793 }
1794 }
1795
1796 pub fn counts(&self) -> Result<StoreCounts> {
1797 let (active_aggregates, active_events) =
1798 self.count_index_entries(AggregateIndex::Active)?;
1799 let (archived_aggregates, archived_events) =
1800 self.count_index_entries(AggregateIndex::Archived)?;
1801
1802 Ok(StoreCounts {
1803 active_aggregates,
1804 archived_aggregates,
1805 active_events,
1806 archived_events,
1807 })
1808 }
1809
1810 fn count_index_entries(&self, index: AggregateIndex) -> Result<(usize, u64)> {
1811 let start = Instant::now();
1812 let metric = match index {
1813 AggregateIndex::Active => "rocksdb_iter_meta_count",
1814 AggregateIndex::Archived => "rocksdb_iter_meta_archived_count",
1815 };
1816
1817 let result = (|| {
1818 let prefix = meta_prefix_for(index);
1819 let iter = self
1820 .db
1821 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
1822
1823 let mut aggregates = 0usize;
1824 let mut events = 0u64;
1825
1826 for item in iter {
1827 let (key, value) = item.map_err(|err| EventError::Storage(err.to_string()))?;
1828 if !key.starts_with(prefix.as_slice()) {
1829 break;
1830 }
1831 if key.len() > prefix.len() && key[prefix.len()] != SEP {
1832 break;
1833 }
1834
1835 let meta: AggregateMeta = serde_json::from_slice(&value).map_err(|err| {
1836 EventError::Storage(format!("failed to deserialize aggregate metadata: {err}"))
1837 })?;
1838 aggregates += 1;
1839 events += meta.version;
1840 }
1841
1842 Ok((aggregates, events))
1843 })();
1844
1845 let duration = start.elapsed().as_secs_f64();
1846 record_store_op(metric, if result.is_ok() { "ok" } else { "err" }, duration);
1847 result
1848 }
1849
1850 fn next_event_id(&self) -> SnowflakeId {
1851 self.next_snowflake_id()
1852 }
1853
1854 fn next_snowflake_id(&self) -> SnowflakeId {
1855 let mut guard = self.id_generator.lock();
1856 guard.next_id()
1857 }
1858
1859 pub fn prepare_payload_from_patch(
1860 &self,
1861 aggregate_type: &str,
1862 aggregate_id: &str,
1863 patch_value: &Value,
1864 ) -> Result<Value> {
1865 if !patch_value.is_array() {
1866 return Err(EventError::InvalidSchema(
1867 "patch must be provided as a JSON array".into(),
1868 ));
1869 }
1870
1871 let top_level_keys =
1872 collect_top_level_keys(patch_value).map_err(EventError::InvalidSchema)?;
1873 if top_level_keys.is_empty() {
1874 return Err(EventError::InvalidSchema(
1875 "patch must modify at least one path".into(),
1876 ));
1877 }
1878
1879 let patch: Patch = serde_json::from_value(patch_value.clone())
1880 .map_err(|err| EventError::InvalidSchema(format!("failed to parse patch: {}", err)))?;
1881
1882 let state_map = self.load_state_map(aggregate_type, aggregate_id)?;
1883 let mut document = state_map_to_value(&state_map);
1884 if !document.is_object() {
1885 document = Value::Object(serde_json::Map::new());
1886 }
1887
1888 json_patch::patch(&mut document, &patch)
1889 .map_err(|err| EventError::InvalidSchema(format!("failed to apply patch: {}", err)))?;
1890
1891 let document_object = document.as_object().ok_or_else(|| {
1892 EventError::InvalidSchema("patched document must be a JSON object".into())
1893 })?;
1894
1895 let mut payload = serde_json::Map::new();
1896 for key in top_level_keys {
1897 let value = document_object.get(&key).ok_or_else(|| {
1898 EventError::InvalidSchema(format!(
1899 "patch removed key '{}' and removals are not supported",
1900 key
1901 ))
1902 })?;
1903 payload.insert(key, value.clone());
1904 }
1905
1906 Ok(Value::Object(payload))
1907 }
1908
1909 pub fn append(&self, input: AppendEvent) -> Result<EventRecord> {
1910 self.ensure_writable()?;
1911 let _guard = self.write_lock.lock();
1912
1913 let tenant = input.tenant.clone();
1914 let references = input.reference_targets.clone();
1915 if let Some(note) = input.note.as_ref() {
1916 if note.chars().count() > MAX_EVENT_NOTE_LENGTH {
1917 return Err(EventError::InvalidSchema(format!(
1918 "event note cannot exceed {} characters",
1919 MAX_EVENT_NOTE_LENGTH
1920 )));
1921 }
1922 }
1923 ensure_event_type_lengths(&input.event_type, input.event_type_raw.as_ref())?;
1924
1925 let aggregate_type = input.aggregate_type.clone();
1926 let aggregate_id = input.aggregate_id.clone();
1927 let event_id = self.next_event_id();
1928
1929 let (mut meta, mut state, previous_meta) =
1930 match self.load_meta_any(&aggregate_type, &aggregate_id)? {
1931 Some((_, AggregateIndex::Archived)) => {
1932 return Err(EventError::AggregateArchived);
1933 }
1934 Some((meta, AggregateIndex::Active)) => {
1935 let previous = meta.clone();
1936 (
1937 meta,
1938 self.load_state_map_from(
1939 AggregateIndex::Active,
1940 &aggregate_type,
1941 &aggregate_id,
1942 )?,
1943 Some(previous),
1944 )
1945 }
1946 None => (
1947 AggregateMeta::new(aggregate_type.clone(), aggregate_id.clone()),
1948 BTreeMap::new(),
1949 None,
1950 ),
1951 };
1952 let record = apply_event(&mut meta, &mut state, input, event_id);
1953 let stored_record = self.encode_record(&record)?;
1954 let encoded_state = self.encode_state_map(&state)?;
1955
1956 let mut batch = WriteBatch::default();
1957 batch_put(
1958 &mut batch,
1959 event_key(&record.aggregate_type, &record.aggregate_id, record.version),
1960 serde_json::to_vec(&stored_record)?,
1961 )?;
1962 batch_put(
1963 &mut batch,
1964 meta_key(&record.aggregate_type, &record.aggregate_id),
1965 serde_json::to_vec(&meta)?,
1966 )?;
1967 batch_put(
1968 &mut batch,
1969 state_key(&record.aggregate_type, &record.aggregate_id),
1970 encoded_state,
1971 )?;
1972 update_reference_index_inner(
1973 &self.db,
1974 &mut batch,
1975 &tenant,
1976 &record.aggregate_type,
1977 &record.aggregate_id,
1978 &references,
1979 )?;
1980 self.sync_timestamp_indexes(
1981 &mut batch,
1982 AggregateIndex::Active,
1983 previous_meta.as_ref(),
1984 Some(&meta),
1985 );
1986
1987 self.write_batch(batch)?;
1988
1989 Ok(record)
1990 }
1991
1992 pub fn append_imported_event(&self, record: EventRecord) -> Result<()> {
1993 self.ensure_writable()?;
1994 let _guard = self.write_lock.lock();
1995
1996 let aggregate_type = record.aggregate_type.clone();
1997 let aggregate_id = record.aggregate_id.clone();
1998
1999 let (mut meta, mut state, previous_meta) =
2000 match self.load_meta_any(&aggregate_type, &aggregate_id)? {
2001 Some((_, AggregateIndex::Archived)) => {
2002 return Err(EventError::AggregateArchived);
2003 }
2004 Some((meta, AggregateIndex::Active)) => {
2005 let previous = meta.clone();
2006 (
2007 meta,
2008 self.load_state_map_from(
2009 AggregateIndex::Active,
2010 &aggregate_type,
2011 &aggregate_id,
2012 )?,
2013 Some(previous),
2014 )
2015 }
2016 None => (
2017 AggregateMeta::new(aggregate_type.clone(), aggregate_id.clone()),
2018 BTreeMap::new(),
2019 None,
2020 ),
2021 };
2022
2023 if meta.version + 1 != record.version {
2024 return Err(EventError::Storage(format!(
2025 "imported event version mismatch for {}::{} (expected {}, found {})",
2026 aggregate_type,
2027 aggregate_id,
2028 meta.version + 1,
2029 record.version
2030 )));
2031 }
2032 let payload_map = payload_to_map(&record.payload);
2033
2034 let computed_hash = hash_event(
2035 &aggregate_type,
2036 &aggregate_id,
2037 record.version,
2038 &record.event_type,
2039 &payload_map,
2040 );
2041
2042 if computed_hash != record.hash {
2043 return Err(EventError::Storage(format!(
2044 "imported event hash mismatch for {}::{} (expected {}, found {})",
2045 aggregate_type, aggregate_id, computed_hash, record.hash
2046 )));
2047 }
2048
2049 for (key, value) in payload_map {
2050 state.insert(key, value);
2051 }
2052
2053 if let Some(metadata) = record.extensions.as_ref() {
2054 let incoming = metadata_to_extensions_map(metadata);
2055 for (key, value) in incoming {
2056 meta.extensions.insert(key, value);
2057 }
2058 }
2059
2060 let record_created_at = record.metadata.created_at;
2061 meta.record_write(record_created_at);
2062 meta.event_hashes.push(record.hash.clone());
2063 meta.version = record.version;
2064 meta.merkle_root = compute_merkle_root(&meta.event_hashes);
2065
2066 if meta.merkle_root != record.merkle_root {
2067 return Err(EventError::Storage(format!(
2068 "imported event merkle mismatch for {}::{} (expected {}, found {})",
2069 aggregate_type, aggregate_id, meta.merkle_root, record.merkle_root
2070 )));
2071 }
2072
2073 let stored_record = self.encode_record(&record)?;
2074
2075 let mut batch = WriteBatch::default();
2076 batch_put(
2077 &mut batch,
2078 event_key(&aggregate_type, &aggregate_id, record.version),
2079 serde_json::to_vec(&stored_record)?,
2080 )?;
2081 self.sync_timestamp_indexes(
2082 &mut batch,
2083 AggregateIndex::Active,
2084 previous_meta.as_ref(),
2085 Some(&meta),
2086 );
2087 batch_put(
2088 &mut batch,
2089 meta_key(&aggregate_type, &aggregate_id),
2090 serde_json::to_vec(&meta)?,
2091 )?;
2092 batch_put(
2093 &mut batch,
2094 state_key(&aggregate_type, &aggregate_id),
2095 self.encode_state_map(&state)?,
2096 )?;
2097
2098 self.write_batch(batch)?;
2099 Ok(())
2100 }
2101
2102 pub fn transaction(&self) -> Result<Transaction<'_>> {
2103 self.ensure_writable()?;
2104 let guard = self.write_lock.lock();
2105 Ok(Transaction {
2106 store: self,
2107 _guard: guard,
2108 pending_events: Vec::new(),
2109 pending_records: Vec::new(),
2110 pending_refs: Vec::new(),
2111 meta_cache: BTreeMap::new(),
2112 state_cache: BTreeMap::new(),
2113 })
2114 }
2115
2116 pub fn get_aggregate_state(
2117 &self,
2118 aggregate_type: &str,
2119 aggregate_id: &str,
2120 ) -> Result<AggregateState> {
2121 let (meta, index) = self
2122 .load_meta_any(aggregate_type, aggregate_id)?
2123 .ok_or(EventError::AggregateNotFound)?;
2124 let state = self.load_state_map_from(index, aggregate_type, aggregate_id)?;
2125
2126 let AggregateMeta {
2127 aggregate_type,
2128 aggregate_id,
2129 version,
2130 merkle_root,
2131 archived,
2132 created_at,
2133 updated_at,
2134 extensions,
2135 ..
2136 } = meta;
2137
2138 Ok(AggregateState {
2139 aggregate_type,
2140 aggregate_id,
2141 version,
2142 state,
2143 extensions,
2144 merkle_root,
2145 created_at,
2146 updated_at,
2147 archived,
2148 })
2149 }
2150
2151 pub fn parse_timestamp_cursor(
2152 &self,
2153 raw: &str,
2154 kind: TimestampIndexKind,
2155 descending: bool,
2156 ) -> Result<AggregateCursor> {
2157 if let Some(cursor) = self.try_shorthand_timestamp_cursor(raw, kind, descending)? {
2158 return Ok(cursor);
2159 }
2160
2161 AggregateCursor::from_str(raw)
2162 }
2163
2164 fn try_shorthand_timestamp_cursor(
2165 &self,
2166 raw: &str,
2167 kind: TimestampIndexKind,
2168 descending: bool,
2169 ) -> Result<Option<AggregateCursor>> {
2170 let parts: Vec<_> = raw.trim().split(':').collect();
2171 if parts.len() != 3 || parts[0] != "ts" {
2172 return Ok(None);
2173 }
2174 let aggregate_type = parts[1];
2175 let aggregate_id = parts[2];
2176 if aggregate_type.is_empty() {
2177 return Err(EventError::InvalidCursor(
2178 "timestamp cursor aggregate_type cannot be empty".into(),
2179 ));
2180 }
2181 if aggregate_id.is_empty() {
2182 return Err(EventError::InvalidCursor(
2183 "timestamp cursor aggregate_id cannot be empty".into(),
2184 ));
2185 }
2186
2187 let aggregate = match self.get_aggregate_state(aggregate_type, aggregate_id) {
2188 Ok(aggregate) => aggregate,
2189 Err(EventError::AggregateNotFound) => {
2190 return Err(EventError::InvalidCursor(format!(
2191 "cursor references unknown aggregate '{}:{}'",
2192 aggregate_type, aggregate_id
2193 )));
2194 }
2195 Err(err) => return Err(err),
2196 };
2197 let timestamp = match kind {
2198 TimestampIndexKind::CreatedAt => aggregate.created_at,
2199 TimestampIndexKind::UpdatedAt => aggregate.updated_at,
2200 }
2201 .ok_or_else(|| {
2202 EventError::InvalidCursor(format!(
2203 "aggregate {}:{} missing {} timestamp",
2204 aggregate_type,
2205 aggregate_id,
2206 match kind {
2207 TimestampIndexKind::CreatedAt => "created_at",
2208 TimestampIndexKind::UpdatedAt => "updated_at",
2209 }
2210 ))
2211 })?;
2212
2213 let index = if aggregate.archived {
2214 CursorIndex::Archived
2215 } else {
2216 CursorIndex::Active
2217 };
2218
2219 Ok(Some(AggregateCursor::new_timestamp(
2220 index,
2221 aggregate.aggregate_type,
2222 aggregate.aggregate_id,
2223 kind,
2224 descending,
2225 timestamp.timestamp_millis(),
2226 )))
2227 }
2228
2229 pub fn list_events(
2230 &self,
2231 aggregate_type: &str,
2232 aggregate_id: &str,
2233 ) -> Result<Vec<EventRecord>> {
2234 self.list_events_paginated(aggregate_type, aggregate_id, 0, None)
2235 }
2236
2237 pub fn merkle_root_at(
2238 &self,
2239 aggregate_type: &str,
2240 aggregate_id: &str,
2241 version: u64,
2242 ) -> Result<Option<String>> {
2243 if version == 0 {
2244 return Ok(Some(empty_root()));
2245 }
2246 let Some((meta, _)) = self.load_meta_any(aggregate_type, aggregate_id)? else {
2247 return Ok(None);
2248 };
2249 if version > meta.version {
2250 return Ok(None);
2251 }
2252 if version == meta.version {
2253 return Ok(Some(meta.merkle_root.clone()));
2254 }
2255 let end = version as usize;
2256 let hashes = &meta.event_hashes[..end];
2257 Ok(Some(compute_merkle_root(hashes)))
2258 }
2259
2260 pub fn event_hashes(
2261 &self,
2262 aggregate_type: &str,
2263 aggregate_id: &str,
2264 ) -> Result<Option<Vec<String>>> {
2265 let Some((meta, _)) = self.load_meta_any(aggregate_type, aggregate_id)? else {
2266 return Ok(None);
2267 };
2268 Ok(Some(meta.event_hashes.clone()))
2269 }
2270
2271 pub fn event_by_version(
2272 &self,
2273 aggregate_type: &str,
2274 aggregate_id: &str,
2275 version: u64,
2276 ) -> Result<Option<EventRecord>> {
2277 if version == 0 {
2278 return Ok(None);
2279 }
2280 let (meta, index) = match self.load_meta_any(aggregate_type, aggregate_id)? {
2281 Some((meta, idx)) => (meta, idx),
2282 None => return Ok(None),
2283 };
2284 if version > meta.version {
2285 return Ok(None);
2286 }
2287
2288 let key = event_key_for(index, aggregate_type, aggregate_id, version);
2289 let value = match self
2290 .db
2291 .get(key)
2292 .map_err(|err| EventError::Storage(err.to_string()))?
2293 {
2294 Some(bytes) => bytes,
2295 None => return Ok(None),
2296 };
2297 let stored: EventRecord = serde_json::from_slice(&value)?;
2298 let decoded = self.decode_record(stored)?;
2299 Ok(Some(decoded))
2300 }
2301
2302 pub fn list_events_paginated(
2303 &self,
2304 aggregate_type: &str,
2305 aggregate_id: &str,
2306 skip: usize,
2307 take: Option<usize>,
2308 ) -> Result<Vec<EventRecord>> {
2309 match self.aggregate_version(aggregate_type, aggregate_id)? {
2310 Some(_) => {}
2311 None => return Err(EventError::AggregateNotFound),
2312 }
2313
2314 if matches!(take, Some(0)) {
2315 return Ok(Vec::new());
2316 }
2317
2318 let from_version = skip as u64;
2319 self.events_after(aggregate_type, aggregate_id, from_version, take)
2320 }
2321
2322 pub fn events_after(
2323 &self,
2324 aggregate_type: &str,
2325 aggregate_id: &str,
2326 from_version: u64,
2327 limit: Option<usize>,
2328 ) -> Result<Vec<EventRecord>> {
2329 let start = Instant::now();
2330 let result = (|| {
2331 let (_, index) = self
2332 .load_meta_any(aggregate_type, aggregate_id)?
2333 .ok_or(EventError::AggregateNotFound)?;
2334 let start_key = event_key_for(index, aggregate_type, aggregate_id, from_version + 1);
2335 let prefix = event_prefix_for(index, aggregate_type, aggregate_id);
2336 let iter = self
2337 .db
2338 .iterator(IteratorMode::From(start_key.as_slice(), Direction::Forward));
2339
2340 let mut events = Vec::new();
2341 for item in iter {
2342 let (key, value) = item.map_err(|err| EventError::Storage(err.to_string()))?;
2343 if !key.starts_with(prefix.as_slice()) {
2344 break;
2345 }
2346 let record: EventRecord = serde_json::from_slice(&value)?;
2347 let record = self.decode_record(record)?;
2348 if record.version <= from_version {
2349 continue;
2350 }
2351 events.push(record);
2352 if let Some(limit) = limit {
2353 if events.len() >= limit {
2354 break;
2355 }
2356 }
2357 }
2358
2359 Ok(events)
2360 })();
2361 let duration = start.elapsed().as_secs_f64();
2362 record_store_op(
2363 "rocksdb_iter_events",
2364 if result.is_ok() { "ok" } else { "err" },
2365 duration,
2366 );
2367 result
2368 }
2369
2370 pub fn events_paginated(
2371 &self,
2372 scope: EventQueryScope<'_>,
2373 archive_scope: EventArchiveScope,
2374 skip: usize,
2375 take: Option<usize>,
2376 sort: Option<&[EventSort]>,
2377 filter: Option<&FilterExpr>,
2378 ) -> Result<Vec<EventRecord>> {
2379 match archive_scope {
2380 EventArchiveScope::ActiveOnly => Ok(self.collect_events_paginated(
2381 AggregateIndex::Active,
2382 scope,
2383 skip,
2384 take,
2385 sort,
2386 filter,
2387 )),
2388 EventArchiveScope::ArchivedOnly => Ok(self.collect_events_paginated(
2389 AggregateIndex::Archived,
2390 scope,
2391 skip,
2392 take,
2393 sort,
2394 filter,
2395 )),
2396 EventArchiveScope::IncludeArchived => {
2397 let mut active = self.collect_events_paginated(
2398 AggregateIndex::Active,
2399 scope,
2400 0,
2401 None,
2402 None,
2403 filter,
2404 );
2405 let mut archived = self.collect_events_paginated(
2406 AggregateIndex::Archived,
2407 scope,
2408 0,
2409 None,
2410 None,
2411 filter,
2412 );
2413 active.append(&mut archived);
2414
2415 if let Some(keys) = sort {
2416 active.sort_by(|a, b| compare_event_sort_keys(a, b, keys));
2417 }
2418
2419 let total = active.len();
2420 let start = skip.min(total);
2421 let end = if let Some(limit) = take {
2422 start.saturating_add(limit).min(total)
2423 } else {
2424 total
2425 };
2426 Ok(active[start..end].to_vec())
2427 }
2428 }
2429 }
2430
2431 pub fn find_event_by_id(&self, event_id: SnowflakeId) -> Result<Option<EventRecord>> {
2432 if let Some(event) = self.find_event_by_id_in_index(AggregateIndex::Active, event_id)? {
2433 return Ok(Some(event));
2434 }
2435 self.find_event_by_id_in_index(AggregateIndex::Archived, event_id)
2436 }
2437
2438 pub fn aggregate_version(
2439 &self,
2440 aggregate_type: &str,
2441 aggregate_id: &str,
2442 ) -> Result<Option<u64>> {
2443 let meta = self.load_meta_any(aggregate_type, aggregate_id)?;
2444 Ok(meta.map(|(meta, _)| meta.version))
2445 }
2446
2447 pub fn list_aggregate_ids(&self, aggregate_type: &str) -> Result<Vec<String>> {
2448 let start = Instant::now();
2449 let result = (|| {
2450 let mut prefix = key_with_segments(&[PREFIX_META, aggregate_type]);
2451 prefix.push(SEP);
2452
2453 let iter = self
2454 .db
2455 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
2456
2457 let mut ids = Vec::new();
2458 for item in iter {
2459 let (key, _) = item.map_err(|err| EventError::Storage(err.to_string()))?;
2460 if !key.starts_with(prefix.as_slice()) {
2461 break;
2462 }
2463 let remainder = &key[prefix.len()..];
2464 if remainder.is_empty() {
2465 continue;
2466 }
2467 let id = std::str::from_utf8(remainder)
2468 .map_err(|err| EventError::Storage(err.to_string()))?;
2469 ids.push(id.to_string());
2470 }
2471 Ok(ids)
2472 })();
2473 let duration = start.elapsed().as_secs_f64();
2474 record_store_op(
2475 "rocksdb_iter_meta_ids",
2476 if result.is_ok() { "ok" } else { "err" },
2477 duration,
2478 );
2479 result
2480 }
2481
2482 pub fn verify(&self, aggregate_type: &str, aggregate_id: &str) -> Result<String> {
2483 let (meta, _) = self
2484 .load_meta_any(aggregate_type, aggregate_id)?
2485 .ok_or(EventError::AggregateNotFound)?;
2486 Ok(meta.merkle_root)
2487 }
2488
2489 pub fn create_snapshot(
2490 &self,
2491 aggregate_type: &str,
2492 aggregate_id: &str,
2493 comment: Option<String>,
2494 ) -> Result<SnapshotRecord> {
2495 self.ensure_writable()?;
2496 let _guard = self.write_lock.lock();
2497 let state = self.get_aggregate_state(aggregate_type, aggregate_id)?;
2498 let created_at = Utc::now();
2499 let record = SnapshotRecord {
2500 snapshot_id: Some(self.next_snowflake_id()),
2501 aggregate_type: state.aggregate_type.clone(),
2502 aggregate_id: state.aggregate_id.clone(),
2503 version: state.version,
2504 state: state.state.clone(),
2505 merkle_root: state.merkle_root.clone(),
2506 created_at,
2507 comment,
2508 };
2509
2510 let key = snapshot_key(aggregate_type, aggregate_id, created_at);
2511 self.db
2512 .put(key.clone(), serde_json::to_vec(&record)?)
2513 .map_err(|err| EventError::Storage(err.to_string()))?;
2514 if let Some(snapshot_id) = record.snapshot_id {
2515 let index_key = snapshot_index_key(snapshot_id);
2516 self.db
2517 .put(index_key, key)
2518 .map_err(|err| EventError::Storage(err.to_string()))?;
2519 }
2520
2521 Ok(record)
2522 }
2523
2524 pub fn list_snapshots(
2525 &self,
2526 aggregate_type: Option<&str>,
2527 aggregate_id: Option<&str>,
2528 version: Option<u64>,
2529 ) -> Result<Vec<SnapshotRecord>> {
2530 let start = Instant::now();
2531 let mut segments = vec![PREFIX_SNAPSHOT];
2532 if let Some(aggregate_type) = aggregate_type {
2533 segments.push(aggregate_type);
2534 if let Some(aggregate_id) = aggregate_id {
2535 segments.push(aggregate_id);
2536 }
2537 }
2538 let prefix = key_with_segments(&segments);
2539 let result = (|| {
2540 let mut snapshots = Vec::new();
2541 let iter = self
2542 .db
2543 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
2544
2545 for item in iter {
2546 let (key, value) = item.map_err(|err| EventError::Storage(err.to_string()))?;
2547 if !key.starts_with(prefix.as_slice()) {
2548 break;
2549 }
2550
2551 let snapshot: SnapshotRecord = serde_json::from_slice(&value).map_err(|err| {
2552 EventError::Storage(format!("failed to deserialize snapshot: {err}"))
2553 })?;
2554 if version.map_or(true, |v| v == snapshot.version) {
2555 snapshots.push(snapshot);
2556 }
2557 }
2558 Ok(snapshots)
2559 })();
2560
2561 let duration = start.elapsed().as_secs_f64();
2562 record_store_op(
2563 "rocksdb_list_snapshots",
2564 if result.is_ok() { "ok" } else { "err" },
2565 duration,
2566 );
2567 result
2568 }
2569
2570 pub fn find_snapshot_by_id(&self, snapshot_id: SnowflakeId) -> Result<Option<SnapshotRecord>> {
2571 let start = Instant::now();
2572 let result = (|| {
2573 let index_key = snapshot_index_key(snapshot_id);
2575 if let Some(snapshot_key) = self
2576 .db
2577 .get(index_key)
2578 .map_err(|err| EventError::Storage(err.to_string()))?
2579 {
2580 let Some(value) = self
2581 .db
2582 .get(snapshot_key)
2583 .map_err(|err| EventError::Storage(err.to_string()))?
2584 else {
2585 return Ok(None);
2586 };
2587 let snapshot: SnapshotRecord = serde_json::from_slice(&value).map_err(|err| {
2588 EventError::Storage(format!("failed to deserialize snapshot: {err}"))
2589 })?;
2590 return Ok(Some(snapshot));
2591 }
2592
2593 let prefix = key_with_segments(&[PREFIX_SNAPSHOT]);
2595 let iter = self
2596 .db
2597 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
2598 for item in iter {
2599 let (key, value) = item.map_err(|err| EventError::Storage(err.to_string()))?;
2600 if !key.starts_with(prefix.as_slice()) {
2601 break;
2602 }
2603 let snapshot: SnapshotRecord = serde_json::from_slice(&value).map_err(|err| {
2604 EventError::Storage(format!("failed to deserialize snapshot: {err}"))
2605 })?;
2606 if snapshot.snapshot_id == Some(snapshot_id) {
2607 return Ok(Some(snapshot));
2608 }
2609 }
2610 Ok(None)
2611 })();
2612
2613 let duration = start.elapsed().as_secs_f64();
2614 record_store_op(
2615 "rocksdb_find_snapshot",
2616 if result.is_ok() { "ok" } else { "err" },
2617 duration,
2618 );
2619 result
2620 }
2621
2622 pub fn set_archive(
2623 &self,
2624 aggregate_type: &str,
2625 aggregate_id: &str,
2626 archived: bool,
2627 comment: Option<String>,
2628 ) -> Result<AggregateState> {
2629 self.ensure_writable()?;
2630 let _guard = self.write_lock.lock();
2631 let (mut meta, current_index) = self
2632 .load_meta_any(aggregate_type, aggregate_id)?
2633 .ok_or(EventError::AggregateNotFound)?;
2634 let previous_meta = meta.clone();
2635 let state_map = self.load_state_map_from(current_index, aggregate_type, aggregate_id)?;
2636
2637 let changed_at = Utc::now();
2638 meta.archived = archived;
2639 if archived {
2640 meta.archived_at = Some(changed_at);
2641 meta.archive_comment = comment;
2642 } else {
2643 meta.archived_at = None;
2644 meta.archive_comment = None;
2645 }
2646 meta.updated_at = Some(changed_at);
2647
2648 let destination_index = if archived {
2649 AggregateIndex::Archived
2650 } else {
2651 AggregateIndex::Active
2652 };
2653
2654 let encoded_meta = serde_json::to_vec(&meta)?;
2655 let encoded_state = self.encode_state_map(&state_map)?;
2656
2657 let mut batch = WriteBatch::default();
2658 if current_index != destination_index {
2659 self.move_event_stream(
2660 aggregate_type,
2661 aggregate_id,
2662 current_index,
2663 destination_index,
2664 &mut batch,
2665 )?;
2666 }
2667 match (current_index, destination_index) {
2668 (AggregateIndex::Active, AggregateIndex::Archived) => {
2669 self.sync_timestamp_indexes(
2670 &mut batch,
2671 AggregateIndex::Active,
2672 Some(&previous_meta),
2673 None,
2674 );
2675 self.sync_timestamp_indexes(
2676 &mut batch,
2677 AggregateIndex::Archived,
2678 None,
2679 Some(&meta),
2680 );
2681 batch.delete(meta_key(aggregate_type, aggregate_id));
2682 batch.delete(state_key(aggregate_type, aggregate_id));
2683 batch_put(
2684 &mut batch,
2685 meta_archived_key(aggregate_type, aggregate_id),
2686 encoded_meta.clone(),
2687 )?;
2688 batch_put(
2689 &mut batch,
2690 state_archived_key(aggregate_type, aggregate_id),
2691 encoded_state.clone(),
2692 )?;
2693 }
2694 (AggregateIndex::Archived, AggregateIndex::Active) => {
2695 self.sync_timestamp_indexes(
2696 &mut batch,
2697 AggregateIndex::Archived,
2698 Some(&previous_meta),
2699 None,
2700 );
2701 self.sync_timestamp_indexes(&mut batch, AggregateIndex::Active, None, Some(&meta));
2702 batch.delete(meta_archived_key(aggregate_type, aggregate_id));
2703 batch.delete(state_archived_key(aggregate_type, aggregate_id));
2704 batch_put(
2705 &mut batch,
2706 meta_key(aggregate_type, aggregate_id),
2707 encoded_meta.clone(),
2708 )?;
2709 batch_put(
2710 &mut batch,
2711 state_key(aggregate_type, aggregate_id),
2712 encoded_state.clone(),
2713 )?;
2714 }
2715 (AggregateIndex::Active, AggregateIndex::Active) => {
2716 self.sync_timestamp_indexes(
2717 &mut batch,
2718 AggregateIndex::Active,
2719 Some(&previous_meta),
2720 Some(&meta),
2721 );
2722 batch_put(
2723 &mut batch,
2724 meta_key(aggregate_type, aggregate_id),
2725 encoded_meta.clone(),
2726 )?;
2727 batch_put(
2728 &mut batch,
2729 state_key(aggregate_type, aggregate_id),
2730 encoded_state.clone(),
2731 )?;
2732 }
2733 (AggregateIndex::Archived, AggregateIndex::Archived) => {
2734 self.sync_timestamp_indexes(
2735 &mut batch,
2736 AggregateIndex::Archived,
2737 Some(&previous_meta),
2738 Some(&meta),
2739 );
2740 batch_put(
2741 &mut batch,
2742 meta_archived_key(aggregate_type, aggregate_id),
2743 encoded_meta.clone(),
2744 )?;
2745 batch_put(
2746 &mut batch,
2747 state_archived_key(aggregate_type, aggregate_id),
2748 encoded_state.clone(),
2749 )?;
2750 }
2751 }
2752
2753 self.write_batch(batch)?;
2754
2755 self.get_aggregate_state(aggregate_type, aggregate_id)
2756 }
2757
2758 fn move_event_stream(
2759 &self,
2760 aggregate_type: &str,
2761 aggregate_id: &str,
2762 from: AggregateIndex,
2763 to: AggregateIndex,
2764 batch: &mut WriteBatch,
2765 ) -> Result<()> {
2766 if from == to {
2767 return Ok(());
2768 }
2769
2770 let prefix = event_prefix_for(from, aggregate_type, aggregate_id);
2771 let mut iter = self
2772 .db
2773 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
2774 let mut records = Vec::new();
2775
2776 while let Some(item) = iter.next() {
2777 let (key, value) = item.map_err(|err| EventError::Storage(err.to_string()))?;
2778 if !key.starts_with(prefix.as_slice()) {
2779 break;
2780 }
2781 let version = parse_event_version(&key)?;
2782 records.push((key, value.to_vec(), version));
2783 }
2784
2785 for (old_key, value, version) in records {
2786 batch.delete(old_key);
2787 batch.put(
2788 event_key_for(to, aggregate_type, aggregate_id, version),
2789 value,
2790 );
2791 }
2792
2793 Ok(())
2794 }
2795
2796 fn encode_record(&self, record: &EventRecord) -> Result<EventRecord> {
2797 if let Some(enc) = &self.encryptor {
2798 if encryption::extract_encrypted_value(&record.payload).is_some() {
2799 return Ok(record.clone());
2800 }
2801 let payload_bytes = serde_json::to_vec(&record.payload)?;
2802 let ciphertext = enc.encrypt_to_string(&payload_bytes)?;
2803 let mut stored = record.clone();
2804 stored.payload = encryption::wrap_encrypted_value(ciphertext);
2805 Ok(stored)
2806 } else {
2807 Ok(record.clone())
2808 }
2809 }
2810
2811 fn decode_record(&self, mut record: EventRecord) -> Result<EventRecord> {
2812 if let Some(enc) = &self.encryptor {
2813 if let Some(ciphertext) = encryption::extract_encrypted_value(&record.payload) {
2814 let bytes = enc.decrypt_from_str(ciphertext)?;
2815 record.payload = serde_json::from_slice(&bytes)?;
2816 }
2817 Ok(record)
2818 } else {
2819 if encryption::extract_encrypted_value(&record.payload).is_some() {
2820 return Err(EventError::Config(
2821 "data encryption key must be configured to read encrypted events".to_string(),
2822 ));
2823 }
2824 Ok(record)
2825 }
2826 }
2827
2828 fn encode_state_map(&self, state: &BTreeMap<String, String>) -> Result<Vec<u8>> {
2829 let json = serde_json::to_vec(state)?;
2830 if let Some(enc) = &self.encryptor {
2831 let ciphertext = enc.encrypt_to_string(&json)?;
2832 Ok(ciphertext.into_bytes())
2833 } else {
2834 Ok(json)
2835 }
2836 }
2837
2838 fn decode_state_map(&self, bytes: &[u8]) -> Result<BTreeMap<String, String>> {
2839 if let Some(enc) = &self.encryptor {
2840 let text = str::from_utf8(bytes).map_err(|err| EventError::Storage(err.to_string()))?;
2841 if encryption::is_encrypted_blob(text.trim()) {
2842 let decrypted = enc.decrypt_from_str(text.trim())?;
2843 Ok(serde_json::from_slice(&decrypted)?)
2844 } else if text.trim().is_empty() {
2845 Ok(BTreeMap::new())
2846 } else {
2847 Ok(serde_json::from_str(text)?)
2848 }
2849 } else {
2850 if let Ok(text) = str::from_utf8(bytes) {
2851 if encryption::is_encrypted_blob(text.trim()) {
2852 return Err(EventError::Config(
2853 "data encryption key must be configured to read encrypted aggregates"
2854 .to_string(),
2855 ));
2856 }
2857 }
2858 Ok(serde_json::from_slice(bytes)?)
2859 }
2860 }
2861
2862 fn ensure_writable(&self) -> Result<()> {
2863 if self.read_only {
2864 Err(EventError::Storage(
2865 "event store opened in read-only mode".into(),
2866 ))
2867 } else {
2868 Ok(())
2869 }
2870 }
2871
2872 fn write_batch(&self, batch: WriteBatch) -> Result<()> {
2873 let start = Instant::now();
2874 let result = self
2875 .db
2876 .write(batch)
2877 .map_err(|err| EventError::Storage(err.to_string()));
2878 let duration = start.elapsed().as_secs_f64();
2879 record_store_op(
2880 "rocksdb_write",
2881 if result.is_ok() { "ok" } else { "err" },
2882 duration,
2883 );
2884 result
2885 }
2886
2887 fn sync_timestamp_indexes(
2888 &self,
2889 batch: &mut WriteBatch,
2890 index: AggregateIndex,
2891 previous: Option<&AggregateMeta>,
2892 current: Option<&AggregateMeta>,
2893 ) {
2894 self.sync_single_timestamp_index(
2895 batch,
2896 TimestampIndexKind::CreatedAt,
2897 index,
2898 previous,
2899 current,
2900 );
2901 self.sync_single_timestamp_index(
2902 batch,
2903 TimestampIndexKind::UpdatedAt,
2904 index,
2905 previous,
2906 current,
2907 );
2908 }
2909
2910 fn sync_single_timestamp_index(
2911 &self,
2912 batch: &mut WriteBatch,
2913 kind: TimestampIndexKind,
2914 index: AggregateIndex,
2915 previous: Option<&AggregateMeta>,
2916 current: Option<&AggregateMeta>,
2917 ) {
2918 let meta_identity = current.or(previous);
2919 let Some(meta) = meta_identity else {
2920 return;
2921 };
2922 let prev_ts = previous.and_then(|meta| meta_timestamp_ms(meta, kind));
2923 let curr_ts = current.and_then(|meta| meta_timestamp_ms(meta, kind));
2924 if prev_ts == curr_ts {
2925 return;
2926 }
2927 if let Some(prev) = prev_ts {
2928 batch.delete(timestamp_index_key(
2929 kind,
2930 index,
2931 prev,
2932 &meta.aggregate_type,
2933 &meta.aggregate_id,
2934 ));
2935 }
2936 if let Some(curr) = curr_ts {
2937 batch.put(
2938 timestamp_index_key(kind, index, curr, &meta.aggregate_type, &meta.aggregate_id),
2939 Vec::new(),
2940 );
2941 }
2942 }
2943
2944 pub fn aggregates(&self) -> Vec<AggregateState> {
2945 self.aggregates_paginated(0, None)
2946 }
2947
2948 pub fn aggregates_paginated(&self, skip: usize, take: Option<usize>) -> Vec<AggregateState> {
2949 self.aggregates_paginated_with_transform_internal(
2950 skip,
2951 take,
2952 None,
2953 AggregateQueryScope::ActiveOnly,
2954 None,
2955 true,
2956 |aggregate| Some(aggregate),
2957 )
2958 }
2959
2960 pub fn aggregates_paginated_with_transform<F>(
2961 &self,
2962 skip: usize,
2963 take: Option<usize>,
2964 sort: Option<&[AggregateSort]>,
2965 scope: AggregateQueryScope,
2966 cursor: Option<&AggregateCursor>,
2967 transform: F,
2968 ) -> Vec<AggregateState>
2969 where
2970 F: FnMut(AggregateState) -> Option<AggregateState>,
2971 {
2972 self.aggregates_paginated_with_transform_internal(
2973 skip, take, sort, scope, cursor, true, transform,
2974 )
2975 }
2976
2977 pub fn aggregates_paginated_without_state<F>(
2978 &self,
2979 skip: usize,
2980 take: Option<usize>,
2981 sort: Option<&[AggregateSort]>,
2982 scope: AggregateQueryScope,
2983 cursor: Option<&AggregateCursor>,
2984 transform: F,
2985 ) -> Vec<AggregateState>
2986 where
2987 F: FnMut(AggregateState) -> Option<AggregateState>,
2988 {
2989 self.aggregates_paginated_with_transform_internal(
2990 skip, take, sort, scope, cursor, false, transform,
2991 )
2992 }
2993
2994 fn aggregates_paginated_with_transform_internal<F>(
2995 &self,
2996 skip: usize,
2997 take: Option<usize>,
2998 sort: Option<&[AggregateSort]>,
2999 scope: AggregateQueryScope,
3000 cursor: Option<&AggregateCursor>,
3001 include_state: bool,
3002 mut transform: F,
3003 ) -> Vec<AggregateState>
3004 where
3005 F: FnMut(AggregateState) -> Option<AggregateState>,
3006 {
3007 if let Some(keys) = sort {
3008 if let Some((kind, descending)) = timestamp_sort_hint(keys) {
3009 return self.collect_timestamp_sorted(
3010 kind,
3011 scope,
3012 skip,
3013 take,
3014 descending,
3015 cursor,
3016 include_state,
3017 &mut transform,
3018 );
3019 }
3020 }
3021 match scope {
3022 AggregateQueryScope::ActiveOnly => self.collect_index_paginated(
3023 AggregateIndex::Active,
3024 skip,
3025 take,
3026 sort,
3027 include_state,
3028 &mut transform,
3029 ),
3030 AggregateQueryScope::ArchivedOnly => self.collect_index_paginated(
3031 AggregateIndex::Archived,
3032 skip,
3033 take,
3034 sort,
3035 include_state,
3036 &mut transform,
3037 ),
3038 AggregateQueryScope::IncludeArchived => {
3039 let mut items = self.collect_index_paginated(
3040 AggregateIndex::Active,
3041 0,
3042 None,
3043 None,
3044 include_state,
3045 &mut transform,
3046 );
3047 let mut archived_items = self.collect_index_paginated(
3048 AggregateIndex::Archived,
3049 0,
3050 None,
3051 None,
3052 include_state,
3053 &mut transform,
3054 );
3055 items.append(&mut archived_items);
3056
3057 if let Some(keys) = sort {
3058 items.sort_by(|a, b| compare_aggregate_sort_keys(a, b, keys));
3059 }
3060
3061 let total = items.len();
3062 let start = skip.min(total);
3063 let end = if let Some(limit) = take {
3064 start.saturating_add(limit).min(total)
3065 } else {
3066 total
3067 };
3068 items[start..end].to_vec()
3069 }
3070 }
3071 }
3072
3073 fn collect_timestamp_sorted<F>(
3074 &self,
3075 kind: TimestampIndexKind,
3076 scope: AggregateQueryScope,
3077 skip: usize,
3078 take: Option<usize>,
3079 descending: bool,
3080 cursor: Option<&AggregateCursor>,
3081 include_state: bool,
3082 transform: &mut F,
3083 ) -> Vec<AggregateState>
3084 where
3085 F: FnMut(AggregateState) -> Option<AggregateState>,
3086 {
3087 match scope {
3088 AggregateQueryScope::ActiveOnly => self.collect_timestamp_index_paginated(
3089 kind,
3090 AggregateIndex::Active,
3091 skip,
3092 take,
3093 descending,
3094 cursor,
3095 include_state,
3096 transform,
3097 ),
3098 AggregateQueryScope::ArchivedOnly => self.collect_timestamp_index_paginated(
3099 kind,
3100 AggregateIndex::Archived,
3101 skip,
3102 take,
3103 descending,
3104 cursor,
3105 include_state,
3106 transform,
3107 ),
3108 AggregateQueryScope::IncludeArchived => self.collect_timestamp_indexes_merged(
3109 kind,
3110 skip,
3111 take,
3112 descending,
3113 cursor,
3114 include_state,
3115 transform,
3116 ),
3117 }
3118 }
3119
3120 fn collect_timestamp_index_paginated<F>(
3121 &self,
3122 kind: TimestampIndexKind,
3123 index: AggregateIndex,
3124 skip: usize,
3125 take: Option<usize>,
3126 descending: bool,
3127 cursor: Option<&AggregateCursor>,
3128 include_state: bool,
3129 transform: &mut F,
3130 ) -> Vec<AggregateState>
3131 where
3132 F: FnMut(AggregateState) -> Option<AggregateState>,
3133 {
3134 let mut stream = TimestampIndexIterator::new(self, kind, index, descending);
3135 let mut items = Vec::new();
3136 let mut skipped = 0usize;
3137 let cursor_key = cursor.and_then(|token| {
3138 token.as_timestamp_key().and_then(
3139 |(cursor_kind, cursor_desc, cursor_ts, cursor_index, agg_type, agg_id)| {
3140 if cursor_kind == kind
3141 && cursor_desc == descending
3142 && cursor_index == CursorIndex::from(index)
3143 {
3144 Some((cursor_ts, agg_type.to_owned(), agg_id.to_owned()))
3145 } else {
3146 None
3147 }
3148 },
3149 )
3150 });
3151 let mut resume = cursor_key;
3152
3153 while let Some(entry) = stream.next_entry() {
3154 let aggregate = match self.load_aggregate_from_index(
3155 index,
3156 &entry.aggregate_type,
3157 &entry.aggregate_id,
3158 include_state,
3159 ) {
3160 Ok(Some(aggregate)) => aggregate,
3161 Ok(None) => continue,
3162 Err(_) => {
3163 stream.mark_error();
3164 continue;
3165 }
3166 };
3167
3168 let mut consume_cursor = false;
3169 if let Some((cursor_ts, cursor_type, cursor_id)) = resume.as_ref() {
3170 let ordering = compare_timestamp_values(
3171 entry.timestamp_ms,
3172 &entry.aggregate_type,
3173 &entry.aggregate_id,
3174 *cursor_ts,
3175 cursor_type,
3176 cursor_id,
3177 );
3178 let skip_entry = if descending {
3179 ordering != StdOrdering::Less
3180 } else {
3181 ordering != StdOrdering::Greater
3182 };
3183 if skip_entry {
3184 continue;
3185 }
3186 consume_cursor = true;
3187 }
3188 if consume_cursor {
3189 resume = None;
3190 }
3191
3192 let Some(aggregate) = transform(aggregate) else {
3193 continue;
3194 };
3195
3196 if skipped < skip {
3197 skipped += 1;
3198 continue;
3199 }
3200 items.push(aggregate);
3201 if let Some(limit) = take {
3202 if items.len() >= limit {
3203 break;
3204 }
3205 }
3206 }
3207
3208 items
3209 }
3210
3211 fn collect_timestamp_indexes_merged<F>(
3212 &self,
3213 kind: TimestampIndexKind,
3214 skip: usize,
3215 take: Option<usize>,
3216 descending: bool,
3217 cursor: Option<&AggregateCursor>,
3218 include_state: bool,
3219 transform: &mut F,
3220 ) -> Vec<AggregateState>
3221 where
3222 F: FnMut(AggregateState) -> Option<AggregateState>,
3223 {
3224 let mut active_stream =
3225 TimestampIndexIterator::new(self, kind, AggregateIndex::Active, descending);
3226 let mut archived_stream =
3227 TimestampIndexIterator::new(self, kind, AggregateIndex::Archived, descending);
3228 let mut next_active = active_stream.next_entry();
3229 let mut next_archived = archived_stream.next_entry();
3230 let mut skipped = 0usize;
3231 let mut items = Vec::new();
3232 let cursor_key = cursor.and_then(|token| {
3233 token.as_timestamp_key().and_then(
3234 |(cursor_kind, cursor_desc, cursor_ts, _cursor_index, agg_type, agg_id)| {
3235 if cursor_kind == kind && cursor_desc == descending {
3236 Some((cursor_ts, agg_type.to_owned(), agg_id.to_owned()))
3237 } else {
3238 None
3239 }
3240 },
3241 )
3242 });
3243 let mut resume = cursor_key;
3244
3245 loop {
3246 let (source_index, entry) = match (&next_active, &next_archived) {
3247 (None, None) => break,
3248 (Some(_), None) => (AggregateIndex::Active, next_active.take()),
3249 (None, Some(_)) => (AggregateIndex::Archived, next_archived.take()),
3250 (Some(active), Some(archived)) => {
3251 if compare_timestamp_entries(active, archived, descending)
3252 != StdOrdering::Greater
3253 {
3254 (AggregateIndex::Active, next_active.take())
3255 } else {
3256 (AggregateIndex::Archived, next_archived.take())
3257 }
3258 }
3259 };
3260
3261 let Some(entry) = entry else {
3262 break;
3263 };
3264
3265 let aggregate = match self.load_aggregate_from_index(
3266 source_index,
3267 &entry.aggregate_type,
3268 &entry.aggregate_id,
3269 include_state,
3270 ) {
3271 Ok(Some(aggregate)) => aggregate,
3272 Ok(None) => {
3273 match source_index {
3274 AggregateIndex::Active => next_active = active_stream.next_entry(),
3275 AggregateIndex::Archived => next_archived = archived_stream.next_entry(),
3276 }
3277 continue;
3278 }
3279 Err(_) => {
3280 match source_index {
3281 AggregateIndex::Active => active_stream.mark_error(),
3282 AggregateIndex::Archived => archived_stream.mark_error(),
3283 }
3284 match source_index {
3285 AggregateIndex::Active => next_active = active_stream.next_entry(),
3286 AggregateIndex::Archived => next_archived = archived_stream.next_entry(),
3287 }
3288 continue;
3289 }
3290 };
3291
3292 let mut consume_cursor = false;
3293 if let Some((cursor_ts, cursor_type, cursor_id)) = resume.as_ref() {
3294 let ordering = compare_timestamp_values(
3295 entry.timestamp_ms,
3296 &entry.aggregate_type,
3297 &entry.aggregate_id,
3298 *cursor_ts,
3299 cursor_type,
3300 cursor_id,
3301 );
3302 let skip_entry = if descending {
3303 ordering != StdOrdering::Less
3304 } else {
3305 ordering != StdOrdering::Greater
3306 };
3307 if skip_entry {
3308 match source_index {
3309 AggregateIndex::Active => next_active = active_stream.next_entry(),
3310 AggregateIndex::Archived => next_archived = archived_stream.next_entry(),
3311 }
3312 continue;
3313 }
3314 consume_cursor = true;
3315 }
3316 if consume_cursor {
3317 resume = None;
3318 }
3319
3320 let Some(aggregate) = transform(aggregate) else {
3321 match source_index {
3322 AggregateIndex::Active => next_active = active_stream.next_entry(),
3323 AggregateIndex::Archived => next_archived = archived_stream.next_entry(),
3324 }
3325 continue;
3326 };
3327
3328 if skipped < skip {
3329 skipped += 1;
3330 } else {
3331 items.push(aggregate);
3332 }
3333
3334 if let Some(limit) = take {
3335 if items.len() >= limit {
3336 break;
3337 }
3338 }
3339
3340 match source_index {
3341 AggregateIndex::Active => next_active = active_stream.next_entry(),
3342 AggregateIndex::Archived => next_archived = archived_stream.next_entry(),
3343 }
3344 }
3345
3346 items
3347 }
3348
3349 fn load_aggregate_from_index(
3350 &self,
3351 index: AggregateIndex,
3352 aggregate_type: &str,
3353 aggregate_id: &str,
3354 include_state: bool,
3355 ) -> Result<Option<AggregateState>> {
3356 let meta = match self.load_meta_from(index, aggregate_type, aggregate_id)? {
3357 Some(meta) => meta,
3358 None => return Ok(None),
3359 };
3360 let AggregateMeta {
3361 aggregate_type,
3362 aggregate_id,
3363 version,
3364 merkle_root,
3365 archived,
3366 created_at,
3367 updated_at,
3368 extensions,
3369 ..
3370 } = meta;
3371 let state =
3372 self.maybe_load_state_map(include_state, index, &aggregate_type, &aggregate_id)?;
3373 Ok(Some(AggregateState {
3374 aggregate_type,
3375 aggregate_id,
3376 version,
3377 state,
3378 extensions,
3379 merkle_root,
3380 created_at,
3381 updated_at,
3382 archived,
3383 }))
3384 }
3385
3386 fn collect_index_paginated<F>(
3387 &self,
3388 index: AggregateIndex,
3389 skip: usize,
3390 take: Option<usize>,
3391 sort: Option<&[AggregateSort]>,
3392 include_state: bool,
3393 transform: &mut F,
3394 ) -> Vec<AggregateState>
3395 where
3396 F: FnMut(AggregateState) -> Option<AggregateState>,
3397 {
3398 let start = Instant::now();
3399 let prefix = meta_prefix_for(index);
3400 let iter = self
3401 .db
3402 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
3403 let mut items = Vec::new();
3404 let mut skipped = 0usize;
3405 let should_sort = sort.map_or(false, |keys| !aggregate_sort_matches_key_order(keys));
3406 let mut status = "ok";
3407 let mut pending = if should_sort { Some(Vec::new()) } else { None };
3408
3409 for item in iter {
3410 let Ok((key, value)) = item else {
3411 continue;
3412 };
3413 if !key.starts_with(prefix.as_slice()) {
3414 break;
3415 }
3416 if key.len() > prefix.len() && key[prefix.len()] != SEP {
3417 break;
3418 }
3419
3420 let meta: AggregateMeta = match serde_json::from_slice(&value) {
3421 Ok(meta) => meta,
3422 Err(_) => continue,
3423 };
3424
3425 let AggregateMeta {
3426 aggregate_type,
3427 aggregate_id,
3428 version,
3429 merkle_root,
3430 archived,
3431 created_at,
3432 updated_at,
3433 extensions,
3434 ..
3435 } = meta;
3436 if should_sort {
3437 if let Some(list) = pending.as_mut() {
3438 list.push(AggregateSummary {
3439 aggregate_type,
3440 aggregate_id,
3441 version,
3442 merkle_root,
3443 archived,
3444 created_at,
3445 updated_at,
3446 extensions,
3447 });
3448 }
3449 continue;
3450 }
3451 let state = match self.maybe_load_state_map(
3452 include_state,
3453 index,
3454 &aggregate_type,
3455 &aggregate_id,
3456 ) {
3457 Ok(state) => state,
3458 Err(_) => {
3459 status = "err";
3460 continue;
3461 }
3462 };
3463
3464 let aggregate = AggregateState {
3465 aggregate_type,
3466 aggregate_id,
3467 version,
3468 state,
3469 extensions,
3470 merkle_root,
3471 created_at,
3472 updated_at,
3473 archived,
3474 };
3475
3476 let Some(aggregate) = transform(aggregate) else {
3477 continue;
3478 };
3479
3480 if skipped < skip && !should_sort {
3481 skipped += 1;
3482 continue;
3483 }
3484
3485 if should_sort {
3486 items.push(aggregate);
3487 continue;
3488 }
3489
3490 items.push(aggregate);
3491
3492 if let Some(limit) = take {
3493 if items.len() >= limit {
3494 break;
3495 }
3496 }
3497 }
3498
3499 if should_sort {
3500 let summaries = pending.unwrap_or_default();
3501 let (mut aggregates, had_errors) =
3502 self.load_states_parallel(index, summaries, include_state);
3503 if had_errors {
3504 status = "err";
3505 }
3506 for aggregate in aggregates.drain(..) {
3507 let Some(aggregate) = transform(aggregate) else {
3508 continue;
3509 };
3510 items.push(aggregate);
3511 }
3512 if let Some(keys) = sort {
3513 items.sort_by(|a, b| compare_aggregate_sort_keys(a, b, keys));
3514 }
3515
3516 let total = items.len();
3517 let start = skip.min(total);
3518 let end = if let Some(limit) = take {
3519 start.saturating_add(limit).min(total)
3520 } else {
3521 total
3522 };
3523 items = items[start..end].to_vec();
3524 }
3525
3526 let duration = start.elapsed().as_secs_f64();
3527 let metric = match index {
3528 AggregateIndex::Active => "rocksdb_iter_meta",
3529 AggregateIndex::Archived => "rocksdb_iter_meta_archived",
3530 };
3531 record_store_op(metric, status, duration);
3532 items
3533 }
3534
3535 pub fn aggregates_page_with_transform<F>(
3536 &self,
3537 cursor: Option<&AggregateCursor>,
3538 take: usize,
3539 scope: AggregateQueryScope,
3540 include_state: bool,
3541 transform: F,
3542 ) -> Result<(Vec<AggregateState>, Option<AggregateCursor>)>
3543 where
3544 F: FnMut(AggregateState) -> Option<AggregateState>,
3545 {
3546 self.aggregates_page_with_transform_internal(cursor, take, scope, include_state, transform)
3547 }
3548
3549 pub fn aggregates_page_without_state<F>(
3550 &self,
3551 cursor: Option<&AggregateCursor>,
3552 take: usize,
3553 scope: AggregateQueryScope,
3554 transform: F,
3555 ) -> Result<(Vec<AggregateState>, Option<AggregateCursor>)>
3556 where
3557 F: FnMut(AggregateState) -> Option<AggregateState>,
3558 {
3559 self.aggregates_page_with_transform_internal(cursor, take, scope, false, transform)
3560 }
3561
3562 fn aggregates_page_with_transform_internal<F>(
3563 &self,
3564 cursor: Option<&AggregateCursor>,
3565 take: usize,
3566 scope: AggregateQueryScope,
3567 include_state: bool,
3568 mut transform: F,
3569 ) -> Result<(Vec<AggregateState>, Option<AggregateCursor>)>
3570 where
3571 F: FnMut(AggregateState) -> Option<AggregateState>,
3572 {
3573 if take == 0 {
3574 return Ok((Vec::new(), None));
3575 }
3576
3577 match scope {
3578 AggregateQueryScope::ActiveOnly => self.collect_index_page(
3579 AggregateIndex::Active,
3580 cursor,
3581 take,
3582 include_state,
3583 &mut transform,
3584 ),
3585 AggregateQueryScope::ArchivedOnly => {
3586 if let Some(cursor) = cursor {
3587 if cursor.index() != CursorIndex::Archived {
3588 return Err(EventError::InvalidCursor(
3589 "archived listings require archived cursor values".into(),
3590 ));
3591 }
3592 }
3593 self.collect_index_page(
3594 AggregateIndex::Archived,
3595 cursor,
3596 take,
3597 include_state,
3598 &mut transform,
3599 )
3600 }
3601 AggregateQueryScope::IncludeArchived => {
3602 let mut remaining = take;
3603 let mut items = Vec::new();
3604
3605 if let Some(cursor) = cursor {
3606 if cursor.index() == CursorIndex::Archived {
3607 let (mut archived, next) = self.collect_index_page(
3608 AggregateIndex::Archived,
3609 Some(cursor),
3610 remaining,
3611 include_state,
3612 &mut transform,
3613 )?;
3614 items.append(&mut archived);
3615 return Ok((items, next));
3616 }
3617 }
3618
3619 let (mut active_items, active_cursor) = self.collect_index_page(
3620 AggregateIndex::Active,
3621 cursor,
3622 remaining,
3623 include_state,
3624 &mut transform,
3625 )?;
3626 remaining = remaining.saturating_sub(active_items.len());
3627 items.append(&mut active_items);
3628 if let Some(next) = active_cursor {
3629 return Ok((items, Some(next)));
3630 }
3631 if remaining == 0 {
3632 return Ok((items, None));
3633 }
3634 let (mut archived_items, archived_cursor) = self.collect_index_page(
3635 AggregateIndex::Archived,
3636 None,
3637 remaining,
3638 include_state,
3639 &mut transform,
3640 )?;
3641 items.append(&mut archived_items);
3642 Ok((items, archived_cursor))
3643 }
3644 }
3645 }
3646
3647 pub fn aggregates_page(
3648 &self,
3649 cursor: Option<&AggregateCursor>,
3650 take: usize,
3651 scope: AggregateQueryScope,
3652 ) -> Result<(Vec<AggregateState>, Option<AggregateCursor>)> {
3653 self.aggregates_page_with_transform(cursor, take, scope, true, |aggregate| Some(aggregate))
3654 }
3655
3656 fn collect_index_page<F>(
3657 &self,
3658 index: AggregateIndex,
3659 cursor: Option<&AggregateCursor>,
3660 take: usize,
3661 include_state: bool,
3662 transform: &mut F,
3663 ) -> Result<(Vec<AggregateState>, Option<AggregateCursor>)>
3664 where
3665 F: FnMut(AggregateState) -> Option<AggregateState>,
3666 {
3667 let start = Instant::now();
3668 let prefix = meta_prefix_for(index);
3669 let mut start_key = prefix.clone();
3670 let mut skip_current = false;
3671 if let Some(position) = cursor {
3672 if position.index() != CursorIndex::from(index) {
3673 return Err(EventError::InvalidCursor(format!(
3674 "cursor points to {} aggregates but scope uses {} records",
3675 position.index(),
3676 CursorIndex::from(index)
3677 )));
3678 }
3679 start_key = meta_key_for(index, position.aggregate_type(), position.aggregate_id());
3680 skip_current = true;
3681 }
3682 let iter = self
3683 .db
3684 .iterator(IteratorMode::From(start_key.as_slice(), Direction::Forward));
3685 let mut items = Vec::new();
3686 let mut status = "ok";
3687 let mut last_cursor = None;
3688
3689 for item in iter {
3690 let Ok((key, value)) = item else {
3691 status = "err";
3692 continue;
3693 };
3694 if !key.starts_with(prefix.as_slice()) {
3695 break;
3696 }
3697 if key.len() > prefix.len() && key[prefix.len()] != SEP {
3698 break;
3699 }
3700 if skip_current && key.as_ref() == start_key.as_slice() {
3701 skip_current = false;
3702 continue;
3703 } else if skip_current {
3704 skip_current = false;
3705 }
3706
3707 let meta: AggregateMeta = match serde_json::from_slice(&value) {
3708 Ok(meta) => meta,
3709 Err(_) => {
3710 status = "err";
3711 continue;
3712 }
3713 };
3714
3715 let AggregateMeta {
3716 aggregate_type,
3717 aggregate_id,
3718 version,
3719 merkle_root,
3720 archived,
3721 created_at,
3722 updated_at,
3723 extensions,
3724 ..
3725 } = meta;
3726 let state = match self.maybe_load_state_map(
3727 include_state,
3728 index,
3729 &aggregate_type,
3730 &aggregate_id,
3731 ) {
3732 Ok(state) => state,
3733 Err(_) => {
3734 status = "err";
3735 continue;
3736 }
3737 };
3738
3739 let aggregate = AggregateState {
3740 aggregate_type,
3741 aggregate_id,
3742 version,
3743 state,
3744 extensions,
3745 merkle_root,
3746 created_at,
3747 updated_at,
3748 archived,
3749 };
3750
3751 let Some(aggregate) = transform(aggregate) else {
3752 continue;
3753 };
3754
3755 last_cursor = Some(AggregateCursor::new(
3756 CursorIndex::from(index),
3757 aggregate.aggregate_type.clone(),
3758 aggregate.aggregate_id.clone(),
3759 ));
3760 items.push(aggregate);
3761
3762 if items.len() >= take {
3763 break;
3764 }
3765 }
3766
3767 let duration = start.elapsed().as_secs_f64();
3768 let metric = match index {
3769 AggregateIndex::Active => "rocksdb_iter_meta",
3770 AggregateIndex::Archived => "rocksdb_iter_meta_archived",
3771 };
3772 record_store_op(metric, status, duration);
3773
3774 let next_cursor = if items.len() >= take {
3775 last_cursor
3776 } else {
3777 None
3778 };
3779
3780 Ok((items, next_cursor))
3781 }
3782
3783 fn load_states_parallel(
3784 &self,
3785 index: AggregateIndex,
3786 summaries: Vec<AggregateSummary>,
3787 include_state: bool,
3788 ) -> (Vec<AggregateState>, bool) {
3789 use rayon::prelude::*;
3790
3791 let results: Vec<std::result::Result<AggregateState, EventError>> = summaries
3792 .into_par_iter()
3793 .map(|summary| {
3794 let AggregateSummary {
3795 aggregate_type,
3796 aggregate_id,
3797 version,
3798 merkle_root,
3799 archived,
3800 created_at,
3801 updated_at,
3802 extensions,
3803 } = summary;
3804 let state = self.maybe_load_state_map(
3805 include_state,
3806 index,
3807 &aggregate_type,
3808 &aggregate_id,
3809 )?;
3810 Ok(AggregateState {
3811 aggregate_type,
3812 aggregate_id,
3813 version,
3814 state,
3815 extensions,
3816 merkle_root,
3817 created_at,
3818 updated_at,
3819 archived,
3820 })
3821 })
3822 .collect();
3823
3824 let mut had_error = false;
3825 let aggregates = results
3826 .into_iter()
3827 .filter_map(|result| match result {
3828 Ok(aggregate) => Some(aggregate),
3829 Err(_) => {
3830 had_error = true;
3831 None
3832 }
3833 })
3834 .collect();
3835
3836 (aggregates, had_error)
3837 }
3838
3839 fn collect_events_paginated(
3840 &self,
3841 index: AggregateIndex,
3842 scope: EventQueryScope<'_>,
3843 skip: usize,
3844 take: Option<usize>,
3845 sort: Option<&[EventSort]>,
3846 filter: Option<&FilterExpr>,
3847 ) -> Vec<EventRecord> {
3848 let start = Instant::now();
3849 let prefix = event_prefix_for_scope(index, scope);
3850 let iter = self
3851 .db
3852 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
3853 let mut items = Vec::new();
3854 let mut skipped = 0usize;
3855 let should_sort = sort.map_or(false, |keys| !event_sort_matches_key_order(keys));
3856 let mut status = "ok";
3857
3858 for item in iter {
3859 let Ok((key, value)) = item else {
3860 status = "err";
3861 continue;
3862 };
3863 if !key.starts_with(prefix.as_slice()) {
3864 break;
3865 }
3866 if key.len() > prefix.len() && key[prefix.len()] != SEP {
3867 break;
3868 }
3869
3870 let raw: EventRecord = match serde_json::from_slice(&value) {
3871 Ok(record) => record,
3872 Err(_) => {
3873 status = "err";
3874 continue;
3875 }
3876 };
3877 let record = match self.decode_record(raw) {
3878 Ok(record) => record,
3879 Err(_) => {
3880 status = "err";
3881 continue;
3882 }
3883 };
3884
3885 if let Some(expr) = filter {
3886 if !expr.matches_event(&record) {
3887 continue;
3888 }
3889 }
3890
3891 if skipped < skip && !should_sort {
3892 skipped += 1;
3893 continue;
3894 }
3895
3896 if should_sort {
3897 items.push(record);
3898 continue;
3899 }
3900
3901 items.push(record);
3902
3903 if let Some(limit) = take {
3904 if items.len() >= limit {
3905 break;
3906 }
3907 }
3908 }
3909
3910 if should_sort {
3911 if let Some(keys) = sort {
3912 items.sort_by(|a, b| compare_event_sort_keys(a, b, keys));
3913 }
3914
3915 let total = items.len();
3916 let start = skip.min(total);
3917 let end = if let Some(limit) = take {
3918 start.saturating_add(limit).min(total)
3919 } else {
3920 total
3921 };
3922 items = items[start..end].to_vec();
3923 }
3924
3925 let duration = start.elapsed().as_secs_f64();
3926 let metric = match index {
3927 AggregateIndex::Active => "rocksdb_iter_events",
3928 AggregateIndex::Archived => "rocksdb_iter_events_archived",
3929 };
3930 record_store_op(metric, status, duration);
3931 items
3932 }
3933
3934 pub fn events_page(
3935 &self,
3936 scope: EventQueryScope<'_>,
3937 archive_scope: EventArchiveScope,
3938 cursor: Option<&EventCursor>,
3939 take: usize,
3940 filter: Option<&FilterExpr>,
3941 ) -> Result<(Vec<EventRecord>, Option<EventCursor>)> {
3942 if take == 0 {
3943 return Ok((Vec::new(), None));
3944 }
3945
3946 match archive_scope {
3947 EventArchiveScope::ActiveOnly => {
3948 self.collect_events_page(AggregateIndex::Active, scope, cursor, take, filter)
3949 }
3950 EventArchiveScope::ArchivedOnly => {
3951 if let Some(cursor) = cursor {
3952 if cursor.index() != CursorIndex::Archived {
3953 return Err(EventError::InvalidCursor(
3954 "archived listings require archived cursor values".into(),
3955 ));
3956 }
3957 }
3958 self.collect_events_page(AggregateIndex::Archived, scope, cursor, take, filter)
3959 }
3960 EventArchiveScope::IncludeArchived => {
3961 if let Some(cursor) = cursor {
3962 if cursor.index() == CursorIndex::Archived {
3963 return self.collect_events_page(
3964 AggregateIndex::Archived,
3965 scope,
3966 Some(cursor),
3967 take,
3968 filter,
3969 );
3970 }
3971 }
3972
3973 let (mut active, next_active) =
3974 self.collect_events_page(AggregateIndex::Active, scope, cursor, take, filter)?;
3975 if let Some(cursor) = next_active {
3976 return Ok((active, Some(cursor)));
3977 }
3978 if active.len() >= take {
3979 return Ok((active, None));
3980 }
3981 let remaining = take.saturating_sub(active.len());
3982 let (mut archived, next_archived) = self.collect_events_page(
3983 AggregateIndex::Archived,
3984 scope,
3985 None,
3986 remaining,
3987 filter,
3988 )?;
3989 active.append(&mut archived);
3990 Ok((active, next_archived))
3991 }
3992 }
3993 }
3994
3995 fn collect_events_page(
3996 &self,
3997 index: AggregateIndex,
3998 scope: EventQueryScope<'_>,
3999 cursor: Option<&EventCursor>,
4000 take: usize,
4001 filter: Option<&FilterExpr>,
4002 ) -> Result<(Vec<EventRecord>, Option<EventCursor>)> {
4003 let start = Instant::now();
4004 let prefix = event_prefix_for_scope(index, scope);
4005 let mut start_key = prefix.clone();
4006 let mut skip_current = false;
4007 if let Some(token) = cursor {
4008 if token.index() != CursorIndex::from(index) {
4009 return Err(EventError::InvalidCursor(format!(
4010 "cursor points to {} events but scope uses {} records",
4011 token.index(),
4012 CursorIndex::from(index)
4013 )));
4014 }
4015 if !event_cursor_matches_scope(token, scope) {
4016 return Err(EventError::InvalidCursor(
4017 "cursor does not match the requested event scope".into(),
4018 ));
4019 }
4020 start_key = event_key_for(
4021 index,
4022 token.aggregate_type(),
4023 token.aggregate_id(),
4024 token.version(),
4025 );
4026 skip_current = true;
4027 }
4028 let iter = self
4029 .db
4030 .iterator(IteratorMode::From(start_key.as_slice(), Direction::Forward));
4031 let mut items = Vec::new();
4032 let mut status = "ok";
4033 let mut last_cursor = None;
4034
4035 for item in iter {
4036 let Ok((key, value)) = item else {
4037 status = "err";
4038 continue;
4039 };
4040 if !key.starts_with(prefix.as_slice()) {
4041 break;
4042 }
4043 if skip_current && key.as_ref() == start_key.as_slice() {
4044 skip_current = false;
4045 continue;
4046 } else if skip_current {
4047 skip_current = false;
4048 }
4049
4050 let raw: EventRecord = match serde_json::from_slice(&value) {
4051 Ok(record) => record,
4052 Err(_) => {
4053 status = "err";
4054 continue;
4055 }
4056 };
4057 let record = match self.decode_record(raw) {
4058 Ok(record) => record,
4059 Err(_) => {
4060 status = "err";
4061 continue;
4062 }
4063 };
4064
4065 if let Some(expr) = filter {
4066 if !expr.matches_event(&record) {
4067 continue;
4068 }
4069 }
4070
4071 last_cursor = Some(EventCursor::new(
4072 CursorIndex::from(index),
4073 record.aggregate_type.clone(),
4074 record.aggregate_id.clone(),
4075 record.version,
4076 ));
4077 items.push(record);
4078
4079 if items.len() >= take {
4080 break;
4081 }
4082 }
4083
4084 let duration = start.elapsed().as_secs_f64();
4085 let metric = match index {
4086 AggregateIndex::Active => "rocksdb_iter_events",
4087 AggregateIndex::Archived => "rocksdb_iter_events_archived",
4088 };
4089 record_store_op(metric, status, duration);
4090
4091 let next_cursor = if items.len() >= take {
4092 last_cursor
4093 } else {
4094 None
4095 };
4096
4097 Ok((items, next_cursor))
4098 }
4099
4100 fn find_event_by_id_in_index(
4101 &self,
4102 index: AggregateIndex,
4103 event_id: SnowflakeId,
4104 ) -> Result<Option<EventRecord>> {
4105 let start = Instant::now();
4106 let prefix = key_with_segments(&[index.event_prefix()]);
4107 let iter = self
4108 .db
4109 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
4110 let mut status = "ok";
4111
4112 for item in iter {
4113 let Ok((key, value)) = item else {
4114 status = "err";
4115 continue;
4116 };
4117 if !key.starts_with(prefix.as_slice()) {
4118 break;
4119 }
4120 if key.len() > prefix.len() && key[prefix.len()] != SEP {
4121 break;
4122 }
4123
4124 let raw: EventRecord = match serde_json::from_slice(&value) {
4125 Ok(record) => record,
4126 Err(_) => {
4127 status = "err";
4128 continue;
4129 }
4130 };
4131 let record = match self.decode_record(raw) {
4132 Ok(record) => record,
4133 Err(_) => {
4134 status = "err";
4135 continue;
4136 }
4137 };
4138
4139 if record.metadata.event_id == event_id {
4140 let duration = start.elapsed().as_secs_f64();
4141 let metric = match index {
4142 AggregateIndex::Active => "rocksdb_scan_event_id",
4143 AggregateIndex::Archived => "rocksdb_scan_event_id_archived",
4144 };
4145 record_store_op(metric, status, duration);
4146 return Ok(Some(record));
4147 }
4148 }
4149
4150 let duration = start.elapsed().as_secs_f64();
4151 let metric = match index {
4152 AggregateIndex::Active => "rocksdb_scan_event_id",
4153 AggregateIndex::Archived => "rocksdb_scan_event_id_archived",
4154 };
4155 record_store_op(metric, status, duration);
4156 Ok(None)
4157 }
4158
4159 pub fn aggregate_positions(&self) -> Result<Vec<AggregatePositionEntry>> {
4160 let start = Instant::now();
4161 let result = (|| {
4162 let prefix = meta_prefix();
4163 let iter = self
4164 .db
4165 .iterator(IteratorMode::From(prefix.as_slice(), Direction::Forward));
4166 let mut items = Vec::new();
4167
4168 for item in iter {
4169 let (key, value) = item.map_err(|err| EventError::Storage(err.to_string()))?;
4170 if !key.starts_with(prefix.as_slice()) {
4171 break;
4172 }
4173 if key.len() > prefix.len() && key[prefix.len()] != SEP {
4174 break;
4175 }
4176
4177 let meta: AggregateMeta = serde_json::from_slice(&value)?;
4178 items.push(AggregatePositionEntry {
4179 aggregate_type: meta.aggregate_type,
4180 aggregate_id: meta.aggregate_id,
4181 version: meta.version,
4182 });
4183 }
4184
4185 Ok(items)
4186 })();
4187 let duration = start.elapsed().as_secs_f64();
4188 record_store_op(
4189 "rocksdb_iter_meta_positions",
4190 if result.is_ok() { "ok" } else { "err" },
4191 duration,
4192 );
4193 result
4194 }
4195
4196 fn load_meta_from(
4197 &self,
4198 index: AggregateIndex,
4199 aggregate_type: &str,
4200 aggregate_id: &str,
4201 ) -> Result<Option<AggregateMeta>> {
4202 let start = Instant::now();
4203 let result = (|| {
4204 let key = meta_key_for(index, aggregate_type, aggregate_id);
4205 let value = self
4206 .db
4207 .get(key)
4208 .map_err(|err| EventError::Storage(err.to_string()))?;
4209 if let Some(value) = value {
4210 Ok(Some(serde_json::from_slice(&value)?))
4211 } else {
4212 Ok(None)
4213 }
4214 })();
4215 let duration = start.elapsed().as_secs_f64();
4216 record_store_op(
4217 "rocksdb_get_meta",
4218 if result.is_ok() { "ok" } else { "err" },
4219 duration,
4220 );
4221 result
4222 }
4223
4224 fn load_meta_any(
4225 &self,
4226 aggregate_type: &str,
4227 aggregate_id: &str,
4228 ) -> Result<Option<(AggregateMeta, AggregateIndex)>> {
4229 if let Some(meta) =
4230 self.load_meta_from(AggregateIndex::Active, aggregate_type, aggregate_id)?
4231 {
4232 return Ok(Some((meta, AggregateIndex::Active)));
4233 }
4234 if let Some(meta) =
4235 self.load_meta_from(AggregateIndex::Archived, aggregate_type, aggregate_id)?
4236 {
4237 return Ok(Some((meta, AggregateIndex::Archived)));
4238 }
4239 Ok(None)
4240 }
4241
4242 fn load_meta(&self, aggregate_type: &str, aggregate_id: &str) -> Result<Option<AggregateMeta>> {
4243 self.load_meta_from(AggregateIndex::Active, aggregate_type, aggregate_id)
4244 }
4245
4246 pub fn create_aggregate(&self, aggregate_type: &str, aggregate_id: &str) -> Result<()> {
4247 self.ensure_writable()?;
4248 let _guard = self.write_lock.lock();
4249
4250 if self.load_meta_any(aggregate_type, aggregate_id)?.is_some() {
4251 return Err(EventError::Storage(format!(
4252 "aggregate {}:{} already exists",
4253 aggregate_type, aggregate_id
4254 )));
4255 }
4256
4257 let meta = AggregateMeta::new(aggregate_type.to_string(), aggregate_id.to_string());
4258 let state: BTreeMap<String, String> = BTreeMap::new();
4259
4260 let start_meta = Instant::now();
4261 let meta_result = self
4262 .db
4263 .put(
4264 meta_key(aggregate_type, aggregate_id),
4265 serde_json::to_vec(&meta)?,
4266 )
4267 .map_err(|err| EventError::Storage(err.to_string()));
4268 let duration_meta = start_meta.elapsed().as_secs_f64();
4269 record_store_op(
4270 "rocksdb_put_meta",
4271 if meta_result.is_ok() { "ok" } else { "err" },
4272 duration_meta,
4273 );
4274 meta_result?;
4275
4276 let start_state = Instant::now();
4277 let state_result = self
4278 .db
4279 .put(
4280 state_key(aggregate_type, aggregate_id),
4281 serde_json::to_vec(&state)?,
4282 )
4283 .map_err(|err| EventError::Storage(err.to_string()));
4284 let duration_state = start_state.elapsed().as_secs_f64();
4285 record_store_op(
4286 "rocksdb_put_state",
4287 if state_result.is_ok() { "ok" } else { "err" },
4288 duration_state,
4289 );
4290 state_result?;
4291
4292 Ok(())
4293 }
4294
4295 pub fn remove_aggregate(&self, aggregate_type: &str, aggregate_id: &str) -> Result<()> {
4296 self.ensure_writable()?;
4297 let _guard = self.write_lock.lock();
4298
4299 let Some(meta) = self.load_meta(aggregate_type, aggregate_id)? else {
4300 return Err(EventError::AggregateNotFound);
4301 };
4302
4303 if meta.version > 0 {
4304 return Err(EventError::Storage(format!(
4305 "aggregate {}:{} cannot be removed while events exist",
4306 aggregate_type, aggregate_id
4307 )));
4308 }
4309
4310 let mut batch = WriteBatch::default();
4311 batch.delete(meta_key(aggregate_type, aggregate_id));
4312 batch.delete(state_key(aggregate_type, aggregate_id));
4313 self.sync_timestamp_indexes(&mut batch, AggregateIndex::Active, Some(&meta), None);
4314 self.write_batch(batch)
4316 }
4317
4318 pub fn update_reference_index(
4319 &self,
4320 tenant: &str,
4321 aggregate_type: &str,
4322 aggregate_id: &str,
4323 references: &[(String, String)],
4324 ) -> Result<()> {
4325 self.ensure_writable()?;
4326 let _guard = self.write_lock.lock();
4327
4328 let source_key = refsrc_key(tenant, aggregate_type, aggregate_id);
4329 let prev: Vec<StoredReferencePath> = self
4330 .db
4331 .get(source_key.clone())
4332 .map_err(|err| EventError::Storage(err.to_string()))?
4333 .map(|bytes| serde_json::from_slice(&bytes))
4334 .transpose()
4335 .map_err(|err| EventError::Serialization(err.to_string()))?
4336 .unwrap_or_default();
4337
4338 let mut batch = WriteBatch::default();
4339
4340 for entry in prev {
4342 let target_key = reftgt_key(tenant, &entry.target);
4343 let existing: Vec<StoredReferrer> = self
4344 .db
4345 .get(&target_key)
4346 .map_err(|err| EventError::Storage(err.to_string()))?
4347 .map(|bytes| serde_json::from_slice(&bytes))
4348 .transpose()
4349 .map_err(|err| EventError::Serialization(err.to_string()))?
4350 .unwrap_or_default();
4351 let filtered: Vec<StoredReferrer> = existing
4352 .into_iter()
4353 .filter(|r| !(r.aggregate_type == aggregate_type && r.aggregate_id == aggregate_id))
4354 .collect();
4355 if filtered.is_empty() {
4356 batch.delete(target_key);
4357 } else {
4358 batch.put(target_key, serde_json::to_vec(&filtered)?);
4359 }
4360 }
4361
4362 let mut new_paths = Vec::new();
4364 for (target, path) in references {
4365 let path_entry = StoredReferencePath {
4366 target: target.clone(),
4367 path: path.clone(),
4368 };
4369 new_paths.push(path_entry.clone());
4370
4371 let target_key = reftgt_key(tenant, target);
4372 let mut existing: Vec<StoredReferrer> = self
4373 .db
4374 .get(&target_key)
4375 .map_err(|err| EventError::Storage(err.to_string()))?
4376 .map(|bytes| serde_json::from_slice(&bytes))
4377 .transpose()
4378 .map_err(|err| EventError::Serialization(err.to_string()))?
4379 .unwrap_or_default();
4380 if !existing.iter().any(|r| {
4381 r.aggregate_type == aggregate_type
4382 && r.aggregate_id == aggregate_id
4383 && r.path == *path
4384 }) {
4385 existing.push(StoredReferrer {
4386 aggregate_type: aggregate_type.to_string(),
4387 aggregate_id: aggregate_id.to_string(),
4388 path: path.clone(),
4389 });
4390 batch.put(target_key, serde_json::to_vec(&existing)?);
4391 }
4392 }
4393
4394 batch.put(source_key, serde_json::to_vec(&new_paths)?);
4395 self.write_batch(batch)
4396 }
4397
4398 pub fn clear_reference_index(
4399 &self,
4400 tenant: &str,
4401 aggregate_type: &str,
4402 aggregate_id: &str,
4403 ) -> Result<()> {
4404 self.update_reference_index(tenant, aggregate_type, aggregate_id, &[])
4405 }
4406
4407 pub fn referrers_for(
4408 &self,
4409 tenant: &str,
4410 target: &str,
4411 ) -> Result<Vec<(String, String, String)>> {
4412 let target_key = reftgt_key(tenant, target);
4413 let existing: Vec<StoredReferrer> = self
4414 .db
4415 .get(&target_key)
4416 .map_err(|err| EventError::Storage(err.to_string()))?
4417 .map(|bytes| serde_json::from_slice(&bytes))
4418 .transpose()
4419 .map_err(|err| EventError::Serialization(err.to_string()))?
4420 .unwrap_or_default();
4421 Ok(existing
4422 .into_iter()
4423 .map(|r| (r.aggregate_type, r.aggregate_id, r.path))
4424 .collect())
4425 }
4426
4427 pub fn referrers_for_any_tenant(
4428 &self,
4429 target: &str,
4430 ) -> Result<Vec<(String, String, String, String)>> {
4431 let mut matches = Vec::new();
4432 let prefix = PREFIX_REF_TARGET.as_bytes();
4433 for entry in self.db.prefix_iterator(prefix) {
4434 let (key, value) = entry.map_err(|err| EventError::Storage(err.to_string()))?;
4435 let segments: Vec<&[u8]> = key.split(|byte| *byte == SEP).collect();
4436 if segments.len() != 3 {
4437 continue;
4438 }
4439 let key_target = match str::from_utf8(segments[2]) {
4440 Ok(value) => value,
4441 Err(_) => continue,
4442 };
4443 if key_target != target {
4444 continue;
4445 }
4446 let tenant = match str::from_utf8(segments[1]) {
4447 Ok(value) => value.to_string(),
4448 Err(_) => continue,
4449 };
4450 let referrers: Vec<StoredReferrer> = serde_json::from_slice(&value).map_err(|err| {
4451 EventError::Serialization(format!("failed to decode referrer index: {err}"))
4452 })?;
4453 for r in referrers {
4454 matches.push((tenant.clone(), r.aggregate_type, r.aggregate_id, r.path));
4455 }
4456 }
4457 Ok(matches)
4458 }
4459
4460 fn load_state_map_from(
4461 &self,
4462 index: AggregateIndex,
4463 aggregate_type: &str,
4464 aggregate_id: &str,
4465 ) -> Result<BTreeMap<String, String>> {
4466 let start = Instant::now();
4467 let result = (|| {
4468 let key = state_key_for(index, aggregate_type, aggregate_id);
4469 let value = self
4470 .db
4471 .get(key)
4472 .map_err(|err| EventError::Storage(err.to_string()))?;
4473 if let Some(value) = value {
4474 self.decode_state_map(&value)
4475 } else {
4476 Ok(BTreeMap::new())
4477 }
4478 })();
4479 let duration = start.elapsed().as_secs_f64();
4480 record_store_op(
4481 "rocksdb_get_state",
4482 if result.is_ok() { "ok" } else { "err" },
4483 duration,
4484 );
4485 result
4486 }
4487
4488 fn maybe_load_state_map(
4489 &self,
4490 include_state: bool,
4491 index: AggregateIndex,
4492 aggregate_type: &str,
4493 aggregate_id: &str,
4494 ) -> Result<BTreeMap<String, String>> {
4495 if include_state {
4496 self.load_state_map_from(index, aggregate_type, aggregate_id)
4497 } else {
4498 Ok(BTreeMap::new())
4499 }
4500 }
4501
4502 fn load_state_map(
4503 &self,
4504 aggregate_type: &str,
4505 aggregate_id: &str,
4506 ) -> Result<BTreeMap<String, String>> {
4507 self.load_state_map_from(AggregateIndex::Active, aggregate_type, aggregate_id)
4508 }
4509}
4510
4511fn configure_db_options(options: &mut Options, read_only: bool) {
4512 if !read_only {
4513 if let Ok(parallelism) = std::thread::available_parallelism() {
4514 let workers = parallelism.get().max(2) as i32;
4515 options.increase_parallelism(workers);
4516 options.set_max_background_jobs(workers);
4517 }
4518 options.set_allow_concurrent_memtable_write(true);
4519 options.set_enable_write_thread_adaptive_yield(true);
4520 options.set_write_buffer_size(64 * 1024 * 1024);
4521 options.set_max_write_buffer_number(4);
4522 options.set_min_write_buffer_number_to_merge(2);
4523 options.set_level_zero_file_num_compaction_trigger(8);
4524 options.set_level_zero_slowdown_writes_trigger(17);
4525 options.set_level_zero_stop_writes_trigger(24);
4526 options.set_target_file_size_base(128 * 1024 * 1024);
4527 options.set_max_total_wal_size(256 * 1024 * 1024);
4528 }
4529 options.set_bytes_per_sync(2 * 1024 * 1024);
4530 options.set_optimize_filters_for_hits(true);
4531}
4532
4533fn meta_prefix_for(index: AggregateIndex) -> Vec<u8> {
4534 key_with_segments(&[index.meta_prefix()])
4535}
4536
4537fn meta_prefix() -> Vec<u8> {
4538 meta_prefix_for(AggregateIndex::Active)
4539}
4540
4541fn meta_key(aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4542 meta_key_for(AggregateIndex::Active, aggregate_type, aggregate_id)
4543}
4544
4545fn meta_archived_key(aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4546 meta_key_for(AggregateIndex::Archived, aggregate_type, aggregate_id)
4547}
4548
4549fn state_key(aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4550 state_key_for(AggregateIndex::Active, aggregate_type, aggregate_id)
4551}
4552
4553fn state_archived_key(aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4554 state_key_for(AggregateIndex::Archived, aggregate_type, aggregate_id)
4555}
4556
4557fn meta_key_for(index: AggregateIndex, aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4558 key_with_segments(&[index.meta_prefix(), aggregate_type, aggregate_id])
4559}
4560
4561fn state_key_for(index: AggregateIndex, aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4562 key_with_segments(&[index.state_prefix(), aggregate_type, aggregate_id])
4563}
4564
4565fn event_prefix_for(index: AggregateIndex, aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4566 let prefix = match index {
4567 AggregateIndex::Active => PREFIX_EVENT,
4568 AggregateIndex::Archived => PREFIX_EVENT_ARCHIVED,
4569 };
4570 let mut key = key_with_segments(&[prefix, aggregate_type, aggregate_id]);
4571 key.push(SEP);
4572 key
4573}
4574
4575fn event_key_for(
4576 index: AggregateIndex,
4577 aggregate_type: &str,
4578 aggregate_id: &str,
4579 version: u64,
4580) -> Vec<u8> {
4581 let mut key = event_prefix_for(index, aggregate_type, aggregate_id);
4582 key.extend_from_slice(&version.to_be_bytes());
4583 key
4584}
4585
4586fn event_key(aggregate_type: &str, aggregate_id: &str, version: u64) -> Vec<u8> {
4587 event_key_for(
4588 AggregateIndex::Active,
4589 aggregate_type,
4590 aggregate_id,
4591 version,
4592 )
4593}
4594
4595fn timestamp_index_prefix(kind: TimestampIndexKind, index: AggregateIndex) -> Vec<u8> {
4596 key_with_segments(&[kind.prefix(index)])
4597}
4598
4599fn timestamp_index_key(
4600 kind: TimestampIndexKind,
4601 index: AggregateIndex,
4602 timestamp_ms: i64,
4603 aggregate_type: &str,
4604 aggregate_id: &str,
4605) -> Vec<u8> {
4606 let mut key = timestamp_index_prefix(kind, index);
4607 key.push(SEP);
4608 key.extend_from_slice(×tamp_ms.to_be_bytes());
4609 key.push(SEP);
4610 key.extend_from_slice(aggregate_type.as_bytes());
4611 key.push(SEP);
4612 key.extend_from_slice(aggregate_id.as_bytes());
4613 key
4614}
4615
4616fn parse_timestamp_index_entry(prefix: &[u8], key: &[u8]) -> Option<TimestampIndexEntry> {
4617 if !key.starts_with(prefix) {
4618 return None;
4619 }
4620 let mut cursor = prefix.len();
4621 if key.get(cursor)? != &SEP {
4622 return None;
4623 }
4624 cursor += 1;
4625 let end_ts = cursor.checked_add(8)?;
4626 let timestamp_bytes: [u8; 8] = key.get(cursor..end_ts)?.try_into().ok()?;
4627 let timestamp_ms = i64::from_be_bytes(timestamp_bytes);
4628 cursor = end_ts;
4629 if key.get(cursor)? != &SEP {
4630 return None;
4631 }
4632 cursor += 1;
4633 let next_sep = key[cursor..]
4634 .iter()
4635 .position(|byte| *byte == SEP)
4636 .map(|offset| cursor + offset)?;
4637 let aggregate_type = std::str::from_utf8(&key[cursor..next_sep])
4638 .ok()?
4639 .to_string();
4640 cursor = next_sep + 1;
4641 if cursor > key.len() {
4642 return None;
4643 }
4644 let aggregate_id = std::str::from_utf8(&key[cursor..]).ok()?.to_string();
4645 Some(TimestampIndexEntry {
4646 aggregate_type,
4647 aggregate_id,
4648 timestamp_ms,
4649 })
4650}
4651
4652fn meta_timestamp_ms(meta: &AggregateMeta, kind: TimestampIndexKind) -> Option<i64> {
4653 match kind {
4654 TimestampIndexKind::CreatedAt => meta.created_at.as_ref().map(|dt| dt.timestamp_millis()),
4655 TimestampIndexKind::UpdatedAt => meta.updated_at.as_ref().map(|dt| dt.timestamp_millis()),
4656 }
4657}
4658
4659fn timestamp_index_metric(kind: TimestampIndexKind, index: AggregateIndex) -> &'static str {
4660 match (kind, index) {
4661 (TimestampIndexKind::CreatedAt, AggregateIndex::Active) => {
4662 "rocksdb_iter_meta_created_index"
4663 }
4664 (TimestampIndexKind::CreatedAt, AggregateIndex::Archived) => {
4665 "rocksdb_iter_meta_created_index_archived"
4666 }
4667 (TimestampIndexKind::UpdatedAt, AggregateIndex::Active) => {
4668 "rocksdb_iter_meta_updated_index"
4669 }
4670 (TimestampIndexKind::UpdatedAt, AggregateIndex::Archived) => {
4671 "rocksdb_iter_meta_updated_index_archived"
4672 }
4673 }
4674}
4675
4676fn compare_timestamp_entries(
4677 lhs: &TimestampIndexEntry,
4678 rhs: &TimestampIndexEntry,
4679 descending: bool,
4680) -> StdOrdering {
4681 let mut ordering = lhs.timestamp_ms.cmp(&rhs.timestamp_ms);
4682 if ordering == StdOrdering::Equal {
4683 ordering = lhs.aggregate_type.cmp(&rhs.aggregate_type);
4684 }
4685 if ordering == StdOrdering::Equal {
4686 ordering = lhs.aggregate_id.cmp(&rhs.aggregate_id);
4687 }
4688 if descending {
4689 ordering.reverse()
4690 } else {
4691 ordering
4692 }
4693}
4694
4695fn compare_timestamp_values(
4696 lhs_ts: i64,
4697 lhs_type: &str,
4698 lhs_id: &str,
4699 rhs_ts: i64,
4700 rhs_type: &str,
4701 rhs_id: &str,
4702) -> StdOrdering {
4703 let mut ordering = lhs_ts.cmp(&rhs_ts);
4704 if ordering == StdOrdering::Equal {
4705 ordering = lhs_type.cmp(rhs_type);
4706 }
4707 if ordering == StdOrdering::Equal {
4708 ordering = lhs_id.cmp(rhs_id);
4709 }
4710 ordering
4711}
4712
4713fn event_prefix_for_scope(index: AggregateIndex, scope: EventQueryScope<'_>) -> Vec<u8> {
4714 match scope {
4715 EventQueryScope::All => key_with_segments(&[index.event_prefix()]),
4716 EventQueryScope::AggregateType(aggregate_type) => {
4717 key_with_segments(&[index.event_prefix(), aggregate_type])
4718 }
4719 EventQueryScope::Aggregate {
4720 aggregate_type,
4721 aggregate_id,
4722 } => key_with_segments(&[index.event_prefix(), aggregate_type, aggregate_id]),
4723 }
4724}
4725
4726fn event_cursor_matches_scope(cursor: &EventCursor, scope: EventQueryScope<'_>) -> bool {
4727 match scope {
4728 EventQueryScope::All => true,
4729 EventQueryScope::AggregateType(aggregate_type) => cursor.aggregate_type() == aggregate_type,
4730 EventQueryScope::Aggregate {
4731 aggregate_type,
4732 aggregate_id,
4733 } => cursor.aggregate_type() == aggregate_type && cursor.aggregate_id() == aggregate_id,
4734 }
4735}
4736
4737fn snapshot_key(aggregate_type: &str, aggregate_id: &str, created_at: DateTime<Utc>) -> Vec<u8> {
4738 let mut key = snapshot_prefix(aggregate_type, aggregate_id);
4739 key.push(SEP);
4740 key.extend_from_slice(&created_at.timestamp_millis().to_be_bytes());
4741 key
4742}
4743
4744fn snapshot_prefix(aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4745 key_with_segments(&[PREFIX_SNAPSHOT, aggregate_type, aggregate_id])
4746}
4747
4748fn snapshot_index_key(snapshot_id: SnowflakeId) -> Vec<u8> {
4749 key_with_segments(&[PREFIX_SNAPSHOT_ID, &snapshot_id.to_string()])
4750}
4751
4752fn parse_event_version(key: &[u8]) -> Result<u64> {
4753 if key.len() < 8 {
4754 return Err(EventError::Storage("event key too short".into()));
4755 }
4756 let mut buf = [0u8; 8];
4757 buf.copy_from_slice(&key[key.len() - 8..]);
4758 Ok(u64::from_be_bytes(buf))
4759}
4760
4761fn key_with_segments(parts: &[&str]) -> Vec<u8> {
4762 let mut key = Vec::new();
4763 let mut iter = parts.iter();
4764 if let Some(first) = iter.next() {
4765 key.extend_from_slice(first.as_bytes());
4766 }
4767 for part in iter {
4768 key.push(SEP);
4769 key.extend_from_slice(part.as_bytes());
4770 }
4771 key
4772}
4773
4774fn refsrc_key(tenant: &str, aggregate_type: &str, aggregate_id: &str) -> Vec<u8> {
4775 key_with_segments(&[PREFIX_REF_SOURCE, tenant, aggregate_type, aggregate_id])
4776}
4777
4778fn reftgt_key(tenant: &str, target: &str) -> Vec<u8> {
4779 key_with_segments(&[PREFIX_REF_TARGET, tenant, target])
4780}
4781
4782fn update_reference_index_inner(
4783 db: &DBWithThreadMode<MultiThreaded>,
4784 batch: &mut WriteBatch,
4785 tenant: &str,
4786 aggregate_type: &str,
4787 aggregate_id: &str,
4788 references: &[(String, String)],
4789) -> Result<()> {
4790 let source_key = refsrc_key(tenant, aggregate_type, aggregate_id);
4791 let prev: Vec<StoredReferencePath> = db
4792 .get(source_key.clone())
4793 .map_err(|err| EventError::Storage(err.to_string()))?
4794 .map(|bytes| serde_json::from_slice(&bytes))
4795 .transpose()
4796 .map_err(|err| EventError::Serialization(err.to_string()))?
4797 .unwrap_or_default();
4798
4799 fn load_target_referrers<'a>(
4800 db: &DBWithThreadMode<MultiThreaded>,
4801 tenant: &str,
4802 target: &str,
4803 cache: &'a mut HashMap<String, Vec<StoredReferrer>>,
4804 ) -> Result<&'a mut Vec<StoredReferrer>> {
4805 match cache.entry(target.to_string()) {
4806 Entry::Occupied(entry) => Ok(entry.into_mut()),
4807 Entry::Vacant(entry) => {
4808 let target_key = reftgt_key(tenant, target);
4809 let existing: Vec<StoredReferrer> = db
4810 .get(&target_key)
4811 .map_err(|err| EventError::Storage(err.to_string()))?
4812 .map(|bytes| serde_json::from_slice(&bytes))
4813 .transpose()
4814 .map_err(|err| EventError::Serialization(err.to_string()))?
4815 .unwrap_or_default();
4816 Ok(entry.insert(existing))
4817 }
4818 }
4819 }
4820
4821 let mut target_referrers: HashMap<String, Vec<StoredReferrer>> = HashMap::new();
4822 let mut updated_targets = BTreeSet::new();
4823
4824 for entry in prev {
4826 let referrers = load_target_referrers(db, tenant, &entry.target, &mut target_referrers)?;
4827 let len_before = referrers.len();
4828 referrers
4829 .retain(|r| !(r.aggregate_type == aggregate_type && r.aggregate_id == aggregate_id));
4830 if referrers.len() != len_before {
4831 updated_targets.insert(entry.target.clone());
4832 }
4833 }
4834
4835 let mut new_paths = Vec::new();
4837 for (target, path) in references {
4838 let path_entry = StoredReferencePath {
4839 target: target.clone(),
4840 path: path.clone(),
4841 };
4842 new_paths.push(path_entry);
4843
4844 let referrers = load_target_referrers(db, tenant, target, &mut target_referrers)?;
4845 if !referrers.iter().any(|r| {
4846 r.aggregate_type == aggregate_type && r.aggregate_id == aggregate_id && r.path == *path
4847 }) {
4848 referrers.push(StoredReferrer {
4849 aggregate_type: aggregate_type.to_string(),
4850 aggregate_id: aggregate_id.to_string(),
4851 path: path.clone(),
4852 });
4853 updated_targets.insert(target.clone());
4854 }
4855 }
4856
4857 for target in updated_targets {
4858 let target_key = reftgt_key(tenant, &target);
4859 if let Some(referrers) = target_referrers.get(&target) {
4860 if referrers.is_empty() {
4861 batch.delete(target_key);
4862 } else {
4863 batch.put(target_key, serde_json::to_vec(referrers)?);
4864 }
4865 }
4866 }
4867
4868 batch.put(source_key, serde_json::to_vec(&new_paths)?);
4869 Ok(())
4870}
4871
4872fn record_store_op(operation: &'static str, status: &'static str, duration: f64) {
4873 let labels = [("operation", operation), ("status", status)];
4874 counter!("eventdbx_store_operations_total", &labels).increment(1);
4875 histogram!("eventdbx_store_operation_duration_seconds", &labels).record(duration);
4876}
4877
4878fn hash_event(
4879 aggregate_type: &str,
4880 aggregate_id: &str,
4881 version: u64,
4882 event_type: &str,
4883 payload: &BTreeMap<String, String>,
4884) -> String {
4885 use sha2::{Digest, Sha256};
4886
4887 let mut hasher = Sha256::new();
4888 hasher.update(aggregate_type.as_bytes());
4889 hasher.update(aggregate_id.as_bytes());
4890 hasher.update(version.to_be_bytes());
4891 hasher.update(event_type.as_bytes());
4892
4893 for (key, value) in payload {
4894 hasher.update(key.as_bytes());
4895 hasher.update(value.as_bytes());
4896 }
4897
4898 hex::encode(hasher.finalize())
4899}
4900
4901pub fn payload_to_map(value: &Value) -> BTreeMap<String, String> {
4902 fn normalize(value: &Value) -> String {
4903 match value {
4904 Value::String(s) => s.clone(),
4905 Value::Null => String::new(),
4906 _ => value.to_string(),
4907 }
4908 }
4909
4910 match value {
4911 Value::Object(map) => map
4912 .iter()
4913 .map(|(k, v)| (k.clone(), normalize(v)))
4914 .collect::<BTreeMap<_, _>>(),
4915 other => {
4916 let mut map = BTreeMap::new();
4917 if !other.is_null() {
4918 map.insert("_value".into(), normalize(other));
4919 }
4920 map
4921 }
4922 }
4923}
4924
4925#[cfg(test)]
4926mod tests {
4927 use super::*;
4928
4929 #[test]
4930 fn computes_consistent_merkle_root() {
4931 let root = compute_merkle_root(&[]);
4932 assert_eq!(root, empty_root());
4933
4934 let root = compute_merkle_root(&["abc".into()]);
4935 let root2 = compute_merkle_root(&["abc".into()]);
4936 assert_eq!(root, root2);
4937 }
4938
4939 #[test]
4940 fn append_and_retrieve_events() {
4941 let dir = tempfile::tempdir().unwrap();
4942 let path = dir.path().join("event_store");
4943
4944 {
4945 let store = EventStore::open(path.clone(), None, 0).unwrap();
4946
4947 let payload = serde_json::json!({ "name": "Alice" });
4948
4949 let record = store
4950 .append(AppendEvent {
4951 aggregate_type: "patient".into(),
4952 aggregate_id: "patient-1".into(),
4953 event_type: "patient-created".into(),
4954 event_type_raw: None,
4955 payload: payload.clone(),
4956 metadata: None,
4957 issued_by: None,
4958 note: None,
4959 tenant: "default".into(),
4960 reference_targets: Vec::new(),
4961 })
4962 .unwrap();
4963
4964 assert_eq!(record.version, 1);
4965 assert_eq!(record.payload["name"], "Alice");
4966
4967 let state = store.get_aggregate_state("patient", "patient-1").unwrap();
4968 assert_eq!(state.version, 1);
4969 assert_eq!(state.state["name"], "Alice");
4970 assert!(!state.archived);
4971
4972 let events = store.list_events("patient", "patient-1").unwrap();
4973 assert_eq!(events.len(), 1);
4974 }
4975 }
4976
4977 #[test]
4978 fn transaction_appends_multiple_events() {
4979 let dir = tempfile::tempdir().unwrap();
4980 let path = dir.path().join("event_store");
4981 let store = EventStore::open(path, None, 0).unwrap();
4982
4983 let mut tx = store.transaction().unwrap();
4984 let first = tx
4985 .append(AppendEvent {
4986 aggregate_type: "order".into(),
4987 aggregate_id: "order-42".into(),
4988 event_type: "order-created".into(),
4989 event_type_raw: None,
4990 payload: serde_json::json!({ "status": "processing" }),
4991 metadata: None,
4992 issued_by: None,
4993 note: None,
4994 tenant: "default".into(),
4995 reference_targets: Vec::new(),
4996 })
4997 .unwrap();
4998 assert_eq!(first.version, 1);
4999
5000 let second = tx
5001 .append(AppendEvent {
5002 aggregate_type: "order".into(),
5003 aggregate_id: "order-42".into(),
5004 event_type: "order-updated".into(),
5005 event_type_raw: None,
5006 payload: serde_json::json!({
5007 "status": "shipped",
5008 "tracking": "abc123"
5009 }),
5010 metadata: None,
5011 issued_by: None,
5012 note: None,
5013 tenant: "default".into(),
5014 reference_targets: Vec::new(),
5015 })
5016 .unwrap();
5017 assert_eq!(second.version, 2);
5018
5019 let committed = tx.commit().unwrap();
5020 assert_eq!(committed.len(), 2);
5021 assert_eq!(committed[0].version, 1);
5022 assert_eq!(committed[1].version, 2);
5023
5024 let state = store.get_aggregate_state("order", "order-42").unwrap();
5025 assert_eq!(state.version, 2);
5026 assert_eq!(state.state["status"], "shipped");
5027 assert_eq!(state.state["tracking"], "abc123");
5028
5029 let events = store.list_events("order", "order-42").unwrap();
5030 assert_eq!(events.len(), 2);
5031 }
5032
5033 #[test]
5034 fn aggregate_sort_key_order_detection() {
5035 assert!(aggregate_sort_matches_key_order(&[]));
5036
5037 let sorts = vec![AggregateSort {
5038 field: AggregateSortField::AggregateType,
5039 descending: false,
5040 }];
5041 assert!(aggregate_sort_matches_key_order(&sorts));
5042
5043 let sorts = vec![
5044 AggregateSort {
5045 field: AggregateSortField::AggregateType,
5046 descending: false,
5047 },
5048 AggregateSort {
5049 field: AggregateSortField::AggregateId,
5050 descending: false,
5051 },
5052 ];
5053 assert!(aggregate_sort_matches_key_order(&sorts));
5054
5055 let invalid = vec![AggregateSort {
5056 field: AggregateSortField::AggregateId,
5057 descending: false,
5058 }];
5059 assert!(!aggregate_sort_matches_key_order(&invalid));
5060
5061 let descending = vec![AggregateSort {
5062 field: AggregateSortField::AggregateType,
5063 descending: true,
5064 }];
5065 assert!(!aggregate_sort_matches_key_order(&descending));
5066 }
5067
5068 #[test]
5069 fn event_sort_key_order_detection() {
5070 assert!(event_sort_matches_key_order(&[]));
5071
5072 let sorts = vec![EventSort {
5073 field: EventSortField::AggregateType,
5074 descending: false,
5075 }];
5076 assert!(event_sort_matches_key_order(&sorts));
5077
5078 let sorts = vec![
5079 EventSort {
5080 field: EventSortField::AggregateType,
5081 descending: false,
5082 },
5083 EventSort {
5084 field: EventSortField::AggregateId,
5085 descending: false,
5086 },
5087 EventSort {
5088 field: EventSortField::Version,
5089 descending: false,
5090 },
5091 ];
5092 assert!(event_sort_matches_key_order(&sorts));
5093
5094 let invalid_field = vec![EventSort {
5095 field: EventSortField::EventType,
5096 descending: false,
5097 }];
5098 assert!(!event_sort_matches_key_order(&invalid_field));
5099
5100 let descending = vec![EventSort {
5101 field: EventSortField::AggregateType,
5102 descending: true,
5103 }];
5104 assert!(!event_sort_matches_key_order(&descending));
5105 }
5106
5107 #[test]
5108 fn aggregates_sort_by_created_at_desc() {
5109 let dir = tempfile::tempdir().unwrap();
5110 let path = dir.path().join("event_store");
5111 let store = EventStore::open(path, None, 0).unwrap();
5112 let base = Utc::now();
5113 let cases = vec![
5114 ("agg-1", base + chrono::Duration::milliseconds(10)),
5115 ("agg-2", base + chrono::Duration::milliseconds(20)),
5116 ("agg-3", base + chrono::Duration::milliseconds(30)),
5117 ];
5118
5119 for (idx, (aggregate_id, created_at)) in cases.iter().enumerate() {
5120 import_history(
5121 &store,
5122 aggregate_id,
5123 &[created_at.to_owned()],
5124 (idx as u64) * 10,
5125 );
5126 }
5127
5128 let sorts = vec![AggregateSort {
5129 field: AggregateSortField::CreatedAt,
5130 descending: true,
5131 }];
5132 let aggregates = store.aggregates_paginated_with_transform(
5133 0,
5134 Some(3),
5135 Some(&sorts),
5136 AggregateQueryScope::ActiveOnly,
5137 None,
5138 |aggregate| Some(aggregate),
5139 );
5140 let ids: Vec<_> = aggregates.into_iter().map(|agg| agg.aggregate_id).collect();
5141 assert_eq!(ids, vec!["agg-3", "agg-2", "agg-1"]);
5142 }
5143
5144 #[test]
5145 fn aggregates_sort_by_updated_at_desc() {
5146 let dir = tempfile::tempdir().unwrap();
5147 let path = dir.path().join("event_store");
5148 let store = EventStore::open(path, None, 0).unwrap();
5149 let base = Utc::now();
5150 import_history(
5151 &store,
5152 "agg-1",
5153 &[
5154 base + chrono::Duration::milliseconds(5),
5155 base + chrono::Duration::milliseconds(50),
5156 ],
5157 100,
5158 );
5159 import_history(
5160 &store,
5161 "agg-2",
5162 &[
5163 base + chrono::Duration::milliseconds(10),
5164 base + chrono::Duration::milliseconds(30),
5165 ],
5166 200,
5167 );
5168
5169 let sorts = vec![AggregateSort {
5170 field: AggregateSortField::UpdatedAt,
5171 descending: true,
5172 }];
5173 let aggregates = store.aggregates_paginated_with_transform(
5174 0,
5175 Some(2),
5176 Some(&sorts),
5177 AggregateQueryScope::ActiveOnly,
5178 None,
5179 |aggregate| Some(aggregate),
5180 );
5181 let ids: Vec<_> = aggregates.into_iter().map(|agg| agg.aggregate_id).collect();
5182 assert_eq!(ids, vec!["agg-1", "agg-2"]);
5183 }
5184
5185 fn import_history(
5186 store: &EventStore,
5187 aggregate_id: &str,
5188 timestamps: &[DateTime<Utc>],
5189 base_event_id: u64,
5190 ) {
5191 let aggregate_type = "chronicle";
5192 let event_type = "created";
5193 let mut hashes = Vec::new();
5194 for (idx, timestamp) in timestamps.iter().enumerate() {
5195 let version = (idx as u64) + 1;
5196 let payload = serde_json::json!({ "seq": version });
5197 let payload_map = payload_to_map(&payload);
5198 let hash = hash_event(
5199 aggregate_type,
5200 aggregate_id,
5201 version,
5202 event_type,
5203 &payload_map,
5204 );
5205 hashes.push(hash.clone());
5206 let merkle_root = compute_merkle_root(&hashes);
5207 let record = EventRecord {
5208 aggregate_type: aggregate_type.into(),
5209 aggregate_id: aggregate_id.to_string(),
5210 event_type: event_type.into(),
5211 event_type_raw: None,
5212 payload: payload.clone(),
5213 extensions: None,
5214 metadata: EventMetadata {
5215 event_id: SnowflakeId::from_u64(base_event_id + version),
5216 created_at: *timestamp,
5217 issued_by: None,
5218 note: None,
5219 },
5220 version,
5221 hash,
5222 merkle_root,
5223 };
5224 store.append_imported_event(record).unwrap();
5225 }
5226 }
5227
5228 #[test]
5229 fn transaction_without_commit_discards_changes() {
5230 let dir = tempfile::tempdir().unwrap();
5231 let path = dir.path().join("event_store");
5232 let store = EventStore::open(path.clone(), None, 0).unwrap();
5233
5234 {
5235 let mut tx = store.transaction().unwrap();
5236 tx.append(AppendEvent {
5237 aggregate_type: "invoice".into(),
5238 aggregate_id: "inv-1".into(),
5239 event_type: "invoice-created".into(),
5240 event_type_raw: None,
5241 payload: serde_json::json!({ "total": "100.00" }),
5242 metadata: None,
5243 issued_by: None,
5244 note: None,
5245 tenant: "default".into(),
5246 reference_targets: Vec::new(),
5247 })
5248 .unwrap();
5249 }
5250
5251 let err = store.list_events("invoice", "inv-1").unwrap_err();
5252 assert!(matches!(err, crate::error::EventError::AggregateNotFound));
5253 }
5254
5255 #[test]
5256 fn snapshots_and_archive_flow() {
5257 let dir = tempfile::tempdir().unwrap();
5258 let path = dir.path().join("event_store");
5259 let store = EventStore::open(path, None, 0).unwrap();
5260
5261 let payload = serde_json::json!({ "status": "active" });
5262
5263 store
5264 .append(AppendEvent {
5265 aggregate_type: "order".into(),
5266 aggregate_id: "order-1".into(),
5267 event_type: "order-created".into(),
5268 event_type_raw: None,
5269 payload,
5270 metadata: None,
5271 issued_by: None,
5272 note: None,
5273 tenant: "default".into(),
5274 reference_targets: Vec::new(),
5275 })
5276 .unwrap();
5277
5278 let snapshot = store
5279 .create_snapshot("order", "order-1", Some("initial".into()))
5280 .unwrap();
5281 assert_eq!(snapshot.version, 1);
5282 assert_eq!(snapshot.comment.as_deref(), Some("initial"));
5283
5284 let state = store
5285 .set_archive("order", "order-1", true, Some("closed".into()))
5286 .unwrap();
5287 assert!(state.archived);
5288
5289 let err = store
5290 .append(AppendEvent {
5291 aggregate_type: "order".into(),
5292 aggregate_id: "order-1".into(),
5293 event_type: "order-updated".into(),
5294 event_type_raw: None,
5295 payload: serde_json::json!({}),
5296 metadata: None,
5297 issued_by: None,
5298 note: None,
5299 tenant: "default".into(),
5300 reference_targets: Vec::new(),
5301 })
5302 .unwrap_err();
5303 assert!(matches!(err, crate::error::EventError::AggregateArchived));
5304
5305 let state = store.set_archive("order", "order-1", false, None).unwrap();
5306 assert!(!state.archived);
5307
5308 let found = store
5310 .find_snapshot_by_id(snapshot.snapshot_id.expect("snapshot id"))
5311 .unwrap();
5312 assert!(found.is_some());
5313 }
5314
5315 #[test]
5316 fn append_event_with_note_persists_metadata() {
5317 let dir = tempfile::tempdir().unwrap();
5318 let path = dir.path().join("event_store");
5319 let store = EventStore::open(path, None, 0).unwrap();
5320
5321 let note_text = "initial import".to_string();
5322 let record = store
5323 .append(AppendEvent {
5324 aggregate_type: "account".into(),
5325 aggregate_id: "acct-1".into(),
5326 event_type: "account-created".into(),
5327 event_type_raw: None,
5328 payload: serde_json::json!({ "status": "active" }),
5329 metadata: None,
5330 issued_by: None,
5331 note: Some(note_text.clone()),
5332 tenant: "default".into(),
5333 reference_targets: Vec::new(),
5334 })
5335 .unwrap();
5336
5337 assert_eq!(record.metadata.note.as_deref(), Some(note_text.as_str()));
5338 }
5339
5340 #[test]
5341 fn reject_event_note_exceeding_limit() {
5342 let dir = tempfile::tempdir().unwrap();
5343 let path = dir.path().join("event_store");
5344 let store = EventStore::open(path, None, 0).unwrap();
5345
5346 let long_note = "x".repeat(MAX_EVENT_NOTE_LENGTH + 1);
5347 let err = store
5348 .append(AppendEvent {
5349 aggregate_type: "account".into(),
5350 aggregate_id: "acct-2".into(),
5351 event_type: "account-created".into(),
5352 event_type_raw: None,
5353 payload: serde_json::json!({ "status": "pending" }),
5354 metadata: None,
5355 issued_by: None,
5356 note: Some(long_note),
5357 tenant: "default".into(),
5358 reference_targets: Vec::new(),
5359 })
5360 .unwrap_err();
5361
5362 assert!(matches!(err, crate::error::EventError::InvalidSchema(_)));
5363 }
5364
5365 #[test]
5366 fn reject_event_type_raw_exceeding_limit() {
5367 let dir = tempfile::tempdir().unwrap();
5368 let path = dir.path().join("event_store");
5369 let store = EventStore::open(path, None, 0).unwrap();
5370
5371 let too_long = "x".repeat(MAX_EVENT_TYPE_LENGTH + 1);
5372 let err = store
5373 .append(AppendEvent {
5374 aggregate_type: "account".into(),
5375 aggregate_id: "acct-3".into(),
5376 event_type: "account-created".into(),
5377 event_type_raw: Some(too_long),
5378 payload: serde_json::json!({ "status": "active" }),
5379 metadata: None,
5380 issued_by: None,
5381 note: None,
5382 tenant: "default".into(),
5383 reference_targets: Vec::new(),
5384 })
5385 .unwrap_err();
5386
5387 assert!(matches!(err, crate::error::EventError::InvalidSchema(_)));
5388 }
5389
5390 #[test]
5391 fn append_event_with_extensions_persists() {
5392 let dir = tempfile::tempdir().unwrap();
5393 let path = dir.path().join("event_store");
5394 let store = EventStore::open(path.clone(), None, 0).unwrap();
5395
5396 let metadata = serde_json::json!({"@plugin": {"mode": "debug"}});
5397 let record = store
5398 .append(AppendEvent {
5399 aggregate_type: "account".into(),
5400 aggregate_id: "acct-3".into(),
5401 event_type: "account-created".into(),
5402 event_type_raw: None,
5403 payload: serde_json::json!({ "status": "active" }),
5404 metadata: Some(metadata.clone()),
5405 issued_by: None,
5406 note: None,
5407 tenant: "default".into(),
5408 reference_targets: Vec::new(),
5409 })
5410 .unwrap();
5411
5412 assert_eq!(record.extensions, Some(metadata.clone()));
5413
5414 drop(store);
5415 let reopened = EventStore::open(path, None, 0).unwrap();
5416 let events = reopened.list_events("account", "acct-3").unwrap();
5417 assert_eq!(events.len(), 1);
5418 assert_eq!(events[0].extensions, Some(metadata));
5419 }
5420
5421 #[test]
5422 fn aggregate_state_includes_extensions_and_flattens() {
5423 let dir = tempfile::tempdir().unwrap();
5424 let path = dir.path().join("event_store");
5425 let store = EventStore::open(path, None, 0).unwrap();
5426
5427 let metadata = serde_json::json!({
5428 "@createdBy": "system",
5429 "@trace": { "id": "abc123" }
5430 });
5431 store
5432 .append(AppendEvent {
5433 aggregate_type: "account".into(),
5434 aggregate_id: "acct-4".into(),
5435 event_type: "account-created".into(),
5436 event_type_raw: None,
5437 payload: serde_json::json!({ "status": "active" }),
5438 metadata: Some(metadata),
5439 issued_by: None,
5440 note: None,
5441 tenant: "default".into(),
5442 reference_targets: Vec::new(),
5443 })
5444 .unwrap();
5445
5446 let state = store
5447 .get_aggregate_state("account", "acct-4")
5448 .expect("state should be readable");
5449
5450 assert_eq!(
5451 state.extensions.get("@created_by"),
5452 Some(&serde_json::json!("system"))
5453 );
5454
5455 let serialized =
5456 serde_json::to_value(&state).expect("aggregate state should serialize to JSON");
5457 assert_eq!(
5458 serialized.get("@created_by"),
5459 Some(&serde_json::json!("system"))
5460 );
5461 assert_eq!(
5462 serialized
5463 .get("@trace")
5464 .and_then(Value::as_object)
5465 .and_then(|trace| trace.get("id")),
5466 Some(&serde_json::json!("abc123"))
5467 );
5468 assert!(serialized.get("@trace").is_some());
5469 }
5470
5471 #[test]
5472 fn aggregate_extensions_merge_across_events() {
5473 let dir = tempfile::tempdir().unwrap();
5474 let path = dir.path().join("event_store");
5475 let store = EventStore::open(path, None, 0).unwrap();
5476
5477 store
5478 .append(AppendEvent {
5479 aggregate_type: "person".into(),
5480 aggregate_id: "p-1".into(),
5481 event_type: "person-created".into(),
5482 event_type_raw: None,
5483 payload: serde_json::json!({ "firstName": "Ada" }),
5484 metadata: Some(serde_json::json!({ "@createdBy": "seed" })),
5485 issued_by: None,
5486 note: None,
5487 tenant: "default".into(),
5488 reference_targets: Vec::new(),
5489 })
5490 .unwrap();
5491
5492 store
5493 .append(AppendEvent {
5494 aggregate_type: "person".into(),
5495 aggregate_id: "p-1".into(),
5496 event_type: "person-updated".into(),
5497 event_type_raw: None,
5498 payload: serde_json::json!({ "lastName": "Lovelace" }),
5499 metadata: Some(serde_json::json!({ "@updatedBy": "cli" })),
5500 issued_by: None,
5501 note: None,
5502 tenant: "default".into(),
5503 reference_targets: Vec::new(),
5504 })
5505 .unwrap();
5506
5507 let state = store
5508 .get_aggregate_state("person", "p-1")
5509 .expect("state should be readable");
5510
5511 assert_eq!(state.version, 2);
5512 assert_eq!(
5513 state.extensions.get("@created_by"),
5514 Some(&serde_json::json!("seed"))
5515 );
5516 assert_eq!(
5517 state.extensions.get("@updated_by"),
5518 Some(&serde_json::json!("cli"))
5519 );
5520 }
5521
5522 #[test]
5523 fn output_value_inlines_extensions_and_hides_raw_type() {
5524 let record = EventRecord {
5525 aggregate_type: "person".into(),
5526 aggregate_id: "p-1".into(),
5527 event_type: "person_updated".into(),
5528 event_type_raw: Some("person.updated".into()),
5529 payload: serde_json::json!({ "status": "active" }),
5530 extensions: Some(serde_json::json!({
5531 "@trace": { "id": "abc123" },
5532 "@channel": "beta"
5533 })),
5534 metadata: EventMetadata {
5535 event_id: SnowflakeId::from_u64(42),
5536 created_at: Utc::now(),
5537 issued_by: None,
5538 note: None,
5539 },
5540 version: 7,
5541 hash: "hash".into(),
5542 merkle_root: "root".into(),
5543 };
5544
5545 let rendered = record.to_output_value().expect("serializes for output");
5546 let object = rendered.as_object().expect("event renders as object");
5547 assert!(
5548 !object.contains_key("extensions"),
5549 "extensions should be folded into metadata"
5550 );
5551 assert!(
5552 !object.contains_key("event_type_raw"),
5553 "raw event type should be hidden in output"
5554 );
5555 let metadata = object
5556 .get("metadata")
5557 .and_then(Value::as_object)
5558 .expect("metadata should be present");
5559 assert_eq!(metadata.get("@channel"), Some(&serde_json::json!("beta")));
5560 assert_eq!(
5561 metadata
5562 .get("@trace")
5563 .and_then(Value::as_object)
5564 .and_then(|trace| trace.get("id")),
5565 Some(&serde_json::json!("abc123"))
5566 );
5567 assert!(metadata.contains_key("created_at"));
5568 }
5569
5570 #[test]
5571 fn prepare_payload_from_patch_updates_nested_fields() {
5572 let dir = tempfile::tempdir().unwrap();
5573 let path = dir.path().join("event_store");
5574 let store = EventStore::open(path.clone(), None, 0).unwrap();
5575
5576 store
5577 .append(AppendEvent {
5578 aggregate_type: "patient".into(),
5579 aggregate_id: "p-1".into(),
5580 event_type: "patient-created".into(),
5581 event_type_raw: None,
5582 payload: serde_json::json!({
5583 "status": "active",
5584 "contact": {
5585 "address": { "city": "Portland" }
5586 }
5587 }),
5588 metadata: None,
5589 issued_by: None,
5590 note: None,
5591 tenant: "default".into(),
5592 reference_targets: Vec::new(),
5593 })
5594 .unwrap();
5595
5596 let patch = serde_json::json!([
5597 { "op": "replace", "path": "/status", "value": "inactive" },
5598 { "op": "replace", "path": "/contact/address/city", "value": "Spokane" }
5599 ]);
5600
5601 let payload = store
5602 .prepare_payload_from_patch("patient", "p-1", &patch)
5603 .unwrap();
5604
5605 assert_eq!(
5606 payload,
5607 serde_json::json!({
5608 "contact": { "address": { "city": "Spokane" } },
5609 "status": "inactive"
5610 })
5611 );
5612 }
5613
5614 #[test]
5615 fn prepare_payload_from_patch_rejects_unsupported_operations() {
5616 let dir = tempfile::tempdir().unwrap();
5617 let path = dir.path().join("event_store");
5618 let store = EventStore::open(path, None, 0).unwrap();
5619
5620 let patch = serde_json::json!([
5621 { "op": "remove", "path": "/status" }
5622 ]);
5623
5624 let err = store
5625 .prepare_payload_from_patch("patient", "p-unknown", &patch)
5626 .unwrap_err();
5627
5628 assert!(matches!(err, crate::error::EventError::InvalidSchema(_)));
5629 }
5630
5631 #[test]
5632 fn prepare_payload_from_patch_supports_add_on_new_aggregate() {
5633 let dir = tempfile::tempdir().unwrap();
5634 let path = dir.path().join("event_store");
5635 let store = EventStore::open(path, None, 0).unwrap();
5636
5637 let patch = serde_json::json!([
5638 { "op": "add", "path": "/status", "value": "inactive" }
5639 ]);
5640
5641 let payload = store
5642 .prepare_payload_from_patch("patient", "p-new", &patch)
5643 .unwrap();
5644
5645 assert_eq!(
5646 payload,
5647 serde_json::json!({
5648 "status": "inactive"
5649 })
5650 );
5651 }
5652
5653 #[test]
5654 fn select_state_field_handles_nested_paths() {
5655 let mut map = BTreeMap::new();
5656 map.insert("status".into(), "\"active\"".into());
5657 map.insert(
5658 "profile".into(),
5659 r#"{"name":{"first":"Ada"},"scores":[10,20]}"#.into(),
5660 );
5661
5662 assert_eq!(
5663 super::select_state_field(&map, "status"),
5664 Some(Value::String("active".into()))
5665 );
5666 assert_eq!(
5667 super::select_state_field(&map, "profile.name.first"),
5668 Some(Value::String("Ada".into()))
5669 );
5670 assert_eq!(
5671 super::select_state_field(&map, "profile.scores.1"),
5672 Some(Value::from(20))
5673 );
5674 assert_eq!(super::select_state_field(&map, "missing"), None);
5675 assert_eq!(super::select_state_field(&map, ""), None);
5676 assert_eq!(super::select_state_field(&map, "profile.scores.5"), None);
5677 }
5678
5679 #[test]
5680 fn reports_store_in_use_on_active_lock() {
5681 let dir = tempfile::tempdir().unwrap();
5682 let path = dir.path().join("event_store");
5683 let _holder = EventStore::open(path.clone(), None, 0).unwrap();
5684
5685 let result = EventStore::open(path, None, 0);
5686 match result {
5687 Err(EventError::Storage(message)) => {
5688 let lower = message.to_ascii_lowercase();
5689 assert!(
5690 lower.contains("in use") || lower.contains("lock"),
5691 "expected lock error message, got {message}"
5692 );
5693 }
5694 Err(other) => panic!("expected storage error for active lock, got {other:?}"),
5695 Ok(_) => panic!("expected store open to fail while lock is held"),
5696 }
5697 }
5698
5699 #[test]
5700 fn recovers_after_lock_is_released() {
5701 use std::{thread, time::Duration};
5702
5703 let dir = tempfile::tempdir().unwrap();
5704 let path = dir.path().join("event_store");
5705
5706 EventStore::open(path.clone(), None, 0).unwrap();
5708
5709 let holder_path = path.clone();
5710 let handle = thread::spawn(move || {
5711 let store = EventStore::open(holder_path, None, 0).unwrap();
5712 thread::sleep(Duration::from_millis(150));
5713 drop(store);
5714 });
5715
5716 thread::sleep(Duration::from_millis(25));
5717 let reopened = EventStore::open(path.clone(), None, 0).unwrap();
5718 drop(reopened);
5719
5720 handle.join().unwrap();
5721 }
5722
5723 #[test]
5724 fn snapshot_does_not_leave_lock_held() {
5725 let dir = tempfile::tempdir().unwrap();
5726 let path = dir.path().join("event_store");
5727
5728 {
5729 let store = EventStore::open(path.clone(), None, 0).unwrap();
5730 store
5731 .append(AppendEvent {
5732 aggregate_type: "account".into(),
5733 aggregate_id: "acct-42".into(),
5734 event_type: "created".into(),
5735 event_type_raw: None,
5736 payload: serde_json::json!({ "status": "active" }),
5737 metadata: None,
5738 issued_by: None,
5739 note: None,
5740 tenant: "default".into(),
5741 reference_targets: Vec::new(),
5742 })
5743 .unwrap();
5744 store
5745 .create_snapshot("account", "acct-42", Some("first".into()))
5746 .unwrap();
5747 }
5748
5749 let reopened = EventStore::open(path, None, 0).unwrap();
5750 let snapshot = reopened
5751 .create_snapshot("account", "acct-42", Some("second".into()))
5752 .unwrap();
5753 assert_eq!(snapshot.aggregate_id, "acct-42");
5754 assert_eq!(snapshot.aggregate_type, "account");
5755 }
5756
5757 #[test]
5758 fn list_snapshots_filters_by_version() {
5759 use std::{thread, time::Duration};
5760
5761 let dir = tempfile::tempdir().unwrap();
5762 let path = dir.path().join("event_store");
5763 let store = EventStore::open(path, None, 0).unwrap();
5764
5765 store
5766 .append(AppendEvent {
5767 aggregate_type: "order".into(),
5768 aggregate_id: "order-9".into(),
5769 event_type: "created".into(),
5770 event_type_raw: None,
5771 payload: serde_json::json!({ "status": "new" }),
5772 metadata: None,
5773 issued_by: None,
5774 note: None,
5775 tenant: "default".into(),
5776 reference_targets: Vec::new(),
5777 })
5778 .unwrap();
5779 store
5780 .create_snapshot("order", "order-9", Some("v1".into()))
5781 .unwrap();
5782
5783 thread::sleep(Duration::from_millis(2));
5784
5785 store
5786 .append(AppendEvent {
5787 aggregate_type: "order".into(),
5788 aggregate_id: "order-9".into(),
5789 event_type: "updated".into(),
5790 event_type_raw: None,
5791 payload: serde_json::json!({ "status": "shipped" }),
5792 metadata: None,
5793 issued_by: None,
5794 note: None,
5795 tenant: "default".into(),
5796 reference_targets: Vec::new(),
5797 })
5798 .unwrap();
5799 store
5800 .create_snapshot("order", "order-9", Some("v2".into()))
5801 .unwrap();
5802
5803 let all = store
5804 .list_snapshots(Some("order"), Some("order-9"), None)
5805 .expect("list snapshots");
5806 assert_eq!(all.len(), 2);
5807 assert!(all.iter().any(|s| s.version == 1));
5808 assert!(all.iter().any(|s| s.version == 2));
5809
5810 let filtered = store
5811 .list_snapshots(Some("order"), Some("order-9"), Some(1))
5812 .expect("list snapshots by version");
5813 assert_eq!(filtered.len(), 1);
5814 assert_eq!(filtered[0].version, 1);
5815 }
5816
5817 #[test]
5818 fn list_snapshots_all_aggregates() {
5819 let dir = tempfile::tempdir().unwrap();
5820 let path = dir.path().join("event_store");
5821 let store = EventStore::open(path, None, 0).unwrap();
5822
5823 for agg_id in &["a-1", "b-2"] {
5824 store
5825 .append(AppendEvent {
5826 aggregate_type: "person".into(),
5827 aggregate_id: agg_id.to_string(),
5828 event_type: "created".into(),
5829 event_type_raw: None,
5830 payload: serde_json::json!({ "name": agg_id }),
5831 metadata: None,
5832 issued_by: None,
5833 note: None,
5834 tenant: "default".into(),
5835 reference_targets: Vec::new(),
5836 })
5837 .unwrap();
5838 store
5839 .create_snapshot("person", agg_id, Some("checkpoint".into()))
5840 .unwrap();
5841 }
5842
5843 let snapshots = store
5844 .list_snapshots(None, None, None)
5845 .expect("list all snapshots");
5846 assert_eq!(snapshots.len(), 2);
5847 }
5848
5849 #[test]
5850 fn referrers_for_any_tenant_returns_cross_tenant_entries() {
5851 let dir = tempfile::tempdir().unwrap();
5852 let path = dir.path().join("event_store");
5853 let store = EventStore::open(path, None, 0).unwrap();
5854
5855 store
5857 .append(AppendEvent {
5858 aggregate_type: "address".into(),
5859 aggregate_id: "addr-1".into(),
5860 event_type: "created".into(),
5861 event_type_raw: None,
5862 payload: serde_json::json!({ "status": "active" }),
5863 metadata: None,
5864 issued_by: None,
5865 note: None,
5866 tenant: "beta".into(),
5867 reference_targets: Vec::new(),
5868 })
5869 .unwrap();
5870
5871 store
5873 .append(AppendEvent {
5874 aggregate_type: "farm".into(),
5875 aggregate_id: "farm-1".into(),
5876 event_type: "created".into(),
5877 event_type_raw: None,
5878 payload: serde_json::json!({ "address": "beta#address#addr-1" }),
5879 metadata: None,
5880 issued_by: None,
5881 note: None,
5882 tenant: "alpha".into(),
5883 reference_targets: vec![("beta#address#addr-1".into(), "/address".into())],
5884 })
5885 .unwrap();
5886
5887 let refs = store
5888 .referrers_for_any_tenant("beta#address#addr-1")
5889 .unwrap();
5890 assert_eq!(refs.len(), 1);
5891 assert_eq!(refs[0].0, "alpha");
5892 assert_eq!(refs[0].1, "farm");
5893 assert_eq!(refs[0].2, "farm-1");
5894 assert_eq!(refs[0].3, "/address");
5895 }
5896}