1use crate::destination::{BackendResourceId, FileFormat, TableFormat};
2use crate::error::InvalidError;
3use crate::filter::{FILTER_EVALUATOR_VERSION, FilterCodec};
4use crate::query::{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;
24 pub const KV_CAS_FENCED: u64 = 1 << 3;
26 pub const AGENT_WORKFLOW: u64 = 1 << 4;
28 pub const KEYWORD_SEARCH: u64 = 1 << 5;
30 pub const WATCH: u64 = 1 << 6;
33 pub const AUTHZ: u64 = 1 << 7;
35 pub const DESTINATIONS: u64 = 1 << 8;
37 pub const KV_FENCED_LEASES: u64 = 1 << 9;
45 pub const CONSUMER_FILTERS: u64 = 1 << 10;
52 pub const GROUP_POLICY_READS: u64 = 1 << 11;
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[non_exhaustive]
66pub struct OpVersions {
67 pub query: u32,
68 pub control: u32,
69 pub kv: u32,
70 pub fork: u32,
71 #[serde(default, skip_serializing_if = "is_zero")]
75 pub agent: u32,
76 #[serde(default, skip_serializing_if = "is_zero")]
81 pub graph: u32,
82 #[serde(default, skip_serializing_if = "is_zero")]
84 pub checkpoint: u32,
85 #[serde(default, skip_serializing_if = "is_zero")]
88 pub filter: u32,
89 #[serde(default, skip_serializing_if = "is_zero_u64")]
95 pub features: u64,
96}
97
98fn is_zero(value: &u32) -> bool {
99 *value == 0
100}
101
102fn is_zero_u64(value: &u64) -> bool {
103 *value == 0
104}
105
106impl OpVersions {
107 pub fn new(query: u32, control: u32, kv: u32, fork: u32) -> Self {
110 Self {
111 query,
112 control,
113 kv,
114 fork,
115 agent: 0,
116 graph: 0,
117 checkpoint: 0,
118 filter: 0,
119 features: 0,
120 }
121 }
122
123 #[must_use]
125 pub fn with_agent(mut self, agent: u32) -> Self {
126 self.agent = agent;
127 self
128 }
129
130 #[must_use]
132 pub fn with_graph(mut self, graph: u32) -> Self {
133 self.graph = graph;
134 self
135 }
136
137 #[must_use]
139 pub fn with_checkpoint(mut self, checkpoint: u32) -> Self {
140 self.checkpoint = checkpoint;
141 self
142 }
143
144 #[must_use]
146 pub fn with_filter(mut self, filter: u32) -> Self {
147 self.filter = filter;
148 self
149 }
150
151 #[must_use]
154 pub fn with_features(mut self, features: u64) -> Self {
155 self.features = features;
156 self
157 }
158
159 pub const fn has_feature(&self, bit: u64) -> bool {
161 self.features & bit == bit
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
171#[non_exhaustive]
172pub struct HelloReply {
173 pub versions: OpVersions,
174}
175
176impl HelloReply {
177 pub fn new(versions: OpVersions) -> Self {
179 Self { versions }
180 }
181}
182
183pub const BACKEND_DESCRIPTOR_VERSION: u32 = 1;
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187#[non_exhaustive]
188pub struct BackendDescriptor {
189 pub descriptor_version: u32,
190 pub resource_id: BackendResourceId,
191 pub mode: BackendMode,
192 pub label: String,
193 pub implementation: BackendImplementation,
194 pub observed_backend_generation: u64,
195 pub runtime_configuration_revision: u64,
196 pub desired_state: BackendDesiredState,
197 pub observed_state: BackendObservedState,
198 pub readiness: BackendReadiness,
199 pub materialization: Vec<MaterializationCapability>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub query: Option<QueryCapabilities>,
202 pub schema: SchemaCapabilities,
203 pub maintenance: MaintenanceCapabilities,
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 materialization: Vec::new(),
228 query: None,
229 schema: SchemaCapabilities::default(),
230 maintenance: MaintenanceCapabilities::default(),
231 limits: BackendLimits::default(),
232 }
233 }
234
235 #[must_use]
236 pub fn with_state(
237 mut self,
238 desired_state: BackendDesiredState,
239 observed_state: BackendObservedState,
240 readiness: BackendReadiness,
241 ) -> Self {
242 self.desired_state = desired_state;
243 self.observed_state = observed_state;
244 self.readiness = readiness;
245 self
246 }
247
248 #[must_use]
249 pub fn with_materialization(mut self, capabilities: Vec<MaterializationCapability>) -> Self {
250 self.materialization = capabilities;
251 self
252 }
253
254 #[must_use]
255 pub fn with_query(mut self, capabilities: QueryCapabilities) -> Self {
256 self.query = Some(capabilities);
257 self
258 }
259
260 #[must_use]
261 pub fn with_schema(mut self, capabilities: SchemaCapabilities) -> Self {
262 self.schema = capabilities;
263 self
264 }
265
266 #[must_use]
267 pub fn with_maintenance(mut self, capabilities: MaintenanceCapabilities) -> Self {
268 self.maintenance = capabilities;
269 self
270 }
271
272 #[must_use]
273 pub fn with_limits(mut self, limits: BackendLimits) -> Self {
274 self.limits = limits;
275 self
276 }
277}
278
279impl Validate for BackendDescriptor {
280 fn validate(&self) -> Result<(), InvalidError> {
281 if self.descriptor_version != BACKEND_DESCRIPTOR_VERSION {
282 return Err(InvalidError::new(format!(
283 "backend descriptor version must be {BACKEND_DESCRIPTOR_VERSION}"
284 )));
285 }
286 if self.resource_id.as_u128() == 0
287 || self.observed_backend_generation == 0
288 || self.runtime_configuration_revision == 0
289 {
290 return Err(InvalidError::new(
291 "backend identity and observed revisions must be nonzero",
292 ));
293 }
294 validate_descriptor_text("backend label", &self.label)?;
295 validate_descriptor_text("backend implementation kind", &self.implementation.kind)?;
296 validate_descriptor_text(
297 "backend implementation version",
298 &self.implementation.version,
299 )?;
300 self.readiness.validate()?;
301 if self.readiness.ready && self.observed_state != BackendObservedState::Ready {
302 return Err(InvalidError::new(
303 "ready backend must report the ready observed state",
304 ));
305 }
306 let mut materialization = BTreeSet::new();
307 for capability in &self.materialization {
308 let key = (capability.file_format as u8, capability.table_format as u8);
309 if !materialization.insert(key) {
310 return Err(InvalidError::new(
311 "backend repeats a materialization capability",
312 ));
313 }
314 }
315 if let Some(query) = &self.query {
316 query.validate()?;
317 }
318 if self.readiness.ready {
319 if self.schema.logical_schema && self.limits.max_schema_fields == 0 {
320 return Err(InvalidError::new(
321 "ready logical-schema backend must advertise a schema-field limit",
322 ));
323 }
324 if !self.materialization.is_empty() && self.limits.max_materialization_file_bytes == 0 {
325 return Err(InvalidError::new(
326 "ready materialization backend must advertise a file-size limit",
327 ));
328 }
329 if self.query.is_some()
330 && (self.limits.max_query_rows == 0
331 || self.limits.max_query_bytes == 0
332 || self.limits.max_scan_bytes == 0
333 || self.limits.max_query_micros == 0
334 || self.limits.max_concurrent_queries == 0)
335 {
336 return Err(InvalidError::new(
337 "ready query backend must advertise nonzero query limits",
338 ));
339 }
340 }
341 Ok(())
342 }
343}
344
345#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "snake_case")]
347#[non_exhaustive]
348pub enum BackendMode {
349 Operational,
350 Lakehouse,
351}
352
353#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct BackendImplementation {
355 pub kind: String,
356 pub version: String,
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(rename_all = "snake_case")]
361#[non_exhaustive]
362pub enum BackendDesiredState {
363 Disabled,
364 Enabled,
365}
366
367#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(rename_all = "snake_case")]
369#[non_exhaustive]
370pub enum BackendObservedState {
371 Disabled,
372 Starting,
373 Ready,
374 Degraded,
375 Unavailable,
376}
377
378#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379pub struct BackendReadiness {
380 pub ready: bool,
381 pub reasons: Vec<BackendReadinessReason>,
382 pub observed_at_micros: u64,
383}
384
385impl BackendReadiness {
386 pub fn not_ready(code: BackendReadinessCode) -> Self {
387 Self {
388 ready: false,
389 reasons: vec![BackendReadinessReason { code, detail: None }],
390 observed_at_micros: 0,
391 }
392 }
393
394 pub fn ready(observed_at_micros: u64) -> Self {
395 Self {
396 ready: true,
397 reasons: Vec::new(),
398 observed_at_micros,
399 }
400 }
401}
402
403impl Validate for BackendReadiness {
404 fn validate(&self) -> Result<(), InvalidError> {
405 if self.ready {
406 if !self.reasons.is_empty() || self.observed_at_micros == 0 {
407 return Err(InvalidError::new(
408 "ready backend requires an observation time and no failure reasons",
409 ));
410 }
411 return Ok(());
412 }
413 if self.reasons.is_empty() {
414 return Err(InvalidError::new(
415 "not-ready backend requires at least one stable reason",
416 ));
417 }
418 if self.reasons.len() > 32 {
419 return Err(InvalidError::new(
420 "backend readiness reason count exceeds cap 32",
421 ));
422 }
423 for reason in &self.reasons {
424 if let Some(detail) = &reason.detail {
425 validate_descriptor_text("backend readiness detail", detail)?;
426 }
427 }
428 Ok(())
429 }
430}
431
432#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
433pub struct BackendReadinessReason {
434 pub code: BackendReadinessCode,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub detail: Option<String>,
437}
438
439#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "snake_case")]
441#[non_exhaustive]
442pub enum BackendReadinessCode {
443 Disabled,
444 ConfigurationPending,
445 ConfigurationRejected,
446 CredentialUnavailable,
447 ObjectStoreUnavailable,
448 CatalogUnavailable,
449 QueryRuntimeUnavailable,
450 GenerationMismatch,
451 ProbeFailed,
452}
453
454#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
455pub struct MaterializationCapability {
456 pub file_format: FileFormat,
457 pub table_format: TableFormat,
458 pub create_table: bool,
459 pub append: bool,
460}
461
462#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
463pub struct QueryCapabilities {
464 pub dialects: Vec<SqlDialect>,
465 pub time_travel: Vec<TimeTravelCapability>,
466 pub consistency: Vec<Consistency>,
467 pub logical_types: Vec<LogicalTypeKind>,
468 pub paging: Vec<QueryPagingCapability>,
469 pub cancellation: bool,
470 pub execution_status: bool,
471 pub raw_sql: bool,
472}
473
474impl Validate for QueryCapabilities {
475 fn validate(&self) -> Result<(), InvalidError> {
476 if self.dialects.is_empty()
477 || self.consistency.is_empty()
478 || self.logical_types.is_empty()
479 || self.paging.is_empty()
480 {
481 return Err(InvalidError::new(
482 "query capabilities require dialect, consistency, and logical-type coverage",
483 ));
484 }
485 ensure_unique(
486 "query dialect",
487 self.dialects.iter().map(|value| *value as u8),
488 )?;
489 ensure_unique(
490 "query time-travel capability",
491 self.time_travel.iter().map(|value| *value as u8),
492 )?;
493 ensure_unique(
494 "query consistency",
495 self.consistency.iter().map(|value| *value as u8),
496 )?;
497 ensure_unique(
498 "query logical type",
499 self.logical_types.iter().map(|value| *value as u8),
500 )?;
501 ensure_unique(
502 "query paging capability",
503 self.paging.iter().map(|value| *value as u8),
504 )?;
505 if self.raw_sql && self.dialects.is_empty() {
506 return Err(InvalidError::new(
507 "raw SQL capability requires an explicit dialect",
508 ));
509 }
510 Ok(())
511 }
512}
513
514fn ensure_unique(label: &str, values: impl IntoIterator<Item = u8>) -> Result<(), InvalidError> {
515 let mut seen = BTreeSet::new();
516 if values.into_iter().all(|value| seen.insert(value)) {
517 Ok(())
518 } else {
519 Err(InvalidError::new(format!("backend repeats a {label}")))
520 }
521}
522
523fn validate_descriptor_text(label: &str, value: &str) -> Result<(), InvalidError> {
524 if value.is_empty() || value.len() > 4096 || value.chars().any(char::is_control) {
525 return Err(InvalidError::new(format!(
526 "{label} must contain 1..=4096 bytes without control characters"
527 )));
528 }
529 Ok(())
530}
531
532#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(rename_all = "snake_case")]
534#[non_exhaustive]
535pub enum TimeTravelCapability {
536 SnapshotId,
537 TimestampMicros,
538}
539
540#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(rename_all = "snake_case")]
542#[non_exhaustive]
543pub enum QueryPagingCapability {
544 Offset,
545 Cursor,
546}
547
548#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
549pub struct SchemaCapabilities {
550 pub logical_schema: bool,
551 pub arrow_ipc_stream: bool,
552 pub schema_evolution: bool,
553}
554
555#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
556pub struct MaintenanceCapabilities {
557 pub expire_snapshots: bool,
558 pub remove_orphan_files: bool,
559 pub compact_data_files: bool,
560}
561
562#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
563pub struct BackendLimits {
564 pub max_query_rows: u64,
565 pub max_query_bytes: u64,
566 pub max_scan_bytes: u64,
567 pub max_query_micros: u64,
568 pub max_concurrent_queries: u32,
569 pub max_schema_fields: u32,
570 pub max_materialization_file_bytes: u64,
571}
572
573const fn backend_ready_by_default() -> bool {
583 true
584}
585
586const fn backend_is_ready(ready: &bool) -> bool {
587 *ready
588}
589
590#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
591#[non_exhaustive]
592pub struct BackendAnnounce {
593 pub versions: OpVersions,
594 #[serde(
595 default = "backend_ready_by_default",
596 skip_serializing_if = "backend_is_ready"
597 )]
598 pub ready: bool,
599 #[serde(default, skip_serializing_if = "Vec::is_empty")]
604 pub backends: Vec<BackendDescriptor>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub topology: Option<WireTopology>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub filters: Option<FilterAnnounce>,
616}
617
618#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
623pub struct FilterAnnounce {
624 pub evaluator_version: u32,
625 #[serde(default, skip_serializing_if = "Vec::is_empty")]
626 pub codecs: Vec<FilterCodec>,
627}
628
629impl FilterAnnounce {
630 pub fn served() -> Self {
632 Self {
633 evaluator_version: FILTER_EVALUATOR_VERSION,
634 codecs: vec![
635 FilterCodec::Json,
636 FilterCodec::HeadersOnly,
637 FilterCodec::Avro,
638 FilterCodec::Protobuf,
639 FilterCodec::Cbor,
640 ],
641 }
642 }
643
644 pub fn evaluates(&self, evaluator_version: u32, codec: FilterCodec) -> bool {
647 self.evaluator_version == evaluator_version && self.codecs.contains(&codec)
648 }
649}
650
651impl BackendAnnounce {
652 pub fn new(versions: OpVersions) -> Self {
654 Self {
655 versions,
656 ready: true,
657 backends: Vec::new(),
658 topology: None,
659 filters: None,
660 }
661 }
662
663 #[must_use]
665 pub fn with_filters(mut self, filters: FilterAnnounce) -> Self {
666 self.filters = Some(filters);
667 self
668 }
669
670 #[must_use]
671 pub const fn unavailable(mut self) -> Self {
672 self.ready = false;
673 self
674 }
675
676 #[must_use]
678 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
679 self.backends = backends;
680 self
681 }
682
683 #[must_use]
685 pub fn with_topology(mut self, topology: WireTopology) -> Self {
686 self.topology = Some(topology);
687 self
688 }
689}
690
691impl Validate for BackendAnnounce {
692 fn validate(&self) -> Result<(), InvalidError> {
693 let mut resources = BTreeSet::new();
694 for backend in &self.backends {
695 backend.validate()?;
696 if !resources.insert(backend.resource_id) {
697 return Err(InvalidError::new(
698 "backend announcement repeats a resource id",
699 ));
700 }
701 }
702 if self.ready
703 && !self.backends.is_empty()
704 && !self.backends.iter().any(|backend| backend.readiness.ready)
705 {
706 return Err(InvalidError::new(
707 "ready backend announcement must contain a ready backend",
708 ));
709 }
710 Ok(())
711 }
712}
713
714#[cfg(all(test, feature = "cbor"))]
715mod tests {
716 use super::*;
717 use crate::codes::{CONTROL_OP_VERSION, FORK_OP_VERSION, KV_OP_VERSION, QUERY_OP_VERSION};
718 use crate::framing::{decode_named, encode_named};
719
720 fn backend(resource_id: u128, mode: BackendMode, label: &str, kind: &str) -> BackendDescriptor {
721 BackendDescriptor::new(
722 BackendResourceId::from_u128(resource_id),
723 mode,
724 label,
725 BackendImplementation {
726 kind: kind.to_owned(),
727 version: "1.0.0".to_owned(),
728 },
729 1,
730 1,
731 )
732 }
733
734 #[test]
735 fn given_a_hello_reply_when_round_tripped_then_should_preserve_versions() {
736 let reply = HelloReply::new(OpVersions::new(
739 QUERY_OP_VERSION,
740 CONTROL_OP_VERSION,
741 KV_OP_VERSION,
742 FORK_OP_VERSION,
743 ));
744 let bytes = encode_named(&reply).expect("hello reply serializes");
745 let back: HelloReply = decode_named(&bytes).expect("hello reply deserializes");
746 assert_eq!(back, reply);
747 }
748
749 #[test]
750 fn given_a_backend_announce_when_round_tripped_then_should_preserve_features() {
751 let announce = BackendAnnounce::new(
752 OpVersions::new(
753 QUERY_OP_VERSION,
754 CONTROL_OP_VERSION,
755 KV_OP_VERSION,
756 FORK_OP_VERSION,
757 )
758 .with_features(feature::KV_CAS | feature::READ_YOUR_WRITES),
759 );
760 let bytes = encode_named(&announce).expect("serializes");
761 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
762 assert_eq!(back, announce);
763 assert!(back.versions.has_feature(feature::KV_CAS));
764 }
765
766 #[test]
767 fn given_an_unavailable_backend_announce_when_round_tripped_then_should_stay_unavailable() {
768 let announce = BackendAnnounce::new(OpVersions::new(
769 QUERY_OP_VERSION,
770 CONTROL_OP_VERSION,
771 KV_OP_VERSION,
772 FORK_OP_VERSION,
773 ))
774 .unavailable();
775 let bytes = encode_named(&announce).expect("serializes");
776 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
777 assert!(!back.ready);
778 }
779
780 #[test]
781 fn given_an_empty_hello_body_when_decoded_then_should_yield_no_versions() {
782 assert!(decode_named::<HelloReply>(&[]).is_err());
785 }
786
787 #[test]
788 fn given_advertised_backends_when_round_tripped_then_should_preserve_them_and_skip_empty() {
789 let announce = BackendAnnounce::new(OpVersions::new(
790 QUERY_OP_VERSION,
791 CONTROL_OP_VERSION,
792 KV_OP_VERSION,
793 FORK_OP_VERSION,
794 ))
795 .with_backends(vec![
796 backend(1, BackendMode::Operational, "Embedded", "embedded"),
797 backend(2, BackendMode::Lakehouse, "Analytics warehouse", "columnar"),
798 ]);
799 let bytes = encode_named(&announce).expect("encodes");
800 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
801 assert_eq!(back, announce);
802 assert_eq!(back.backends.len(), 2);
803 assert_eq!(
804 back.backends[1].resource_id,
805 BackendResourceId::from_u128(2)
806 );
807 assert_eq!(back.backends[1].implementation.kind, "columnar");
808 assert_eq!(back.backends[1].label, "Analytics warehouse");
809
810 let plain = BackendAnnounce::new(OpVersions::new(1, 1, 1, 1));
813 let json = serde_json::to_string(&plain).expect("json");
814 assert!(!json.contains("backends"), "empty backends omitted: {json}");
815 }
816
817 #[test]
818 fn given_announce_without_topology_when_decoded_then_should_default_none() {
819 let announce = BackendAnnounce::new(OpVersions::new(
820 QUERY_OP_VERSION,
821 CONTROL_OP_VERSION,
822 KV_OP_VERSION,
823 FORK_OP_VERSION,
824 ));
825 let bytes = encode_named(&announce).expect("encodes");
826 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
827 assert_eq!(back.topology, None);
828 let json = serde_json::to_string(&announce).expect("json");
829 assert!(
830 !json.contains("topology"),
831 "absent topology omitted: {json}"
832 );
833 }
834
835 #[test]
836 fn given_announce_with_topology_when_round_tripped_then_should_preserve_it() {
837 let custom = WireTopology {
838 ops_stream: "custom-ops".to_owned(),
839 ..WireTopology::default()
840 };
841 let announce = BackendAnnounce::new(OpVersions::new(
842 QUERY_OP_VERSION,
843 CONTROL_OP_VERSION,
844 KV_OP_VERSION,
845 FORK_OP_VERSION,
846 ))
847 .with_topology(custom.clone());
848 let bytes = encode_named(&announce).expect("encodes");
849 let back: BackendAnnounce = decode_named(&bytes).expect("decodes");
850 assert_eq!(back.topology, Some(custom));
851 }
852
853 #[test]
854 fn given_advertised_features_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
855 let versions = OpVersions::new(
856 QUERY_OP_VERSION,
857 CONTROL_OP_VERSION,
858 KV_OP_VERSION,
859 FORK_OP_VERSION,
860 )
861 .with_features(feature::KV_CAS | feature::READ_YOUR_WRITES);
862 assert!(versions.has_feature(feature::KV_CAS));
863 assert!(versions.has_feature(feature::READ_YOUR_WRITES));
864 assert!(!versions.has_feature(feature::STRONG_CONSISTENCY));
865 assert!(versions.has_feature(feature::KV_CAS | feature::READ_YOUR_WRITES));
867 assert!(!versions.has_feature(feature::KV_CAS | feature::STRONG_CONSISTENCY));
868 let reply = HelloReply::new(versions);
869 let bytes = encode_named(&reply).expect("encodes");
870 let back: HelloReply = decode_named(&bytes).expect("decodes");
871 assert_eq!(back, reply);
872 assert!(back.versions.has_feature(feature::READ_YOUR_WRITES));
873 let plain = HelloReply::new(OpVersions::new(1, 1, 1, 1));
876 let json = serde_json::to_string(&plain).expect("json");
877 assert!(!json.contains("features"), "zero features omitted: {json}");
878 }
879
880 #[test]
881 fn given_announced_filter_evaluation_when_round_tripped_then_should_gate_codecs_and_versions() {
882 let announce = BackendAnnounce::new(OpVersions::new(1, 1, 1, 1))
883 .with_filters(FilterAnnounce::served());
884 let bytes = encode_named(&announce).expect("serializes");
885 let back: BackendAnnounce = decode_named(&bytes).expect("deserializes");
886 let filters = back.filters.expect("the evaluation is announced");
887 assert!(filters.evaluates(FILTER_EVALUATOR_VERSION, FilterCodec::Json));
888 assert!(!filters.evaluates(FILTER_EVALUATOR_VERSION + 1, FilterCodec::Json));
889 assert!(!filters.evaluates(FILTER_EVALUATOR_VERSION, FilterCodec::Unknown));
890 let plain =
891 encode_named(&BackendAnnounce::new(OpVersions::new(1, 1, 1, 1))).expect("serializes");
892 let older: BackendAnnounce = decode_named(&plain).expect("deserializes");
893 assert_eq!(older.filters, None);
894 }
895}