1use crate::destination::BackendResourceId;
2use crate::error::InvalidError;
3use crate::filter::{FILTER_EVALUATOR_VERSION, FilterCodec};
4use crate::query::{AggFunc, Consistency, SqlDialect};
5use crate::schema::LogicalTypeKind;
6use crate::topology::WireTopology;
7use crate::validate::Validate;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeSet;
10
11pub mod feature {
18 pub const KV_CAS: u64 = 1 << 0;
20 pub const READ_YOUR_WRITES: u64 = 1 << 1;
22 pub const STRONG_CONSISTENCY: u64 = 1 << 2;
25 pub const KV_CAS_FENCED: u64 = 1 << 3;
27 pub const AGENT_WORKFLOW: u64 = 1 << 4;
30 pub const KEYWORD_SEARCH: u64 = 1 << 5;
32 pub const WATCH: u64 = 1 << 6;
35 pub const AUTHZ: u64 = 1 << 7;
37 pub const DESTINATIONS: u64 = 1 << 8;
40 pub const KV_FENCED_LEASES: u64 = 1 << 9;
48 pub const CONSUMER_FILTERS: u64 = 1 << 10;
55 pub const GROUP_POLICY_READS: u64 = 1 << 11;
62 pub const SESSIONS: u64 = 1 << 12;
64 pub const STREAM_TENANCY: u64 = 1 << 13;
68 pub const MATERIALIZATIONS: u64 = 1 << 14;
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[non_exhaustive]
79pub struct OpVersions {
80 pub query: u32,
81 pub control: u32,
82 pub kv: u32,
83 pub fork: u32,
84 #[serde(default, skip_serializing_if = "is_zero")]
88 pub agent: u32,
89 #[serde(default, skip_serializing_if = "is_zero")]
94 pub graph: u32,
95 #[serde(default, skip_serializing_if = "is_zero")]
98 pub filter: u32,
99 #[serde(default, skip_serializing_if = "is_zero_u64")]
105 pub features: u64,
106}
107
108fn is_zero(value: &u32) -> bool {
109 *value == 0
110}
111
112fn is_zero_u64(value: &u64) -> bool {
113 *value == 0
114}
115
116impl OpVersions {
117 pub fn new(query: u32, control: u32, kv: u32, fork: u32) -> Self {
120 Self {
121 query,
122 control,
123 kv,
124 fork,
125 agent: 0,
126 graph: 0,
127 filter: 0,
128 features: 0,
129 }
130 }
131
132 #[must_use]
134 pub fn with_agent(mut self, agent: u32) -> Self {
135 self.agent = agent;
136 self
137 }
138
139 #[must_use]
141 pub fn with_graph(mut self, graph: u32) -> Self {
142 self.graph = graph;
143 self
144 }
145
146 #[must_use]
148 pub fn with_filter(mut self, filter: u32) -> Self {
149 self.filter = filter;
150 self
151 }
152
153 #[must_use]
156 pub fn with_features(mut self, features: u64) -> Self {
157 self.features = features;
158 self
159 }
160
161 pub const fn has_feature(&self, bit: u64) -> bool {
163 self.features & bit == bit
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
173#[non_exhaustive]
174pub struct HelloReply {
175 pub versions: OpVersions,
176}
177
178impl HelloReply {
179 pub fn new(versions: OpVersions) -> Self {
181 Self { versions }
182 }
183}
184
185pub const BACKEND_DESCRIPTOR_VERSION: u32 = 1;
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189#[non_exhaustive]
190pub struct BackendDescriptor {
191 pub descriptor_version: u32,
192 pub resource_id: BackendResourceId,
193 pub mode: BackendMode,
194 pub label: String,
195 pub implementation: BackendImplementation,
196 pub observed_backend_generation: u64,
197 pub runtime_configuration_revision: u64,
198 pub desired_state: BackendDesiredState,
199 pub observed_state: BackendObservedState,
200 pub readiness: BackendReadiness,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub query: Option<QueryCapabilities>,
203 pub schema: SchemaCapabilities,
204 pub limits: BackendLimits,
205}
206
207impl BackendDescriptor {
208 pub fn new(
209 resource_id: BackendResourceId,
210 mode: BackendMode,
211 label: impl Into<String>,
212 implementation: BackendImplementation,
213 observed_backend_generation: u64,
214 runtime_configuration_revision: u64,
215 ) -> Self {
216 Self {
217 descriptor_version: BACKEND_DESCRIPTOR_VERSION,
218 resource_id,
219 mode,
220 label: label.into(),
221 implementation,
222 observed_backend_generation,
223 runtime_configuration_revision,
224 desired_state: BackendDesiredState::Disabled,
225 observed_state: BackendObservedState::Disabled,
226 readiness: BackendReadiness::not_ready(BackendReadinessCode::Disabled),
227 query: None,
228 schema: SchemaCapabilities::default(),
229 limits: BackendLimits::default(),
230 }
231 }
232
233 #[must_use]
234 pub fn with_state(
235 mut self,
236 desired_state: BackendDesiredState,
237 observed_state: BackendObservedState,
238 readiness: BackendReadiness,
239 ) -> Self {
240 self.desired_state = desired_state;
241 self.observed_state = observed_state;
242 self.readiness = readiness;
243 self
244 }
245
246 #[must_use]
247 pub fn with_query(mut self, capabilities: QueryCapabilities) -> Self {
248 self.query = Some(capabilities);
249 self
250 }
251
252 #[must_use]
253 pub fn with_schema(mut self, capabilities: SchemaCapabilities) -> Self {
254 self.schema = capabilities;
255 self
256 }
257
258 #[must_use]
259 pub fn with_limits(mut self, limits: BackendLimits) -> Self {
260 self.limits = limits;
261 self
262 }
263}
264
265impl Validate for BackendDescriptor {
266 fn validate(&self) -> Result<(), InvalidError> {
267 if self.descriptor_version != BACKEND_DESCRIPTOR_VERSION {
268 return Err(InvalidError::new(format!(
269 "backend descriptor version must be {BACKEND_DESCRIPTOR_VERSION}"
270 )));
271 }
272 if self.resource_id.as_u128() == 0
273 || self.observed_backend_generation == 0
274 || self.runtime_configuration_revision == 0
275 {
276 return Err(InvalidError::new(
277 "backend identity and observed revisions must be nonzero",
278 ));
279 }
280 validate_descriptor_text("backend label", &self.label)?;
281 validate_descriptor_text("backend implementation kind", &self.implementation.kind)?;
282 validate_descriptor_text(
283 "backend implementation version",
284 &self.implementation.version,
285 )?;
286 self.readiness.validate()?;
287 if self.readiness.ready && self.observed_state != BackendObservedState::Ready {
288 return Err(InvalidError::new(
289 "ready backend must report the ready observed state",
290 ));
291 }
292 if let Some(query) = &self.query {
293 query.validate()?;
294 }
295 if self.readiness.ready {
296 if self.schema.logical_schema && self.limits.max_schema_fields == 0 {
297 return Err(InvalidError::new(
298 "ready logical-schema backend must advertise a schema-field limit",
299 ));
300 }
301 if self.query.is_some()
302 && (self.limits.max_query_rows == 0
303 || self.limits.max_query_bytes == 0
304 || self.limits.max_scan_bytes == 0
305 || self.limits.max_query_micros == 0
306 || self.limits.max_concurrent_queries == 0)
307 {
308 return Err(InvalidError::new(
309 "ready query backend must advertise nonzero query limits",
310 ));
311 }
312 }
313 Ok(())
314 }
315}
316
317#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case")]
319#[non_exhaustive]
320pub enum BackendMode {
321 Operational,
322}
323
324#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
325pub struct BackendImplementation {
326 pub kind: String,
327 pub version: String,
328}
329
330#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
331#[serde(rename_all = "snake_case")]
332#[non_exhaustive]
333pub enum BackendDesiredState {
334 Disabled,
335 Enabled,
336}
337
338#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340#[non_exhaustive]
341pub enum BackendObservedState {
342 Disabled,
343 Starting,
344 Ready,
345 Degraded,
346 Unavailable,
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
350pub struct BackendReadiness {
351 pub ready: bool,
352 pub reasons: Vec<BackendReadinessReason>,
353 pub observed_at_micros: u64,
354}
355
356impl BackendReadiness {
357 pub fn not_ready(code: BackendReadinessCode) -> Self {
358 Self {
359 ready: false,
360 reasons: vec![BackendReadinessReason { code, detail: None }],
361 observed_at_micros: 0,
362 }
363 }
364
365 pub fn ready(observed_at_micros: u64) -> Self {
366 Self {
367 ready: true,
368 reasons: Vec::new(),
369 observed_at_micros,
370 }
371 }
372}
373
374impl Validate for BackendReadiness {
375 fn validate(&self) -> Result<(), InvalidError> {
376 if self.ready {
377 if !self.reasons.is_empty() || self.observed_at_micros == 0 {
378 return Err(InvalidError::new(
379 "ready backend requires an observation time and no failure reasons",
380 ));
381 }
382 return Ok(());
383 }
384 if self.reasons.is_empty() {
385 return Err(InvalidError::new(
386 "not-ready backend requires at least one stable reason",
387 ));
388 }
389 if self.reasons.len() > 32 {
390 return Err(InvalidError::new(
391 "backend readiness reason count exceeds cap 32",
392 ));
393 }
394 for reason in &self.reasons {
395 if let Some(detail) = &reason.detail {
396 validate_descriptor_text("backend readiness detail", detail)?;
397 }
398 }
399 Ok(())
400 }
401}
402
403#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
404pub struct BackendReadinessReason {
405 pub code: BackendReadinessCode,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub detail: Option<String>,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
411#[serde(rename_all = "snake_case")]
412#[non_exhaustive]
413pub enum BackendReadinessCode {
414 Disabled,
415 ConfigurationPending,
416 ConfigurationRejected,
417 CredentialUnavailable,
418 ObjectStoreUnavailable,
419 CatalogUnavailable,
420 QueryRuntimeUnavailable,
421 GenerationMismatch,
422 ProbeFailed,
423 QueryIdentityRejected,
425}
426
427#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
428pub struct QueryCapabilities {
429 pub dialects: Vec<SqlDialect>,
430 pub consistency: Vec<Consistency>,
431 pub logical_types: Vec<LogicalTypeKind>,
432 pub raw_sql: bool,
433 #[serde(default, skip_serializing_if = "Vec::is_empty")]
435 pub aggregates: Vec<AggFunc>,
436 #[serde(default)]
437 pub vector_search: bool,
438 #[serde(default)]
439 pub text_search: bool,
440}
441
442impl Validate for QueryCapabilities {
443 fn validate(&self) -> Result<(), InvalidError> {
444 if self.dialects.is_empty() || self.consistency.is_empty() || self.logical_types.is_empty()
445 {
446 return Err(InvalidError::new(
447 "query capabilities require dialect, consistency, and logical-type coverage",
448 ));
449 }
450 ensure_unique(
451 "query dialect",
452 self.dialects.iter().map(|value| *value as u8),
453 )?;
454 ensure_unique(
455 "query consistency",
456 self.consistency.iter().map(|value| *value as u8),
457 )?;
458 ensure_unique(
459 "query logical type",
460 self.logical_types.iter().map(|value| *value as u8),
461 )?;
462 ensure_unique(
463 "query aggregate",
464 self.aggregates.iter().map(|value| *value as u8),
465 )?;
466 if self.raw_sql && self.dialects.is_empty() {
467 return Err(InvalidError::new(
468 "raw SQL capability requires an explicit dialect",
469 ));
470 }
471 Ok(())
472 }
473}
474
475fn ensure_unique(label: &str, values: impl IntoIterator<Item = u8>) -> Result<(), InvalidError> {
476 let mut seen = BTreeSet::new();
477 if values.into_iter().all(|value| seen.insert(value)) {
478 Ok(())
479 } else {
480 Err(InvalidError::new(format!("backend repeats a {label}")))
481 }
482}
483
484fn validate_descriptor_text(label: &str, value: &str) -> Result<(), InvalidError> {
485 if value.is_empty() || value.len() > 4096 || value.chars().any(char::is_control) {
486 return Err(InvalidError::new(format!(
487 "{label} must contain 1..=4096 bytes without control characters"
488 )));
489 }
490 Ok(())
491}
492
493#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
494pub struct SchemaCapabilities {
495 pub logical_schema: bool,
496 pub arrow_ipc_stream: bool,
497 pub schema_evolution: bool,
498}
499
500#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
501pub struct BackendLimits {
502 pub max_query_rows: u64,
503 pub max_query_bytes: u64,
504 pub max_scan_bytes: u64,
505 pub max_query_micros: u64,
506 pub max_concurrent_queries: u32,
507 pub max_schema_fields: u32,
508 #[serde(default)]
510 pub max_payload_bytes: u64,
511}
512
513const fn backend_ready_by_default() -> bool {
523 true
524}
525
526const fn backend_is_ready(ready: &bool) -> bool {
527 *ready
528}
529
530#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
531#[non_exhaustive]
532pub struct BackendAnnounce {
533 pub versions: OpVersions,
534 #[serde(
535 default = "backend_ready_by_default",
536 skip_serializing_if = "backend_is_ready"
537 )]
538 pub ready: bool,
539 #[serde(default, skip_serializing_if = "Vec::is_empty")]
544 pub backends: Vec<BackendDescriptor>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub topology: Option<WireTopology>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub filters: Option<FilterAnnounce>,
556}
557
558#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
563pub struct FilterAnnounce {
564 pub evaluator_version: u32,
565 #[serde(default, skip_serializing_if = "Vec::is_empty")]
566 pub codecs: Vec<FilterCodec>,
567 #[serde(default, skip_serializing_if = "is_zero_u32")]
570 pub max_wait_ms: u32,
571}
572
573impl FilterAnnounce {
574 pub fn served() -> Self {
576 Self {
577 evaluator_version: FILTER_EVALUATOR_VERSION,
578 codecs: vec![
579 FilterCodec::Json,
580 FilterCodec::HeadersOnly,
581 FilterCodec::Avro,
582 FilterCodec::Protobuf,
583 FilterCodec::Cbor,
584 ],
585 max_wait_ms: 0,
586 }
587 }
588
589 pub fn evaluates(&self, evaluator_version: u32, codec: FilterCodec) -> bool {
592 self.evaluator_version == evaluator_version && self.codecs.contains(&codec)
593 }
594}
595
596impl BackendAnnounce {
597 pub fn new(versions: OpVersions) -> Self {
599 Self {
600 versions,
601 ready: true,
602 backends: Vec::new(),
603 topology: None,
604 filters: None,
605 }
606 }
607
608 #[must_use]
610 pub fn with_filters(mut self, filters: FilterAnnounce) -> Self {
611 self.filters = Some(filters);
612 self
613 }
614
615 #[must_use]
616 pub const fn unavailable(mut self) -> Self {
617 self.ready = false;
618 self
619 }
620
621 #[must_use]
623 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
624 self.backends = backends;
625 self
626 }
627
628 #[must_use]
630 pub fn with_topology(mut self, topology: WireTopology) -> Self {
631 self.topology = Some(topology);
632 self
633 }
634}
635
636impl Validate for BackendAnnounce {
637 fn validate(&self) -> Result<(), InvalidError> {
638 let mut resources = BTreeSet::new();
639 for backend in &self.backends {
640 backend.validate()?;
641 if !resources.insert(backend.resource_id) {
642 return Err(InvalidError::new(
643 "backend announcement repeats a resource id",
644 ));
645 }
646 }
647 if self.ready
648 && !self.backends.is_empty()
649 && !self.backends.iter().any(|backend| backend.readiness.ready)
650 {
651 return Err(InvalidError::new(
652 "ready backend announcement must contain a ready backend",
653 ));
654 }
655 Ok(())
656 }
657}
658
659#[cfg(all(test, feature = "cbor"))]
660mod tests {
661 use super::*;
662 use crate::codes::{CONTROL_OP_VERSION, FORK_OP_VERSION, KV_OP_VERSION, QUERY_OP_VERSION};
663 use crate::framing::{decode_named, encode_named};
664
665 fn backend(resource_id: u128, mode: BackendMode, label: &str, kind: &str) -> BackendDescriptor {
666 BackendDescriptor::new(
667 BackendResourceId::from_u128(resource_id),
668 mode,
669 label,
670 BackendImplementation {
671 kind: kind.to_owned(),
672 version: "1.0.0".to_owned(),
673 },
674 1,
675 1,
676 )
677 }
678
679 #[test]
680 fn given_a_hello_reply_when_round_tripped_then_should_preserve_versions() {
681 let reply = HelloReply::new(OpVersions::new(
684 QUERY_OP_VERSION,
685 CONTROL_OP_VERSION,
686 KV_OP_VERSION,
687 FORK_OP_VERSION,
688 ));
689 let bytes = encode_named(&reply).expect("hello reply serializes");
690 let back: HelloReply = decode_named(&bytes).expect("hello reply deserializes");
691 assert_eq!(back, reply);
692 }
693
694 #[test]
695 fn given_a_backend_announce_when_round_tripped_then_should_preserve_features() {
696 let announce = BackendAnnounce::new(
697 OpVersions::new(
698 QUERY_OP_VERSION,
699 CONTROL_OP_VERSION,
700 KV_OP_VERSION,
701 FORK_OP_VERSION,
702 )
703 .with_features(feature::KV_CAS | feature::READ_YOUR_WRITES),
704 );
705 let bytes = encode_named(&announce).expect("serializes");
706 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
707 assert_eq!(back, announce);
708 assert!(back.versions.has_feature(feature::KV_CAS));
709 }
710
711 #[test]
712 fn given_an_unavailable_backend_announce_when_round_tripped_then_should_stay_unavailable() {
713 let announce = BackendAnnounce::new(OpVersions::new(
714 QUERY_OP_VERSION,
715 CONTROL_OP_VERSION,
716 KV_OP_VERSION,
717 FORK_OP_VERSION,
718 ))
719 .unavailable();
720 let bytes = encode_named(&announce).expect("serializes");
721 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
722 assert!(!back.ready);
723 }
724
725 #[test]
726 fn given_an_empty_hello_body_when_decoded_then_should_yield_no_versions() {
727 assert!(decode_named::<HelloReply>(&[]).is_err());
730 }
731
732 #[test]
733 fn given_advertised_backends_when_round_tripped_then_should_preserve_them_and_skip_empty() {
734 let announce = BackendAnnounce::new(OpVersions::new(
735 QUERY_OP_VERSION,
736 CONTROL_OP_VERSION,
737 KV_OP_VERSION,
738 FORK_OP_VERSION,
739 ))
740 .with_backends(vec![
741 backend(1, BackendMode::Operational, "Embedded", "embedded"),
742 backend(
743 2,
744 BackendMode::Operational,
745 "Analytics warehouse",
746 "columnar",
747 ),
748 ]);
749 let bytes = encode_named(&announce).expect("encodes");
750 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
751 assert_eq!(back, announce);
752 assert_eq!(back.backends.len(), 2);
753 assert_eq!(
754 back.backends[1].resource_id,
755 BackendResourceId::from_u128(2)
756 );
757 assert_eq!(back.backends[1].implementation.kind, "columnar");
758 assert_eq!(back.backends[1].label, "Analytics warehouse");
759
760 let plain = BackendAnnounce::new(OpVersions::new(1, 1, 1, 1));
763 let json = serde_json::to_string(&plain).expect("json");
764 assert!(!json.contains("backends"), "empty backends omitted: {json}");
765 }
766
767 #[test]
768 fn given_announce_without_topology_when_decoded_then_should_default_none() {
769 let announce = BackendAnnounce::new(OpVersions::new(
770 QUERY_OP_VERSION,
771 CONTROL_OP_VERSION,
772 KV_OP_VERSION,
773 FORK_OP_VERSION,
774 ));
775 let bytes = encode_named(&announce).expect("encodes");
776 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
777 assert_eq!(back.topology, None);
778 let json = serde_json::to_string(&announce).expect("json");
779 assert!(
780 !json.contains("topology"),
781 "absent topology omitted: {json}"
782 );
783 }
784
785 #[test]
786 fn given_announce_with_topology_when_round_tripped_then_should_preserve_it() {
787 let custom = WireTopology {
788 ops_stream: "custom-ops".to_owned(),
789 ..WireTopology::default()
790 };
791 let announce = BackendAnnounce::new(OpVersions::new(
792 QUERY_OP_VERSION,
793 CONTROL_OP_VERSION,
794 KV_OP_VERSION,
795 FORK_OP_VERSION,
796 ))
797 .with_topology(custom.clone());
798 let bytes = encode_named(&announce).expect("encodes");
799 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
800 assert_eq!(back.topology, Some(custom));
801 }
802
803 #[test]
804 fn given_advertised_features_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
805 let versions = OpVersions::new(
806 QUERY_OP_VERSION,
807 CONTROL_OP_VERSION,
808 KV_OP_VERSION,
809 FORK_OP_VERSION,
810 )
811 .with_features(feature::KV_CAS | feature::READ_YOUR_WRITES);
812 assert!(versions.has_feature(feature::KV_CAS));
813 assert!(versions.has_feature(feature::READ_YOUR_WRITES));
814 assert!(!versions.has_feature(feature::STRONG_CONSISTENCY));
815 assert!(versions.has_feature(feature::KV_CAS | feature::READ_YOUR_WRITES));
817 assert!(!versions.has_feature(feature::KV_CAS | feature::STRONG_CONSISTENCY));
818 let reply = HelloReply::new(versions);
819 let bytes = encode_named(&reply).expect("encodes");
820 let back: HelloReply = decode_named(&bytes).expect("decodes");
821 assert_eq!(back, reply);
822 assert!(back.versions.has_feature(feature::READ_YOUR_WRITES));
823 let plain = HelloReply::new(OpVersions::new(1, 1, 1, 1));
826 let json = serde_json::to_string(&plain).expect("json");
827 assert!(!json.contains("features"), "zero features omitted: {json}");
828 }
829
830 #[test]
831 fn given_announced_filter_evaluation_when_round_tripped_then_should_gate_codecs_and_versions() {
832 let announce = BackendAnnounce::new(OpVersions::new(1, 1, 1, 1))
833 .with_filters(FilterAnnounce::served());
834 let bytes = encode_named(&announce).expect("serializes");
835 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
836 let filters = back.filters.expect("the evaluation is announced");
837 assert!(filters.evaluates(FILTER_EVALUATOR_VERSION, FilterCodec::Json));
838 assert!(!filters.evaluates(FILTER_EVALUATOR_VERSION + 1, FilterCodec::Json));
839 assert!(!filters.evaluates(FILTER_EVALUATOR_VERSION, FilterCodec::Unknown));
840 let plain =
841 encode_named(&BackendAnnounce::new(OpVersions::new(1, 1, 1, 1))).expect("serializes");
842 let older: BackendAnnounce = decode_named(&plain).expect("deserializes");
843 assert_eq!(older.filters, None);
844 }
845
846 #[test]
847 fn given_the_feature_bits_when_read_then_should_match_the_pinned_values() {
848 assert_eq!(
849 [
850 feature::KV_CAS,
851 feature::READ_YOUR_WRITES,
852 feature::STRONG_CONSISTENCY,
853 feature::KV_CAS_FENCED,
854 feature::AGENT_WORKFLOW,
855 feature::KEYWORD_SEARCH,
856 feature::WATCH,
857 feature::AUTHZ,
858 feature::DESTINATIONS,
859 feature::KV_FENCED_LEASES,
860 feature::CONSUMER_FILTERS,
861 feature::GROUP_POLICY_READS,
862 feature::SESSIONS,
863 feature::STREAM_TENANCY,
864 feature::MATERIALIZATIONS,
865 ],
866 [
867 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384
868 ]
869 );
870 }
871
872 #[test]
873 fn given_op_versions_with_zero_surfaces_when_encoded_then_should_omit_them_and_keep_set_ones() {
874 let advertised = OpVersions::new(1, 1, 1, 1)
875 .with_agent(2)
876 .with_graph(3)
877 .with_filter(4);
878 let json = serde_json::to_value(advertised).expect("encode advertised surfaces");
879 let decoded: OpVersions = serde_json::from_value(json).expect("decode advertised surfaces");
880 assert_eq!(decoded, advertised);
881 let plain =
882 serde_json::to_value(OpVersions::new(1, 1, 1, 1)).expect("encode base surfaces");
883 assert_eq!(
884 (plain.get("agent"), plain.get("graph"), plain.get("filter")),
885 (None, None, None)
886 );
887 }
888
889 #[test]
890 fn given_a_ready_announce_with_a_filter_wait_when_encoded_then_should_keep_only_the_wait() {
891 let waiting =
892 BackendAnnounce::new(OpVersions::new(1, 1, 1, 1)).with_filters(FilterAnnounce {
893 max_wait_ms: 250,
894 ..FilterAnnounce::served()
895 });
896 let json = serde_json::to_value(&waiting).expect("encode a ready announce");
897 assert!(
898 json.get("ready").is_none(),
899 "a ready announce omits ready: {json}"
900 );
901 let decoded: BackendAnnounce =
902 serde_json::from_value(json).expect("decode a ready announce");
903 assert_eq!(decoded, waiting);
904 let served =
905 serde_json::to_value(FilterAnnounce::served()).expect("encode the served evaluation");
906 assert!(
907 served.get("max_wait_ms").is_none(),
908 "a zero wait is omitted: {served}"
909 );
910 }
911
912 #[test]
913 fn given_a_backend_descriptor_when_validated_then_should_refuse_each_broken_invariant() {
914 let ready = ready_backend(1);
915 ready.validate().expect("a ready backend with every limit");
916 backend(1, BackendMode::Operational, "Embedded", "embedded")
917 .validate()
918 .expect("a backend that is not ready");
919 BackendDescriptor {
920 label: "l".repeat(4096),
921 ..ready.clone()
922 }
923 .validate()
924 .expect("a label at the cap");
925 let refused = [
926 (
927 "another descriptor version",
928 BackendDescriptor {
929 descriptor_version: BACKEND_DESCRIPTOR_VERSION + 1,
930 ..ready.clone()
931 },
932 ),
933 (
934 "a zero resource id",
935 BackendDescriptor {
936 resource_id: BackendResourceId::from_u128(0),
937 ..ready.clone()
938 },
939 ),
940 (
941 "a zero backend generation",
942 BackendDescriptor {
943 observed_backend_generation: 0,
944 ..ready.clone()
945 },
946 ),
947 (
948 "a zero configuration revision",
949 BackendDescriptor {
950 runtime_configuration_revision: 0,
951 ..ready.clone()
952 },
953 ),
954 (
955 "an empty label",
956 BackendDescriptor {
957 label: String::new(),
958 ..ready.clone()
959 },
960 ),
961 (
962 "a label past the cap",
963 BackendDescriptor {
964 label: "l".repeat(4097),
965 ..ready.clone()
966 },
967 ),
968 (
969 "a ready backend still starting",
970 BackendDescriptor {
971 observed_state: BackendObservedState::Starting,
972 ..ready.clone()
973 },
974 ),
975 (
976 "a logical schema without a field limit",
977 BackendDescriptor {
978 limits: BackendLimits {
979 max_schema_fields: 0,
980 ..ready.limits.clone()
981 },
982 ..ready.clone()
983 },
984 ),
985 ];
986 for (case, descriptor) in refused {
987 assert!(descriptor.validate().is_err(), "{case} must be refused");
988 }
989 let limits = &ready.limits;
990 let zero_query_limits = [
991 BackendLimits {
992 max_query_rows: 0,
993 ..limits.clone()
994 },
995 BackendLimits {
996 max_query_bytes: 0,
997 ..limits.clone()
998 },
999 BackendLimits {
1000 max_scan_bytes: 0,
1001 ..limits.clone()
1002 },
1003 BackendLimits {
1004 max_query_micros: 0,
1005 ..limits.clone()
1006 },
1007 BackendLimits {
1008 max_concurrent_queries: 0,
1009 ..limits.clone()
1010 },
1011 ];
1012 for limits in zero_query_limits {
1013 let descriptor = BackendDescriptor {
1014 limits,
1015 ..ready.clone()
1016 };
1017 assert!(
1018 descriptor.validate().is_err(),
1019 "a ready query backend needs every query limit"
1020 );
1021 }
1022 }
1023
1024 #[test]
1025 fn given_backend_readiness_when_validated_then_should_need_a_time_or_a_bounded_reason_list() {
1026 let not_ready = |reasons: usize| BackendReadiness {
1027 ready: false,
1028 reasons: vec![
1029 BackendReadinessReason {
1030 code: BackendReadinessCode::ProbeFailed,
1031 detail: None,
1032 };
1033 reasons
1034 ],
1035 observed_at_micros: 0,
1036 };
1037 BackendReadiness::ready(5)
1038 .validate()
1039 .expect("ready with an observation time");
1040 not_ready(32).validate().expect("reasons at the cap");
1041 assert!(not_ready(33).validate().is_err(), "reasons past the cap");
1042 assert!(
1043 BackendReadiness::ready(0).validate().is_err(),
1044 "ready without an observation time"
1045 );
1046 }
1047
1048 #[test]
1049 fn given_query_capabilities_when_validated_then_should_need_coverage_without_repeats() {
1050 capabilities()
1051 .validate()
1052 .expect("full coverage with raw SQL");
1053 let refused = [
1054 (
1055 "no dialect",
1056 QueryCapabilities {
1057 dialects: Vec::new(),
1058 raw_sql: false,
1059 ..capabilities()
1060 },
1061 ),
1062 (
1063 "no consistency level",
1064 QueryCapabilities {
1065 consistency: Vec::new(),
1066 ..capabilities()
1067 },
1068 ),
1069 (
1070 "no logical type",
1071 QueryCapabilities {
1072 logical_types: Vec::new(),
1073 ..capabilities()
1074 },
1075 ),
1076 (
1077 "a repeated dialect",
1078 QueryCapabilities {
1079 dialects: vec![SqlDialect::Postgres, SqlDialect::Postgres],
1080 ..capabilities()
1081 },
1082 ),
1083 ];
1084 for (case, rejected) in refused {
1085 assert!(rejected.validate().is_err(), "{case} must be refused");
1086 }
1087 }
1088
1089 #[test]
1090 fn given_a_backend_announce_when_validated_then_should_need_unique_ids_and_a_ready_backend() {
1091 let versions = OpVersions::new(1, 1, 1, 1);
1092 let not_ready = backend(2, BackendMode::Operational, "Analytics", "columnar");
1093 BackendAnnounce::new(versions)
1094 .validate()
1095 .expect("a ready announce without backends");
1096 BackendAnnounce::new(versions)
1097 .with_backends(vec![ready_backend(1), not_ready.clone()])
1098 .validate()
1099 .expect("a ready announce with a ready backend");
1100 assert!(
1101 BackendAnnounce::new(versions)
1102 .with_backends(vec![not_ready])
1103 .validate()
1104 .is_err(),
1105 "a ready announce without a ready backend"
1106 );
1107 assert!(
1108 BackendAnnounce::new(versions)
1109 .with_backends(vec![ready_backend(1), ready_backend(1)])
1110 .validate()
1111 .is_err(),
1112 "a repeated resource id"
1113 );
1114 }
1115
1116 fn ready_backend(resource_id: u128) -> BackendDescriptor {
1117 backend(
1118 resource_id,
1119 BackendMode::Operational,
1120 "Embedded",
1121 "embedded",
1122 )
1123 .with_state(
1124 BackendDesiredState::Enabled,
1125 BackendObservedState::Ready,
1126 BackendReadiness::ready(5),
1127 )
1128 .with_query(capabilities())
1129 .with_schema(SchemaCapabilities {
1130 logical_schema: true,
1131 arrow_ipc_stream: false,
1132 schema_evolution: false,
1133 })
1134 .with_limits(BackendLimits {
1135 max_query_rows: 1000,
1136 max_query_bytes: 1_048_576,
1137 max_scan_bytes: 1_073_741_824,
1138 max_query_micros: 1_000_000,
1139 max_concurrent_queries: 8,
1140 max_schema_fields: 64,
1141 max_payload_bytes: 0,
1142 })
1143 }
1144
1145 fn capabilities() -> QueryCapabilities {
1146 QueryCapabilities {
1147 dialects: vec![SqlDialect::Postgres],
1148 consistency: vec![Consistency::Eventual],
1149 logical_types: vec![LogicalTypeKind::String],
1150 raw_sql: true,
1151 aggregates: vec![AggFunc::Count],
1152 vector_search: false,
1153 text_search: false,
1154 }
1155 }
1156}
1157const fn is_zero_u32(value: &u32) -> bool {
1158 *value == 0
1159}