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