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