1use std::cmp::Ordering;
46
47use base64::Engine as _;
48use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
49use serde::{Deserialize, Deserializer, Serialize, Serializer};
50use serde_json::Value;
51
52use crate::decimal::Decimal;
53
54const TAG: &str = "$ant";
56const VAL: &str = "v";
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum PropertyValue {
62 Null,
65 Bool(bool),
67 Long(i64),
69 Float(f64),
72 Text(String),
74 Json(Value),
76
77 Int32(i32),
81 Int16(i16),
83 Decimal(Decimal),
85 Date(NaiveDate),
87 Time(NaiveTime),
89 Timestamp(DateTime<FixedOffset>),
92 Uuid(uuid::Uuid),
94 Bytes(Vec<u8>),
96 Array(Vec<PropertyValue>),
99}
100
101impl PropertyValue {
102 pub fn as_str(&self) -> Option<&str> {
104 if let Self::Text(s) = self {
105 Some(s.as_str())
106 } else {
107 None
108 }
109 }
110
111 pub fn type_name(&self) -> &'static str {
113 match self {
114 Self::Null => "null",
115 Self::Bool(_) => "bool",
116 Self::Long(_) => "long",
117 Self::Float(_) => "float",
118 Self::Text(_) => "text",
119 Self::Json(_) => "json",
120 Self::Int32(_) => "int32",
121 Self::Int16(_) => "int16",
122 Self::Decimal(_) => "decimal",
123 Self::Date(_) => "date",
124 Self::Time(_) => "time",
125 Self::Timestamp(_) => "timestamp",
126 Self::Uuid(_) => "uuid",
127 Self::Bytes(_) => "bytes",
128 Self::Array(_) => "array",
129 }
130 }
131
132 fn b64() -> base64::engine::general_purpose::GeneralPurpose {
134 base64::engine::general_purpose::STANDARD
135 }
136
137 fn payload(&self) -> Option<Value> {
140 Some(match self {
141 Self::Int32(i) => Value::from(*i),
142 Self::Int16(i) => Value::from(*i),
143 Self::Decimal(d) => Value::String(d.to_string()),
144 Self::Date(d) => Value::String(d.format("%Y-%m-%d").to_string()),
145 Self::Time(t) => Value::String(t.format("%H:%M:%S%.6f").to_string()),
148 Self::Timestamp(ts) => Value::String(ts.to_rfc3339()),
149 Self::Uuid(u) => Value::String(u.to_string()),
150 Self::Bytes(b) => Value::String(Self::b64().encode(b)),
151 Self::Array(items) => Value::Array(items.iter().map(Self::to_json).collect()),
152 _ => return None,
153 })
154 }
155
156 pub fn to_json(&self) -> Value {
160 match self {
161 Self::Null => Value::Null,
162 Self::Bool(b) => Value::Bool(*b),
163 Self::Long(i) => Value::from(*i),
164 Self::Float(f) => serde_json::Number::from_f64(*f)
165 .map(Value::Number)
166 .unwrap_or(Value::Null),
167 Self::Text(s) => Value::String(s.clone()),
168 Self::Json(v) => v.clone(),
169 other => {
170 let mut o = serde_json::Map::with_capacity(2);
171 o.insert(TAG.into(), Value::String(other.type_name().into()));
172 o.insert(VAL.into(), other.payload().expect("parity variant"));
173 Value::Object(o)
174 }
175 }
176 }
177
178 pub fn to_compat_json(&self) -> Value {
188 match self {
189 Self::Int32(i) => Value::from(*i),
192 Self::Int16(i) => Value::from(*i),
193 Self::Decimal(d) => Value::String(d.to_string()),
197 Self::Date(_) | Self::Time(_) | Self::Timestamp(_) | Self::Uuid(_) | Self::Bytes(_) => {
198 self.payload().expect("string-payload variant")
199 }
200 Self::Array(items) => Value::Array(items.iter().map(Self::to_compat_json).collect()),
201 legacy => legacy.to_json(),
202 }
203 }
204
205 pub fn from_json(v: Value) -> Self {
210 match v {
211 Value::Null => Self::Null,
212 Value::Bool(b) => Self::Bool(b),
213 Value::Number(n) => n
214 .as_i64()
215 .map(Self::Long)
216 .or_else(|| n.as_f64().map(Self::Float))
217 .unwrap_or(Self::Null),
218 Value::String(s) => Self::Text(s),
219 Value::Array(_) => Self::Json(v),
220 Value::Object(ref o) => match Self::from_object(o) {
221 Some(typed) => typed,
222 None => Self::Json(v),
223 },
224 }
225 }
226
227 fn from_object(o: &serde_json::Map<String, Value>) -> Option<Self> {
231 if o.len() != 2 {
232 return None;
233 }
234 let tag = o.get(TAG)?.as_str()?;
235 let v = o.get(VAL)?;
236 let text = || v.as_str();
237 Some(match tag {
238 "int32" => Self::Int32(i32::try_from(v.as_i64()?).ok()?),
239 "int16" => Self::Int16(i16::try_from(v.as_i64()?).ok()?),
240 "decimal" => Self::Decimal(Decimal::parse(text()?)?),
241 "date" => Self::Date(NaiveDate::parse_from_str(text()?, "%Y-%m-%d").ok()?),
242 "time" => Self::Time(parse_time(text()?)?),
243 "timestamp" => Self::Timestamp(DateTime::parse_from_rfc3339(text()?).ok()?),
244 "uuid" => Self::Uuid(uuid::Uuid::parse_str(text()?).ok()?),
245 "bytes" => Self::Bytes(Self::b64().decode(text()?).ok()?),
246 "array" => Self::Array(v.as_array()?.iter().cloned().map(Self::from_json).collect()),
247 _ => return None,
248 })
249 }
250}
251
252fn parse_time(s: &str) -> Option<NaiveTime> {
254 NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
255 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
256 .ok()
257}
258
259impl Serialize for PropertyValue {
260 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
261 self.to_json().serialize(s)
262 }
263}
264
265impl<'de> Deserialize<'de> for PropertyValue {
266 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
267 Ok(Self::from_json(Value::deserialize(d)?))
268 }
269}
270
271fn rank(v: &PropertyValue) -> u8 {
281 match v {
282 PropertyValue::Null => 0,
283 PropertyValue::Bool(_) => 1,
284 PropertyValue::Long(_)
286 | PropertyValue::Int32(_)
287 | PropertyValue::Int16(_)
288 | PropertyValue::Float(_)
289 | PropertyValue::Decimal(_) => 2,
290 PropertyValue::Date(_) => 3,
291 PropertyValue::Time(_) => 4,
292 PropertyValue::Timestamp(_) => 5,
293 PropertyValue::Text(_) => 6,
294 PropertyValue::Uuid(_) => 7,
295 PropertyValue::Bytes(_) => 8,
296 PropertyValue::Array(_) => 9,
297 PropertyValue::Json(_) => 10,
298 }
299}
300
301fn as_int(v: &PropertyValue) -> Option<i128> {
303 Some(match v {
304 PropertyValue::Long(i) => *i as i128,
305 PropertyValue::Int32(i) => *i as i128,
306 PropertyValue::Int16(i) => *i as i128,
307 _ => return None,
308 })
309}
310
311fn as_exact(v: &PropertyValue) -> Option<Decimal> {
315 match v {
316 PropertyValue::Decimal(d) => Some(*d),
317 other => Decimal::from_parts(as_int(other)?, 0),
318 }
319}
320
321fn as_f64(v: &PropertyValue) -> Option<f64> {
322 match v {
323 PropertyValue::Float(f) => Some(*f),
324 PropertyValue::Decimal(d) => d.to_string().parse().ok(),
325 other => as_int(other).map(|i| i as f64),
326 }
327}
328
329impl PropertyValue {
330 pub fn cmp_value(&self, other: &Self) -> Ordering {
339 use PropertyValue as P;
340 match (self, other) {
341 (P::Bool(a), P::Bool(b)) => a.cmp(b),
342 (P::Text(a), P::Text(b)) => a.cmp(b),
343 (P::Date(a), P::Date(b)) => a.cmp(b),
344 (P::Time(a), P::Time(b)) => a.cmp(b),
345 (P::Timestamp(a), P::Timestamp(b)) => a.cmp(b),
348 (P::Uuid(a), P::Uuid(b)) => a.cmp(b),
349 (P::Bytes(a), P::Bytes(b)) => a.cmp(b),
350 (P::Array(a), P::Array(b)) => a
351 .iter()
352 .zip(b.iter())
353 .map(|(x, y)| x.cmp_value(y))
354 .find(|o| *o != Ordering::Equal)
355 .unwrap_or_else(|| a.len().cmp(&b.len())),
356 (P::Json(a), P::Json(b)) => a.to_string().cmp(&b.to_string()),
357 _ if rank(self) == rank(other) => {
358 match (as_exact(self), as_exact(other)) {
361 (Some(a), Some(b)) => a.cmp_value(&b),
362 _ => match (as_f64(self), as_f64(other)) {
363 (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
364 _ => Ordering::Equal,
365 },
366 }
367 }
368 _ => rank(self).cmp(&rank(other)),
369 }
370 }
371
372 pub fn is_sql_typed(&self) -> bool {
375 !matches!(
376 self,
377 Self::Null
378 | Self::Bool(_)
379 | Self::Long(_)
380 | Self::Float(_)
381 | Self::Text(_)
382 | Self::Json(_)
383 )
384 }
385
386 pub fn coerce_like(&self, like: &Self) -> Self {
399 if rank(self) == rank(like) || !like.is_sql_typed() {
401 return self.clone();
402 }
403 let text = match self {
404 Self::Text(s) => s.clone(),
405 Self::Long(i) => i.to_string(),
407 Self::Float(f) => f.to_string(),
408 _ => return self.clone(),
409 };
410 let coerced = match like {
411 Self::Decimal(_) => Decimal::parse(&text).map(Self::Decimal),
412 Self::Date(_) => NaiveDate::parse_from_str(&text, "%Y-%m-%d")
413 .ok()
414 .map(Self::Date),
415 Self::Time(_) => parse_time(&text).map(Self::Time),
416 Self::Timestamp(_) => DateTime::parse_from_rfc3339(&text)
417 .ok()
418 .map(Self::Timestamp),
419 Self::Uuid(_) => uuid::Uuid::parse_str(&text).ok().map(Self::Uuid),
420 Self::Bytes(_) => Self::b64().decode(&text).ok().map(Self::Bytes),
421 _ => None,
422 };
423 coerced.unwrap_or_else(|| self.clone())
424 }
425
426 pub fn eq_value(&self, other: &Self) -> bool {
432 self.cmp_value(other) == Ordering::Equal && rank(self) == rank(other)
433 }
434}
435
436impl PartialOrd for PropertyValue {
437 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
438 Some(self.cmp_value(other))
439 }
440}
441
442#[cfg(feature = "utoipa")]
451impl<'s> utoipa::ToSchema<'s> for PropertyValue {
452 fn schema() -> (&'s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
453 use utoipa::openapi::schema::{ObjectBuilder, OneOfBuilder, SchemaType};
454 use utoipa::openapi::RefOr;
455
456 fn arm(
458 tag: &str,
459 payload: RefOr<utoipa::openapi::Schema>,
460 desc: &str,
461 ) -> RefOr<utoipa::openapi::Schema> {
462 ObjectBuilder::new()
463 .description(Some(desc.to_string()))
464 .property(
465 TAG,
466 ObjectBuilder::new()
467 .schema_type(SchemaType::String)
468 .enum_values(Some([tag])),
469 )
470 .required(TAG)
471 .property(VAL, payload)
472 .required(VAL)
473 .into()
474 }
475
476 fn envelope(
478 tag: &str,
479 ty: SchemaType,
480 format: Option<&str>,
481 desc: &str,
482 ) -> RefOr<utoipa::openapi::Schema> {
483 let mut v = ObjectBuilder::new().schema_type(ty);
484 if let Some(f) = format {
485 v = v.format(Some(utoipa::openapi::SchemaFormat::Custom(f.into())));
486 }
487 arm(tag, v.into(), desc)
488 }
489
490 let scalar = |t: SchemaType, desc: &str| -> RefOr<utoipa::openapi::Schema> {
491 ObjectBuilder::new()
492 .schema_type(t)
493 .description(Some(desc.to_string()))
494 .into()
495 };
496
497 let schema = OneOfBuilder::new()
498 .description(Some(
499 "A typed property value. Legacy scalars are bare JSON; SQL-parity types \
500 are tagged envelopes of the form {\"$ant\":\"<type>\",\"v\":<payload>}. \
501 The /public/v1 (OpenSPG-compatible) endpoints always emit the bare \
502 scalar form."
503 .to_string(),
504 ))
505 .item(scalar(SchemaType::String, "SQL TEXT/VARCHAR."))
506 .item(scalar(SchemaType::Boolean, "SQL BOOLEAN."))
507 .item(scalar(SchemaType::Integer, "SQL BIGINT."))
508 .item(scalar(SchemaType::Number, "SQL DOUBLE PRECISION."))
509 .item(scalar(SchemaType::Object, "JSON/JSONB document."))
510 .item(envelope(
511 "int32",
512 SchemaType::Integer,
513 Some("int32"),
514 "SQL INT.",
515 ))
516 .item(envelope(
517 "int16",
518 SchemaType::Integer,
519 Some("int32"),
520 "SQL SMALLINT.",
521 ))
522 .item(envelope(
523 "decimal",
524 SchemaType::String,
525 None,
526 "SQL DECIMAL/NUMERIC. A canonical decimal STRING, never a JSON number: \
527 JSON numbers are parsed as f64 by most clients, which corrupts money.",
528 ))
529 .item(envelope(
530 "date",
531 SchemaType::String,
532 Some("date"),
533 "SQL DATE (YYYY-MM-DD).",
534 ))
535 .item(envelope(
536 "time",
537 SchemaType::String,
538 None,
539 "SQL TIME (HH:MM:SS.ffffff).",
540 ))
541 .item(envelope(
542 "timestamp",
543 SchemaType::String,
544 Some("date-time"),
545 "SQL TIMESTAMP WITH TIME ZONE, RFC3339. The UTC offset is part of the \
546 value and is preserved as sent.",
547 ))
548 .item(envelope(
549 "uuid",
550 SchemaType::String,
551 Some("uuid"),
552 "SQL UUID/UNIQUEIDENTIFIER.",
553 ))
554 .item(envelope(
555 "bytes",
556 SchemaType::String,
557 Some("byte"),
558 "SQL BLOB/BYTEA, base64 (standard alphabet, padded).",
559 ))
560 .item(arm(
564 "array",
565 utoipa::openapi::ArrayBuilder::new()
566 .items(utoipa::openapi::Ref::from_schema_name("PropertyValue"))
567 .into(),
568 "SQL array. Elements are themselves PropertyValues, so a typed array \
569 keeps its element types.",
570 ))
571 .build();
572 (
573 "PropertyValue",
574 RefOr::T(utoipa::openapi::Schema::OneOf(schema)),
575 )
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 fn round_trip(v: &PropertyValue) -> PropertyValue {
584 let s = serde_json::to_string(v).unwrap();
585 serde_json::from_str(&s).unwrap()
586 }
587
588 #[test]
589 fn legacy_scalars_keep_their_exact_historical_wire_form() {
590 for (v, json) in [
593 (PropertyValue::Null, "null"),
594 (PropertyValue::Bool(true), "true"),
595 (PropertyValue::Long(42), "42"),
596 (PropertyValue::Float(1.5), "1.5"),
597 (PropertyValue::Text("hi".into()), "\"hi\""),
598 ] {
599 assert_eq!(serde_json::to_string(&v).unwrap(), json);
600 assert_eq!(round_trip(&v), v);
601 }
602 let j = PropertyValue::Json(serde_json::json!({"a":[1,2]}));
603 assert_eq!(serde_json::to_string(&j).unwrap(), r#"{"a":[1,2]}"#);
604 assert_eq!(round_trip(&j), j);
605 }
606
607 #[test]
608 fn every_sql_type_round_trips_unchanged() {
609 for v in sample_values() {
610 assert_eq!(round_trip(&v), v, "{} did not round-trip", v.type_name());
611 }
612 }
613
614 fn sample_values() -> Vec<PropertyValue> {
615 vec![
616 PropertyValue::Int32(-2_147_483_648),
617 PropertyValue::Int16(-32_768),
618 PropertyValue::Decimal(Decimal::parse("12345678901234567.89").unwrap()),
619 PropertyValue::Date(NaiveDate::from_ymd_opt(2024, 3, 1).unwrap()),
620 PropertyValue::Time(NaiveTime::from_hms_micro_opt(12, 30, 45, 123456).unwrap()),
621 PropertyValue::Timestamp(
622 DateTime::parse_from_rfc3339("2024-03-01T12:00:00+02:00").unwrap(),
623 ),
624 PropertyValue::Uuid(
625 uuid::Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(),
626 ),
627 PropertyValue::Bytes(vec![0, 1, 2, 253, 254, 255]),
628 PropertyValue::Array(vec![
629 PropertyValue::Text("a".into()),
630 PropertyValue::Long(1),
631 PropertyValue::Float(2.5),
632 ]),
633 ]
634 }
635
636 #[test]
637 fn money_survives_the_wire_to_the_digit() {
638 let exact = "12345678901234567.89";
639 let v = PropertyValue::Decimal(Decimal::parse(exact).unwrap());
640 let wire = serde_json::to_string(&v).unwrap();
641 assert_eq!(wire, r#"{"$ant":"decimal","v":"12345678901234567.89"}"#);
642 match round_trip(&v) {
643 PropertyValue::Decimal(d) => assert_eq!(d.to_string(), exact),
644 other => panic!("became {other:?}"),
645 }
646 }
647
648 #[test]
649 fn timestamp_keeps_its_offset_rather_than_normalizing_to_utc() {
650 let v = PropertyValue::Timestamp(
651 DateTime::parse_from_rfc3339("2024-03-01T12:00:00+02:00").unwrap(),
652 );
653 assert!(serde_json::to_string(&v).unwrap().contains("+02:00"));
654 match round_trip(&v) {
655 PropertyValue::Timestamp(t) => {
656 assert_eq!(t.to_rfc3339(), "2024-03-01T12:00:00+02:00");
657 assert_eq!(t.offset().local_minus_utc(), 7200);
658 }
659 other => panic!("became {other:?}"),
660 }
661 }
662
663 #[test]
664 fn a_document_containing_the_tag_key_is_still_a_document() {
665 let doc = serde_json::json!({"$ant": "decimal", "v": "1.0", "mine": true});
667 assert_eq!(
668 PropertyValue::from_json(doc.clone()),
669 PropertyValue::Json(doc)
670 );
671 let unknown = serde_json::json!({"$ant": "wat", "v": 1});
673 assert_eq!(
674 PropertyValue::from_json(unknown.clone()),
675 PropertyValue::Json(unknown)
676 );
677 let bad = serde_json::json!({"$ant": "date", "v": "not-a-date"});
679 assert_eq!(
680 PropertyValue::from_json(bad.clone()),
681 PropertyValue::Json(bad)
682 );
683 }
684
685 #[test]
686 fn compat_json_flattens_to_the_pre_parity_shape() {
687 use serde_json::json;
688 let cases = [
689 (PropertyValue::Int32(7), json!(7)),
690 (PropertyValue::Int16(7), json!(7)),
691 (
692 PropertyValue::Decimal(Decimal::parse("12.34").unwrap()),
693 json!("12.34"),
694 ),
695 (
696 PropertyValue::Date(NaiveDate::from_ymd_opt(2024, 3, 1).unwrap()),
697 json!("2024-03-01"),
698 ),
699 (
700 PropertyValue::Uuid(
701 uuid::Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(),
702 ),
703 json!("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
704 ),
705 (PropertyValue::Bytes(vec![1, 2, 3]), json!("AQID")),
706 ];
707 for (v, want) in cases {
708 assert_eq!(v.to_compat_json(), want, "{}", v.type_name());
709 assert!(!v.to_compat_json().to_string().contains(TAG));
711 }
712 let arr = PropertyValue::Array(vec![PropertyValue::Int32(1), PropertyValue::Int32(2)]);
714 assert_eq!(arr.to_compat_json(), json!([1, 2]));
715 }
716
717 #[test]
718 fn numerics_compare_exactly_across_widths() {
719 let long = PropertyValue::Long(10);
720 let i32v = PropertyValue::Int32(10);
721 let i16v = PropertyValue::Int16(10);
722 let dec = PropertyValue::Decimal(Decimal::parse("10.00").unwrap());
723 for a in [&long, &i32v, &i16v, &dec] {
724 for b in [&long, &i32v, &i16v, &dec] {
725 assert_eq!(a.cmp_value(b), Ordering::Equal, "{a:?} vs {b:?}");
726 }
727 }
728 assert_eq!(
729 PropertyValue::Int32(9).cmp_value(&PropertyValue::Long(10)),
730 Ordering::Less
731 );
732 let a = PropertyValue::Decimal(Decimal::parse("100000000000000000.01").unwrap());
735 let b = PropertyValue::Decimal(Decimal::parse("100000000000000000.02").unwrap());
736 assert_eq!(a.cmp_value(&b), Ordering::Less);
737 }
738
739 #[test]
740 fn each_type_orders_within_itself() {
741 let d = |s: &str| PropertyValue::Date(NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap());
742 assert_eq!(d("2024-01-01").cmp_value(&d("2024-06-01")), Ordering::Less);
743
744 let t = |s: &str| PropertyValue::Time(parse_time(s).unwrap());
745 assert_eq!(t("01:00:00").cmp_value(&t("23:59:59")), Ordering::Less);
746
747 let ts = |s: &str| PropertyValue::Timestamp(DateTime::parse_from_rfc3339(s).unwrap());
749 assert_eq!(
750 ts("2024-03-01T12:00:00+02:00").cmp_value(&ts("2024-03-01T10:00:00Z")),
751 Ordering::Equal
752 );
753 assert_eq!(
754 ts("2024-03-01T12:00:00+02:00").cmp_value(&ts("2024-03-01T12:00:00Z")),
755 Ordering::Less
756 );
757
758 assert_eq!(
759 PropertyValue::Bytes(vec![1, 2]).cmp_value(&PropertyValue::Bytes(vec![1, 3])),
760 Ordering::Less
761 );
762 assert_eq!(
763 PropertyValue::Bool(false).cmp_value(&PropertyValue::Bool(true)),
764 Ordering::Less
765 );
766 assert_eq!(
767 PropertyValue::Text("a".into()).cmp_value(&PropertyValue::Text("b".into())),
768 Ordering::Less
769 );
770 let arr =
772 |v: Vec<i64>| PropertyValue::Array(v.into_iter().map(PropertyValue::Long).collect());
773 assert_eq!(arr(vec![1, 2]).cmp_value(&arr(vec![1, 3])), Ordering::Less);
774 assert_eq!(arr(vec![1]).cmp_value(&arr(vec![1, 0])), Ordering::Less);
775 }
776
777 #[test]
778 fn every_variant_is_comparable_against_every_other() {
779 let all: Vec<PropertyValue> = std::iter::once(PropertyValue::Null)
783 .chain([
784 PropertyValue::Bool(true),
785 PropertyValue::Long(1),
786 PropertyValue::Float(1.0),
787 PropertyValue::Text("x".into()),
788 PropertyValue::Json(serde_json::json!({})),
789 ])
790 .chain(sample_values())
791 .collect();
792 for a in &all {
793 for b in &all {
794 let ab = a.cmp_value(b);
795 assert_eq!(ab.reverse(), b.cmp_value(a), "asymmetric: {a:?} vs {b:?}");
796 }
797 assert_eq!(a.cmp_value(a), Ordering::Equal);
798 }
799 let mut sorted = all.clone();
801 sorted.sort_by(|a, b| a.cmp_value(b));
802 assert_eq!(sorted.len(), all.len());
803 }
804
805 #[test]
806 fn eq_value_is_by_value_but_not_across_kinds() {
807 assert!(PropertyValue::Long(1).eq_value(&PropertyValue::Int32(1)));
808 assert!(PropertyValue::Decimal(Decimal::parse("1.0").unwrap())
809 .eq_value(&PropertyValue::Long(1)));
810 assert!(!PropertyValue::Text("1".into()).eq_value(&PropertyValue::Long(1)));
813 }
814
815 #[test]
816 fn a_query_literal_is_pulled_to_the_stored_type() {
817 let stored = PropertyValue::Date(NaiveDate::from_ymd_opt(2024, 3, 1).unwrap());
819 let lit = PropertyValue::Text("2024-03-01".into());
820 assert!(lit.coerce_like(&stored).eq_value(&stored));
821
822 let amount = PropertyValue::Decimal(Decimal::parse("10.50").unwrap());
825 let nine = PropertyValue::Text("9".into()).coerce_like(&amount);
826 assert_eq!(nine.cmp_value(&amount), Ordering::Less);
827
828 assert!(PropertyValue::Long(10)
830 .coerce_like(&amount)
831 .cmp_value(&amount)
832 .is_lt());
833
834 let ts =
836 PropertyValue::Timestamp(DateTime::parse_from_rfc3339("2024-03-01T10:00:00Z").unwrap());
837 let other = PropertyValue::Text("2024-03-01T12:00:00+02:00".into()).coerce_like(&ts);
838 assert!(other.eq_value(&ts));
839
840 let junk = PropertyValue::Text("not-a-date".into());
843 assert_eq!(junk.coerce_like(&stored), junk);
844 assert!(!junk.coerce_like(&stored).eq_value(&stored));
845 }
846
847 #[test]
848 fn bytes_survive_arbitrary_binary() {
849 let raw: Vec<u8> = (0u8..=255).collect();
850 let v = PropertyValue::Bytes(raw.clone());
851 match round_trip(&v) {
852 PropertyValue::Bytes(b) => assert_eq!(b, raw),
853 other => panic!("became {other:?}"),
854 }
855 }
856
857 #[test]
858 fn nested_arrays_keep_element_types() {
859 let v = PropertyValue::Array(vec![
860 PropertyValue::Array(vec![PropertyValue::Decimal(
861 Decimal::parse("0.10").unwrap(),
862 )]),
863 PropertyValue::Uuid(uuid::Uuid::nil()),
864 ]);
865 assert_eq!(round_trip(&v), v);
866 }
867}