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