1use crate::error::{DecodeError, EncodeError};
2use crate::list::ListValue;
3use crate::property::{PropertyValue, PropertyValueRef};
4use crate::spatial::{GeographyValue, GeometryValue, PointValue};
5use crate::storage::{StorageValue, StorageValueRef};
6use crate::tag;
7use crate::temporal::{LocalDateTimeValue, LocalTimeValue, ZonedDateTimeValue, ZonedTimeValue};
8use crate::vector::VectorValue;
9use std::str::FromStr;
10
11pub fn encode_property_value(value: &PropertyValue) -> Result<StorageValue, EncodeError> {
12 match value {
13 PropertyValue::Null => Ok(StorageValue::Null),
14 PropertyValue::Bool(false) => Ok(StorageValue::Blob(vec![tag::BOOL_FALSE])),
15 PropertyValue::Bool(true) => Ok(StorageValue::Blob(vec![tag::BOOL_TRUE])),
16 PropertyValue::Integer(v) => Ok(StorageValue::Integer(*v)),
17 PropertyValue::Float(v) if v.is_finite() => Ok(StorageValue::Real(*v)),
18 PropertyValue::Float(_) => Err(EncodeError::NonCanonicalValue(
19 "non-finite floats are not canonical",
20 )),
21 PropertyValue::String(v) => Ok(StorageValue::Text(v.clone())),
22 PropertyValue::Date(v) => encode_text_blob(tag::DATE, &v.to_string()),
23 PropertyValue::LocalTime(v) => encode_text_blob(tag::LOCAL_TIME, &local_time_text(v)),
24 PropertyValue::ZonedTime(v) => encode_text_blob(tag::ZONED_TIME, &zoned_time_text(v)),
25 PropertyValue::LocalDateTime(v) => {
26 encode_text_blob(tag::LOCAL_DATETIME, &local_datetime_text(v))
27 }
28 PropertyValue::ZonedDateTime(v) => {
29 encode_text_blob(tag::ZONED_DATETIME, &zoned_datetime_text(v))
30 }
31 PropertyValue::Duration(v) => encode_text_blob(tag::DURATION, &v.to_string()),
32 PropertyValue::Point(v) => Ok(StorageValue::Blob(v.encode_blob()?)),
33 PropertyValue::Geometry(v) => Ok(StorageValue::Blob(v.encode_blob()?)),
34 PropertyValue::Geography(v) => Ok(StorageValue::Blob(v.encode_blob()?)),
35 PropertyValue::List(v) => Ok(StorageValue::Blob(v.encode_blob()?)),
36 PropertyValue::Vector(v) => Ok(StorageValue::Blob(v.encode_blob()?)),
37 PropertyValue::Bytes(v) => {
38 let mut out = Vec::with_capacity(v.len() + 1);
39 out.push(tag::BINARY);
40 out.extend_from_slice(v);
41 Ok(StorageValue::Blob(out))
42 }
43 }
44}
45
46pub fn decode_property_value(value: StorageValueRef<'_>) -> Result<PropertyValue, DecodeError> {
47 match value {
48 StorageValueRef::Null => Ok(PropertyValue::Null),
49 StorageValueRef::Integer(v) => Ok(PropertyValue::Integer(v)),
50 StorageValueRef::Real(v) => Ok(PropertyValue::Float(v)),
51 StorageValueRef::Text(v) => Ok(PropertyValue::String(v.to_string())),
52 StorageValueRef::Blob(blob) => decode_property_blob(blob),
53 }
54}
55
56fn encode_text_blob(tag: u8, value: &str) -> Result<StorageValue, EncodeError> {
57 let mut out = Vec::with_capacity(1 + value.len());
58 out.push(tag);
59 out.extend_from_slice(value.as_bytes());
60 Ok(StorageValue::Blob(out))
61}
62
63pub(crate) fn temporal_value_text(value: PropertyValueRef<'_>) -> Option<String> {
64 match value {
65 PropertyValueRef::Date(v) => Some(v.to_string()),
66 PropertyValueRef::LocalTime(v) => Some(local_time_text(v)),
67 PropertyValueRef::ZonedTime(v) => Some(zoned_time_text(v)),
68 PropertyValueRef::LocalDateTime(v) => Some(local_datetime_text(v)),
69 PropertyValueRef::ZonedDateTime(v) => Some(zoned_datetime_text(v)),
70 PropertyValueRef::Duration(v) => Some(v.to_string()),
71 _ => None,
72 }
73}
74
75fn local_time_text(value: &LocalTimeValue) -> String {
76 let mut out = format!("{:02}:{:02}", value.hour(), value.minute());
77 if value.second() != 0 || value.nanos() != 0 {
78 out.push_str(&format!(":{:02}", value.second()));
79 push_fraction(&mut out, value.nanos());
80 }
81 out
82}
83
84fn zoned_time_text(value: &ZonedTimeValue) -> String {
85 format!("{}{}", local_time_text(&value.time), value.offset)
86}
87
88fn local_datetime_text(value: &LocalDateTimeValue) -> String {
89 format!("{}T{}", value.date(), local_time_text(&value.time()))
90}
91
92fn zoned_datetime_text(value: &ZonedDateTimeValue) -> String {
93 let mut out = format!("{}{}", local_datetime_text(&value.datetime), value.offset);
94 if let Some(zone_id) = &value.zone_id {
95 out.push('[');
96 out.push_str(zone_id);
97 out.push(']');
98 }
99 out
100}
101
102fn push_fraction(out: &mut String, nanos: u32) {
103 if nanos == 0 {
104 return;
105 }
106
107 let mut frac = format!("{nanos:09}");
108 while frac.ends_with('0') {
109 frac.pop();
110 }
111 out.push('.');
112 out.push_str(&frac);
113}
114
115fn decode_property_blob(blob: &[u8]) -> Result<PropertyValue, DecodeError> {
116 let Some(tag) = blob.first().copied() else {
117 return Err(DecodeError::EmptyInput);
118 };
119 match tag {
120 tag::BOOL_FALSE => Ok(PropertyValue::Bool(false)),
121 tag::BOOL_TRUE => Ok(PropertyValue::Bool(true)),
122 tag::BINARY => Ok(PropertyValue::Bytes(blob[1..].to_vec())),
123 tag::DATE => Ok(PropertyValue::Date(parse_temporal(&blob[1..])?)),
124 tag::LOCAL_TIME => Ok(PropertyValue::LocalTime(parse_temporal(&blob[1..])?)),
125 tag::ZONED_TIME => Ok(PropertyValue::ZonedTime(parse_temporal(&blob[1..])?)),
126 tag::LOCAL_DATETIME => Ok(PropertyValue::LocalDateTime(parse_temporal(&blob[1..])?)),
127 tag::ZONED_DATETIME => Ok(PropertyValue::ZonedDateTime(parse_temporal(&blob[1..])?)),
128 tag::DURATION => Ok(PropertyValue::Duration(parse_temporal(&blob[1..])?)),
129 tag::POINT => Ok(PropertyValue::Point(PointValue::decode_blob(blob)?)),
130 tag::GEOMETRY => Ok(PropertyValue::Geometry(GeometryValue::decode_blob(blob)?)),
131 tag::GEOGRAPHY => Ok(PropertyValue::Geography(GeographyValue::decode_blob(blob)?)),
132 tag::LIST_JSON
133 | tag::STRING_LIST_JSON
134 | tag::BOOL_LIST
135 | tag::INT64_LIST
136 | tag::FLOAT64_LIST => Ok(PropertyValue::List(ListValue::decode_blob(blob)?)),
137 tag::VECTOR => Ok(PropertyValue::Vector(VectorValue::decode_blob(blob)?)),
138 actual => Err(DecodeError::UnexpectedTag {
139 expected: "property tag",
140 actual,
141 }),
142 }
143}
144
145fn parse_temporal<T>(payload: &[u8]) -> Result<T, DecodeError>
146where
147 T: FromStr<Err = DecodeError>,
148{
149 let text = std::str::from_utf8(payload).map_err(|_| DecodeError::InvalidUtf8)?;
150 text.parse()
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::list::ListValue;
157 use crate::spatial::{
158 GeographyValue, GeometryShape, GeometryValue, LineStringValue, LinearRingValue, PointValue,
159 PolygonValue, Position,
160 };
161 use crate::storage::StorageValue;
162 use crate::temporal::{
163 DateValue, DurationValue, LocalDateTimeValue, LocalTimeValue, ZonedDateTimeValue,
164 ZonedTimeValue,
165 };
166 use crate::vector::{VectorStorage, VectorType, VectorValue};
167
168 fn assert_roundtrip(value: PropertyValue) {
169 let decoded =
170 decode_property_value(encode_property_value(&value).unwrap().as_ref()).unwrap();
171 assert_eq!(decoded, value);
172 }
173
174 fn sample_xyz_ring() -> LinearRingValue {
175 LinearRingValue::new(vec![
176 Position::Xyz {
177 x: 0.0,
178 y: 0.0,
179 z: 1.0,
180 },
181 Position::Xyz {
182 x: 4.0,
183 y: 0.0,
184 z: 1.0,
185 },
186 Position::Xyz {
187 x: 4.0,
188 y: 4.0,
189 z: 1.0,
190 },
191 Position::Xyz {
192 x: 0.0,
193 y: 0.0,
194 z: 1.0,
195 },
196 ])
197 .unwrap()
198 }
199
200 fn sample_supported_shapes() -> Vec<GeometryShape> {
201 vec![
202 GeometryShape::Point(Position::Xyz {
203 x: 12.5,
204 y: 55.7,
205 z: 9.0,
206 }),
207 GeometryShape::LineString(
208 LineStringValue::new(vec![
209 Position::Xyz {
210 x: 12.5,
211 y: 55.7,
212 z: 9.0,
213 },
214 Position::Xyz {
215 x: 13.1,
216 y: 56.0,
217 z: 10.5,
218 },
219 ])
220 .unwrap(),
221 ),
222 GeometryShape::Polygon(PolygonValue::new(vec![sample_xyz_ring()]).unwrap()),
223 GeometryShape::MultiPoint(vec![
224 Position::Xyz {
225 x: 1.0,
226 y: 2.0,
227 z: 3.0,
228 },
229 Position::Xyz {
230 x: 4.0,
231 y: 5.0,
232 z: 6.0,
233 },
234 ]),
235 GeometryShape::MultiLineString(vec![
236 LineStringValue::new(vec![
237 Position::Xyz {
238 x: 0.0,
239 y: 0.0,
240 z: 1.0,
241 },
242 Position::Xyz {
243 x: 1.0,
244 y: 1.0,
245 z: 1.0,
246 },
247 ])
248 .unwrap(),
249 LineStringValue::new(vec![
250 Position::Xyz {
251 x: 2.0,
252 y: 2.0,
253 z: 2.0,
254 },
255 Position::Xyz {
256 x: 3.0,
257 y: 3.0,
258 z: 2.0,
259 },
260 ])
261 .unwrap(),
262 ]),
263 GeometryShape::MultiPolygon(vec![
264 PolygonValue::new(vec![sample_xyz_ring()]).unwrap(),
265 PolygonValue::new(vec![sample_xyz_ring()]).unwrap(),
266 ]),
267 GeometryShape::GeometryCollection(vec![
268 GeometryShape::Point(Position::Xyz {
269 x: 12.5,
270 y: 55.7,
271 z: 9.0,
272 }),
273 GeometryShape::LineString(
274 LineStringValue::new(vec![
275 Position::Xyz {
276 x: 12.5,
277 y: 55.7,
278 z: 9.0,
279 },
280 Position::Xyz {
281 x: 13.1,
282 y: 56.0,
283 z: 10.5,
284 },
285 ])
286 .unwrap(),
287 ),
288 ]),
289 ]
290 }
291
292 #[test]
293 fn roundtrip_null() {
294 let storage = encode_property_value(&PropertyValue::Null).unwrap();
295 assert_eq!(storage, StorageValue::Null);
296 let decoded = decode_property_value(storage.as_ref()).unwrap();
297 assert_eq!(decoded, PropertyValue::Null);
298 }
299
300 #[test]
301 fn roundtrip_bool() {
302 let storage = encode_property_value(&PropertyValue::Bool(true)).unwrap();
303 assert_eq!(storage, StorageValue::Blob(vec![tag::BOOL_TRUE]));
304 let decoded = decode_property_value(storage.as_ref()).unwrap();
305 assert_eq!(decoded, PropertyValue::Bool(true));
306 }
307
308 #[test]
309 fn roundtrip_integer() {
310 let storage = encode_property_value(&PropertyValue::Integer(42)).unwrap();
311 assert_eq!(storage, StorageValue::Integer(42));
312 let decoded = decode_property_value(storage.as_ref()).unwrap();
313 assert_eq!(decoded, PropertyValue::Integer(42));
314 }
315
316 #[test]
317 fn roundtrip_float() {
318 let storage = encode_property_value(&PropertyValue::Float(42.5)).unwrap();
319 assert_eq!(storage, StorageValue::Real(42.5));
320 let decoded = decode_property_value(storage.as_ref()).unwrap();
321 assert_eq!(decoded, PropertyValue::Float(42.5));
322 }
323
324 #[test]
325 fn roundtrip_string() {
326 let storage = encode_property_value(&PropertyValue::String("hello".into())).unwrap();
327 assert_eq!(storage, StorageValue::Text("hello".into()));
328 let decoded = decode_property_value(storage.as_ref()).unwrap();
329 assert_eq!(decoded, PropertyValue::String("hello".into()));
330 }
331
332 #[test]
333 fn roundtrip_date() {
334 assert_roundtrip(PropertyValue::Date(DateValue::new(2026, 4, 6).unwrap()));
335 }
336
337 #[test]
338 fn roundtrip_local_time_with_fraction() {
339 assert_roundtrip(PropertyValue::LocalTime(
340 LocalTimeValue::new(21, 40, 32, 142_000_000).unwrap(),
341 ));
342 }
343
344 #[test]
345 fn temporal_storage_omits_zero_seconds() {
346 let storage = encode_property_value(&PropertyValue::LocalTime(
347 LocalTimeValue::new(10, 35, 0, 0).unwrap(),
348 ))
349 .unwrap();
350 assert_eq!(storage, StorageValue::Blob(b"\x1110:35".to_vec()));
351
352 let storage = encode_property_value(&PropertyValue::ZonedTime(
353 "10:35:00-08:00".parse::<ZonedTimeValue>().unwrap(),
354 ))
355 .unwrap();
356 assert_eq!(storage, StorageValue::Blob(b"\x1210:35-08:00".to_vec()));
357 }
358
359 #[test]
360 fn roundtrip_zoned_time() {
361 assert_roundtrip(PropertyValue::ZonedTime(
362 "12:00:00+01:00".parse::<ZonedTimeValue>().unwrap(),
363 ));
364 }
365
366 #[test]
367 fn roundtrip_local_datetime() {
368 assert_roundtrip(PropertyValue::LocalDateTime(
369 LocalDateTimeValue::new(2026, 4, 6, 12, 0, 1, 120_000_000).unwrap(),
370 ));
371 }
372
373 #[test]
374 fn roundtrip_zoned_datetime_offset_only() {
375 assert_roundtrip(PropertyValue::ZonedDateTime(
376 "2026-04-06T12:00:00+01:00"
377 .parse::<ZonedDateTimeValue>()
378 .unwrap(),
379 ));
380 }
381
382 #[cfg(any(feature = "tzdb-system", feature = "tzdb-bundled"))]
383 #[test]
384 fn roundtrip_zoned_datetime_named_zone() {
385 assert_roundtrip(PropertyValue::ZonedDateTime(
386 "2026-04-06T12:00:00+01:00[Europe/Lisbon]"
387 .parse::<ZonedDateTimeValue>()
388 .unwrap(),
389 ));
390 }
391
392 #[test]
393 fn roundtrip_duration() {
394 assert_roundtrip(PropertyValue::Duration(
395 "P2M10DT2H30M".parse::<DurationValue>().unwrap(),
396 ));
397 }
398
399 #[test]
400 fn roundtrip_string_list_json() {
401 assert_roundtrip(PropertyValue::List(ListValue::String(vec![
402 Some("Ada".into()),
403 None,
404 Some("Bob".into()),
405 ])));
406 }
407
408 #[test]
409 fn roundtrip_string_list_json_with_escapes_and_unicode() {
410 assert_roundtrip(PropertyValue::List(ListValue::String(vec![
411 Some("Ada \"Lovelace\"".into()),
412 Some("line\\break".into()),
413 Some("Lisboa €".into()),
414 None,
415 ])));
416 }
417
418 #[test]
419 fn roundtrip_bool_list() {
420 assert_roundtrip(PropertyValue::List(ListValue::Bool(vec![
421 Some(true),
422 None,
423 Some(false),
424 Some(true),
425 ])));
426 }
427
428 #[test]
429 fn roundtrip_bool_list_without_nulls() {
430 assert_roundtrip(PropertyValue::List(ListValue::Bool(vec![
431 Some(true),
432 Some(false),
433 Some(true),
434 Some(false),
435 Some(true),
436 Some(true),
437 Some(false),
438 Some(false),
439 Some(true),
440 ])));
441 }
442
443 #[test]
444 fn roundtrip_bool_list_crosses_bitmap_byte_boundary() {
445 assert_roundtrip(PropertyValue::List(ListValue::Bool(vec![
446 None,
447 Some(true),
448 Some(false),
449 Some(true),
450 Some(false),
451 Some(false),
452 Some(true),
453 None,
454 None,
455 ])));
456 }
457
458 #[test]
459 fn roundtrip_empty_bool_list() {
460 assert_roundtrip(PropertyValue::List(ListValue::Bool(vec![])));
461 }
462
463 #[test]
464 fn roundtrip_int_list() {
465 assert_roundtrip(PropertyValue::List(ListValue::Int64(vec![
466 Some(1),
467 None,
468 Some(2),
469 Some(4),
470 ])));
471 }
472
473 #[test]
474 fn roundtrip_int_list_without_nulls() {
475 assert_roundtrip(PropertyValue::List(ListValue::Int64(vec![
476 Some(-9),
477 Some(-1),
478 Some(0),
479 Some(1),
480 Some(2),
481 Some(3),
482 Some(5),
483 Some(8),
484 Some(13),
485 ])));
486 }
487
488 #[test]
489 fn roundtrip_int_list_crosses_bitmap_byte_boundary() {
490 assert_roundtrip(PropertyValue::List(ListValue::Int64(vec![
491 Some(-9),
492 None,
493 Some(0),
494 Some(1),
495 Some(2),
496 Some(3),
497 Some(5),
498 None,
499 Some(13),
500 ])));
501 }
502
503 #[test]
504 fn roundtrip_empty_int_list() {
505 assert_roundtrip(PropertyValue::List(ListValue::Int64(vec![])));
506 }
507
508 #[test]
509 fn roundtrip_float_list() {
510 assert_roundtrip(PropertyValue::List(ListValue::Float64(vec![
511 Some(1.5),
512 None,
513 Some(2.25),
514 ])));
515 }
516
517 #[test]
518 fn roundtrip_float_list_without_nulls() {
519 assert_roundtrip(PropertyValue::List(ListValue::Float64(vec![
520 Some(-9.5),
521 Some(-1.25),
522 Some(0.0),
523 Some(1.0),
524 Some(2.0),
525 Some(3.5),
526 Some(5.75),
527 Some(8.0),
528 Some(13.125),
529 ])));
530 }
531
532 #[test]
533 fn roundtrip_float_list_crosses_bitmap_byte_boundary() {
534 assert_roundtrip(PropertyValue::List(ListValue::Float64(vec![
535 Some(-9.5),
536 None,
537 Some(0.0),
538 Some(1.0),
539 Some(2.25),
540 Some(3.5),
541 Some(5.75),
542 None,
543 Some(13.125),
544 ])));
545 }
546
547 #[test]
548 fn roundtrip_empty_float_list() {
549 assert_roundtrip(PropertyValue::List(ListValue::Float64(vec![])));
550 }
551
552 #[test]
553 fn roundtrip_generic_list() {
554 assert_roundtrip(PropertyValue::List(ListValue::Generic(vec![
555 PropertyValue::Integer(1),
556 PropertyValue::Bool(true),
557 PropertyValue::String("x".into()),
558 PropertyValue::List(ListValue::Generic(vec![PropertyValue::Integer(2)])),
559 ])));
560 }
561
562 #[test]
563 fn roundtrip_generic_list_with_null_and_escaped_strings() {
564 assert_roundtrip(PropertyValue::List(ListValue::Generic(vec![
565 PropertyValue::Null,
566 PropertyValue::String("Ada \"Lovelace\"".into()),
567 PropertyValue::String("slash\\\\path".into()),
568 PropertyValue::String("Lisboa €".into()),
569 PropertyValue::List(ListValue::Generic(vec![
570 PropertyValue::Null,
571 PropertyValue::String("nested".into()),
572 ])),
573 ])));
574 }
575
576 #[test]
577 fn generic_list_rejects_temporal_value() {
578 let value = PropertyValue::List(ListValue::Generic(vec![PropertyValue::Date(
579 DateValue::new(2026, 4, 6).unwrap(),
580 )]));
581 assert!(encode_property_value(&value).is_err());
582 }
583
584 #[test]
585 fn roundtrip_vector() {
586 assert_roundtrip(PropertyValue::Vector(VectorValue {
587 coord_type: VectorType::Float32,
588 values: VectorStorage::F32(vec![1.05, 0.123, 5.0]),
589 }));
590 }
591
592 #[test]
593 fn roundtrip_all_vector_storage_types() {
594 let cases = vec![
595 PropertyValue::Vector(VectorValue {
596 coord_type: VectorType::Integer8,
597 values: VectorStorage::I8(vec![-3, 0, 12]),
598 }),
599 PropertyValue::Vector(VectorValue {
600 coord_type: VectorType::Integer16,
601 values: VectorStorage::I16(vec![-300, 0, 1200]),
602 }),
603 PropertyValue::Vector(VectorValue {
604 coord_type: VectorType::Integer32,
605 values: VectorStorage::I32(vec![-30_000, 0, 120_000]),
606 }),
607 PropertyValue::Vector(VectorValue {
608 coord_type: VectorType::Integer64,
609 values: VectorStorage::I64(vec![-3_000_000, 0, 12_000_000]),
610 }),
611 PropertyValue::Vector(VectorValue {
612 coord_type: VectorType::Float64,
613 values: VectorStorage::F64(vec![1.25, -5.5, 0.0]),
614 }),
615 ];
616 for case in cases {
617 assert_roundtrip(case);
618 }
619 }
620
621 #[test]
622 fn roundtrip_bytes() {
623 assert_roundtrip(PropertyValue::Bytes(vec![0x00, 0x01, 0xfe, 0xff]));
624 }
625
626 #[test]
627 fn roundtrip_point() {
628 assert_roundtrip(PropertyValue::Point(PointValue {
629 srid: 4326,
630 position: Position::Xy { x: 12.5, y: 55.7 },
631 }));
632 }
633
634 #[test]
635 fn roundtrip_point_xyz() {
636 assert_roundtrip(PropertyValue::Point(PointValue {
637 srid: 4326,
638 position: Position::Xyz {
639 x: 12.5,
640 y: 55.7,
641 z: 9.0,
642 },
643 }));
644 }
645
646 #[test]
647 fn roundtrip_geometry_family() {
648 for shape in sample_supported_shapes() {
649 assert_roundtrip(PropertyValue::Geometry(GeometryValue { srid: 4326, shape }));
650 }
651 }
652
653 #[test]
654 fn roundtrip_geography_family() {
655 for shape in sample_supported_shapes() {
656 assert_roundtrip(PropertyValue::Geography(GeographyValue {
657 srid: 4326,
658 shape,
659 }));
660 }
661 }
662}