1use std::error::Error;
8use std::fmt::{self, Display, Formatter};
9
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 CollectionId, CollectionIndexRef, CollectionSecurityGeneration, Epoch, IndexMode, IngestAuthorityV1, LogicalMillis,
15 ManifestGeneration, PolicyGeneration, RootCutId, SchemaGeneration, SegmentId, SeqNo, ShardId, ShardKey,
16};
17
18pub const DERIVED_OVERLOAD_FORMAT_VERSION: u16 = 3;
19pub const MAX_DERIVED_CONTRACT_BYTES: usize = 1_048_576;
20pub const MAX_CACHE_SHARDS: usize = 512;
21pub const MAX_CACHE_RESULT_WINDOW: u32 = 100_000;
22pub const MAX_CACHE_ENTRY_BYTES: u64 = 16 * 1_024 * 1_024;
23pub const MAX_CACHE_COLLECTION_BYTES: u64 = 512 * 1_024 * 1_024;
24pub const MAX_CACHE_COLLECTION_ENTRIES: u32 = 4_096;
25pub const MAX_PROMOTED_FIELDS: u16 = 64;
26pub const MAX_HEAT_RECORDS: u32 = 16_384;
27pub const MAX_DISTINCT_QUERIES_PER_HEAT_RECORD: u32 = 1_000_000;
28pub const MAX_PROMOTION_EXTRA_BYTES: u64 = 1_073_741_824;
29pub const MAX_PROMOTED_COLUMN_SIDECAR_BYTES: usize = 64 * 1_024 * 1_024;
30pub const MAX_PROMOTED_COLUMN_ROWS: u32 = 1_000_000;
31pub const PROMOTED_COLUMN_SIDECAR_NAME: &str = "field-promotion.v1.json";
32pub const MAX_OVERLOAD_BACKLOG_BYTES: u64 = 1_073_741_824;
33pub const MAX_OVERLOAD_BACKLOG_AGE_MILLIS: u64 = 300_000;
34pub const MAX_OVERLOAD_COLLECTION_SHARE_BYTES: u64 = 268_435_456;
35pub const MAX_OVERLOAD_RECOVERY_OPS_PER_PASS: u32 = 10_000;
36pub const MAX_PENDING_OVERLAY_DOCUMENTS: u64 = 1_000_000;
37
38fn digest_is_nonzero(value: &[u8; 32]) -> bool {
39 value.iter().any(|byte| *byte != 0)
40}
41
42fn require_nonzero_digest(value: &[u8; 32], name: &'static str) -> Result<(), DerivedContractError> {
43 if digest_is_nonzero(value) {
44 Ok(())
45 } else {
46 Err(DerivedContractError::Violation(name))
47 }
48}
49
50macro_rules! semantic_digest {
51 ($name:ident, $description:literal) => {
52 #[doc = $description]
53 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
54 #[serde(transparent)]
55 pub struct $name([u8; 32]);
56
57 impl $name {
58 pub fn new(value: [u8; 32]) -> Result<Self, DerivedContractError> {
59 require_nonzero_digest(&value, concat!(stringify!($name), " cannot be zero"))?;
60 Ok(Self(value))
61 }
62
63 #[must_use]
64 pub const fn get(self) -> [u8; 32] {
65 self.0
66 }
67
68 fn validate(self) -> Result<(), DerivedContractError> {
69 require_nonzero_digest(&self.0, concat!(stringify!($name), " cannot be zero"))
70 }
71 }
72 };
73}
74
75macro_rules! semantic_generation {
76 ($name:ident, $description:literal) => {
77 #[doc = $description]
78 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
79 #[serde(transparent)]
80 pub struct $name(u64);
81
82 impl $name {
83 pub const fn new(value: u64) -> Result<Self, DerivedContractError> {
84 if value == 0 {
85 Err(DerivedContractError::Violation(concat!(
86 stringify!($name),
87 " cannot be zero"
88 )))
89 } else {
90 Ok(Self(value))
91 }
92 }
93
94 #[must_use]
95 pub const fn get(self) -> u64 {
96 self.0
97 }
98
99 fn validate(self) -> Result<(), DerivedContractError> {
100 Self::new(self.0).map(|_| ())
101 }
102 }
103 };
104}
105
106semantic_digest!(AuthorizedScopeDigestV1, "Digest of the exact authorized query scope.");
107semantic_digest!(NormalizedPlanDigestV1, "Digest of one canonical normalized query plan.");
108semantic_digest!(OutputSemanticsDigestV1, "Digest of result-affecting output semantics.");
109semantic_digest!(FeatureFlagsDigestV1, "Digest of result-affecting feature flags.");
110semantic_digest!(SnapshotDigestV1, "Digest of one exact PIT snapshot binding.");
111semantic_digest!(FieldPathDigestV1, "Opaque digest of one logical field path.");
112semantic_generation!(
113 CollectionResetGenerationV1,
114 "Collection-wide result-cache reset generation."
115);
116semantic_generation!(
117 DeletionOverlayGenerationV1,
118 "Current privacy/deletion overlay generation."
119);
120semantic_generation!(KeyAccessGenerationV1, "Current key-access authorization generation.");
121semantic_generation!(PromotionDecisionGenerationV1, "Physical promotion decision generation.");
122
123pub fn field_path_digest_v1(path: &str) -> Result<FieldPathDigestV1, DerivedContractError> {
126 if !crate::is_canonical_field_path(path) {
127 return Err(DerivedContractError::Violation(
128 "field path is not canonical and cannot be promoted",
129 ));
130 }
131 let mut hasher = blake3::Hasher::new();
132 hasher.update(b"lodestone/p13/field-path/v1\0");
133 hasher.update(path.as_bytes());
134 FieldPathDigestV1::new(*hasher.finalize().as_bytes())
135}
136
137#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
139#[serde(deny_unknown_fields)]
140pub struct CacheShardGenerationV1 {
141 pub shard_id: ShardId,
142 pub manifest_generation: ManifestGeneration,
143}
144
145#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
147#[serde(rename_all = "snake_case")]
148pub enum CacheFreshnessV1 {
149 Committed,
150 Realtime,
151}
152
153#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
155#[serde(rename_all = "snake_case")]
156pub enum CacheReuseScopeV1 {
157 CompleteCommittedResult,
158 CommittedSubresultOfRealtime,
159}
160
161#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
163#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
164pub enum CacheSnapshotBindingV1 {
165 Current,
166 Pit { snapshot_digest: SnapshotDigestV1 },
167 RetainedHistory { root_cut_id: RootCutId },
168}
169
170impl CacheSnapshotBindingV1 {
171 fn validate(self) -> Result<(), DerivedContractError> {
172 match self {
173 Self::Current | Self::RetainedHistory { .. } => Ok(()),
174 Self::Pit { snapshot_digest } => snapshot_digest.validate(),
175 }
176 }
177}
178
179#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
181#[serde(deny_unknown_fields)]
182pub struct ResultCacheKeyV1 {
183 pub format_version: u16,
184 pub collection_index_ref: CollectionIndexRef,
185 pub collection_security_generation: CollectionSecurityGeneration,
186 pub authorized_scope_digest: AuthorizedScopeDigestV1,
187 pub policy_generation: PolicyGeneration,
188 pub collection_reset_generation: CollectionResetGenerationV1,
189 pub deletion_overlay_generation: DeletionOverlayGenerationV1,
190 pub key_access_generation: KeyAccessGenerationV1,
191 pub normalized_plan_digest: NormalizedPlanDigestV1,
192 pub output_semantics_digest: OutputSemanticsDigestV1,
193 pub expected_shard_count: u16,
194 pub shard_generations: Vec<CacheShardGenerationV1>,
195 pub mapping_generation: SchemaGeneration,
196 pub freshness: CacheFreshnessV1,
197 pub reuse_scope: CacheReuseScopeV1,
198 pub from: u32,
199 pub limit: u32,
200 pub feature_flags_digest: FeatureFlagsDigestV1,
201 pub snapshot: CacheSnapshotBindingV1,
202}
203
204impl ResultCacheKeyV1 {
205 pub fn validate(&self) -> Result<(), DerivedContractError> {
206 validate_format(self.format_version)?;
207 if self.collection_security_generation.get() == 0
208 || self.policy_generation.get() == 0
209 || self.mapping_generation.get() == 0
210 {
211 return Err(DerivedContractError::Violation(
212 "cache generations and authorization revisions must be nonzero",
213 ));
214 }
215 self.authorized_scope_digest.validate()?;
216 self.collection_reset_generation.validate()?;
217 self.deletion_overlay_generation.validate()?;
218 self.key_access_generation.validate()?;
219 self.normalized_plan_digest.validate()?;
220 self.output_semantics_digest.validate()?;
221 self.feature_flags_digest.validate()?;
222 if self.shard_generations.is_empty() || self.shard_generations.len() > MAX_CACHE_SHARDS {
223 return Err(DerivedContractError::Budget {
224 field: "cache shard generations",
225 observed: self.shard_generations.len() as u64,
226 maximum: MAX_CACHE_SHARDS as u64,
227 });
228 }
229 if usize::from(self.expected_shard_count) != self.shard_generations.len() {
230 return Err(DerivedContractError::Violation(
231 "cache shard vector does not contain the complete expected shard count",
232 ));
233 }
234 let mut previous = None;
235 for shard in &self.shard_generations {
236 if shard.manifest_generation.get() == 0 || previous.is_some_and(|prior| prior >= shard.shard_id) {
237 return Err(DerivedContractError::Violation(
238 "cache shard generations must be nonzero, unique, and strictly ordered",
239 ));
240 }
241 previous = Some(shard.shard_id);
242 }
243 if self.limit == 0
244 || self
245 .from
246 .checked_add(self.limit)
247 .is_none_or(|window| window > MAX_CACHE_RESULT_WINDOW)
248 {
249 return Err(DerivedContractError::Violation(
250 "cache result window is outside the bounded contract",
251 ));
252 }
253 match (self.freshness, self.reuse_scope) {
254 (CacheFreshnessV1::Committed, CacheReuseScopeV1::CompleteCommittedResult)
255 | (CacheFreshnessV1::Realtime, CacheReuseScopeV1::CommittedSubresultOfRealtime) => {}
256 _ => {
257 return Err(DerivedContractError::Violation(
258 "realtime requests may cache only an immutable committed sub-result",
259 ));
260 }
261 }
262 self.snapshot.validate()
263 }
264}
265
266#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
268#[serde(deny_unknown_fields)]
269pub struct ResultCacheBudgetV1 {
270 pub format_version: u16,
271 pub collection_id: CollectionId,
272 pub maximum_entry_bytes: u64,
273 pub maximum_collection_bytes: u64,
274 pub maximum_collection_entries: u32,
275}
276
277impl ResultCacheBudgetV1 {
278 pub fn validate(self) -> Result<(), DerivedContractError> {
279 validate_format(self.format_version)?;
280 if self.maximum_entry_bytes == 0
281 || self.maximum_entry_bytes > MAX_CACHE_ENTRY_BYTES
282 || self.maximum_collection_bytes < self.maximum_entry_bytes
283 || self.maximum_collection_bytes > MAX_CACHE_COLLECTION_BYTES
284 || self.maximum_collection_entries == 0
285 || self.maximum_collection_entries > MAX_CACHE_COLLECTION_ENTRIES
286 {
287 return Err(DerivedContractError::Violation(
288 "result-cache budget is outside the bounded contract",
289 ));
290 }
291 Ok(())
292 }
293}
294
295#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
297#[serde(deny_unknown_fields)]
298pub struct ResultCacheMetricsV1 {
299 pub format_version: u16,
300 pub collection_id: CollectionId,
301 pub hits: u64,
302 pub misses: u64,
303 pub admissions: u64,
304 pub bypasses: u64,
305 pub evictions: u64,
306 pub collection_bytes: u64,
307 pub collection_entries: u32,
308}
309
310impl ResultCacheMetricsV1 {
311 pub fn validate(self, budget: ResultCacheBudgetV1) -> Result<(), DerivedContractError> {
312 validate_format(self.format_version)?;
313 budget.validate()?;
314 if self.collection_id != budget.collection_id
315 || self.collection_bytes > budget.maximum_collection_bytes
316 || self.collection_entries > budget.maximum_collection_entries
317 {
318 return Err(DerivedContractError::Violation(
319 "result-cache metrics exceed the recorded collection budget",
320 ));
321 }
322 Ok(())
323 }
324}
325
326#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
328#[serde(rename_all = "snake_case")]
329pub enum PromotionPrivacyV1 {
330 Ordinary,
331 Sensitive,
332 Restricted,
333}
334
335#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
337#[serde(rename_all = "snake_case")]
338pub enum PromotedFieldTypeV1 {
339 Keyword,
340 I64,
341 U64,
342 F64,
343 DateMillis,
344 Boolean,
345}
346
347#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
349#[serde(deny_unknown_fields)]
350pub struct FieldHeatAggregateV1 {
351 pub format_version: u16,
352 pub collection_index_ref: CollectionIndexRef,
353 pub mapping_generation: SchemaGeneration,
354 pub field_path_digest: FieldPathDigestV1,
355 pub window_start_logical_millis: LogicalMillis,
356 pub window_end_logical_millis: LogicalMillis,
357 pub filter_uses: u64,
358 pub sort_uses: u64,
359 pub aggregation_uses: u64,
360 pub distinct_query_count: u32,
361 pub estimated_cardinality: u64,
362 pub estimated_extra_bytes: u64,
363 pub privacy: PromotionPrivacyV1,
364 pub field_type: PromotedFieldTypeV1,
365 pub observed_type_conflict: bool,
366 pub path_policy_eligible: bool,
367}
368
369impl FieldHeatAggregateV1 {
370 pub fn validate(&self) -> Result<(), DerivedContractError> {
371 validate_format(self.format_version)?;
372 if self.mapping_generation.get() == 0
373 || self.window_start_logical_millis.get() >= self.window_end_logical_millis.get()
374 || self.distinct_query_count == 0
375 || self.distinct_query_count > MAX_DISTINCT_QUERIES_PER_HEAT_RECORD
376 || self.estimated_extra_bytes > MAX_PROMOTION_EXTRA_BYTES
377 {
378 return Err(DerivedContractError::Violation(
379 "field heat aggregate is outside the bounded contract",
380 ));
381 }
382 self.field_path_digest.validate()?;
383 let total_uses = self
384 .filter_uses
385 .checked_add(self.sort_uses)
386 .and_then(|uses| uses.checked_add(self.aggregation_uses))
387 .ok_or(DerivedContractError::ArithmeticOverflow)?;
388 if total_uses == 0 || u64::from(self.distinct_query_count) > total_uses {
389 return Err(DerivedContractError::Violation(
390 "field heat requires enough physical operations for its distinct-query count",
391 ));
392 }
393 Ok(())
394 }
395}
396
397#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
399#[serde(deny_unknown_fields)]
400pub struct FieldPromotionPolicyV1 {
401 pub format_version: u16,
402 pub collection_index_ref: CollectionIndexRef,
403 pub mapping_generation: SchemaGeneration,
404 pub policy_generation: PolicyGeneration,
405 pub enabled: bool,
406 pub minimum_query_count: u32,
407 pub maximum_promoted_fields: u16,
408 pub maximum_cardinality: u64,
409 pub maximum_extra_bytes: u64,
410 pub allow_sensitive: bool,
411}
412
413impl FieldPromotionPolicyV1 {
414 pub fn validate(self) -> Result<(), DerivedContractError> {
415 validate_format(self.format_version)?;
416 if self.mapping_generation.get() == 0
417 || self.policy_generation.get() == 0
418 || self.minimum_query_count == 0
419 || self.minimum_query_count > MAX_DISTINCT_QUERIES_PER_HEAT_RECORD
420 || self.maximum_promoted_fields == 0
421 || self.maximum_promoted_fields > MAX_PROMOTED_FIELDS
422 || self.maximum_cardinality == 0
423 || self.maximum_extra_bytes == 0
424 || self.maximum_extra_bytes > MAX_PROMOTION_EXTRA_BYTES
425 {
426 return Err(DerivedContractError::Violation(
427 "field-promotion policy is outside the bounded contract",
428 ));
429 }
430 Ok(())
431 }
432
433 pub fn admits(
435 self,
436 heat: &FieldHeatAggregateV1,
437 currently_promoted_fields: u16,
438 currently_promoted_extra_bytes: u64,
439 ) -> Result<bool, DerivedContractError> {
440 self.validate()?;
441 heat.validate()?;
442 if heat.collection_index_ref != self.collection_index_ref || heat.mapping_generation != self.mapping_generation
443 {
444 return Err(DerivedContractError::Violation(
445 "field heat does not belong to the promotion policy scope and mapping generation",
446 ));
447 }
448 if currently_promoted_fields > self.maximum_promoted_fields
449 || currently_promoted_extra_bytes > self.maximum_extra_bytes
450 {
451 return Err(DerivedContractError::Violation(
452 "current promoted layout exceeds the governing policy",
453 ));
454 }
455 let projected_extra_bytes = currently_promoted_extra_bytes
456 .checked_add(heat.estimated_extra_bytes)
457 .ok_or(DerivedContractError::ArithmeticOverflow)?;
458 Ok(self.enabled
459 && heat.distinct_query_count >= self.minimum_query_count
460 && heat.estimated_cardinality <= self.maximum_cardinality
461 && projected_extra_bytes <= self.maximum_extra_bytes
462 && currently_promoted_fields < self.maximum_promoted_fields
463 && !heat.observed_type_conflict
464 && heat.path_policy_eligible
465 && heat.privacy != PromotionPrivacyV1::Restricted
466 && (heat.privacy != PromotionPrivacyV1::Sensitive || self.allow_sensitive))
467 }
468}
469
470#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
472#[serde(deny_unknown_fields)]
473pub struct PromotedFieldLayoutV1 {
474 pub format_version: u16,
475 pub collection_index_ref: CollectionIndexRef,
476 pub segment_id: SegmentId,
477 pub field_path_digest: FieldPathDigestV1,
478 pub mapping_generation: SchemaGeneration,
479 pub decision_generation: PromotionDecisionGenerationV1,
480 pub field_type: PromotedFieldTypeV1,
481}
482
483impl PromotedFieldLayoutV1 {
484 pub fn validate(self) -> Result<(), DerivedContractError> {
485 validate_format(self.format_version)?;
486 self.field_path_digest.validate()?;
487 self.decision_generation.validate()?;
488 if self.segment_id.get() == 0 || self.mapping_generation.get() == 0 {
489 return Err(DerivedContractError::Violation(
490 "promoted layout generations must be nonzero",
491 ));
492 }
493 Ok(())
494 }
495}
496
497#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
499#[serde(deny_unknown_fields)]
500pub struct FieldPromotionStatusV1 {
501 pub format_version: u16,
502 pub collection_index_ref: CollectionIndexRef,
503 pub mapping_generation: SchemaGeneration,
504 pub policy_generation: PolicyGeneration,
505 pub last_decision_generation: Option<PromotionDecisionGenerationV1>,
506 pub heat_records: u32,
507 pub promoted_fields: u16,
508 pub extra_bytes: u64,
509 pub enabled: bool,
510}
511
512impl FieldPromotionStatusV1 {
513 pub fn validate(self, policy: FieldPromotionPolicyV1) -> Result<(), DerivedContractError> {
514 validate_format(self.format_version)?;
515 policy.validate()?;
516 if self.collection_index_ref != policy.collection_index_ref
517 || self.mapping_generation != policy.mapping_generation
518 || self.policy_generation != policy.policy_generation
519 || self.enabled != policy.enabled
520 || self.heat_records > MAX_HEAT_RECORDS
521 || self.promoted_fields > policy.maximum_promoted_fields
522 || self.extra_bytes > policy.maximum_extra_bytes
523 || (self.promoted_fields > 0 && self.last_decision_generation.is_none())
524 || (self.promoted_fields == 0 && self.extra_bytes != 0)
525 {
526 return Err(DerivedContractError::Violation(
527 "field-promotion status is inconsistent with the governing policy",
528 ));
529 }
530 if let Some(generation) = self.last_decision_generation {
531 generation.validate()?;
532 }
533 Ok(())
534 }
535}
536
537#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
540#[serde(tag = "kind", content = "value", rename_all = "snake_case", deny_unknown_fields)]
541pub enum PromotedFieldValueV1 {
542 Keyword(String),
543 I64(i64),
544 U64(u64),
545 F64Bits(u64),
546 DateMillis(u64),
547 Boolean(bool),
548}
549
550impl PromotedFieldValueV1 {
551 pub fn validate_for_type(&self, field_type: PromotedFieldTypeV1) -> Result<(), DerivedContractError> {
552 let matches_type = matches!(
553 (self, field_type),
554 (Self::Keyword(_), PromotedFieldTypeV1::Keyword)
555 | (Self::I64(_), PromotedFieldTypeV1::I64)
556 | (Self::U64(_), PromotedFieldTypeV1::U64)
557 | (Self::F64Bits(_), PromotedFieldTypeV1::F64)
558 | (Self::DateMillis(_), PromotedFieldTypeV1::DateMillis)
559 | (Self::Boolean(_), PromotedFieldTypeV1::Boolean)
560 );
561 if !matches_type {
562 return Err(DerivedContractError::Violation(
563 "promoted column value does not match its closed physical type",
564 ));
565 }
566 match self {
567 Self::Keyword(value) if value.len() > crate::MAX_MAPPING_FIELD_NAME_BYTES.saturating_mul(64) => Err(
568 DerivedContractError::Violation("promoted keyword value exceeds the bounded scalar representation"),
569 ),
570 Self::F64Bits(bits) if !f64::from_bits(*bits).is_finite() => Err(DerivedContractError::Violation(
571 "promoted floating-point value must be finite",
572 )),
573 Self::F64Bits(bits) if f64::from_bits(*bits) == 0.0 && *bits != 0.0_f64.to_bits() => Err(
574 DerivedContractError::Violation("promoted floating-point zero must use canonical positive-zero bits"),
575 ),
576 _ => Ok(()),
577 }
578 }
579}
580
581#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
584#[serde(deny_unknown_fields)]
585pub struct PromotedFieldRowV1 {
586 pub document_ordinal: u32,
587 pub value: PromotedFieldValueV1,
588}
589
590#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
592#[serde(deny_unknown_fields)]
593pub struct PromotedFieldColumnV1 {
594 pub layout: PromotedFieldLayoutV1,
595 pub rows: Vec<PromotedFieldRowV1>,
596}
597
598impl PromotedFieldColumnV1 {
599 pub fn validate(&self, document_count: u32) -> Result<(), DerivedContractError> {
600 self.layout.validate()?;
601 if self.rows.len() > document_count as usize || self.rows.len() > MAX_PROMOTED_COLUMN_ROWS as usize {
602 return Err(DerivedContractError::Budget {
603 field: "promoted column rows",
604 observed: self.rows.len() as u64,
605 maximum: u64::from(document_count.min(MAX_PROMOTED_COLUMN_ROWS)),
606 });
607 }
608 let mut previous = None;
609 for row in &self.rows {
610 if row.document_ordinal >= document_count || previous.is_some_and(|prior| prior >= row.document_ordinal) {
611 return Err(DerivedContractError::Violation(
612 "promoted column rows must be unique, ordered, and inside the segment",
613 ));
614 }
615 row.value.validate_for_type(self.layout.field_type)?;
616 previous = Some(row.document_ordinal);
617 }
618 Ok(())
619 }
620}
621
622#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
625#[serde(deny_unknown_fields)]
626pub struct PromotedColumnSidecarV1 {
627 pub format_version: u16,
628 pub collection_index_ref: CollectionIndexRef,
629 pub segment_id: SegmentId,
630 pub mapping_generation: SchemaGeneration,
631 pub document_count: u32,
632 pub columns: Vec<PromotedFieldColumnV1>,
633}
634
635impl PromotedColumnSidecarV1 {
636 pub fn validate(&self) -> Result<(), DerivedContractError> {
637 validate_format(self.format_version)?;
638 if self.segment_id.get() == 0
639 || self.mapping_generation.get() == 0
640 || self.document_count > MAX_PROMOTED_COLUMN_ROWS
641 || self.columns.is_empty()
642 || self.columns.len() > usize::from(MAX_PROMOTED_FIELDS)
643 {
644 return Err(DerivedContractError::Violation(
645 "promoted column sidecar is outside the bounded segment contract",
646 ));
647 }
648 let mut previous_digest = None;
649 let mut decision_generations = std::collections::BTreeSet::new();
650 for column in &self.columns {
651 let layout = column.layout;
652 if layout.collection_index_ref != self.collection_index_ref
653 || layout.segment_id != self.segment_id
654 || layout.mapping_generation != self.mapping_generation
655 || previous_digest.is_some_and(|previous| previous >= layout.field_path_digest.get())
656 {
657 return Err(DerivedContractError::Violation(
658 "promoted sidecar columns must match the segment and be strictly digest ordered",
659 ));
660 }
661 if !decision_generations.insert(layout.decision_generation.get()) {
662 return Err(DerivedContractError::Violation(
663 "promoted sidecar decision generations must be unique",
664 ));
665 }
666 column.validate(self.document_count)?;
667 previous_digest = Some(layout.field_path_digest.get());
668 }
669 Ok(())
670 }
671
672 #[must_use]
673 pub fn column(&self, digest: FieldPathDigestV1) -> Option<&PromotedFieldColumnV1> {
674 self.columns
675 .binary_search_by_key(&digest.get(), |column| column.layout.field_path_digest.get())
676 .ok()
677 .and_then(|index| self.columns.get(index))
678 }
679}
680
681#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
682#[serde(rename_all = "snake_case")]
683pub enum OverloadModeV1 {
684 Shed,
685 Buffer,
686}
687
688#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
690#[serde(deny_unknown_fields)]
691pub struct OverloadPolicyV1 {
692 pub format_version: u16,
693 pub collection_index_ref: CollectionIndexRef,
694 pub policy_generation: PolicyGeneration,
695 pub mode: OverloadModeV1,
696 pub maximum_backlog_bytes: u64,
697 pub maximum_backlog_age_millis: u64,
698 pub maximum_collection_share_bytes: u64,
699 pub maximum_recovery_ops_per_pass: u32,
700 pub maximum_pending_overlay_documents: u64,
701}
702
703impl OverloadPolicyV1 {
704 pub fn validate(self) -> Result<(), DerivedContractError> {
705 validate_format(self.format_version)?;
706 if self.policy_generation.get() == 0
707 || self.maximum_backlog_bytes == 0
708 || self.maximum_backlog_bytes > MAX_OVERLOAD_BACKLOG_BYTES
709 || self.maximum_backlog_age_millis == 0
710 || self.maximum_backlog_age_millis > MAX_OVERLOAD_BACKLOG_AGE_MILLIS
711 || self.maximum_collection_share_bytes == 0
712 || self.maximum_collection_share_bytes > self.maximum_backlog_bytes
713 || self.maximum_collection_share_bytes > MAX_OVERLOAD_COLLECTION_SHARE_BYTES
714 || self.maximum_recovery_ops_per_pass == 0
715 || self.maximum_recovery_ops_per_pass > MAX_OVERLOAD_RECOVERY_OPS_PER_PASS
716 || self.maximum_pending_overlay_documents == 0
717 || self.maximum_pending_overlay_documents > MAX_PENDING_OVERLAY_DOCUMENTS
718 {
719 return Err(DerivedContractError::Violation(
720 "overload policy is outside the bounded contract",
721 ));
722 }
723 Ok(())
724 }
725}
726
727#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
728#[serde(rename_all = "snake_case")]
729pub enum OverloadDispositionV1 {
730 Shed,
731 BufferRegisteredWal,
732 PauseReplayableSource,
733 RefuseStateMode,
734 RefuseMissingMutableOverlay,
735}
736
737pub fn overload_disposition_v1(
739 policy: OverloadPolicyV1,
740 index_mode: IndexMode,
741 ingest_authority: &IngestAuthorityV1,
742 exact_mutable_overlay_available: bool,
743) -> Result<OverloadDispositionV1, DerivedContractError> {
744 policy.validate()?;
745 let validation_mode =
750 if index_mode == IndexMode::State && matches!(ingest_authority, IngestAuthorityV1::ReplayableSource { .. }) {
751 IndexMode::AppendLog
752 } else {
753 index_mode
754 };
755 ingest_authority
756 .validate_for_mode(validation_mode)
757 .map_err(DerivedContractError::source)?;
758 if policy.mode == OverloadModeV1::Shed {
759 return Ok(OverloadDispositionV1::Shed);
760 }
761 if index_mode == IndexMode::State {
762 return Ok(OverloadDispositionV1::RefuseStateMode);
763 }
764 if matches!(ingest_authority, IngestAuthorityV1::ReplayableSource { .. }) {
765 return Ok(OverloadDispositionV1::PauseReplayableSource);
766 }
767 match index_mode {
768 IndexMode::AppendLog => Ok(OverloadDispositionV1::BufferRegisteredWal),
769 IndexMode::Mutable if exact_mutable_overlay_available => Ok(OverloadDispositionV1::BufferRegisteredWal),
770 IndexMode::Mutable => Ok(OverloadDispositionV1::RefuseMissingMutableOverlay),
771 IndexMode::State => Ok(OverloadDispositionV1::RefuseStateMode),
772 }
773}
774
775#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
777#[serde(deny_unknown_fields)]
778pub struct PendingMutationOverlayV1 {
779 pub format_version: u16,
780 pub shard_key: ShardKey,
781 pub writer_epoch: Epoch,
782 pub registered_through: SeqNo,
783 pub indexed_through: SeqNo,
784 pub get_overlay_through: SeqNo,
785 pub conditional_write_through: SeqNo,
786 pub collector_visibility_through: SeqNo,
787 pub subscription_through: SeqNo,
788 pub pending_documents: u64,
789 pub pending_tombstones: u64,
790 pub pending_bytes: u64,
791 pub oldest_age_millis: u64,
792}
793
794impl PendingMutationOverlayV1 {
795 pub fn validate(self) -> Result<(), DerivedContractError> {
796 validate_format(self.format_version)?;
797 let durable = self.registered_through.get();
798 let has_no_pending_work = self.pending_documents == 0
799 && self.pending_tombstones == 0
800 && self.pending_bytes == 0
801 && self.oldest_age_millis == 0;
802 if self.writer_epoch.get() == 0
803 || self.indexed_through.get() > durable
804 || self.get_overlay_through.get() != durable
805 || self.conditional_write_through.get() != durable
806 || self.collector_visibility_through.get() != durable
807 || self.subscription_through.get() != durable
808 || self.pending_documents > MAX_PENDING_OVERLAY_DOCUMENTS
809 || self.pending_tombstones > self.pending_documents
810 || self.pending_bytes > MAX_OVERLOAD_BACKLOG_BYTES
811 || self.oldest_age_millis > MAX_OVERLOAD_BACKLOG_AGE_MILLIS
812 || (self.indexed_through == self.registered_through && !has_no_pending_work)
813 {
814 return Err(DerivedContractError::Violation(
815 "pending mutation overlay does not exactly cover the registered WAL cut",
816 ));
817 }
818 Ok(())
819 }
820
821 pub fn lag_operations(self) -> Result<u64, DerivedContractError> {
822 self.validate()?;
823 Ok(self.registered_through.get() - self.indexed_through.get())
824 }
825}
826
827#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
828#[serde(rename_all = "snake_case")]
829pub enum OverloadShedReasonV1 {
830 BacklogBytes,
831 BacklogAge,
832 CollectionShare,
833 RecoveryWork,
834 PendingOverlayDocuments,
835}
836
837pub fn overload_shed_reason_v1(
839 policy: OverloadPolicyV1,
840 backlog_bytes: u64,
841 backlog_age_millis: u64,
842 collection_share_bytes: u64,
843 recovery_ops_this_pass: u32,
844 pending_overlay_documents: u64,
845) -> Result<Option<OverloadShedReasonV1>, DerivedContractError> {
846 policy.validate()?;
847 Ok(if backlog_bytes > policy.maximum_backlog_bytes {
848 Some(OverloadShedReasonV1::BacklogBytes)
849 } else if backlog_age_millis > policy.maximum_backlog_age_millis {
850 Some(OverloadShedReasonV1::BacklogAge)
851 } else if collection_share_bytes > policy.maximum_collection_share_bytes {
852 Some(OverloadShedReasonV1::CollectionShare)
853 } else if recovery_ops_this_pass > policy.maximum_recovery_ops_per_pass {
854 Some(OverloadShedReasonV1::RecoveryWork)
855 } else if pending_overlay_documents > policy.maximum_pending_overlay_documents {
856 Some(OverloadShedReasonV1::PendingOverlayDocuments)
857 } else {
858 None
859 })
860}
861
862#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
864#[serde(deny_unknown_fields)]
865pub struct OverloadStatusV1 {
866 pub format_version: u16,
867 pub shard_key: ShardKey,
868 pub writer_epoch: Epoch,
869 pub configured_mode: OverloadModeV1,
870 pub effective_disposition: OverloadDispositionV1,
871 pub registered_through: SeqNo,
872 pub indexed_through: SeqNo,
873 pub lag_operations: u64,
874 pub backlog_bytes: u64,
875 pub oldest_age_millis: u64,
876 pub collection_share_bytes: u64,
877 pub recovery_ops_this_pass: u32,
878 pub pending_overlay_documents: u64,
879 pub shed_reason: Option<OverloadShedReasonV1>,
880}
881
882impl OverloadStatusV1 {
883 pub fn validate(
884 self,
885 policy: OverloadPolicyV1,
886 index_mode: IndexMode,
887 ingest_authority: &IngestAuthorityV1,
888 exact_mutable_overlay_available: bool,
889 ) -> Result<(), DerivedContractError> {
890 validate_format(self.format_version)?;
891 policy.validate()?;
892 if self.writer_epoch.get() == 0
893 || self.shard_key.collection_index_ref() != policy.collection_index_ref
894 || self.configured_mode != policy.mode
895 || self.indexed_through.get() > self.registered_through.get()
896 || self.lag_operations != self.registered_through.get() - self.indexed_through.get()
897 || self.collection_share_bytes > self.backlog_bytes
898 {
899 return Err(DerivedContractError::Violation(
900 "overload status watermarks or policy binding are inconsistent",
901 ));
902 }
903 let expected_reason = overload_shed_reason_v1(
904 policy,
905 self.backlog_bytes,
906 self.oldest_age_millis,
907 self.collection_share_bytes,
908 self.recovery_ops_this_pass,
909 self.pending_overlay_documents,
910 )?;
911 let configured_disposition =
912 overload_disposition_v1(policy, index_mode, ingest_authority, exact_mutable_overlay_available)?;
913 let expected_disposition = if expected_reason.is_some() {
914 OverloadDispositionV1::Shed
915 } else {
916 configured_disposition
917 };
918 if self.shed_reason != expected_reason || self.effective_disposition != expected_disposition {
919 return Err(DerivedContractError::Violation(
920 "overload status does not report the deterministic cap disposition",
921 ));
922 }
923 Ok(())
924 }
925}
926
927pub fn encode_result_cache_key_v1(value: &ResultCacheKeyV1) -> Result<Vec<u8>, DerivedContractError> {
928 value.validate()?;
929 encode_bounded(value)
930}
931
932pub fn decode_result_cache_key_v1(bytes: &[u8]) -> Result<ResultCacheKeyV1, DerivedContractError> {
933 decode_current(bytes, ResultCacheKeyV1::validate)
934}
935
936pub fn encode_result_cache_budget_v1(value: &ResultCacheBudgetV1) -> Result<Vec<u8>, DerivedContractError> {
937 value.validate()?;
938 encode_bounded(value)
939}
940
941pub fn decode_result_cache_budget_v1(bytes: &[u8]) -> Result<ResultCacheBudgetV1, DerivedContractError> {
942 decode_current(bytes, |value: &ResultCacheBudgetV1| (*value).validate())
943}
944
945pub fn encode_result_cache_metrics_v1(
946 value: &ResultCacheMetricsV1,
947 budget: ResultCacheBudgetV1,
948) -> Result<Vec<u8>, DerivedContractError> {
949 value.validate(budget)?;
950 encode_bounded(value)
951}
952
953pub fn decode_result_cache_metrics_v1(
954 bytes: &[u8],
955 budget: ResultCacheBudgetV1,
956) -> Result<ResultCacheMetricsV1, DerivedContractError> {
957 decode_current(bytes, |value: &ResultCacheMetricsV1| (*value).validate(budget))
958}
959
960pub fn encode_field_heat_v1(value: &FieldHeatAggregateV1) -> Result<Vec<u8>, DerivedContractError> {
961 value.validate()?;
962 encode_bounded(value)
963}
964
965pub fn decode_field_heat_v1(bytes: &[u8]) -> Result<FieldHeatAggregateV1, DerivedContractError> {
966 decode_current(bytes, FieldHeatAggregateV1::validate)
967}
968
969pub fn encode_field_promotion_policy_v1(value: &FieldPromotionPolicyV1) -> Result<Vec<u8>, DerivedContractError> {
970 value.validate()?;
971 encode_bounded(value)
972}
973
974pub fn decode_field_promotion_policy_v1(bytes: &[u8]) -> Result<FieldPromotionPolicyV1, DerivedContractError> {
975 decode_current(bytes, |value: &FieldPromotionPolicyV1| (*value).validate())
976}
977
978pub fn encode_promoted_field_layout_v1(value: &PromotedFieldLayoutV1) -> Result<Vec<u8>, DerivedContractError> {
979 value.validate()?;
980 encode_bounded(value)
981}
982
983pub fn decode_promoted_field_layout_v1(bytes: &[u8]) -> Result<PromotedFieldLayoutV1, DerivedContractError> {
984 decode_current(bytes, |value: &PromotedFieldLayoutV1| (*value).validate())
985}
986
987pub fn encode_field_promotion_status_v1(
988 value: &FieldPromotionStatusV1,
989 policy: FieldPromotionPolicyV1,
990) -> Result<Vec<u8>, DerivedContractError> {
991 value.validate(policy)?;
992 encode_bounded(value)
993}
994
995pub fn decode_field_promotion_status_v1(
996 bytes: &[u8],
997 policy: FieldPromotionPolicyV1,
998) -> Result<FieldPromotionStatusV1, DerivedContractError> {
999 decode_current(bytes, |value: &FieldPromotionStatusV1| (*value).validate(policy))
1000}
1001
1002pub fn encode_promoted_column_sidecar_v1(value: &PromotedColumnSidecarV1) -> Result<Vec<u8>, DerivedContractError> {
1003 value.validate()?;
1004 let bytes = serde_json::to_vec(value).map_err(DerivedContractError::serde)?;
1005 if bytes.len() > MAX_PROMOTED_COLUMN_SIDECAR_BYTES {
1006 return Err(DerivedContractError::Budget {
1007 field: "promoted column sidecar bytes",
1008 observed: bytes.len() as u64,
1009 maximum: MAX_PROMOTED_COLUMN_SIDECAR_BYTES as u64,
1010 });
1011 }
1012 Ok(bytes)
1013}
1014
1015pub fn decode_promoted_column_sidecar_v1(bytes: &[u8]) -> Result<PromotedColumnSidecarV1, DerivedContractError> {
1016 if bytes.len() > MAX_PROMOTED_COLUMN_SIDECAR_BYTES {
1017 return Err(DerivedContractError::Budget {
1018 field: "promoted column sidecar bytes",
1019 observed: bytes.len() as u64,
1020 maximum: MAX_PROMOTED_COLUMN_SIDECAR_BYTES as u64,
1021 });
1022 }
1023 #[derive(Deserialize)]
1024 struct FormatProbe {
1025 format_version: Option<u16>,
1026 }
1027 let probe: FormatProbe = serde_json::from_slice(bytes).map_err(DerivedContractError::serde)?;
1028 validate_format(probe.format_version.ok_or(DerivedContractError::MissingFormatVersion)?)?;
1029 let decoded: PromotedColumnSidecarV1 = serde_json::from_slice(bytes).map_err(DerivedContractError::serde)?;
1030 decoded.validate()?;
1031 if serde_json::to_vec(&decoded).map_err(DerivedContractError::serde)? != bytes {
1032 return Err(DerivedContractError::NonCanonical);
1033 }
1034 Ok(decoded)
1035}
1036
1037pub fn encode_overload_policy_v1(value: &OverloadPolicyV1) -> Result<Vec<u8>, DerivedContractError> {
1038 value.validate()?;
1039 encode_bounded(value)
1040}
1041
1042pub fn decode_overload_policy_v1(bytes: &[u8]) -> Result<OverloadPolicyV1, DerivedContractError> {
1043 decode_current(bytes, |value: &OverloadPolicyV1| (*value).validate())
1044}
1045
1046pub fn encode_overload_status_v1(
1047 value: &OverloadStatusV1,
1048 policy: OverloadPolicyV1,
1049 index_mode: IndexMode,
1050 ingest_authority: &IngestAuthorityV1,
1051 exact_mutable_overlay_available: bool,
1052) -> Result<Vec<u8>, DerivedContractError> {
1053 value.validate(policy, index_mode, ingest_authority, exact_mutable_overlay_available)?;
1054 encode_bounded(value)
1055}
1056
1057pub fn decode_overload_status_v1(
1058 bytes: &[u8],
1059 policy: OverloadPolicyV1,
1060 index_mode: IndexMode,
1061 ingest_authority: &IngestAuthorityV1,
1062 exact_mutable_overlay_available: bool,
1063) -> Result<OverloadStatusV1, DerivedContractError> {
1064 decode_current(bytes, |value: &OverloadStatusV1| {
1065 (*value).validate(policy, index_mode, ingest_authority, exact_mutable_overlay_available)
1066 })
1067}
1068
1069pub fn encode_pending_overlay_v1(value: &PendingMutationOverlayV1) -> Result<Vec<u8>, DerivedContractError> {
1070 value.validate()?;
1071 encode_bounded(value)
1072}
1073
1074pub fn decode_pending_overlay_v1(bytes: &[u8]) -> Result<PendingMutationOverlayV1, DerivedContractError> {
1075 decode_current(bytes, |value: &PendingMutationOverlayV1| (*value).validate())
1076}
1077
1078fn encode_bounded<T: Serialize>(value: &T) -> Result<Vec<u8>, DerivedContractError> {
1079 let bytes = serde_json::to_vec(value).map_err(DerivedContractError::serde)?;
1080 if bytes.len() > MAX_DERIVED_CONTRACT_BYTES {
1081 return Err(DerivedContractError::Budget {
1082 field: "derived contract bytes",
1083 observed: bytes.len() as u64,
1084 maximum: MAX_DERIVED_CONTRACT_BYTES as u64,
1085 });
1086 }
1087 Ok(bytes)
1088}
1089
1090fn decode_current<T>(
1091 bytes: &[u8],
1092 validate: impl FnOnce(&T) -> Result<(), DerivedContractError>,
1093) -> Result<T, DerivedContractError>
1094where
1095 T: DeserializeOwned + Serialize,
1096{
1097 #[derive(Deserialize)]
1098 struct FormatProbe {
1099 format_version: Option<u16>,
1100 }
1101
1102 if bytes.len() > MAX_DERIVED_CONTRACT_BYTES {
1103 return Err(DerivedContractError::Budget {
1104 field: "derived contract bytes",
1105 observed: bytes.len() as u64,
1106 maximum: MAX_DERIVED_CONTRACT_BYTES as u64,
1107 });
1108 }
1109 let probe: FormatProbe = serde_json::from_slice(bytes).map_err(DerivedContractError::serde)?;
1110 let version = probe.format_version.ok_or(DerivedContractError::MissingFormatVersion)?;
1111 validate_format(version)?;
1112 let decoded: T = serde_json::from_slice(bytes).map_err(DerivedContractError::serde)?;
1113 validate(&decoded)?;
1114 if serde_json::to_vec(&decoded).map_err(DerivedContractError::serde)? != bytes {
1115 return Err(DerivedContractError::NonCanonical);
1116 }
1117 Ok(decoded)
1118}
1119
1120fn validate_format(version: u16) -> Result<(), DerivedContractError> {
1121 if version == DERIVED_OVERLOAD_FORMAT_VERSION {
1122 Ok(())
1123 } else {
1124 Err(DerivedContractError::UnsupportedFormat(version))
1125 }
1126}
1127
1128#[derive(Clone, Debug, Eq, PartialEq)]
1129pub enum DerivedContractError {
1130 UnsupportedFormat(u16),
1131 MissingFormatVersion,
1132 NonCanonical,
1133 Budget {
1134 field: &'static str,
1135 observed: u64,
1136 maximum: u64,
1137 },
1138 Violation(&'static str),
1139 ArithmeticOverflow,
1140 Source(String),
1141 Serde(String),
1142}
1143
1144impl DerivedContractError {
1145 fn serde(error: serde_json::Error) -> Self {
1146 Self::Serde(error.to_string())
1147 }
1148
1149 fn source(error: impl Display) -> Self {
1150 Self::Source(error.to_string())
1151 }
1152}
1153
1154impl Display for DerivedContractError {
1155 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1156 match self {
1157 Self::UnsupportedFormat(version) => write!(formatter, "unsupported derived/overload format {version}"),
1158 Self::MissingFormatVersion => formatter.write_str("derived/overload format version is missing"),
1159 Self::NonCanonical => formatter.write_str("derived/overload bytes are not canonical current format"),
1160 Self::Budget {
1161 field,
1162 observed,
1163 maximum,
1164 } => write!(formatter, "{field} {observed} exceeds {maximum}"),
1165 Self::Violation(message) => formatter.write_str(message),
1166 Self::ArithmeticOverflow => formatter.write_str("derived/overload arithmetic overflowed"),
1167 Self::Source(message) => write!(formatter, "derived/overload source authority refused input: {message}"),
1168 Self::Serde(message) => write!(formatter, "derived/overload codec refused input: {message}"),
1169 }
1170 }
1171}
1172
1173impl Error for DerivedContractError {}
1174
1175#[cfg(test)]
1176mod tests {
1177 use super::*;
1178 use crate::{CollectionId, IndexId};
1179
1180 fn scope() -> Result<CollectionIndexRef, Box<dyn Error>> {
1181 Ok(CollectionIndexRef::new(CollectionId::new(1)?, IndexId::new(1)?))
1182 }
1183
1184 fn cache_key() -> Result<ResultCacheKeyV1, Box<dyn Error>> {
1185 Ok(ResultCacheKeyV1 {
1186 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1187 collection_index_ref: scope()?,
1188 collection_security_generation: CollectionSecurityGeneration::new(1),
1189 authorized_scope_digest: AuthorizedScopeDigestV1::new([1; 32])?,
1190 policy_generation: PolicyGeneration::new(1),
1191 collection_reset_generation: CollectionResetGenerationV1::new(1)?,
1192 deletion_overlay_generation: DeletionOverlayGenerationV1::new(1)?,
1193 key_access_generation: KeyAccessGenerationV1::new(1)?,
1194 normalized_plan_digest: NormalizedPlanDigestV1::new([2; 32])?,
1195 output_semantics_digest: OutputSemanticsDigestV1::new([3; 32])?,
1196 expected_shard_count: 1,
1197 shard_generations: vec![CacheShardGenerationV1 {
1198 shard_id: ShardId::new(0),
1199 manifest_generation: ManifestGeneration::new(1),
1200 }],
1201 mapping_generation: SchemaGeneration::new(1),
1202 freshness: CacheFreshnessV1::Committed,
1203 reuse_scope: CacheReuseScopeV1::CompleteCommittedResult,
1204 from: 0,
1205 limit: 10,
1206 feature_flags_digest: FeatureFlagsDigestV1::new([4; 32])?,
1207 snapshot: CacheSnapshotBindingV1::Current,
1208 })
1209 }
1210
1211 #[test]
1212 fn cache_key_is_exact_and_realtime_is_subresult_only() -> Result<(), Box<dyn Error>> {
1213 let key = cache_key()?;
1214 let bytes = encode_result_cache_key_v1(&key)?;
1215 assert_eq!(decode_result_cache_key_v1(&bytes)?, key);
1216 let mut stale = key.clone();
1217 stale.freshness = CacheFreshnessV1::Realtime;
1218 assert!(matches!(stale.validate(), Err(DerivedContractError::Violation(_))));
1219 stale.reuse_scope = CacheReuseScopeV1::CommittedSubresultOfRealtime;
1220 stale.validate()?;
1221 let budget = ResultCacheBudgetV1 {
1222 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1223 collection_id: CollectionId::new(1)?,
1224 maximum_entry_bytes: 1_024,
1225 maximum_collection_bytes: 4_096,
1226 maximum_collection_entries: 8,
1227 };
1228 let budget_bytes = encode_result_cache_budget_v1(&budget)?;
1229 assert_eq!(decode_result_cache_budget_v1(&budget_bytes)?, budget);
1230 let metrics = ResultCacheMetricsV1 {
1231 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1232 collection_id: CollectionId::new(1)?,
1233 hits: 1,
1234 misses: 1,
1235 admissions: 1,
1236 bypasses: 1,
1237 evictions: 0,
1238 collection_bytes: 1_024,
1239 collection_entries: 1,
1240 };
1241 let metrics_bytes = encode_result_cache_metrics_v1(&metrics, budget)?;
1242 assert_eq!(decode_result_cache_metrics_v1(&metrics_bytes, budget)?, metrics);
1243 Ok(())
1244 }
1245
1246 #[test]
1247 fn promotion_is_physical_bounded_and_privacy_constrained() -> Result<(), Box<dyn Error>> {
1248 let heat = FieldHeatAggregateV1 {
1249 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1250 collection_index_ref: scope()?,
1251 mapping_generation: SchemaGeneration::new(1),
1252 field_path_digest: FieldPathDigestV1::new([7; 32])?,
1253 window_start_logical_millis: LogicalMillis::new(1),
1254 window_end_logical_millis: LogicalMillis::new(2),
1255 filter_uses: 4,
1256 sort_uses: 1,
1257 aggregation_uses: 1,
1258 distinct_query_count: 4,
1259 estimated_cardinality: 100,
1260 estimated_extra_bytes: 1_024,
1261 privacy: PromotionPrivacyV1::Ordinary,
1262 field_type: PromotedFieldTypeV1::Keyword,
1263 observed_type_conflict: false,
1264 path_policy_eligible: true,
1265 };
1266 let policy = FieldPromotionPolicyV1 {
1267 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1268 collection_index_ref: scope()?,
1269 mapping_generation: SchemaGeneration::new(1),
1270 policy_generation: PolicyGeneration::new(1),
1271 enabled: true,
1272 minimum_query_count: 3,
1273 maximum_promoted_fields: 4,
1274 maximum_cardinality: 1_000,
1275 maximum_extra_bytes: 2_048,
1276 allow_sensitive: false,
1277 };
1278 let policy_bytes = encode_field_promotion_policy_v1(&policy)?;
1279 assert_eq!(decode_field_promotion_policy_v1(&policy_bytes)?, policy);
1280 assert!(policy.admits(&heat, 0, 0)?);
1281 let mut restricted = heat.clone();
1282 restricted.privacy = PromotionPrivacyV1::Restricted;
1283 assert!(!policy.admits(&restricted, 0, 0)?);
1284 let mut conflict = heat.clone();
1285 conflict.observed_type_conflict = true;
1286 assert!(!policy.admits(&conflict, 0, 0)?);
1287 assert!(!policy.admits(&heat, policy.maximum_promoted_fields, 0)?);
1288 assert!(!policy.admits(&heat, 0, policy.maximum_extra_bytes)?);
1289 let mut stale_heat = heat.clone();
1290 stale_heat.mapping_generation = SchemaGeneration::new(2);
1291 assert!(matches!(
1292 policy.admits(&stale_heat, 0, 0),
1293 Err(DerivedContractError::Violation(_))
1294 ));
1295 let mut impossible_heat = heat.clone();
1296 impossible_heat.distinct_query_count = 7;
1297 assert!(matches!(
1298 impossible_heat.validate(),
1299 Err(DerivedContractError::Violation(_))
1300 ));
1301 let layout = PromotedFieldLayoutV1 {
1302 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1303 collection_index_ref: scope()?,
1304 segment_id: SegmentId::new(1),
1305 field_path_digest: heat.field_path_digest,
1306 mapping_generation: heat.mapping_generation,
1307 decision_generation: PromotionDecisionGenerationV1::new(1)?,
1308 field_type: heat.field_type,
1309 };
1310 let layout_bytes = encode_promoted_field_layout_v1(&layout)?;
1311 assert_eq!(decode_promoted_field_layout_v1(&layout_bytes)?, layout);
1312 let status = FieldPromotionStatusV1 {
1313 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1314 collection_index_ref: scope()?,
1315 mapping_generation: SchemaGeneration::new(1),
1316 policy_generation: PolicyGeneration::new(1),
1317 last_decision_generation: Some(PromotionDecisionGenerationV1::new(1)?),
1318 heat_records: 1,
1319 promoted_fields: 1,
1320 extra_bytes: 1_024,
1321 enabled: true,
1322 };
1323 let status_bytes = encode_field_promotion_status_v1(&status, policy)?;
1324 assert_eq!(decode_field_promotion_status_v1(&status_bytes, policy)?, status);
1325 let mut stale_status = status;
1326 stale_status.mapping_generation = SchemaGeneration::new(2);
1327 assert!(matches!(
1328 stale_status.validate(policy),
1329 Err(DerivedContractError::Violation(_))
1330 ));
1331 Ok(())
1332 }
1333
1334 #[test]
1335 fn promoted_column_sidecar_is_strict_current_and_segment_bound() -> Result<(), Box<dyn Error>> {
1336 let digest = field_path_digest_v1("service.latency")?;
1337 assert_eq!(digest, field_path_digest_v1("service.latency")?);
1338 assert_ne!(digest, field_path_digest_v1("service.duration")?);
1339 let sidecar = PromotedColumnSidecarV1 {
1340 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1341 collection_index_ref: scope()?,
1342 segment_id: SegmentId::new(4),
1343 mapping_generation: SchemaGeneration::new(2),
1344 document_count: 3,
1345 columns: vec![PromotedFieldColumnV1 {
1346 layout: PromotedFieldLayoutV1 {
1347 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1348 collection_index_ref: scope()?,
1349 segment_id: SegmentId::new(4),
1350 field_path_digest: digest,
1351 mapping_generation: SchemaGeneration::new(2),
1352 decision_generation: PromotionDecisionGenerationV1::new(3)?,
1353 field_type: PromotedFieldTypeV1::U64,
1354 },
1355 rows: vec![
1356 PromotedFieldRowV1 {
1357 document_ordinal: 0,
1358 value: PromotedFieldValueV1::U64(10),
1359 },
1360 PromotedFieldRowV1 {
1361 document_ordinal: 2,
1362 value: PromotedFieldValueV1::U64(20),
1363 },
1364 ],
1365 }],
1366 };
1367 let bytes = encode_promoted_column_sidecar_v1(&sidecar)?;
1368 assert_eq!(decode_promoted_column_sidecar_v1(&bytes)?, sidecar);
1369
1370 let mut future = serde_json::from_slice::<serde_json::Value>(&bytes)?;
1371 future["format_version"] = serde_json::json!(DERIVED_OVERLOAD_FORMAT_VERSION.saturating_add(1));
1372 assert!(matches!(
1373 decode_promoted_column_sidecar_v1(&serde_json::to_vec(&future)?),
1374 Err(DerivedContractError::UnsupportedFormat(version))
1375 if version == DERIVED_OVERLOAD_FORMAT_VERSION + 1
1376 ));
1377 let mut unknown = serde_json::from_slice::<serde_json::Value>(&bytes)?;
1378 unknown["unknown"] = serde_json::json!(true);
1379 assert!(matches!(
1380 decode_promoted_column_sidecar_v1(&serde_json::to_vec(&unknown)?),
1381 Err(DerivedContractError::Serde(_))
1382 ));
1383 let current_version = format!("\"format_version\":{DERIVED_OVERLOAD_FORMAT_VERSION}");
1384 let duplicate_version = format!("{current_version},{current_version}");
1385 let duplicate = String::from_utf8(bytes.clone())?.replacen(¤t_version, &duplicate_version, 1);
1386 assert!(matches!(
1387 decode_promoted_column_sidecar_v1(duplicate.as_bytes()),
1388 Err(DerivedContractError::Serde(_))
1389 ));
1390 let mut wrong_type = sidecar.clone();
1391 wrong_type.columns[0].rows[0].value = PromotedFieldValueV1::Keyword("10".to_owned());
1392 assert!(matches!(
1393 encode_promoted_column_sidecar_v1(&wrong_type),
1394 Err(DerivedContractError::Violation(_))
1395 ));
1396 let mut negative_zero = sidecar.clone();
1397 negative_zero.columns[0].layout.field_type = PromotedFieldTypeV1::F64;
1398 negative_zero.columns[0].rows[0].value = PromotedFieldValueV1::F64Bits((-0.0_f64).to_bits());
1399 negative_zero.columns[0].rows[1].value = PromotedFieldValueV1::F64Bits(1.0_f64.to_bits());
1400 assert!(matches!(
1401 encode_promoted_column_sidecar_v1(&negative_zero),
1402 Err(DerivedContractError::Violation(_))
1403 ));
1404 let mut empty = sidecar.clone();
1405 empty.columns.clear();
1406 assert!(matches!(
1407 encode_promoted_column_sidecar_v1(&empty),
1408 Err(DerivedContractError::Violation(_))
1409 ));
1410 let mut duplicate_generation = sidecar.clone();
1411 let mut duplicate_column = duplicate_generation.columns[0].clone();
1412 duplicate_column.layout.field_path_digest = field_path_digest_v1("service.duration")?;
1413 duplicate_generation.columns.push(duplicate_column);
1414 assert!(matches!(
1415 encode_promoted_column_sidecar_v1(&duplicate_generation),
1416 Err(DerivedContractError::Violation(_))
1417 ));
1418 let mut unordered = sidecar;
1419 unordered.columns[0].rows.swap(0, 1);
1420 assert!(matches!(
1421 encode_promoted_column_sidecar_v1(&unordered),
1422 Err(DerivedContractError::Violation(_))
1423 ));
1424 Ok(())
1425 }
1426
1427 #[test]
1428 fn overload_buffer_never_becomes_a_second_authority() -> Result<(), Box<dyn Error>> {
1429 let policy = OverloadPolicyV1 {
1430 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1431 collection_index_ref: scope()?,
1432 policy_generation: PolicyGeneration::new(1),
1433 mode: OverloadModeV1::Buffer,
1434 maximum_backlog_bytes: 1_024,
1435 maximum_backlog_age_millis: 100,
1436 maximum_collection_share_bytes: 512,
1437 maximum_recovery_ops_per_pass: 100,
1438 maximum_pending_overlay_documents: 100,
1439 };
1440 let policy_bytes = encode_overload_policy_v1(&policy)?;
1441 assert_eq!(decode_overload_policy_v1(&policy_bytes)?, policy);
1442 let push_authority = IngestAuthorityV1::push_with_wal();
1443 assert_eq!(
1444 overload_disposition_v1(policy, IndexMode::AppendLog, &push_authority, false)?,
1445 OverloadDispositionV1::BufferRegisteredWal
1446 );
1447 assert_eq!(
1448 overload_disposition_v1(policy, IndexMode::Mutable, &push_authority, false)?,
1449 OverloadDispositionV1::RefuseMissingMutableOverlay
1450 );
1451 assert_eq!(
1452 overload_disposition_v1(policy, IndexMode::State, &push_authority, true)?,
1453 OverloadDispositionV1::RefuseStateMode
1454 );
1455 let invalid_authority = IngestAuthorityV1::PushWithWal { format_version: 3 };
1456 assert!(matches!(
1457 overload_disposition_v1(policy, IndexMode::AppendLog, &invalid_authority, false),
1458 Err(DerivedContractError::Source(_))
1459 ));
1460 let overlay = PendingMutationOverlayV1 {
1461 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1462 shard_key: ShardKey::new(scope()?, ShardId::new(0)),
1463 writer_epoch: Epoch::new(1),
1464 registered_through: SeqNo::new(7),
1465 indexed_through: SeqNo::new(3),
1466 get_overlay_through: SeqNo::new(7),
1467 conditional_write_through: SeqNo::new(7),
1468 collector_visibility_through: SeqNo::new(7),
1469 subscription_through: SeqNo::new(7),
1470 pending_documents: 4,
1471 pending_tombstones: 2,
1472 pending_bytes: 128,
1473 oldest_age_millis: 10,
1474 };
1475 overlay.validate()?;
1476 assert_eq!(overlay.lag_operations()?, 4);
1477 let mut inconsistent_zero_lag = overlay;
1478 inconsistent_zero_lag.indexed_through = inconsistent_zero_lag.registered_through;
1479 assert!(matches!(
1480 inconsistent_zero_lag.validate(),
1481 Err(DerivedContractError::Violation(_))
1482 ));
1483 assert_eq!(
1484 overload_shed_reason_v1(policy, 100, 10, 100, 101, 4)?,
1485 Some(OverloadShedReasonV1::RecoveryWork)
1486 );
1487 let status = OverloadStatusV1 {
1488 format_version: DERIVED_OVERLOAD_FORMAT_VERSION,
1489 shard_key: ShardKey::new(scope()?, ShardId::new(0)),
1490 writer_epoch: Epoch::new(1),
1491 configured_mode: OverloadModeV1::Buffer,
1492 effective_disposition: OverloadDispositionV1::BufferRegisteredWal,
1493 registered_through: SeqNo::new(7),
1494 indexed_through: SeqNo::new(3),
1495 lag_operations: 4,
1496 backlog_bytes: 100,
1497 oldest_age_millis: 10,
1498 collection_share_bytes: 100,
1499 recovery_ops_this_pass: 10,
1500 pending_overlay_documents: 4,
1501 shed_reason: None,
1502 };
1503 let status_bytes = encode_overload_status_v1(&status, policy, IndexMode::AppendLog, &push_authority, false)?;
1504 assert_eq!(
1505 decode_overload_status_v1(&status_bytes, policy, IndexMode::AppendLog, &push_authority, false,)?,
1506 status
1507 );
1508 let mut false_state_status = status;
1509 false_state_status.effective_disposition = OverloadDispositionV1::BufferRegisteredWal;
1510 assert!(matches!(
1511 false_state_status.validate(policy, IndexMode::State, &push_authority, true),
1512 Err(DerivedContractError::Violation(_))
1513 ));
1514 let mut impossible_share = status;
1515 impossible_share.collection_share_bytes = impossible_share.backlog_bytes + 1;
1516 assert!(matches!(
1517 impossible_share.validate(policy, IndexMode::AppendLog, &push_authority, false),
1518 Err(DerivedContractError::Violation(_))
1519 ));
1520 Ok(())
1521 }
1522
1523 #[test]
1524 fn codec_refuses_future_unknown_duplicate_and_noncanonical() -> Result<(), Box<dyn Error>> {
1525 let bytes = encode_result_cache_key_v1(&cache_key()?)?;
1526 let mut future: serde_json::Value = serde_json::from_slice(&bytes)?;
1527 future["format_version"] = serde_json::Value::from(DERIVED_OVERLOAD_FORMAT_VERSION.saturating_add(1));
1528 assert_eq!(
1529 decode_result_cache_key_v1(&serde_json::to_vec(&future)?),
1530 Err(DerivedContractError::UnsupportedFormat(
1531 DERIVED_OVERLOAD_FORMAT_VERSION + 1
1532 ))
1533 );
1534 let mut unknown: serde_json::Value = serde_json::from_slice(&bytes)?;
1535 unknown["unknown_cache_key"] = serde_json::Value::Bool(true);
1536 assert!(matches!(
1537 decode_result_cache_key_v1(&serde_json::to_vec(&unknown)?),
1538 Err(DerivedContractError::Serde(_))
1539 ));
1540 let text = String::from_utf8(bytes)?;
1541 let current_version = format!("\"format_version\":{DERIVED_OVERLOAD_FORMAT_VERSION}");
1542 let duplicate_version = format!("{current_version},{current_version}");
1543 let duplicate = text.replacen(¤t_version, &duplicate_version, 1);
1544 assert!(matches!(
1545 decode_result_cache_key_v1(duplicate.as_bytes()),
1546 Err(DerivedContractError::Serde(_))
1547 ));
1548 let spaced = format!(" {text}");
1549 assert_eq!(
1550 decode_result_cache_key_v1(spaced.as_bytes()),
1551 Err(DerivedContractError::NonCanonical)
1552 );
1553 Ok(())
1554 }
1555}