1use std::{collections::HashMap, marker::PhantomData};
21
22use tracing::warn;
23
24use crate::{
25 data_converters::{
26 GenericPayloadConverter, PayloadConversionError, PayloadConverter, SerializationContext,
27 SerializationContextData,
28 },
29 protos::temporal::api::{
30 common::v1::{Payload, SearchAttributes as ProtoSearchAttributes},
31 enums::v1::IndexedValueType,
32 },
33};
34
35const TYPE_METADATA_KEY: &str = "type";
37
38#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum SearchAttributeError {
42 #[error("failed to convert search attribute payload: {0}")]
44 PayloadConversion(#[from] PayloadConversionError),
45
46 #[error("invalid search attribute payload: {reason}")]
48 InvalidPayload {
49 reason: String,
51 },
52
53 #[error("invalid timestamp: {0}")]
55 InvalidTimestamp(String),
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub struct Timestamp {
69 seconds: i64,
70 nanos: i32,
71}
72
73impl Timestamp {
74 const MAX_NANOS: i32 = 999_999_999;
76
77 pub fn new(seconds: i64, nanos: i32) -> Self {
85 Self {
86 seconds,
87 nanos: nanos.clamp(0, Self::MAX_NANOS),
88 }
89 }
90
91 pub fn seconds(&self) -> i64 {
93 self.seconds
94 }
95
96 pub fn nanos(&self) -> i32 {
98 self.nanos
99 }
100
101 pub fn to_prost(&self) -> prost_types::Timestamp {
103 prost_types::Timestamp {
104 seconds: self.seconds,
105 nanos: self.nanos,
106 }
107 }
108}
109
110impl std::fmt::Display for Timestamp {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match timestamp_to_rfc3339(self) {
115 Ok(s) => f.write_str(&s),
116 Err(_) => write!(f, "Timestamp({}, {})", self.seconds, self.nanos),
117 }
118 }
119}
120
121impl From<prost_types::Timestamp> for Timestamp {
122 fn from(ts: prost_types::Timestamp) -> Self {
123 Timestamp::new(ts.seconds, ts.nanos)
124 }
125}
126
127impl From<Timestamp> for prost_types::Timestamp {
128 fn from(ts: Timestamp) -> Self {
129 prost_types::Timestamp {
130 seconds: ts.seconds(),
131 nanos: ts.nanos(),
132 }
133 }
134}
135
136impl From<std::time::SystemTime> for Timestamp {
137 fn from(st: std::time::SystemTime) -> Self {
138 match st.duration_since(std::time::UNIX_EPOCH) {
139 Ok(dur) => Timestamp::new(dur.as_secs() as i64, dur.subsec_nanos() as i32),
140 Err(e) => {
141 let dur = e.duration();
144 let secs = dur.as_secs() as i64;
145 let nanos = dur.subsec_nanos();
146 if nanos == 0 {
147 Timestamp::new(-secs, 0)
148 } else {
149 Timestamp::new(-(secs + 1), (1_000_000_000 - nanos) as i32)
150 }
151 }
152 }
153 }
154}
155
156impl TryFrom<Timestamp> for std::time::SystemTime {
157 type Error = SearchAttributeError;
158
159 fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
160 let epoch = std::time::UNIX_EPOCH;
161 if ts.seconds >= 0 {
162 epoch
163 .checked_add(std::time::Duration::new(
164 ts.seconds as u64,
165 ts.nanos.max(0) as u32,
166 ))
167 .ok_or_else(|| {
168 SearchAttributeError::InvalidTimestamp(
169 "timestamp out of SystemTime range".into(),
170 )
171 })
172 } else {
173 let abs_secs = ts.seconds.unsigned_abs();
176 let nanos = ts.nanos.max(0) as u32;
177 let dur = if nanos == 0 {
178 std::time::Duration::new(abs_secs, 0)
179 } else {
180 std::time::Duration::new(abs_secs - 1, 1_000_000_000 - nanos)
181 };
182 epoch.checked_sub(dur).ok_or_else(|| {
183 SearchAttributeError::InvalidTimestamp("timestamp out of SystemTime range".into())
184 })
185 }
186 }
187}
188
189mod private {
194 pub trait Sealed {}
195 impl Sealed for bool {}
196 impl Sealed for i64 {}
197 impl Sealed for f64 {}
198 impl Sealed for String {}
199 impl Sealed for super::Timestamp {}
200 impl Sealed for Vec<String> {}
201}
202
203pub trait SearchAttributeValue: private::Sealed + Clone + Sized {
208 fn to_search_attribute_payload(
210 &self,
211 indexed_value_type: IndexedValueType,
212 ) -> Result<Payload, SearchAttributeError>;
213
214 fn from_search_attribute_payload(payload: &Payload) -> Result<Self, SearchAttributeError>;
216
217 fn default_indexed_value_type() -> IndexedValueType;
223}
224
225fn type_metadata_str(ivt: IndexedValueType) -> &'static str {
230 match ivt {
231 IndexedValueType::Bool => "Bool",
232 IndexedValueType::Int => "Int",
233 IndexedValueType::Double => "Double",
234 IndexedValueType::Keyword => "Keyword",
235 IndexedValueType::Text => "Text",
236 IndexedValueType::Datetime => "Datetime",
237 IndexedValueType::KeywordList => "KeywordList",
238 IndexedValueType::Unspecified => "Unspecified",
239 }
240}
241
242fn encode_json_search_attr<T: serde::Serialize + 'static>(
247 value: &T,
248 indexed_value_type: IndexedValueType,
249) -> Result<Payload, SearchAttributeError> {
250 let converter = PayloadConverter::serde_json();
251 let context = SerializationContext {
252 data: &SerializationContextData::None,
253 converter: &converter,
254 };
255 let mut payload = converter.to_payload(&context, value)?;
256 payload.metadata.insert(
257 TYPE_METADATA_KEY.to_string(),
258 type_metadata_str(indexed_value_type).as_bytes().to_vec(),
259 );
260 Ok(payload)
261}
262
263fn decode_json_search_attr<T: serde::de::DeserializeOwned + 'static>(
267 payload: &Payload,
268) -> Result<T, SearchAttributeError> {
269 let converter = PayloadConverter::serde_json();
270 let context = SerializationContext {
271 data: &SerializationContextData::None,
272 converter: &converter,
273 };
274 Ok(converter.from_payload(&context, payload.clone())?)
275}
276
277macro_rules! impl_simple_search_attribute_value {
284 ($ty:ty, $ivt:expr) => {
285 impl SearchAttributeValue for $ty {
286 fn to_search_attribute_payload(
287 &self,
288 indexed_value_type: IndexedValueType,
289 ) -> Result<Payload, SearchAttributeError> {
290 encode_json_search_attr(self, indexed_value_type)
291 }
292
293 fn from_search_attribute_payload(
294 payload: &Payload,
295 ) -> Result<Self, SearchAttributeError> {
296 decode_json_search_attr(payload)
297 }
298
299 fn default_indexed_value_type() -> IndexedValueType {
300 $ivt
301 }
302 }
303 };
304}
305
306impl_simple_search_attribute_value!(bool, IndexedValueType::Bool);
307impl_simple_search_attribute_value!(i64, IndexedValueType::Int);
308impl_simple_search_attribute_value!(String, IndexedValueType::Keyword);
309impl_simple_search_attribute_value!(Vec<String>, IndexedValueType::KeywordList);
310
311impl SearchAttributeValue for f64 {
314 fn to_search_attribute_payload(
315 &self,
316 indexed_value_type: IndexedValueType,
317 ) -> Result<Payload, SearchAttributeError> {
318 if !self.is_finite() {
319 return Err(SearchAttributeError::InvalidPayload {
320 reason: format!("f64 search attribute value must be finite, got {}", self),
321 });
322 }
323 encode_json_search_attr(self, indexed_value_type)
324 }
325
326 fn from_search_attribute_payload(payload: &Payload) -> Result<Self, SearchAttributeError> {
327 decode_json_search_attr(payload)
328 }
329
330 fn default_indexed_value_type() -> IndexedValueType {
331 IndexedValueType::Double
332 }
333}
334
335fn timestamp_to_rfc3339(ts: &Timestamp) -> Result<String, SearchAttributeError> {
341 use chrono::{DateTime, Utc};
342
343 let nanos = u32::try_from(ts.nanos()).unwrap_or(0);
344 let dt = DateTime::<Utc>::from_timestamp(ts.seconds(), nanos).ok_or_else(|| {
345 SearchAttributeError::InvalidTimestamp(format!(
346 "cannot represent seconds={} nanos={} as DateTime",
347 ts.seconds(),
348 ts.nanos()
349 ))
350 })?;
351 Ok(dt.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true))
352}
353
354fn rfc3339_to_timestamp(s: &str) -> Result<Timestamp, SearchAttributeError> {
356 use chrono::DateTime;
357
358 let s = s.trim_matches('"');
361 let dt = DateTime::parse_from_rfc3339(s).map_err(|e| {
362 SearchAttributeError::InvalidTimestamp(format!("failed to parse RFC3339 '{}': {}", s, e))
363 })?;
364 Ok(Timestamp::new(
365 dt.timestamp(),
366 dt.timestamp_subsec_nanos() as i32,
367 ))
368}
369
370impl SearchAttributeValue for Timestamp {
371 fn to_search_attribute_payload(
372 &self,
373 indexed_value_type: IndexedValueType,
374 ) -> Result<Payload, SearchAttributeError> {
375 let rfc3339 = timestamp_to_rfc3339(self)?;
376 encode_json_search_attr(&rfc3339, indexed_value_type)
377 }
378
379 fn from_search_attribute_payload(payload: &Payload) -> Result<Self, SearchAttributeError> {
380 let s: String = decode_json_search_attr(payload)?;
381 rfc3339_to_timestamp(&s)
382 }
383
384 fn default_indexed_value_type() -> IndexedValueType {
385 IndexedValueType::Datetime
386 }
387}
388
389#[derive(Debug, Clone, Copy)]
410pub struct SearchAttributeKey<T: SearchAttributeValue> {
411 name: &'static str,
412 indexed_value_type: IndexedValueType,
413 _marker: PhantomData<T>,
414}
415
416impl<T: SearchAttributeValue> SearchAttributeKey<T> {
417 pub fn name(&self) -> &'static str {
419 self.name
420 }
421
422 pub fn indexed_value_type(&self) -> IndexedValueType {
424 self.indexed_value_type
425 }
426
427 pub fn value_set(&self, val: T) -> SearchAttributeUpdate {
436 self.try_value_set(val)
437 .expect("search attribute serialization failed (use try_value_set for non-finite f64 or out-of-range timestamps)")
438 }
439
440 pub fn try_value_set(&self, val: T) -> Result<SearchAttributeUpdate, SearchAttributeError> {
443 let payload = val.to_search_attribute_payload(self.indexed_value_type)?;
444 Ok(SearchAttributeUpdate {
445 name: self.name.to_string(),
446 payload: Some(payload),
447 })
448 }
449
450 pub fn value_unset(&self) -> SearchAttributeUpdate {
452 SearchAttributeUpdate {
453 name: self.name.to_string(),
454 payload: None,
455 }
456 }
457}
458
459impl SearchAttributeKey<bool> {
460 pub const fn bool(name: &'static str) -> Self {
462 Self {
463 name,
464 indexed_value_type: IndexedValueType::Bool,
465 _marker: PhantomData,
466 }
467 }
468}
469
470impl SearchAttributeKey<i64> {
471 pub const fn int(name: &'static str) -> Self {
473 Self {
474 name,
475 indexed_value_type: IndexedValueType::Int,
476 _marker: PhantomData,
477 }
478 }
479}
480
481impl SearchAttributeKey<f64> {
482 pub const fn float(name: &'static str) -> Self {
484 Self {
485 name,
486 indexed_value_type: IndexedValueType::Double,
487 _marker: PhantomData,
488 }
489 }
490}
491
492impl SearchAttributeKey<String> {
493 pub const fn keyword(name: &'static str) -> Self {
495 Self {
496 name,
497 indexed_value_type: IndexedValueType::Keyword,
498 _marker: PhantomData,
499 }
500 }
501
502 pub const fn text(name: &'static str) -> Self {
504 Self {
505 name,
506 indexed_value_type: IndexedValueType::Text,
507 _marker: PhantomData,
508 }
509 }
510}
511
512impl SearchAttributeKey<Timestamp> {
513 pub const fn datetime(name: &'static str) -> Self {
515 Self {
516 name,
517 indexed_value_type: IndexedValueType::Datetime,
518 _marker: PhantomData,
519 }
520 }
521}
522
523impl SearchAttributeKey<Vec<String>> {
524 pub const fn keyword_list(name: &'static str) -> Self {
526 Self {
527 name,
528 indexed_value_type: IndexedValueType::KeywordList,
529 _marker: PhantomData,
530 }
531 }
532}
533
534#[derive(Debug, Clone)]
549pub struct SearchAttributeUpdate {
550 pub(crate) name: String,
551 pub(crate) payload: Option<Payload>,
552}
553
554impl SearchAttributeUpdate {
555 pub fn name(&self) -> &str {
557 &self.name
558 }
559
560 pub fn is_unset(&self) -> bool {
562 self.payload.is_none()
563 }
564}
565
566#[derive(Debug, Clone, Default, PartialEq)]
573pub struct SearchAttributes {
574 fields: HashMap<String, Payload>,
575}
576
577impl SearchAttributes {
578 pub fn new(updates: impl IntoIterator<Item = SearchAttributeUpdate>) -> Self {
582 let mut fields = HashMap::new();
583 for update in updates {
584 match update.payload {
585 Some(payload) => {
586 fields.insert(update.name, payload);
587 }
588 None => {
589 fields.remove(&update.name);
590 }
591 }
592 }
593 Self { fields }
594 }
595
596 pub fn apply(&mut self, update: SearchAttributeUpdate) {
600 match update.payload {
601 Some(payload) => {
602 self.fields.insert(update.name, payload);
603 }
604 None => {
605 self.fields.remove(&update.name);
606 }
607 }
608 }
609
610 pub fn get<T: SearchAttributeValue>(&self, key: &SearchAttributeKey<T>) -> Option<T> {
613 let payload = self.fields.get(key.name())?;
614 match T::from_search_attribute_payload(payload) {
615 Ok(val) => Some(val),
616 Err(e) => {
617 warn!(
618 key = key.name(),
619 error = %e,
620 "Failed to deserialize search attribute; returning None. \
621 Use try_get() for explicit error handling."
622 );
623 None
624 }
625 }
626 }
627
628 pub fn try_get<T: SearchAttributeValue>(
632 &self,
633 key: &SearchAttributeKey<T>,
634 ) -> Result<Option<T>, SearchAttributeError> {
635 match self.fields.get(key.name()) {
636 None => Ok(None),
637 Some(payload) => T::from_search_attribute_payload(payload).map(Some),
638 }
639 }
640
641 pub fn contains_key<T: SearchAttributeValue>(&self, key: &SearchAttributeKey<T>) -> bool {
643 self.fields.contains_key(key.name())
644 }
645
646 pub fn is_empty(&self) -> bool {
648 self.fields.is_empty()
649 }
650
651 pub fn len(&self) -> usize {
653 self.fields.len()
654 }
655
656 pub fn keys(&self) -> impl Iterator<Item = &str> {
658 self.fields.keys().map(|s| s.as_str())
659 }
660
661 pub fn raw_payload(&self, name: &str) -> Option<&Payload> {
665 self.fields.get(name)
666 }
667
668 pub fn to_proto(&self) -> ProtoSearchAttributes {
670 ProtoSearchAttributes {
671 indexed_fields: self.fields.clone(),
672 }
673 }
674
675 pub fn into_proto(self) -> ProtoSearchAttributes {
678 ProtoSearchAttributes {
679 indexed_fields: self.fields,
680 }
681 }
682
683 pub fn from_proto(attrs: &ProtoSearchAttributes) -> Self {
685 Self {
686 fields: attrs.indexed_fields.clone(),
687 }
688 }
689}
690
691impl From<ProtoSearchAttributes> for SearchAttributes {
692 fn from(attrs: ProtoSearchAttributes) -> Self {
694 Self {
695 fields: attrs.indexed_fields,
696 }
697 }
698}
699
700impl SearchAttributes {
701 pub fn updates_to_proto(
705 updates: impl IntoIterator<Item = SearchAttributeUpdate>,
706 ) -> ProtoSearchAttributes {
707 let mut indexed_fields = HashMap::new();
708 for update in updates {
709 let payload = update.payload.unwrap_or_default();
710 indexed_fields.insert(update.name, payload);
711 }
712 ProtoSearchAttributes { indexed_fields }
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 const BOOL_KEY: SearchAttributeKey<bool> = SearchAttributeKey::bool("my_bool");
721 const INT_KEY: SearchAttributeKey<i64> = SearchAttributeKey::int("my_int");
722 const FLOAT_KEY: SearchAttributeKey<f64> = SearchAttributeKey::float("my_float");
723 const KW_KEY: SearchAttributeKey<String> = SearchAttributeKey::keyword("my_keyword");
724 const TEXT_KEY: SearchAttributeKey<String> = SearchAttributeKey::text("my_text");
725 const DT_KEY: SearchAttributeKey<Timestamp> = SearchAttributeKey::datetime("my_datetime");
726 const KWL_KEY: SearchAttributeKey<Vec<String>> =
727 SearchAttributeKey::keyword_list("my_keyword_list");
728
729 fn assert_payload_metadata(payload: &Payload, expected_type: &str) {
730 assert_eq!(
731 payload.metadata.get("encoding").unwrap(),
732 b"json/plain".as_slice()
733 );
734 assert_eq!(
735 payload.metadata.get(TYPE_METADATA_KEY).unwrap(),
736 expected_type.as_bytes()
737 );
738 }
739
740 #[test]
741 fn round_trip_bool() {
742 let val = true;
743 let payload = val
744 .to_search_attribute_payload(IndexedValueType::Bool)
745 .unwrap();
746 assert_payload_metadata(&payload, "Bool");
747 assert!(bool::from_search_attribute_payload(&payload).unwrap());
748 }
749
750 #[test]
751 fn round_trip_int() {
752 let val: i64 = -42;
753 let payload = val
754 .to_search_attribute_payload(IndexedValueType::Int)
755 .unwrap();
756 assert_payload_metadata(&payload, "Int");
757 assert_eq!(i64::from_search_attribute_payload(&payload).unwrap(), -42);
758 }
759
760 #[test]
761 fn round_trip_double() {
762 let val: f64 = 1.23;
763 let payload = val
764 .to_search_attribute_payload(IndexedValueType::Double)
765 .unwrap();
766 assert_payload_metadata(&payload, "Double");
767 let decoded = f64::from_search_attribute_payload(&payload).unwrap();
768 assert!((decoded - 1.23).abs() < f64::EPSILON);
769 }
770
771 #[test]
772 fn round_trip_keyword() {
773 let val = "hello".to_string();
774 let payload = val
775 .to_search_attribute_payload(IndexedValueType::Keyword)
776 .unwrap();
777 assert_payload_metadata(&payload, "Keyword");
778 assert_eq!(
779 String::from_search_attribute_payload(&payload).unwrap(),
780 "hello"
781 );
782 }
783
784 #[test]
785 fn round_trip_text() {
786 let val = "some long text".to_string();
787 let payload = val
788 .to_search_attribute_payload(IndexedValueType::Text)
789 .unwrap();
790 assert_payload_metadata(&payload, "Text");
791 assert_eq!(
792 String::from_search_attribute_payload(&payload).unwrap(),
793 "some long text"
794 );
795 }
796
797 #[test]
798 fn round_trip_datetime() {
799 let ts = Timestamp::new(1_700_000_000, 123_456_789);
800 let payload = ts
801 .to_search_attribute_payload(IndexedValueType::Datetime)
802 .unwrap();
803 assert_payload_metadata(&payload, "Datetime");
804
805 let json_str: String = serde_json::from_slice(&payload.data).unwrap();
806 assert!(json_str.ends_with('Z'));
807 assert!(json_str.contains('T'));
808
809 let decoded = Timestamp::from_search_attribute_payload(&payload).unwrap();
810 assert_eq!(decoded.seconds(), ts.seconds());
811 assert_eq!(decoded.nanos(), ts.nanos());
812
813 let attrs = SearchAttributes::new([DT_KEY.value_set(ts.clone())]);
814 let got = attrs.get(&DT_KEY).unwrap();
815 assert_eq!(got.seconds(), ts.seconds());
816 assert_eq!(got.nanos(), ts.nanos());
817 }
818
819 #[test]
820 fn round_trip_datetime_no_nanos() {
821 let ts = Timestamp::new(0, 0);
822 let payload = ts
823 .to_search_attribute_payload(IndexedValueType::Datetime)
824 .unwrap();
825 let decoded = Timestamp::from_search_attribute_payload(&payload).unwrap();
826 assert_eq!(decoded.seconds(), 0);
827 assert_eq!(decoded.nanos(), 0);
828 }
829
830 #[test]
831 fn round_trip_keyword_list() {
832 let val = vec!["a".to_string(), "b".to_string(), "c".to_string()];
833 let payload = val
834 .to_search_attribute_payload(IndexedValueType::KeywordList)
835 .unwrap();
836 assert_payload_metadata(&payload, "KeywordList");
837 assert_eq!(
838 Vec::<String>::from_search_attribute_payload(&payload).unwrap(),
839 vec!["a", "b", "c"]
840 );
841 }
842
843 #[test]
844 fn typed_search_attributes_new_and_get() {
845 let attrs = SearchAttributes::new([
846 BOOL_KEY.value_set(true),
847 INT_KEY.value_set(99),
848 FLOAT_KEY.value_set(2.72),
849 KW_KEY.value_set("kw_val".into()),
850 TEXT_KEY.value_set("text_val".into()),
851 KWL_KEY.value_set(vec!["x".into(), "y".into()]),
852 ]);
853
854 assert_eq!(attrs.len(), 6);
855 assert!(!attrs.is_empty());
856 assert_eq!(attrs.get(&BOOL_KEY), Some(true));
857 assert_eq!(attrs.get(&INT_KEY), Some(99));
858 assert!((attrs.get(&FLOAT_KEY).unwrap() - 2.72).abs() < f64::EPSILON);
859 assert_eq!(attrs.get(&KW_KEY), Some("kw_val".into()));
860 assert_eq!(attrs.get(&TEXT_KEY), Some("text_val".into()));
861 assert_eq!(
862 attrs.get(&KWL_KEY),
863 Some(vec!["x".to_string(), "y".to_string()])
864 );
865 }
866
867 #[test]
868 fn to_proto_from_proto_round_trip() {
869 let attrs = SearchAttributes::new([BOOL_KEY.value_set(false), INT_KEY.value_set(7)]);
870
871 let proto = attrs.to_proto();
872 assert_eq!(proto.indexed_fields.len(), 2);
873
874 let restored = SearchAttributes::from_proto(&proto);
875 assert_eq!(restored.get(&BOOL_KEY), Some(false));
876 assert_eq!(restored.get(&INT_KEY), Some(7));
877 }
878
879 #[test]
880 fn value_unset_removes_entry() {
881 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true), BOOL_KEY.value_unset()]);
882 assert!(attrs.is_empty());
883 assert_eq!(attrs.get(&BOOL_KEY), None);
884 }
885
886 #[test]
887 fn keyword_vs_text_disambiguation() {
888 let kw_update = KW_KEY.value_set("same_value".into());
889 let text_update = TEXT_KEY.value_set("same_value".into());
890
891 let kw_payload = kw_update.payload.as_ref().unwrap();
892 let text_payload = text_update.payload.as_ref().unwrap();
893
894 assert_eq!(
895 kw_payload.metadata.get(TYPE_METADATA_KEY).unwrap(),
896 b"Keyword"
897 );
898 assert_eq!(
899 text_payload.metadata.get(TYPE_METADATA_KEY).unwrap(),
900 b"Text"
901 );
902
903 assert_eq!(KW_KEY.indexed_value_type(), IndexedValueType::Keyword);
904 assert_eq!(TEXT_KEY.indexed_value_type(), IndexedValueType::Text);
905 }
906
907 #[test]
908 fn get_returns_none_for_missing_key() {
909 let attrs = SearchAttributes::default();
910 assert_eq!(attrs.get(&BOOL_KEY), None);
911 assert!(!attrs.contains_key(&INT_KEY));
912 }
913
914 #[test]
915 fn get_returns_none_for_type_mismatch() {
916 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true)]);
917 let mismatched_key = SearchAttributeKey::<i64>::int("my_bool");
919 assert_eq!(attrs.get(&mismatched_key), None);
920 }
921
922 #[test]
923 fn updates_to_proto_includes_empty_payload_for_unset() {
924 let proto =
925 SearchAttributes::updates_to_proto([BOOL_KEY.value_set(true), INT_KEY.value_unset()]);
926
927 let bool_payload = proto.indexed_fields.get("my_bool").unwrap();
928 assert!(!bool_payload.data.is_empty());
929
930 let int_payload = proto.indexed_fields.get("my_int").unwrap();
931 assert!(int_payload.data.is_empty());
932 assert!(int_payload.metadata.is_empty());
933 }
934
935 #[test]
936 fn contains_key_returns_true_when_present() {
937 let attrs = SearchAttributes::new([INT_KEY.value_set(42)]);
938 assert!(attrs.contains_key(&INT_KEY));
939 }
940
941 #[test]
942 fn timestamp_rfc3339_format() {
943 let ts = Timestamp::new(1_700_000_000, 0);
944 let rfc = timestamp_to_rfc3339(&ts).unwrap();
945 assert_eq!(rfc, "2023-11-14T22:13:20.000000000Z");
947 }
948
949 #[test]
950 fn timestamp_rfc3339_with_nanos() {
951 let ts = Timestamp::new(1_700_000_000, 500_000_000);
952 let rfc = timestamp_to_rfc3339(&ts).unwrap();
953 assert_eq!(rfc, "2023-11-14T22:13:20.500000000Z");
954
955 let parsed = rfc3339_to_timestamp(&rfc).unwrap();
956 assert_eq!(parsed.seconds(), ts.seconds());
957 assert_eq!(parsed.nanos(), ts.nanos());
958 }
959
960 #[test]
961 fn search_attribute_update_accessors() {
962 let set = BOOL_KEY.value_set(true);
963 assert_eq!(set.name(), "my_bool");
964 assert!(!set.is_unset());
965
966 let unset = BOOL_KEY.value_unset();
967 assert_eq!(unset.name(), "my_bool");
968 assert!(unset.is_unset());
969 }
970
971 #[test]
972 fn timestamp_from_prost_types() {
973 let prost_ts = prost_types::Timestamp {
974 seconds: 1_000_000,
975 nanos: 42,
976 };
977 let ts: Timestamp = prost_ts.into();
978 assert_eq!(ts.seconds(), 1_000_000);
979 assert_eq!(ts.nanos(), 42);
980
981 let back: prost_types::Timestamp = ts.into();
982 assert_eq!(back.seconds, 1_000_000);
983 assert_eq!(back.nanos, 42);
984 }
985
986 #[test]
987 fn timestamp_from_system_time() {
988 let st = std::time::UNIX_EPOCH + std::time::Duration::new(1_700_000_000, 123_456_700);
991 let ts: Timestamp = st.into();
992 assert_eq!(ts.seconds(), 1_700_000_000);
993 assert_eq!(ts.nanos(), 123_456_700);
994
995 let back: std::time::SystemTime = ts.try_into().unwrap();
996 assert_eq!(back, st);
997 }
998
999 #[test]
1002 fn timestamp_pre_epoch_normalized() {
1003 let st = std::time::UNIX_EPOCH - std::time::Duration::new(1, 250_000_000);
1005 let ts: Timestamp = st.into();
1006 assert_eq!(ts.seconds(), -2);
1007 assert_eq!(ts.nanos(), 750_000_000);
1008
1009 let back: std::time::SystemTime = ts.try_into().unwrap();
1010 assert_eq!(back, st);
1011 }
1012
1013 #[test]
1014 fn timestamp_pre_epoch_exact_second() {
1015 let st = std::time::UNIX_EPOCH - std::time::Duration::new(5, 0);
1017 let ts: Timestamp = st.into();
1018 assert_eq!(ts.seconds(), -5);
1019 assert_eq!(ts.nanos(), 0);
1020
1021 let back: std::time::SystemTime = ts.try_into().unwrap();
1022 assert_eq!(back, st);
1023 }
1024
1025 #[test]
1026 fn timestamp_pre_epoch_rfc3339_round_trip() {
1027 let ts = Timestamp::new(-2, 750_000_000);
1028 let payload = ts
1029 .to_search_attribute_payload(IndexedValueType::Datetime)
1030 .unwrap();
1031 let decoded = Timestamp::from_search_attribute_payload(&payload).unwrap();
1032 assert_eq!(decoded.seconds(), ts.seconds());
1033 assert_eq!(decoded.nanos(), ts.nanos());
1034 }
1035
1036 #[test]
1037 #[should_panic(expected = "search attribute serialization failed")]
1038 fn value_set_panics_on_nan() {
1039 FLOAT_KEY.value_set(f64::NAN);
1040 }
1041
1042 #[test]
1043 #[should_panic(expected = "search attribute serialization failed")]
1044 fn value_set_panics_on_infinity() {
1045 FLOAT_KEY.value_set(f64::INFINITY);
1046 }
1047
1048 #[test]
1049 fn try_value_set_returns_error_on_nan() {
1050 let result = FLOAT_KEY.try_value_set(f64::NAN);
1051 assert!(result.is_err());
1052 }
1053
1054 #[test]
1055 fn try_value_set_returns_error_on_infinity() {
1056 let result = FLOAT_KEY.try_value_set(f64::INFINITY);
1057 assert!(result.is_err());
1058 }
1059
1060 #[test]
1061 fn round_trip_empty_string() {
1062 let val = String::new();
1063 let payload = val
1064 .to_search_attribute_payload(IndexedValueType::Keyword)
1065 .unwrap();
1066 assert_eq!(String::from_search_attribute_payload(&payload).unwrap(), "");
1067 }
1068
1069 #[test]
1070 fn round_trip_empty_keyword_list() {
1071 let val: Vec<String> = vec![];
1072 let payload = val
1073 .to_search_attribute_payload(IndexedValueType::KeywordList)
1074 .unwrap();
1075 assert_eq!(
1076 Vec::<String>::from_search_attribute_payload(&payload).unwrap(),
1077 Vec::<String>::new()
1078 );
1079 }
1080
1081 #[test]
1082 fn round_trip_large_int_boundaries() {
1083 for val in [i64::MAX, i64::MIN, 0i64] {
1084 let payload = val
1085 .to_search_attribute_payload(IndexedValueType::Int)
1086 .unwrap();
1087 assert_eq!(i64::from_search_attribute_payload(&payload).unwrap(), val);
1088 }
1089 }
1090
1091 #[test]
1092 fn decode_missing_encoding_metadata() {
1093 let payload = Payload {
1094 metadata: HashMap::new(),
1095 data: b"true".to_vec(),
1096 ..Default::default()
1097 };
1098 let result = bool::from_search_attribute_payload(&payload);
1099 assert!(result.is_err());
1100 }
1101
1102 #[test]
1103 fn decode_wrong_encoding_metadata() {
1104 let mut metadata = HashMap::new();
1105 metadata.insert("encoding".to_string(), b"binary/plain".to_vec());
1106 let payload = Payload {
1107 metadata,
1108 data: b"true".to_vec(),
1109 ..Default::default()
1110 };
1111 let result = bool::from_search_attribute_payload(&payload);
1112 assert!(result.is_err());
1113 }
1114
1115 #[test]
1116 fn decode_garbage_json_data() {
1117 let mut metadata = HashMap::new();
1118 metadata.insert("encoding".to_string(), b"json/plain".to_vec());
1119 let payload = Payload {
1120 metadata,
1121 data: b"not-valid-json!!!".to_vec(),
1122 ..Default::default()
1123 };
1124 let result = bool::from_search_attribute_payload(&payload);
1125 assert!(result.is_err());
1126 }
1127
1128 #[test]
1129 fn keys_returns_attribute_names() {
1130 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true), INT_KEY.value_set(42)]);
1131 let mut keys: Vec<&str> = attrs.keys().collect();
1132 keys.sort();
1133 assert_eq!(keys, vec!["my_bool", "my_int"]);
1134 }
1135
1136 #[test]
1137 fn raw_payload_returns_payload() {
1138 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true)]);
1139 let payload = attrs.raw_payload("my_bool").unwrap();
1140 assert!(!payload.data.is_empty());
1141 assert!(attrs.raw_payload("nonexistent").is_none());
1142 }
1143
1144 #[test]
1145 fn into_proto_moves_without_clone() {
1146 let attrs = SearchAttributes::new([INT_KEY.value_set(7)]);
1147 let proto = attrs.into_proto();
1148 assert_eq!(proto.indexed_fields.len(), 1);
1149 }
1150
1151 #[test]
1152 fn search_attribute_key_is_copy() {
1153 let key = BOOL_KEY;
1154 let key2 = key; assert_eq!(key.name(), key2.name());
1156 }
1157
1158 #[test]
1159 fn timestamp_new_clamps_negative_nanos() {
1160 let ts = Timestamp::new(100, -42);
1161 assert_eq!(ts.seconds(), 100);
1162 assert_eq!(ts.nanos(), 0); }
1164
1165 #[test]
1166 fn timestamp_new_clamps_excessive_nanos() {
1167 let ts = Timestamp::new(100, 2_000_000_000);
1168 assert_eq!(ts.seconds(), 100);
1169 assert_eq!(ts.nanos(), 999_999_999); }
1171
1172 #[test]
1173 fn timestamp_to_prost_round_trips() {
1174 let ts = Timestamp::new(1_700_000_000, 123_456_789);
1175 let prost_ts = ts.to_prost();
1176 assert_eq!(prost_ts.seconds, 1_700_000_000);
1177 assert_eq!(prost_ts.nanos, 123_456_789);
1178 let back: Timestamp = prost_ts.into();
1179 assert_eq!(back, ts);
1180 }
1181
1182 #[test]
1183 fn apply_inserts_and_removes() {
1184 let mut attrs = SearchAttributes::new([INT_KEY.value_set(42)]);
1185 assert_eq!(attrs.get(&INT_KEY), Some(42));
1186
1187 attrs.apply(INT_KEY.value_set(99));
1189 assert_eq!(attrs.get(&INT_KEY), Some(99));
1190
1191 attrs.apply(INT_KEY.value_unset());
1193 assert_eq!(attrs.get(&INT_KEY), None);
1194 assert!(attrs.is_empty());
1195 }
1196
1197 #[test]
1198 fn from_owned_proto_moves_without_clone() {
1199 let proto = ProtoSearchAttributes {
1200 indexed_fields: {
1201 let mut m = HashMap::new();
1202 m.insert("k".to_string(), INT_KEY.value_set(7).payload.unwrap());
1203 m
1204 },
1205 };
1206 let attrs: SearchAttributes = proto.into();
1207 assert_eq!(attrs.get(&SearchAttributeKey::int("k")), Some(7));
1208 }
1209
1210 #[test]
1211 fn search_attributes_equality() {
1212 let a = SearchAttributes::new([BOOL_KEY.value_set(true), INT_KEY.value_set(42)]);
1213 let b = SearchAttributes::new([BOOL_KEY.value_set(true), INT_KEY.value_set(42)]);
1214 let c = SearchAttributes::new([BOOL_KEY.value_set(false), INT_KEY.value_set(42)]);
1215 assert_eq!(a, b);
1216 assert_ne!(a, c);
1217 }
1218
1219 #[test]
1220 fn from_proto_trait_matches_from_proto_method() {
1221 let updates = [INT_KEY.value_set(99), BOOL_KEY.value_set(true)];
1222 let proto = SearchAttributes::new(updates).to_proto();
1223 let via_method = SearchAttributes::from_proto(&proto);
1224 let via_trait: SearchAttributes = proto.into();
1225 assert_eq!(via_method, via_trait);
1226 }
1227}