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::new(&SerializationContextData::None, &converter);
252 let mut payload = converter.to_payload(&context, value)?;
253 payload.metadata.insert(
254 TYPE_METADATA_KEY.to_string(),
255 type_metadata_str(indexed_value_type).as_bytes().to_vec(),
256 );
257 Ok(payload)
258}
259
260fn decode_json_search_attr<T: serde::de::DeserializeOwned + 'static>(
264 payload: &Payload,
265) -> Result<T, SearchAttributeError> {
266 let converter = PayloadConverter::serde_json();
267 let context = SerializationContext::new(&SerializationContextData::None, &converter);
268 Ok(converter.from_payload(&context, payload.clone())?)
269}
270
271macro_rules! impl_simple_search_attribute_value {
278 ($ty:ty, $ivt:expr) => {
279 impl SearchAttributeValue for $ty {
280 fn to_search_attribute_payload(
281 &self,
282 indexed_value_type: IndexedValueType,
283 ) -> Result<Payload, SearchAttributeError> {
284 encode_json_search_attr(self, indexed_value_type)
285 }
286
287 fn from_search_attribute_payload(
288 payload: &Payload,
289 ) -> Result<Self, SearchAttributeError> {
290 decode_json_search_attr(payload)
291 }
292
293 fn default_indexed_value_type() -> IndexedValueType {
294 $ivt
295 }
296 }
297 };
298}
299
300impl_simple_search_attribute_value!(bool, IndexedValueType::Bool);
301impl_simple_search_attribute_value!(i64, IndexedValueType::Int);
302impl_simple_search_attribute_value!(String, IndexedValueType::Keyword);
303impl_simple_search_attribute_value!(Vec<String>, IndexedValueType::KeywordList);
304
305impl SearchAttributeValue for f64 {
308 fn to_search_attribute_payload(
309 &self,
310 indexed_value_type: IndexedValueType,
311 ) -> Result<Payload, SearchAttributeError> {
312 if !self.is_finite() {
313 return Err(SearchAttributeError::InvalidPayload {
314 reason: format!("f64 search attribute value must be finite, got {}", self),
315 });
316 }
317 encode_json_search_attr(self, indexed_value_type)
318 }
319
320 fn from_search_attribute_payload(payload: &Payload) -> Result<Self, SearchAttributeError> {
321 decode_json_search_attr(payload)
322 }
323
324 fn default_indexed_value_type() -> IndexedValueType {
325 IndexedValueType::Double
326 }
327}
328
329fn timestamp_to_rfc3339(ts: &Timestamp) -> Result<String, SearchAttributeError> {
335 use chrono::{DateTime, Utc};
336
337 let nanos = u32::try_from(ts.nanos()).unwrap_or(0);
338 let dt = DateTime::<Utc>::from_timestamp(ts.seconds(), nanos).ok_or_else(|| {
339 SearchAttributeError::InvalidTimestamp(format!(
340 "cannot represent seconds={} nanos={} as DateTime",
341 ts.seconds(),
342 ts.nanos()
343 ))
344 })?;
345 Ok(dt.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true))
346}
347
348fn rfc3339_to_timestamp(s: &str) -> Result<Timestamp, SearchAttributeError> {
350 use chrono::DateTime;
351
352 let s = s.trim_matches('"');
355 let dt = DateTime::parse_from_rfc3339(s).map_err(|e| {
356 SearchAttributeError::InvalidTimestamp(format!("failed to parse RFC3339 '{}': {}", s, e))
357 })?;
358 Ok(Timestamp::new(
359 dt.timestamp(),
360 dt.timestamp_subsec_nanos() as i32,
361 ))
362}
363
364impl SearchAttributeValue for Timestamp {
365 fn to_search_attribute_payload(
366 &self,
367 indexed_value_type: IndexedValueType,
368 ) -> Result<Payload, SearchAttributeError> {
369 let rfc3339 = timestamp_to_rfc3339(self)?;
370 encode_json_search_attr(&rfc3339, indexed_value_type)
371 }
372
373 fn from_search_attribute_payload(payload: &Payload) -> Result<Self, SearchAttributeError> {
374 let s: String = decode_json_search_attr(payload)?;
375 rfc3339_to_timestamp(&s)
376 }
377
378 fn default_indexed_value_type() -> IndexedValueType {
379 IndexedValueType::Datetime
380 }
381}
382
383#[derive(Debug, Clone, Copy)]
404pub struct SearchAttributeKey<T: SearchAttributeValue> {
405 name: &'static str,
406 indexed_value_type: IndexedValueType,
407 _marker: PhantomData<T>,
408}
409
410impl<T: SearchAttributeValue> SearchAttributeKey<T> {
411 pub fn name(&self) -> &'static str {
413 self.name
414 }
415
416 pub fn indexed_value_type(&self) -> IndexedValueType {
418 self.indexed_value_type
419 }
420
421 pub fn value_set(&self, val: T) -> SearchAttributeUpdate {
430 self.try_value_set(val)
431 .expect("search attribute serialization failed (use try_value_set for non-finite f64 or out-of-range timestamps)")
432 }
433
434 pub fn try_value_set(&self, val: T) -> Result<SearchAttributeUpdate, SearchAttributeError> {
437 let payload = val.to_search_attribute_payload(self.indexed_value_type)?;
438 Ok(SearchAttributeUpdate {
439 name: self.name.to_string(),
440 payload: Some(payload),
441 })
442 }
443
444 pub fn value_unset(&self) -> SearchAttributeUpdate {
446 SearchAttributeUpdate {
447 name: self.name.to_string(),
448 payload: None,
449 }
450 }
451}
452
453impl SearchAttributeKey<bool> {
454 pub const fn bool(name: &'static str) -> Self {
456 Self {
457 name,
458 indexed_value_type: IndexedValueType::Bool,
459 _marker: PhantomData,
460 }
461 }
462}
463
464impl SearchAttributeKey<i64> {
465 pub const fn int(name: &'static str) -> Self {
467 Self {
468 name,
469 indexed_value_type: IndexedValueType::Int,
470 _marker: PhantomData,
471 }
472 }
473}
474
475impl SearchAttributeKey<f64> {
476 pub const fn float(name: &'static str) -> Self {
478 Self {
479 name,
480 indexed_value_type: IndexedValueType::Double,
481 _marker: PhantomData,
482 }
483 }
484}
485
486impl SearchAttributeKey<String> {
487 pub const fn keyword(name: &'static str) -> Self {
489 Self {
490 name,
491 indexed_value_type: IndexedValueType::Keyword,
492 _marker: PhantomData,
493 }
494 }
495
496 pub const fn text(name: &'static str) -> Self {
498 Self {
499 name,
500 indexed_value_type: IndexedValueType::Text,
501 _marker: PhantomData,
502 }
503 }
504}
505
506impl SearchAttributeKey<Timestamp> {
507 pub const fn datetime(name: &'static str) -> Self {
509 Self {
510 name,
511 indexed_value_type: IndexedValueType::Datetime,
512 _marker: PhantomData,
513 }
514 }
515}
516
517impl SearchAttributeKey<Vec<String>> {
518 pub const fn keyword_list(name: &'static str) -> Self {
520 Self {
521 name,
522 indexed_value_type: IndexedValueType::KeywordList,
523 _marker: PhantomData,
524 }
525 }
526}
527
528#[derive(Debug, Clone)]
543pub struct SearchAttributeUpdate {
544 pub(crate) name: String,
545 pub(crate) payload: Option<Payload>,
546}
547
548impl SearchAttributeUpdate {
549 pub fn name(&self) -> &str {
551 &self.name
552 }
553
554 pub fn is_unset(&self) -> bool {
556 self.payload.is_none()
557 }
558}
559
560#[derive(Debug, Clone, Default, PartialEq)]
567pub struct SearchAttributes {
568 fields: HashMap<String, Payload>,
569}
570
571impl SearchAttributes {
572 pub fn new(updates: impl IntoIterator<Item = SearchAttributeUpdate>) -> Self {
576 let mut fields = HashMap::new();
577 for update in updates {
578 match update.payload {
579 Some(payload) => {
580 fields.insert(update.name, payload);
581 }
582 None => {
583 fields.remove(&update.name);
584 }
585 }
586 }
587 Self { fields }
588 }
589
590 pub fn apply(&mut self, update: SearchAttributeUpdate) {
594 match update.payload {
595 Some(payload) => {
596 self.fields.insert(update.name, payload);
597 }
598 None => {
599 self.fields.remove(&update.name);
600 }
601 }
602 }
603
604 pub fn get<T: SearchAttributeValue>(&self, key: &SearchAttributeKey<T>) -> Option<T> {
607 let payload = self.fields.get(key.name())?;
608 match T::from_search_attribute_payload(payload) {
609 Ok(val) => Some(val),
610 Err(e) => {
611 warn!(
612 key = key.name(),
613 error = %e,
614 "Failed to deserialize search attribute; returning None. \
615 Use try_get() for explicit error handling."
616 );
617 None
618 }
619 }
620 }
621
622 pub fn try_get<T: SearchAttributeValue>(
626 &self,
627 key: &SearchAttributeKey<T>,
628 ) -> Result<Option<T>, SearchAttributeError> {
629 match self.fields.get(key.name()) {
630 None => Ok(None),
631 Some(payload) => T::from_search_attribute_payload(payload).map(Some),
632 }
633 }
634
635 pub fn contains_key<T: SearchAttributeValue>(&self, key: &SearchAttributeKey<T>) -> bool {
637 self.fields.contains_key(key.name())
638 }
639
640 pub fn is_empty(&self) -> bool {
642 self.fields.is_empty()
643 }
644
645 pub fn len(&self) -> usize {
647 self.fields.len()
648 }
649
650 pub fn keys(&self) -> impl Iterator<Item = &str> {
652 self.fields.keys().map(|s| s.as_str())
653 }
654
655 pub fn raw_payload(&self, name: &str) -> Option<&Payload> {
659 self.fields.get(name)
660 }
661
662 pub fn to_proto(&self) -> ProtoSearchAttributes {
664 ProtoSearchAttributes {
665 indexed_fields: self.fields.clone(),
666 }
667 }
668
669 pub fn into_proto(self) -> ProtoSearchAttributes {
672 ProtoSearchAttributes {
673 indexed_fields: self.fields,
674 }
675 }
676
677 pub fn from_proto(attrs: &ProtoSearchAttributes) -> Self {
679 Self {
680 fields: attrs.indexed_fields.clone(),
681 }
682 }
683}
684
685impl From<ProtoSearchAttributes> for SearchAttributes {
686 fn from(attrs: ProtoSearchAttributes) -> Self {
688 Self {
689 fields: attrs.indexed_fields,
690 }
691 }
692}
693
694impl SearchAttributes {
695 pub fn updates_to_proto(
699 updates: impl IntoIterator<Item = SearchAttributeUpdate>,
700 ) -> ProtoSearchAttributes {
701 let mut indexed_fields = HashMap::new();
702 for update in updates {
703 let payload = update.payload.unwrap_or_default();
704 indexed_fields.insert(update.name, payload);
705 }
706 ProtoSearchAttributes { indexed_fields }
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 const BOOL_KEY: SearchAttributeKey<bool> = SearchAttributeKey::bool("my_bool");
715 const INT_KEY: SearchAttributeKey<i64> = SearchAttributeKey::int("my_int");
716 const FLOAT_KEY: SearchAttributeKey<f64> = SearchAttributeKey::float("my_float");
717 const KW_KEY: SearchAttributeKey<String> = SearchAttributeKey::keyword("my_keyword");
718 const TEXT_KEY: SearchAttributeKey<String> = SearchAttributeKey::text("my_text");
719 const DT_KEY: SearchAttributeKey<Timestamp> = SearchAttributeKey::datetime("my_datetime");
720 const KWL_KEY: SearchAttributeKey<Vec<String>> =
721 SearchAttributeKey::keyword_list("my_keyword_list");
722
723 fn assert_payload_metadata(payload: &Payload, expected_type: &str) {
724 assert_eq!(
725 payload.metadata.get("encoding").unwrap(),
726 b"json/plain".as_slice()
727 );
728 assert_eq!(
729 payload.metadata.get(TYPE_METADATA_KEY).unwrap(),
730 expected_type.as_bytes()
731 );
732 }
733
734 #[test]
735 fn round_trip_bool() {
736 let val = true;
737 let payload = val
738 .to_search_attribute_payload(IndexedValueType::Bool)
739 .unwrap();
740 assert_payload_metadata(&payload, "Bool");
741 assert!(bool::from_search_attribute_payload(&payload).unwrap());
742 }
743
744 #[test]
745 fn round_trip_int() {
746 let val: i64 = -42;
747 let payload = val
748 .to_search_attribute_payload(IndexedValueType::Int)
749 .unwrap();
750 assert_payload_metadata(&payload, "Int");
751 assert_eq!(i64::from_search_attribute_payload(&payload).unwrap(), -42);
752 }
753
754 #[test]
755 fn round_trip_double() {
756 let val: f64 = 1.23;
757 let payload = val
758 .to_search_attribute_payload(IndexedValueType::Double)
759 .unwrap();
760 assert_payload_metadata(&payload, "Double");
761 let decoded = f64::from_search_attribute_payload(&payload).unwrap();
762 assert!((decoded - 1.23).abs() < f64::EPSILON);
763 }
764
765 #[test]
766 fn round_trip_keyword() {
767 let val = "hello".to_string();
768 let payload = val
769 .to_search_attribute_payload(IndexedValueType::Keyword)
770 .unwrap();
771 assert_payload_metadata(&payload, "Keyword");
772 assert_eq!(
773 String::from_search_attribute_payload(&payload).unwrap(),
774 "hello"
775 );
776 }
777
778 #[test]
779 fn round_trip_text() {
780 let val = "some long text".to_string();
781 let payload = val
782 .to_search_attribute_payload(IndexedValueType::Text)
783 .unwrap();
784 assert_payload_metadata(&payload, "Text");
785 assert_eq!(
786 String::from_search_attribute_payload(&payload).unwrap(),
787 "some long text"
788 );
789 }
790
791 #[test]
792 fn round_trip_datetime() {
793 let ts = Timestamp::new(1_700_000_000, 123_456_789);
794 let payload = ts
795 .to_search_attribute_payload(IndexedValueType::Datetime)
796 .unwrap();
797 assert_payload_metadata(&payload, "Datetime");
798
799 let json_str: String = serde_json::from_slice(&payload.data).unwrap();
800 assert!(json_str.ends_with('Z'));
801 assert!(json_str.contains('T'));
802
803 let decoded = Timestamp::from_search_attribute_payload(&payload).unwrap();
804 assert_eq!(decoded.seconds(), ts.seconds());
805 assert_eq!(decoded.nanos(), ts.nanos());
806
807 let attrs = SearchAttributes::new([DT_KEY.value_set(ts.clone())]);
808 let got = attrs.get(&DT_KEY).unwrap();
809 assert_eq!(got.seconds(), ts.seconds());
810 assert_eq!(got.nanos(), ts.nanos());
811 }
812
813 #[test]
814 fn round_trip_datetime_no_nanos() {
815 let ts = Timestamp::new(0, 0);
816 let payload = ts
817 .to_search_attribute_payload(IndexedValueType::Datetime)
818 .unwrap();
819 let decoded = Timestamp::from_search_attribute_payload(&payload).unwrap();
820 assert_eq!(decoded.seconds(), 0);
821 assert_eq!(decoded.nanos(), 0);
822 }
823
824 #[test]
825 fn round_trip_keyword_list() {
826 let val = vec!["a".to_string(), "b".to_string(), "c".to_string()];
827 let payload = val
828 .to_search_attribute_payload(IndexedValueType::KeywordList)
829 .unwrap();
830 assert_payload_metadata(&payload, "KeywordList");
831 assert_eq!(
832 Vec::<String>::from_search_attribute_payload(&payload).unwrap(),
833 vec!["a", "b", "c"]
834 );
835 }
836
837 #[test]
838 fn typed_search_attributes_new_and_get() {
839 let attrs = SearchAttributes::new([
840 BOOL_KEY.value_set(true),
841 INT_KEY.value_set(99),
842 FLOAT_KEY.value_set(2.72),
843 KW_KEY.value_set("kw_val".into()),
844 TEXT_KEY.value_set("text_val".into()),
845 KWL_KEY.value_set(vec!["x".into(), "y".into()]),
846 ]);
847
848 assert_eq!(attrs.len(), 6);
849 assert!(!attrs.is_empty());
850 assert_eq!(attrs.get(&BOOL_KEY), Some(true));
851 assert_eq!(attrs.get(&INT_KEY), Some(99));
852 assert!((attrs.get(&FLOAT_KEY).unwrap() - 2.72).abs() < f64::EPSILON);
853 assert_eq!(attrs.get(&KW_KEY), Some("kw_val".into()));
854 assert_eq!(attrs.get(&TEXT_KEY), Some("text_val".into()));
855 assert_eq!(
856 attrs.get(&KWL_KEY),
857 Some(vec!["x".to_string(), "y".to_string()])
858 );
859 }
860
861 #[test]
862 fn to_proto_from_proto_round_trip() {
863 let attrs = SearchAttributes::new([BOOL_KEY.value_set(false), INT_KEY.value_set(7)]);
864
865 let proto = attrs.to_proto();
866 assert_eq!(proto.indexed_fields.len(), 2);
867
868 let restored = SearchAttributes::from_proto(&proto);
869 assert_eq!(restored.get(&BOOL_KEY), Some(false));
870 assert_eq!(restored.get(&INT_KEY), Some(7));
871 }
872
873 #[test]
874 fn value_unset_removes_entry() {
875 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true), BOOL_KEY.value_unset()]);
876 assert!(attrs.is_empty());
877 assert_eq!(attrs.get(&BOOL_KEY), None);
878 }
879
880 #[test]
881 fn keyword_vs_text_disambiguation() {
882 let kw_update = KW_KEY.value_set("same_value".into());
883 let text_update = TEXT_KEY.value_set("same_value".into());
884
885 let kw_payload = kw_update.payload.as_ref().unwrap();
886 let text_payload = text_update.payload.as_ref().unwrap();
887
888 assert_eq!(
889 kw_payload.metadata.get(TYPE_METADATA_KEY).unwrap(),
890 b"Keyword"
891 );
892 assert_eq!(
893 text_payload.metadata.get(TYPE_METADATA_KEY).unwrap(),
894 b"Text"
895 );
896
897 assert_eq!(KW_KEY.indexed_value_type(), IndexedValueType::Keyword);
898 assert_eq!(TEXT_KEY.indexed_value_type(), IndexedValueType::Text);
899 }
900
901 #[test]
902 fn get_returns_none_for_missing_key() {
903 let attrs = SearchAttributes::default();
904 assert_eq!(attrs.get(&BOOL_KEY), None);
905 assert!(!attrs.contains_key(&INT_KEY));
906 }
907
908 #[test]
909 fn get_returns_none_for_type_mismatch() {
910 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true)]);
911 let mismatched_key = SearchAttributeKey::<i64>::int("my_bool");
913 assert_eq!(attrs.get(&mismatched_key), None);
914 }
915
916 #[test]
917 fn updates_to_proto_includes_empty_payload_for_unset() {
918 let proto =
919 SearchAttributes::updates_to_proto([BOOL_KEY.value_set(true), INT_KEY.value_unset()]);
920
921 let bool_payload = proto.indexed_fields.get("my_bool").unwrap();
922 assert!(!bool_payload.data.is_empty());
923
924 let int_payload = proto.indexed_fields.get("my_int").unwrap();
925 assert!(int_payload.data.is_empty());
926 assert!(int_payload.metadata.is_empty());
927 }
928
929 #[test]
930 fn contains_key_returns_true_when_present() {
931 let attrs = SearchAttributes::new([INT_KEY.value_set(42)]);
932 assert!(attrs.contains_key(&INT_KEY));
933 }
934
935 #[test]
936 fn timestamp_rfc3339_format() {
937 let ts = Timestamp::new(1_700_000_000, 0);
938 let rfc = timestamp_to_rfc3339(&ts).unwrap();
939 assert_eq!(rfc, "2023-11-14T22:13:20.000000000Z");
941 }
942
943 #[test]
944 fn timestamp_rfc3339_with_nanos() {
945 let ts = Timestamp::new(1_700_000_000, 500_000_000);
946 let rfc = timestamp_to_rfc3339(&ts).unwrap();
947 assert_eq!(rfc, "2023-11-14T22:13:20.500000000Z");
948
949 let parsed = rfc3339_to_timestamp(&rfc).unwrap();
950 assert_eq!(parsed.seconds(), ts.seconds());
951 assert_eq!(parsed.nanos(), ts.nanos());
952 }
953
954 #[test]
955 fn search_attribute_update_accessors() {
956 let set = BOOL_KEY.value_set(true);
957 assert_eq!(set.name(), "my_bool");
958 assert!(!set.is_unset());
959
960 let unset = BOOL_KEY.value_unset();
961 assert_eq!(unset.name(), "my_bool");
962 assert!(unset.is_unset());
963 }
964
965 #[test]
966 fn timestamp_from_prost_types() {
967 let prost_ts = prost_types::Timestamp {
968 seconds: 1_000_000,
969 nanos: 42,
970 };
971 let ts: Timestamp = prost_ts.into();
972 assert_eq!(ts.seconds(), 1_000_000);
973 assert_eq!(ts.nanos(), 42);
974
975 let back: prost_types::Timestamp = ts.into();
976 assert_eq!(back.seconds, 1_000_000);
977 assert_eq!(back.nanos, 42);
978 }
979
980 #[test]
981 fn timestamp_from_system_time() {
982 let st = std::time::UNIX_EPOCH + std::time::Duration::new(1_700_000_000, 123_456_700);
985 let ts: Timestamp = st.into();
986 assert_eq!(ts.seconds(), 1_700_000_000);
987 assert_eq!(ts.nanos(), 123_456_700);
988
989 let back: std::time::SystemTime = ts.try_into().unwrap();
990 assert_eq!(back, st);
991 }
992
993 #[test]
996 fn timestamp_pre_epoch_normalized() {
997 let st = std::time::UNIX_EPOCH - std::time::Duration::new(1, 250_000_000);
999 let ts: Timestamp = st.into();
1000 assert_eq!(ts.seconds(), -2);
1001 assert_eq!(ts.nanos(), 750_000_000);
1002
1003 let back: std::time::SystemTime = ts.try_into().unwrap();
1004 assert_eq!(back, st);
1005 }
1006
1007 #[test]
1008 fn timestamp_pre_epoch_exact_second() {
1009 let st = std::time::UNIX_EPOCH - std::time::Duration::new(5, 0);
1011 let ts: Timestamp = st.into();
1012 assert_eq!(ts.seconds(), -5);
1013 assert_eq!(ts.nanos(), 0);
1014
1015 let back: std::time::SystemTime = ts.try_into().unwrap();
1016 assert_eq!(back, st);
1017 }
1018
1019 #[test]
1020 fn timestamp_pre_epoch_rfc3339_round_trip() {
1021 let ts = Timestamp::new(-2, 750_000_000);
1022 let payload = ts
1023 .to_search_attribute_payload(IndexedValueType::Datetime)
1024 .unwrap();
1025 let decoded = Timestamp::from_search_attribute_payload(&payload).unwrap();
1026 assert_eq!(decoded.seconds(), ts.seconds());
1027 assert_eq!(decoded.nanos(), ts.nanos());
1028 }
1029
1030 #[test]
1031 #[should_panic(expected = "search attribute serialization failed")]
1032 fn value_set_panics_on_nan() {
1033 FLOAT_KEY.value_set(f64::NAN);
1034 }
1035
1036 #[test]
1037 #[should_panic(expected = "search attribute serialization failed")]
1038 fn value_set_panics_on_infinity() {
1039 FLOAT_KEY.value_set(f64::INFINITY);
1040 }
1041
1042 #[test]
1043 fn try_value_set_returns_error_on_nan() {
1044 let result = FLOAT_KEY.try_value_set(f64::NAN);
1045 assert!(result.is_err());
1046 }
1047
1048 #[test]
1049 fn try_value_set_returns_error_on_infinity() {
1050 let result = FLOAT_KEY.try_value_set(f64::INFINITY);
1051 assert!(result.is_err());
1052 }
1053
1054 #[test]
1055 fn round_trip_empty_string() {
1056 let val = String::new();
1057 let payload = val
1058 .to_search_attribute_payload(IndexedValueType::Keyword)
1059 .unwrap();
1060 assert_eq!(String::from_search_attribute_payload(&payload).unwrap(), "");
1061 }
1062
1063 #[test]
1064 fn round_trip_empty_keyword_list() {
1065 let val: Vec<String> = vec![];
1066 let payload = val
1067 .to_search_attribute_payload(IndexedValueType::KeywordList)
1068 .unwrap();
1069 assert_eq!(
1070 Vec::<String>::from_search_attribute_payload(&payload).unwrap(),
1071 Vec::<String>::new()
1072 );
1073 }
1074
1075 #[test]
1076 fn round_trip_large_int_boundaries() {
1077 for val in [i64::MAX, i64::MIN, 0i64] {
1078 let payload = val
1079 .to_search_attribute_payload(IndexedValueType::Int)
1080 .unwrap();
1081 assert_eq!(i64::from_search_attribute_payload(&payload).unwrap(), val);
1082 }
1083 }
1084
1085 #[test]
1086 fn decode_missing_encoding_metadata() {
1087 let payload = Payload {
1088 metadata: HashMap::new(),
1089 data: b"true".to_vec(),
1090 ..Default::default()
1091 };
1092 let result = bool::from_search_attribute_payload(&payload);
1093 assert!(result.is_err());
1094 }
1095
1096 #[test]
1097 fn decode_wrong_encoding_metadata() {
1098 let mut metadata = HashMap::new();
1099 metadata.insert("encoding".to_string(), b"binary/plain".to_vec());
1100 let payload = Payload {
1101 metadata,
1102 data: b"true".to_vec(),
1103 ..Default::default()
1104 };
1105 let result = bool::from_search_attribute_payload(&payload);
1106 assert!(result.is_err());
1107 }
1108
1109 #[test]
1110 fn decode_garbage_json_data() {
1111 let mut metadata = HashMap::new();
1112 metadata.insert("encoding".to_string(), b"json/plain".to_vec());
1113 let payload = Payload {
1114 metadata,
1115 data: b"not-valid-json!!!".to_vec(),
1116 ..Default::default()
1117 };
1118 let result = bool::from_search_attribute_payload(&payload);
1119 assert!(result.is_err());
1120 }
1121
1122 #[test]
1123 fn keys_returns_attribute_names() {
1124 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true), INT_KEY.value_set(42)]);
1125 let mut keys: Vec<&str> = attrs.keys().collect();
1126 keys.sort();
1127 assert_eq!(keys, vec!["my_bool", "my_int"]);
1128 }
1129
1130 #[test]
1131 fn raw_payload_returns_payload() {
1132 let attrs = SearchAttributes::new([BOOL_KEY.value_set(true)]);
1133 let payload = attrs.raw_payload("my_bool").unwrap();
1134 assert!(!payload.data.is_empty());
1135 assert!(attrs.raw_payload("nonexistent").is_none());
1136 }
1137
1138 #[test]
1139 fn into_proto_moves_without_clone() {
1140 let attrs = SearchAttributes::new([INT_KEY.value_set(7)]);
1141 let proto = attrs.into_proto();
1142 assert_eq!(proto.indexed_fields.len(), 1);
1143 }
1144
1145 #[test]
1146 fn search_attribute_key_is_copy() {
1147 let key = BOOL_KEY;
1148 let key2 = key; assert_eq!(key.name(), key2.name());
1150 }
1151
1152 #[test]
1153 fn timestamp_new_clamps_negative_nanos() {
1154 let ts = Timestamp::new(100, -42);
1155 assert_eq!(ts.seconds(), 100);
1156 assert_eq!(ts.nanos(), 0); }
1158
1159 #[test]
1160 fn timestamp_new_clamps_excessive_nanos() {
1161 let ts = Timestamp::new(100, 2_000_000_000);
1162 assert_eq!(ts.seconds(), 100);
1163 assert_eq!(ts.nanos(), 999_999_999); }
1165
1166 #[test]
1167 fn timestamp_to_prost_round_trips() {
1168 let ts = Timestamp::new(1_700_000_000, 123_456_789);
1169 let prost_ts = ts.to_prost();
1170 assert_eq!(prost_ts.seconds, 1_700_000_000);
1171 assert_eq!(prost_ts.nanos, 123_456_789);
1172 let back: Timestamp = prost_ts.into();
1173 assert_eq!(back, ts);
1174 }
1175
1176 #[test]
1177 fn apply_inserts_and_removes() {
1178 let mut attrs = SearchAttributes::new([INT_KEY.value_set(42)]);
1179 assert_eq!(attrs.get(&INT_KEY), Some(42));
1180
1181 attrs.apply(INT_KEY.value_set(99));
1183 assert_eq!(attrs.get(&INT_KEY), Some(99));
1184
1185 attrs.apply(INT_KEY.value_unset());
1187 assert_eq!(attrs.get(&INT_KEY), None);
1188 assert!(attrs.is_empty());
1189 }
1190
1191 #[test]
1192 fn from_owned_proto_moves_without_clone() {
1193 let proto = ProtoSearchAttributes {
1194 indexed_fields: {
1195 let mut m = HashMap::new();
1196 m.insert("k".to_string(), INT_KEY.value_set(7).payload.unwrap());
1197 m
1198 },
1199 };
1200 let attrs: SearchAttributes = proto.into();
1201 assert_eq!(attrs.get(&SearchAttributeKey::int("k")), Some(7));
1202 }
1203
1204 #[test]
1205 fn search_attributes_equality() {
1206 let a = SearchAttributes::new([BOOL_KEY.value_set(true), INT_KEY.value_set(42)]);
1207 let b = SearchAttributes::new([BOOL_KEY.value_set(true), INT_KEY.value_set(42)]);
1208 let c = SearchAttributes::new([BOOL_KEY.value_set(false), INT_KEY.value_set(42)]);
1209 assert_eq!(a, b);
1210 assert_ne!(a, c);
1211 }
1212
1213 #[test]
1214 fn from_proto_trait_matches_from_proto_method() {
1215 let updates = [INT_KEY.value_set(99), BOOL_KEY.value_set(true)];
1216 let proto = SearchAttributes::new(updates).to_proto();
1217 let via_method = SearchAttributes::from_proto(&proto);
1218 let via_trait: SearchAttributes = proto.into();
1219 assert_eq!(via_method, via_trait);
1220 }
1221}