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