1#[cfg(test)]
4use std::cell::Cell;
5use std::collections::{HashMap, HashSet};
6use std::sync::{Arc, LazyLock};
7
8use arrow::array::{
9 Array, FixedSizeBinaryArray, FixedSizeBinaryBuilder, TimestampMicrosecondArray,
10 TimestampMicrosecondBuilder, UInt32Array, UInt32Builder,
11};
12use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
13use arrow::record_batch::RecordBatch;
14use graphforge_core::canonical::{
15 CANONICAL_CONTRACT_VERSION, CanonicalDomain, CanonicalWriter, fingerprint,
16};
17use uuid::{Uuid, Version};
18
19use crate::{KnowledgeError, MAX_KNOWLEDGE_ROWS, SchemaRegistryEntry};
20
21pub const VALID_TIME_CAPABILITY_VERSION: u32 = 1;
23pub const ASSERTION_VALIDITY_CONTRACT_VERSION: u32 = 1;
25pub const ASSERTION_VALIDITY_POLICY_VERSION: u32 = 1;
27
28pub static ASSERTION_VALIDITY_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
30 Arc::new(Schema::new(vec![
31 uuid_field("validity_event_uuid", false),
32 uuid_field("assertion_uuid", false),
33 timestamp_field("valid_from", true),
34 timestamp_field("valid_to", true),
35 uuid_field("reasoning_uuid", true),
36 uuid_field("provenance_uuid", false),
37 timestamp_field("recorded_at", false),
38 Field::new("contract_version", DataType::UInt32, false),
39 ]))
40});
41
42#[cfg(test)]
43std::thread_local! {
44 static EVENT_VALIDATION_CALLS: Cell<usize> = const { Cell::new(0) };
45}
46
47static ASSERTION_VALIDITY_SCHEMA_FINGERPRINT: LazyLock<[u8; 32]> = LazyLock::new(|| {
48 fingerprint(
49 CanonicalDomain::Schema,
50 CANONICAL_CONTRACT_VERSION,
51 b"assertion_validity/1|validity_event_uuid:fixed[16]:required|assertion_uuid:fixed[16]:required|valid_from:timestamp_us_utc:nullable|valid_to:timestamp_us_utc:nullable|reasoning_uuid:fixed[16]:nullable|provenance_uuid:fixed[16]:required|recorded_at:timestamp_us_utc:required|contract_version:u32:required",
52 )
53 .expect("registered assertion-validity schema is within canonical bounds")
54});
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct AssertionValidityEvent {
59 pub validity_event_uuid: Uuid,
61 pub assertion_uuid: Uuid,
63 pub valid_from_micros: Option<i64>,
65 pub valid_to_micros: Option<i64>,
67 pub reasoning_uuid: Option<Uuid>,
69 pub provenance_uuid: Uuid,
71 pub recorded_at_micros: i64,
73 pub contract_version: u32,
75}
76
77impl AssertionValidityEvent {
78 #[allow(clippy::too_many_arguments)]
80 pub fn new(
81 validity_event_uuid: Uuid,
82 assertion_uuid: Uuid,
83 valid_from_micros: Option<i64>,
84 valid_to_micros: Option<i64>,
85 reasoning_uuid: Option<Uuid>,
86 provenance_uuid: Uuid,
87 recorded_at_micros: i64,
88 ) -> Result<Self, KnowledgeError> {
89 let event = Self {
90 validity_event_uuid,
91 assertion_uuid,
92 valid_from_micros,
93 valid_to_micros,
94 reasoning_uuid,
95 provenance_uuid,
96 recorded_at_micros,
97 contract_version: ASSERTION_VALIDITY_CONTRACT_VERSION,
98 };
99 validate_event(&event)?;
100 Ok(event)
101 }
102
103 #[must_use]
107 pub fn contains(&self, valid_time_micros: i64) -> bool {
108 self.valid_from_micros
109 .is_none_or(|from| from <= valid_time_micros)
110 && self.valid_to_micros.is_none_or(|to| valid_time_micros < to)
111 }
112}
113
114#[derive(Clone, Debug, Default, Eq, PartialEq)]
116pub struct AssertionValidityLedger {
117 events: Vec<AssertionValidityEvent>,
119}
120
121impl AssertionValidityLedger {
122 #[must_use]
141 pub fn events(&self) -> &[AssertionValidityEvent] {
142 &self.events
143 }
144
145 #[must_use]
147 pub fn into_events(self) -> Vec<AssertionValidityEvent> {
148 self.events
149 }
150
151 pub fn new(events: Vec<AssertionValidityEvent>) -> Result<Self, KnowledgeError> {
153 Self::new_with_limit(events, MAX_KNOWLEDGE_ROWS)
154 }
155
156 fn new_with_limit(
157 mut events: Vec<AssertionValidityEvent>,
158 row_limit: usize,
159 ) -> Result<Self, KnowledgeError> {
160 validate_event_count(events.len(), row_limit)?;
161 let mut ids = HashSet::with_capacity(events.len());
162 for event in &events {
163 validate_event(event)?;
164 if !ids.insert(event.validity_event_uuid) {
165 return Err(KnowledgeError::Duplicate("validity_event_uuid"));
166 }
167 }
168 events.sort_by_key(|row| (row.recorded_at_micros, row.validity_event_uuid));
169 Ok(Self { events })
170 }
171
172 pub fn merge(&self, staged: &Self) -> Result<Self, KnowledgeError> {
180 self.merge_with_limit(staged, MAX_KNOWLEDGE_ROWS)
181 }
182
183 fn merge_with_limit(&self, staged: &Self, row_limit: usize) -> Result<Self, KnowledgeError> {
184 let by_id = self
185 .events
186 .iter()
187 .map(|row| (row.validity_event_uuid, row))
188 .collect::<HashMap<_, _>>();
189 let mut appended_count = 0_usize;
190 for event in &staged.events {
191 if let Some(existing) = by_id.get(&event.validity_event_uuid) {
192 if *existing != event {
193 return Err(KnowledgeError::Conflict("validity_event_uuid"));
194 }
195 } else {
196 appended_count = appended_count.saturating_add(1);
197 }
198 }
199 validate_event_count(self.events.len().saturating_add(appended_count), row_limit)?;
200 if appended_count == 0 {
201 return Ok(Self {
202 events: self.events.clone(),
203 });
204 }
205
206 let mut events = self.events.clone();
207 events.reserve(appended_count);
208 events.extend(
209 staged
210 .events
211 .iter()
212 .filter(|event| !by_id.contains_key(&event.validity_event_uuid))
213 .cloned(),
214 );
215 events.sort_by_key(|row| (row.recorded_at_micros, row.validity_event_uuid));
216 Ok(Self { events })
217 }
218
219 #[must_use]
221 pub fn current_for_at(
222 &self,
223 assertion_uuid: Uuid,
224 transaction_cutoff_micros: i64,
225 ) -> Option<&AssertionValidityEvent> {
226 self.events
227 .iter()
228 .filter(|row| {
229 row.assertion_uuid == assertion_uuid
230 && row.recorded_at_micros <= transaction_cutoff_micros
231 })
232 .max_by_key(|row| (row.recorded_at_micros, row.validity_event_uuid))
233 }
234
235 #[must_use]
237 pub fn is_valid_at(
238 &self,
239 assertion_uuid: Uuid,
240 transaction_cutoff_micros: i64,
241 valid_time_micros: i64,
242 ) -> Option<bool> {
243 self.current_for_at(assertion_uuid, transaction_cutoff_micros)
244 .map(|event| event.contains(valid_time_micros))
245 }
246
247 pub fn event_fingerprint(&self, validity_event_uuid: Uuid) -> Result<[u8; 32], KnowledgeError> {
249 let row = self
250 .events
251 .iter()
252 .find(|row| row.validity_event_uuid == validity_event_uuid)
253 .ok_or(KnowledgeError::Dangling("validity_event_uuid"))?;
254 validity_fingerprint_for_row(row)
255 }
256
257 pub fn event_fingerprints(&self) -> Result<Vec<(Uuid, [u8; 32])>, KnowledgeError> {
259 self.events
260 .iter()
261 .map(|row| Ok((row.validity_event_uuid, validity_fingerprint_for_row(row)?)))
262 .collect()
263 }
264
265 pub fn batch(&self) -> Result<RecordBatch, KnowledgeError> {
267 let mut ids = FixedSizeBinaryBuilder::with_capacity(self.events.len(), 16);
268 let mut assertions = FixedSizeBinaryBuilder::with_capacity(self.events.len(), 16);
269 let mut from = TimestampMicrosecondBuilder::new().with_timezone("UTC");
270 let mut to = TimestampMicrosecondBuilder::new().with_timezone("UTC");
271 let mut reasoning = FixedSizeBinaryBuilder::with_capacity(self.events.len(), 16);
272 let mut provenance = FixedSizeBinaryBuilder::with_capacity(self.events.len(), 16);
273 let mut times = TimestampMicrosecondBuilder::new().with_timezone("UTC");
274 let mut versions = UInt32Builder::new();
275 for row in &self.events {
276 append_uuid(&mut ids, row.validity_event_uuid, "validity_event_uuid")?;
277 append_uuid(&mut assertions, row.assertion_uuid, "assertion_uuid")?;
278 from.append_option(row.valid_from_micros);
279 to.append_option(row.valid_to_micros);
280 append_optional_uuid(&mut reasoning, row.reasoning_uuid)?;
281 append_uuid(&mut provenance, row.provenance_uuid, "provenance_uuid")?;
282 times.append_value(row.recorded_at_micros);
283 versions.append_value(row.contract_version);
284 }
285 RecordBatch::try_new(
286 Arc::clone(&ASSERTION_VALIDITY_SCHEMA),
287 vec![
288 Arc::new(ids.finish()),
289 Arc::new(assertions.finish()),
290 Arc::new(from.finish()),
291 Arc::new(to.finish()),
292 Arc::new(reasoning.finish()),
293 Arc::new(provenance.finish()),
294 Arc::new(times.finish()),
295 Arc::new(versions.finish()),
296 ],
297 )
298 .map_err(|_| invalid("assertion_validity", "Arrow batch construction failed"))
299 }
300
301 pub fn from_batches(batches: &[RecordBatch]) -> Result<Self, KnowledgeError> {
303 let mut events = Vec::new();
304 for batch in batches {
305 if batch.schema().as_ref() != ASSERTION_VALIDITY_SCHEMA.as_ref() {
306 return Err(invalid("assertion_validity.schema", "schema mismatch"));
307 }
308 let ids = fixed(batch, "validity_event_uuid")?;
309 let assertions = fixed(batch, "assertion_uuid")?;
310 let from = timestamp(batch, "valid_from")?;
311 let to = timestamp(batch, "valid_to")?;
312 let reasoning = fixed(batch, "reasoning_uuid")?;
313 let provenance = fixed(batch, "provenance_uuid")?;
314 let times = timestamp(batch, "recorded_at")?;
315 let versions = uint32(batch, "contract_version")?;
316 for row in 0..batch.num_rows() {
317 events.push(AssertionValidityEvent {
318 validity_event_uuid: uuid_at(ids, row, "validity_event_uuid")?,
319 assertion_uuid: uuid_at(assertions, row, "assertion_uuid")?,
320 valid_from_micros: optional_timestamp_at(from, row),
321 valid_to_micros: optional_timestamp_at(to, row),
322 reasoning_uuid: optional_uuid_at(reasoning, row, "reasoning_uuid")?,
323 provenance_uuid: uuid_at(provenance, row, "provenance_uuid")?,
324 recorded_at_micros: times.value(row),
325 contract_version: versions.value(row),
326 });
327 }
328 }
329 Self::new(events)
330 }
331}
332
333fn validity_fingerprint_for_row(row: &AssertionValidityEvent) -> Result<[u8; 32], KnowledgeError> {
334 let mut writer = CanonicalWriter::new();
335 writer.raw(row.validity_event_uuid.as_bytes())?;
336 writer.raw(row.assertion_uuid.as_bytes())?;
337 optional_i64(&mut writer, row.valid_from_micros)?;
338 optional_i64(&mut writer, row.valid_to_micros)?;
339 optional_uuid(&mut writer, row.reasoning_uuid)?;
340 writer.raw(row.provenance_uuid.as_bytes())?;
341 writer.i64(row.recorded_at_micros)?;
342 writer.u32(row.contract_version)?;
343 fingerprint(
344 CanonicalDomain::AssertionValidity,
345 CANONICAL_CONTRACT_VERSION,
346 &writer.finish(),
347 )
348 .map_err(Into::into)
349}
350
351pub(crate) fn schema_registry_entry() -> SchemaRegistryEntry {
352 SchemaRegistryEntry {
353 capability_id: "valid_time",
354 capability_version: VALID_TIME_CAPABILITY_VERSION,
355 record_family: "assertion_validity_events",
356 record_version: ASSERTION_VALIDITY_CONTRACT_VERSION,
357 schema: Arc::clone(&ASSERTION_VALIDITY_SCHEMA),
358 schema_fingerprint: *ASSERTION_VALIDITY_SCHEMA_FINGERPRINT,
359 enum_registry_versions: &[(
360 "assertion_validity_policy",
361 ASSERTION_VALIDITY_POLICY_VERSION,
362 )],
363 sort_key: &["recorded_at", "validity_event_uuid"],
364 diff_identity_fields: &["validity_event_uuid"],
365 diff_record_uuid_field: Some("validity_event_uuid"),
366 fingerprint_domain: CanonicalDomain::AssertionValidity,
367 owner: "graphforge-knowledge",
368 implementation_issue: 781,
369 max_rows: MAX_KNOWLEDGE_ROWS,
370 }
371}
372
373fn validate_event(row: &AssertionValidityEvent) -> Result<(), KnowledgeError> {
374 #[cfg(test)]
375 EVENT_VALIDATION_CALLS.with(|calls| calls.set(calls.get() + 1));
376
377 if row.contract_version != ASSERTION_VALIDITY_CONTRACT_VERSION {
378 return Err(invalid(
379 "assertion_validity.contract_version",
380 "unsupported version",
381 ));
382 }
383 require_v7(row.validity_event_uuid, "validity_event_uuid")?;
384 require_v7(row.assertion_uuid, "assertion_uuid")?;
385 if let Some(reasoning_uuid) = row.reasoning_uuid {
386 require_v7(reasoning_uuid, "reasoning_uuid")?;
387 }
388 require_uuid(row.provenance_uuid, "provenance_uuid")?;
389 if matches!(
390 (row.valid_from_micros, row.valid_to_micros),
391 (Some(from), Some(to)) if from > to
392 ) {
393 return Err(invalid(
394 "assertion_validity.interval",
395 "valid_from must not exceed valid_to",
396 ));
397 }
398 Ok(())
399}
400
401fn validate_event_count(count: usize, limit: usize) -> Result<(), KnowledgeError> {
402 if count > limit {
403 return Err(KnowledgeError::Limit {
404 participant: "assertion_validity_events",
405 observed: count,
406 limit,
407 });
408 }
409 Ok(())
410}
411
412fn optional_i64(writer: &mut CanonicalWriter, value: Option<i64>) -> Result<(), KnowledgeError> {
413 match value {
414 Some(value) => {
415 writer.u8(1)?;
416 writer.i64(value)?;
417 }
418 None => writer.u8(0)?,
419 }
420 Ok(())
421}
422
423fn optional_uuid(writer: &mut CanonicalWriter, value: Option<Uuid>) -> Result<(), KnowledgeError> {
424 match value {
425 Some(value) => {
426 writer.u8(1)?;
427 writer.raw(value.as_bytes())?;
428 }
429 None => writer.u8(0)?,
430 }
431 Ok(())
432}
433
434fn append_uuid(
435 builder: &mut FixedSizeBinaryBuilder,
436 value: Uuid,
437 field: &'static str,
438) -> Result<(), KnowledgeError> {
439 builder
440 .append_value(value.as_bytes())
441 .map_err(|_| invalid(field, "invalid UUID width"))
442}
443
444fn append_optional_uuid(
445 builder: &mut FixedSizeBinaryBuilder,
446 value: Option<Uuid>,
447) -> Result<(), KnowledgeError> {
448 if let Some(value) = value {
449 append_uuid(builder, value, "reasoning_uuid")?;
450 } else {
451 builder.append_null();
452 }
453 Ok(())
454}
455
456fn optional_uuid_at(
457 values: &FixedSizeBinaryArray,
458 row: usize,
459 field: &'static str,
460) -> Result<Option<Uuid>, KnowledgeError> {
461 (!values.is_null(row))
462 .then(|| uuid_at(values, row, field))
463 .transpose()
464}
465
466fn optional_timestamp_at(values: &TimestampMicrosecondArray, row: usize) -> Option<i64> {
467 (!values.is_null(row)).then(|| values.value(row))
468}
469
470fn require_v7(value: Uuid, field: &'static str) -> Result<(), KnowledgeError> {
471 if value.get_version() != Some(Version::SortRand) {
472 return Err(invalid(field, "must be UUIDv7"));
473 }
474 require_uuid(value, field)
475}
476
477fn require_uuid(value: Uuid, field: &'static str) -> Result<(), KnowledgeError> {
478 if value.is_nil() {
479 return Err(invalid(field, "must not be nil"));
480 }
481 Ok(())
482}
483
484const fn invalid(field: &'static str, message: &'static str) -> KnowledgeError {
485 KnowledgeError::Invalid { field, message }
486}
487
488fn uuid_field(name: &str, nullable: bool) -> Field {
489 Field::new(name, DataType::FixedSizeBinary(16), nullable)
490}
491
492fn timestamp_field(name: &str, nullable: bool) -> Field {
493 Field::new(
494 name,
495 DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
496 nullable,
497 )
498}
499
500fn fixed<'a>(
501 batch: &'a RecordBatch,
502 name: &'static str,
503) -> Result<&'a FixedSizeBinaryArray, KnowledgeError> {
504 batch
505 .column_by_name(name)
506 .and_then(|value| value.as_any().downcast_ref())
507 .ok_or_else(|| invalid(name, "column type mismatch"))
508}
509
510fn timestamp<'a>(
511 batch: &'a RecordBatch,
512 name: &'static str,
513) -> Result<&'a TimestampMicrosecondArray, KnowledgeError> {
514 batch
515 .column_by_name(name)
516 .and_then(|value| value.as_any().downcast_ref())
517 .ok_or_else(|| invalid(name, "column type mismatch"))
518}
519
520fn uint32<'a>(
521 batch: &'a RecordBatch,
522 name: &'static str,
523) -> Result<&'a UInt32Array, KnowledgeError> {
524 batch
525 .column_by_name(name)
526 .and_then(|value| value.as_any().downcast_ref())
527 .ok_or_else(|| invalid(name, "column type mismatch"))
528}
529
530fn uuid_at(
531 values: &FixedSizeBinaryArray,
532 row: usize,
533 field: &'static str,
534) -> Result<Uuid, KnowledgeError> {
535 Uuid::from_slice(values.value(row)).map_err(|_| invalid(field, "invalid UUID bytes"))
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use std::time::{Duration, Instant};
542
543 fn uuid7(seed: u8) -> Uuid {
544 let mut bytes = [seed; 16];
545 bytes[6] = (bytes[6] & 0x0f) | 0x70;
546 bytes[8] = (bytes[8] & 0x3f) | 0x80;
547 Uuid::from_bytes(bytes)
548 }
549
550 fn event(
551 id: u8,
552 assertion: u8,
553 from: Option<i64>,
554 to: Option<i64>,
555 recorded_at: i64,
556 ) -> AssertionValidityEvent {
557 AssertionValidityEvent::new(
558 uuid7(id),
559 uuid7(assertion),
560 from,
561 to,
562 Some(uuid7(id.wrapping_add(40))),
563 uuid7(id.wrapping_add(80)),
564 recorded_at,
565 )
566 .unwrap()
567 }
568
569 #[test]
570 fn half_open_unbounded_empty_and_invalid_intervals_are_explicit() {
571 let always = event(1, 20, None, None, 1);
572 assert!(always.contains(i64::MIN));
573 assert!(always.contains(i64::MAX));
574
575 let bounded = event(2, 20, Some(10), Some(20), 2);
576 assert!(!bounded.contains(9));
577 assert!(bounded.contains(10));
578 assert!(bounded.contains(19));
579 assert!(!bounded.contains(20));
580
581 let empty = event(3, 20, Some(10), Some(10), 3);
582 assert!(!empty.contains(10));
583 assert!(
584 AssertionValidityEvent::new(
585 uuid7(4),
586 uuid7(20),
587 Some(11),
588 Some(10),
589 None,
590 uuid7(84),
591 4,
592 )
593 .is_err()
594 );
595 }
596
597 #[test]
598 fn transaction_cutoff_and_uuid_tie_breaking_preserve_prior_views() {
599 let ledger = AssertionValidityLedger::new(vec![
600 event(1, 20, Some(0), Some(10), 5),
601 event(2, 20, Some(10), None, 5),
602 event(3, 20, None, Some(0), 8),
603 ])
604 .unwrap();
605 assert_eq!(
606 ledger.current_for_at(uuid7(20), 4),
607 None,
608 "no event is visible before its transaction time"
609 );
610 assert_eq!(
611 ledger
612 .current_for_at(uuid7(20), 5)
613 .unwrap()
614 .validity_event_uuid,
615 uuid7(2)
616 );
617 assert_eq!(ledger.is_valid_at(uuid7(20), 5, 10), Some(true));
618 assert_eq!(ledger.is_valid_at(uuid7(20), 7, 10), Some(true));
619 assert_eq!(ledger.is_valid_at(uuid7(20), 8, 10), Some(false));
620 }
621
622 #[test]
623 fn round_trip_merge_replay_and_fingerprint_are_deterministic() {
624 let first = event(2, 20, Some(10), None, 2);
625 let second = event(1, 21, None, Some(10), 1);
626 let ledger = AssertionValidityLedger::new(vec![first.clone(), second]).unwrap();
627 let decoded = AssertionValidityLedger::from_batches(&[ledger.batch().unwrap()]).unwrap();
628 assert_eq!(decoded, ledger);
629 assert_eq!(decoded.events()[0].validity_event_uuid, uuid7(1));
630 assert_eq!(
631 decoded.event_fingerprint(uuid7(2)).unwrap(),
632 ledger.event_fingerprint(uuid7(2)).unwrap()
633 );
634 assert_eq!(
635 ledger
636 .merge(&AssertionValidityLedger::new(vec![first.clone()]).unwrap())
637 .unwrap(),
638 ledger
639 );
640
641 let mut conflicting = first;
642 conflicting.valid_to_micros = Some(30);
643 let conflicting = AssertionValidityLedger::new(vec![conflicting]).unwrap();
644 assert!(matches!(
645 ledger.merge(&conflicting),
646 Err(KnowledgeError::Conflict("validity_event_uuid"))
647 ));
648 }
649
650 fn unvalidated_event(seed: u8) -> AssertionValidityEvent {
651 AssertionValidityEvent {
652 validity_event_uuid: uuid7(seed),
653 assertion_uuid: uuid7(seed.wrapping_add(20)),
654 valid_from_micros: None,
655 valid_to_micros: None,
656 reasoning_uuid: None,
657 provenance_uuid: uuid7(seed.wrapping_add(40)),
658 recorded_at_micros: i64::from(seed),
659 contract_version: ASSERTION_VALIDITY_CONTRACT_VERSION,
660 }
661 }
662
663 fn benchmark_uuid(seed: u64) -> Uuid {
664 let mut bytes = [0; 16];
665 bytes[8..].copy_from_slice(&seed.to_be_bytes());
666 bytes[6] = 0x70;
667 bytes[8] = (bytes[8] & 0x3f) | 0x80;
668 Uuid::from_bytes(bytes)
669 }
670
671 fn merge_with_full_revalidation(
672 existing: &AssertionValidityLedger,
673 staged: &AssertionValidityLedger,
674 ) -> Result<AssertionValidityLedger, KnowledgeError> {
675 let mut events = existing.events.clone();
676 let mut by_id = events
677 .iter()
678 .cloned()
679 .map(|row| (row.validity_event_uuid, row))
680 .collect::<HashMap<_, _>>();
681 for row in staged.events() {
682 if let Some(previous) = by_id.get(&row.validity_event_uuid) {
683 if previous != row {
684 return Err(KnowledgeError::Conflict("validity_event_uuid"));
685 }
686 } else {
687 events.push(row.clone());
688 by_id.insert(row.validity_event_uuid, row.clone());
689 }
690 }
691 AssertionValidityLedger::new(events)
692 }
693
694 fn median(mut values: Vec<Duration>) -> Duration {
695 values.sort_unstable();
696 values[values.len() / 2]
697 }
698
699 fn digest(existing: &AssertionValidityLedger, staged: &AssertionValidityLedger) -> String {
700 let mut writer = CanonicalWriter::new();
701 for ledger in [existing, staged] {
702 writer.u64(ledger.events().len() as u64).unwrap();
703 for row in ledger.events() {
704 writer.raw(row.validity_event_uuid.as_bytes()).unwrap();
705 writer.raw(row.assertion_uuid.as_bytes()).unwrap();
706 optional_i64(&mut writer, row.valid_from_micros).unwrap();
707 optional_i64(&mut writer, row.valid_to_micros).unwrap();
708 optional_uuid(&mut writer, row.reasoning_uuid).unwrap();
709 writer.raw(row.provenance_uuid.as_bytes()).unwrap();
710 writer.i64(row.recorded_at_micros).unwrap();
711 writer.u32(row.contract_version).unwrap();
712 }
713 }
714 let bytes = fingerprint(
715 CanonicalDomain::AssertionValidity,
716 CANONICAL_CONTRACT_VERSION,
717 &writer.finish(),
718 )
719 .unwrap();
720 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
721 }
722
723 #[test]
724 #[ignore = "manual quiet-host before/after merge-cost measurement"]
725 fn quiet_host_merge_cost_measurement() {
726 for existing_count in [1_000_u64, 10_000] {
727 let existing = AssertionValidityLedger::new(
728 (0..existing_count)
729 .map(|seed| AssertionValidityEvent {
730 validity_event_uuid: benchmark_uuid(seed + 1),
731 assertion_uuid: benchmark_uuid(seed + 20_000),
732 valid_from_micros: None,
733 valid_to_micros: None,
734 reasoning_uuid: None,
735 provenance_uuid: benchmark_uuid(seed + 40_000),
736 recorded_at_micros: i64::try_from(seed).unwrap(),
737 contract_version: ASSERTION_VALIDITY_CONTRACT_VERSION,
738 })
739 .collect(),
740 )
741 .unwrap();
742 let staged = AssertionValidityLedger::new(vec![AssertionValidityEvent {
743 validity_event_uuid: benchmark_uuid(existing_count + 1),
744 assertion_uuid: benchmark_uuid(existing_count + 20_001),
745 valid_from_micros: None,
746 valid_to_micros: None,
747 reasoning_uuid: None,
748 provenance_uuid: benchmark_uuid(existing_count + 40_001),
749 recorded_at_micros: i64::try_from(existing_count).unwrap(),
750 contract_version: ASSERTION_VALIDITY_CONTRACT_VERSION,
751 }])
752 .unwrap();
753 assert_eq!(
754 merge_with_full_revalidation(&existing, &staged).unwrap(),
755 existing.merge(&staged).unwrap()
756 );
757 let repetitions = 9;
758 let mut before = Vec::with_capacity(repetitions);
759 let mut after = Vec::with_capacity(repetitions);
760 for _ in 0..repetitions {
761 let start = Instant::now();
762 std::hint::black_box(merge_with_full_revalidation(&existing, &staged).unwrap());
763 before.push(start.elapsed());
764
765 let start = Instant::now();
766 std::hint::black_box(existing.merge(&staged).unwrap());
767 after.push(start.elapsed());
768 }
769 let before_median = median(before);
770 let after_median = median(after);
771 let divisor = existing_count as f64;
772 println!(
773 "existing_rows={existing_count} staged_rows=1 repetitions={repetitions} input_sha256={} before_ns={} before_ns_per_existing_row={:.3} after_ns={} after_ns_per_existing_row={:.3}",
774 digest(&existing, &staged),
775 before_median.as_nanos(),
776 before_median.as_nanos() as f64 / divisor,
777 after_median.as_nanos(),
778 after_median.as_nanos() as f64 / divisor,
779 );
780 }
781 }
782
783 #[test]
784 fn merge_validates_staged_rows_once_and_never_revalidates_existing_rows() {
785 let base = AssertionValidityLedger::new(vec![
786 event(1, 20, None, None, 1),
787 event(2, 21, None, None, 3),
788 event(3, 22, None, None, 5),
789 ])
790 .unwrap();
791 let staged_event = event(4, 23, None, None, 2);
792
793 EVENT_VALIDATION_CALLS.with(|calls| calls.set(0));
794 let staged = AssertionValidityLedger::new(vec![staged_event]).unwrap();
795 EVENT_VALIDATION_CALLS.with(|calls| assert_eq!(calls.get(), 1));
796
797 EVENT_VALIDATION_CALLS.with(|calls| calls.set(0));
798 let merged = base.merge(&staged).unwrap();
799
800 EVENT_VALIDATION_CALLS.with(|calls| assert_eq!(calls.get(), 0));
801 assert_eq!(merged.events().len(), 4);
802 assert_eq!(
803 merged
804 .events()
805 .iter()
806 .map(|row| row.validity_event_uuid)
807 .collect::<Vec<_>>(),
808 vec![uuid7(1), uuid7(4), uuid7(2), uuid7(3)]
809 );
810 }
811
812 #[test]
813 fn merge_enforces_combined_row_limit_after_exact_replays_are_removed() {
814 let base = AssertionValidityLedger::new(vec![
815 event(1, 20, None, None, 1),
816 event(2, 21, None, None, 2),
817 ])
818 .unwrap();
819 let staged = AssertionValidityLedger::new(vec![
820 event(3, 22, None, None, 3),
821 event(4, 23, None, None, 4),
822 event(5, 24, None, None, 5),
823 ])
824 .unwrap();
825
826 assert!(matches!(
827 base.merge_with_limit(&staged, 3),
828 Err(KnowledgeError::Limit {
829 participant: "assertion_validity_events",
830 observed: 5,
831 limit: 3,
832 })
833 ));
834
835 let replayed = base
836 .merge_with_limit(
837 &AssertionValidityLedger::new(vec![event(1, 20, None, None, 1)]).unwrap(),
838 2,
839 )
840 .unwrap();
841 assert_eq!(replayed, base);
842 }
843
844 #[test]
845 fn merge_reports_conflict_before_combined_limit() {
846 let base = AssertionValidityLedger::new(vec![
847 event(1, 20, None, None, 1),
848 event(2, 21, None, None, 2),
849 ])
850 .unwrap();
851 let staged = AssertionValidityLedger::new(vec![
852 event(3, 22, None, None, 3),
853 event(4, 23, None, None, 4),
854 event(1, 25, None, None, 5),
855 ])
856 .unwrap();
857
858 assert!(matches!(
859 base.merge_with_limit(&staged, 3),
860 Err(KnowledgeError::Conflict("validity_event_uuid"))
861 ));
862 }
863
864 #[test]
865 fn event_count_limit_accepts_the_limit_and_rejects_the_first_excess() {
866 assert!(validate_event_count(MAX_KNOWLEDGE_ROWS, MAX_KNOWLEDGE_ROWS).is_ok());
867 assert!(matches!(
868 validate_event_count(MAX_KNOWLEDGE_ROWS + 1, MAX_KNOWLEDGE_ROWS),
869 Err(KnowledgeError::Limit {
870 participant: "assertion_validity_events",
871 observed,
872 limit: MAX_KNOWLEDGE_ROWS,
873 }) if observed == MAX_KNOWLEDGE_ROWS + 1
874 ));
875 }
876
877 #[test]
878 fn constructor_enforces_the_configured_row_limit() {
879 assert!(
880 AssertionValidityLedger::new_with_limit(
881 vec![unvalidated_event(1), unvalidated_event(2)],
882 2,
883 )
884 .is_ok()
885 );
886 assert!(matches!(
887 AssertionValidityLedger::new_with_limit(
888 vec![
889 unvalidated_event(1),
890 unvalidated_event(2),
891 unvalidated_event(3)
892 ],
893 2,
894 ),
895 Err(KnowledgeError::Limit {
896 participant: "assertion_validity_events",
897 observed: 3,
898 limit: 2,
899 })
900 ));
901 }
902
903 #[test]
904 fn duplicate_validity_event_uuid_is_rejected() {
905 let first = event(1, 20, None, None, 1);
906 let duplicate = event(1, 21, None, None, 2);
907 assert!(matches!(
908 AssertionValidityLedger::new(vec![first, duplicate]),
909 Err(KnowledgeError::Duplicate("validity_event_uuid"))
910 ));
911 }
912
913 #[test]
914 fn assertion_validity_contract_version_must_be_supported() {
915 let mut row = unvalidated_event(1);
916 row.contract_version = ASSERTION_VALIDITY_CONTRACT_VERSION + 1;
917 assert!(matches!(
918 AssertionValidityLedger::new(vec![row]),
919 Err(KnowledgeError::Invalid {
920 field: "assertion_validity.contract_version",
921 ..
922 })
923 ));
924 }
925
926 #[test]
927 fn validity_event_uuid_must_be_uuidv7() {
928 let mut row = unvalidated_event(1);
929 row.validity_event_uuid = Uuid::from_u128(1);
930 assert!(matches!(
931 AssertionValidityLedger::new(vec![row]),
932 Err(KnowledgeError::Invalid {
933 field: "validity_event_uuid",
934 ..
935 })
936 ));
937 }
938
939 #[test]
940 fn assertion_uuid_must_be_uuidv7() {
941 let mut row = unvalidated_event(1);
942 row.assertion_uuid = Uuid::from_u128(1);
943 assert!(matches!(
944 AssertionValidityLedger::new(vec![row]),
945 Err(KnowledgeError::Invalid {
946 field: "assertion_uuid",
947 ..
948 })
949 ));
950 }
951
952 #[test]
953 fn optional_reasoning_uuid_must_be_uuidv7() {
954 let mut row = unvalidated_event(1);
955 row.reasoning_uuid = Some(Uuid::from_u128(1));
956 assert!(matches!(
957 AssertionValidityLedger::new(vec![row]),
958 Err(KnowledgeError::Invalid {
959 field: "reasoning_uuid",
960 ..
961 })
962 ));
963 }
964
965 #[test]
966 fn provenance_uuid_must_not_be_nil() {
967 let mut row = unvalidated_event(1);
968 row.provenance_uuid = Uuid::nil();
969 assert!(matches!(
970 AssertionValidityLedger::new(vec![row]),
971 Err(KnowledgeError::Invalid {
972 field: "provenance_uuid",
973 ..
974 })
975 ));
976 }
977
978 #[test]
979 fn valid_time_interval_must_not_run_backwards() {
980 let mut row = unvalidated_event(1);
981 row.valid_from_micros = Some(2);
982 row.valid_to_micros = Some(1);
983 assert!(matches!(
984 AssertionValidityLedger::new(vec![row]),
985 Err(KnowledgeError::Invalid {
986 field: "assertion_validity.interval",
987 ..
988 })
989 ));
990 }
991
992 #[test]
993 fn schema_registry_freezes_capability_family_and_order() {
994 let entry = schema_registry_entry();
995 assert_eq!(entry.capability_id, "valid_time");
996 assert_eq!(entry.capability_version, 1);
997 assert_eq!(entry.record_family, "assertion_validity_events");
998 assert_eq!(entry.sort_key, &["recorded_at", "validity_event_uuid"]);
999 assert_eq!(entry.implementation_issue, 781);
1000 }
1001}