Skip to main content

vortex_array/scalar/
proto.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Protobuf serialization and deserialization for scalars.
5
6use num_traits::ToBytes;
7use num_traits::ToPrimitive;
8use prost::Message;
9use vortex_buffer::BufferString;
10use vortex_buffer::ByteBuffer;
11use vortex_dtype::DType;
12use vortex_dtype::PType;
13use vortex_dtype::half::f16;
14use vortex_dtype::i256;
15use vortex_error::VortexExpect;
16use vortex_error::VortexResult;
17use vortex_error::vortex_bail;
18use vortex_error::vortex_ensure;
19use vortex_error::vortex_err;
20use vortex_proto::scalar as pb;
21use vortex_proto::scalar::ListValue;
22use vortex_proto::scalar::scalar_value::Kind;
23use vortex_session::VortexSession;
24
25use crate::scalar::DecimalValue;
26use crate::scalar::PValue;
27use crate::scalar::Scalar;
28use crate::scalar::ScalarValue;
29
30////////////////////////////////////////////////////////////////////////////////////////////////////
31// Serialize INTO proto.
32////////////////////////////////////////////////////////////////////////////////////////////////////
33
34impl From<&Scalar> for pb::Scalar {
35    fn from(value: &Scalar) -> Self {
36        pb::Scalar {
37            dtype: Some(
38                (value.dtype())
39                    .try_into()
40                    .vortex_expect("Failed to convert DType to proto"),
41            ),
42            value: Some(ScalarValue::to_proto(value.value())),
43        }
44    }
45}
46
47impl ScalarValue {
48    /// Ideally, we would not have this function and instead implement this `From` implementation:
49    ///
50    /// ```ignore
51    /// impl From<Option<&ScalarValue>> for pb::ScalarValue { ... }
52    /// ```
53    ///
54    /// However, we are not allowed to do this because of the Orphan rule (`Option` and
55    /// `pb::ScalarValue` are not types defined in this crate). So we must make this a method on
56    /// `vortex_array::scalar::ScalarValue` directly.
57    pub fn to_proto(this: Option<&Self>) -> pb::ScalarValue {
58        match this {
59            None => pb::ScalarValue {
60                kind: Some(Kind::NullValue(0)),
61            },
62            Some(this) => pb::ScalarValue::from(this),
63        }
64    }
65
66    /// Serialize an optional [`ScalarValue`] to protobuf bytes (handles null values).
67    pub fn to_proto_bytes<B: Default + bytes::BufMut>(value: Option<&ScalarValue>) -> B {
68        let proto = Self::to_proto(value);
69        let mut buf = B::default();
70        proto
71            .encode(&mut buf)
72            .vortex_expect("Failed to encode scalar value");
73        buf
74    }
75}
76
77impl From<&ScalarValue> for pb::ScalarValue {
78    fn from(value: &ScalarValue) -> Self {
79        match value {
80            ScalarValue::Bool(v) => pb::ScalarValue {
81                kind: Some(Kind::BoolValue(*v)),
82            },
83            ScalarValue::Primitive(v) => pb::ScalarValue::from(v),
84            ScalarValue::Decimal(v) => {
85                let inner_value = match v {
86                    DecimalValue::I8(v) => v.to_le_bytes().to_vec(),
87                    DecimalValue::I16(v) => v.to_le_bytes().to_vec(),
88                    DecimalValue::I32(v) => v.to_le_bytes().to_vec(),
89                    DecimalValue::I64(v) => v.to_le_bytes().to_vec(),
90                    DecimalValue::I128(v128) => v128.to_le_bytes().to_vec(),
91                    DecimalValue::I256(v256) => v256.to_le_bytes().to_vec(),
92                };
93
94                pb::ScalarValue {
95                    kind: Some(Kind::BytesValue(inner_value)),
96                }
97            }
98            ScalarValue::Utf8(v) => pb::ScalarValue {
99                kind: Some(Kind::StringValue(v.to_string())),
100            },
101            ScalarValue::Binary(v) => pb::ScalarValue {
102                kind: Some(Kind::BytesValue(v.to_vec())),
103            },
104            ScalarValue::List(v) => {
105                let mut values = Vec::with_capacity(v.len());
106                for elem in v.iter() {
107                    values.push(ScalarValue::to_proto(elem.as_ref()));
108                }
109                pb::ScalarValue {
110                    kind: Some(Kind::ListValue(ListValue { values })),
111                }
112            }
113        }
114    }
115}
116
117impl From<&PValue> for pb::ScalarValue {
118    fn from(value: &PValue) -> Self {
119        match value {
120            PValue::I8(v) => pb::ScalarValue {
121                kind: Some(Kind::Int64Value(*v as i64)),
122            },
123            PValue::I16(v) => pb::ScalarValue {
124                kind: Some(Kind::Int64Value(*v as i64)),
125            },
126            PValue::I32(v) => pb::ScalarValue {
127                kind: Some(Kind::Int64Value(*v as i64)),
128            },
129            PValue::I64(v) => pb::ScalarValue {
130                kind: Some(Kind::Int64Value(*v)),
131            },
132            PValue::U8(v) => pb::ScalarValue {
133                kind: Some(Kind::Uint64Value(*v as u64)),
134            },
135            PValue::U16(v) => pb::ScalarValue {
136                kind: Some(Kind::Uint64Value(*v as u64)),
137            },
138            PValue::U32(v) => pb::ScalarValue {
139                kind: Some(Kind::Uint64Value(*v as u64)),
140            },
141            PValue::U64(v) => pb::ScalarValue {
142                kind: Some(Kind::Uint64Value(*v)),
143            },
144            PValue::F16(v) => pb::ScalarValue {
145                kind: Some(Kind::F16Value(v.to_bits() as u64)),
146            },
147            PValue::F32(v) => pb::ScalarValue {
148                kind: Some(Kind::F32Value(*v)),
149            },
150            PValue::F64(v) => pb::ScalarValue {
151                kind: Some(Kind::F64Value(*v)),
152            },
153        }
154    }
155}
156
157////////////////////////////////////////////////////////////////////////////////////////////////////
158// Serialize FROM proto.
159////////////////////////////////////////////////////////////////////////////////////////////////////
160
161impl Scalar {
162    /// Creates a [`Scalar`] from a [protobuf `ScalarValue`](pb::ScalarValue) representation.
163    ///
164    /// Note that we need to provide a [`DType`] since protobuf serialization only supports 64-bit
165    /// integers, and serializing _into_ protobuf loses that type information.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if type validation fails.
170    pub fn from_proto_value(value: &pb::ScalarValue, dtype: &DType) -> VortexResult<Self> {
171        let scalar_value = ScalarValue::from_proto(value, dtype)?;
172
173        Scalar::try_new(dtype.clone(), scalar_value)
174    }
175
176    /// Creates a [`Scalar`] from its [protobuf](pb::Scalar) representation.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the protobuf is missing required fields or if type validation fails.
181    pub fn from_proto(value: &pb::Scalar, session: &VortexSession) -> VortexResult<Self> {
182        let dtype = DType::from_proto(
183            value
184                .dtype
185                .as_ref()
186                .ok_or_else(|| vortex_err!(Serde: "Scalar missing dtype"))?,
187            session,
188        )?;
189
190        let pb_scalar_value: &pb::ScalarValue = value
191            .value
192            .as_ref()
193            .ok_or_else(|| vortex_err!(Serde: "Scalar missing value"))?;
194
195        let value: Option<ScalarValue> = ScalarValue::from_proto(pb_scalar_value, &dtype)?;
196
197        Scalar::try_new(dtype, value)
198    }
199}
200
201impl ScalarValue {
202    /// Deserialize a [`ScalarValue`] from protobuf bytes.
203    ///
204    /// Note that we need to provide a [`DType`] since protobuf serialization only supports 64-bit
205    /// integers, and serializing _into_ protobuf loses that type information.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if decoding or type validation fails.
210    pub fn from_proto_bytes(bytes: &[u8], dtype: &DType) -> VortexResult<Option<Self>> {
211        let proto = pb::ScalarValue::decode(bytes)?;
212        Self::from_proto(&proto, dtype)
213    }
214
215    /// Creates a [`ScalarValue`] from its [protobuf](pb::ScalarValue) representation.
216    ///
217    /// Note that we need to provide a [`DType`] since protobuf serialization only supports 64-bit
218    /// integers, and serializing _into_ protobuf loses that type information.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the protobuf value cannot be converted to the given [`DType`].
223    pub fn from_proto(value: &pb::ScalarValue, dtype: &DType) -> VortexResult<Option<Self>> {
224        let kind = value
225            .kind
226            .as_ref()
227            .ok_or_else(|| vortex_err!(Serde: "Scalar value missing kind"))?;
228
229        // `DType::Extension` store their serialized values using the storage `DType`.
230        let dtype = match dtype {
231            DType::Extension(ext) => ext.storage_dtype(),
232            _ => dtype,
233        };
234
235        Ok(Some(match kind {
236            Kind::NullValue(_) => return Ok(None),
237            Kind::BoolValue(v) => bool_from_proto(*v, dtype)?,
238            Kind::Int64Value(v) => int64_from_proto(*v, dtype)?,
239            Kind::Uint64Value(v) => uint64_from_proto(*v, dtype)?,
240            Kind::F16Value(v) => f16_from_proto(*v, dtype)?,
241            Kind::F32Value(v) => f32_from_proto(*v, dtype)?,
242            Kind::F64Value(v) => f64_from_proto(*v, dtype)?,
243            Kind::StringValue(s) => string_from_proto(s, dtype)?,
244            Kind::BytesValue(b) => bytes_from_proto(b, dtype)?,
245            Kind::ListValue(v) => list_from_proto(v, dtype)?,
246        }))
247    }
248}
249
250/// Deserialize a [`ScalarValue::Bool`] from a protobuf `BoolValue`.
251fn bool_from_proto(v: bool, dtype: &DType) -> VortexResult<ScalarValue> {
252    vortex_ensure!(
253        dtype.is_boolean(),
254        Serde: "expected Bool dtype for BoolValue, got {dtype}"
255    );
256
257    Ok(ScalarValue::Bool(v))
258}
259
260/// Deserialize a [`ScalarValue::Primitive`] from a protobuf `Int64Value`.
261///
262/// Protobuf consolidates all signed integers into `i64`, so we narrow back to the original
263/// type using the provided [`DType`].
264fn int64_from_proto(v: i64, dtype: &DType) -> VortexResult<ScalarValue> {
265    vortex_ensure!(
266        dtype.is_primitive(),
267        Serde: "expected Primitive dtype for Int64Value, got {dtype}"
268    );
269
270    let pvalue = match dtype.as_ptype() {
271        PType::I8 => v.to_i8().map(PValue::I8),
272        PType::I16 => v.to_i16().map(PValue::I16),
273        PType::I32 => v.to_i32().map(PValue::I32),
274        PType::I64 => Some(PValue::I64(v)),
275        ptype => vortex_bail!(
276            Serde: "expected signed integer ptype for Int64Value, got {ptype}"
277        ),
278    }
279    .ok_or_else(|| vortex_err!(Serde: "Int64 value {v} out of range for dtype {dtype}"))?;
280
281    Ok(ScalarValue::Primitive(pvalue))
282}
283
284/// Deserialize a [`ScalarValue::Primitive`] from a protobuf `Uint64Value`.
285///
286/// Protobuf consolidates all unsigned integers into `u64`, so we narrow back to the original
287/// type using the provided [`DType`]. Also handles the backwards-compatible case where `f16`
288/// values were serialized as `u64` (via `f16::to_bits() as u64`).
289fn uint64_from_proto(v: u64, dtype: &DType) -> VortexResult<ScalarValue> {
290    vortex_ensure!(
291        dtype.is_primitive(),
292        Serde: "expected Primitive dtype for Uint64Value, got {dtype}"
293    );
294
295    let pvalue = match dtype.as_ptype() {
296        PType::U8 => v.to_u8().map(PValue::U8),
297        PType::U16 => v.to_u16().map(PValue::U16),
298        PType::U32 => v.to_u32().map(PValue::U32),
299        PType::U64 => Some(PValue::U64(v)),
300        // Backwards compatibility: f16 values were previously serialized as u64.
301        PType::F16 => v.to_u16().map(f16::from_bits).map(PValue::F16),
302        ptype => vortex_bail!(
303            Serde: "expected unsigned integer ptype for Uint64Value, got {ptype}"
304        ),
305    }
306    .ok_or_else(|| vortex_err!(Serde: "Uint64 value {v} out of range for dtype {dtype}"))?;
307
308    Ok(ScalarValue::Primitive(pvalue))
309}
310
311/// Deserialize a [`ScalarValue::Primitive`] from a protobuf `F16Value`.
312fn f16_from_proto(v: u64, dtype: &DType) -> VortexResult<ScalarValue> {
313    vortex_ensure!(
314        matches!(dtype, DType::Primitive(PType::F16, _)),
315        Serde: "expected F16 dtype for F16Value, got {dtype}"
316    );
317
318    let bits = u16::try_from(v)
319        .map_err(|_| vortex_err!(Serde: "f16 bitwise representation has more than 16 bits: {v}"))?;
320
321    Ok(ScalarValue::Primitive(PValue::F16(f16::from_bits(bits))))
322}
323
324/// Deserialize a [`ScalarValue::Primitive`] from a protobuf `F32Value`.
325fn f32_from_proto(v: f32, dtype: &DType) -> VortexResult<ScalarValue> {
326    vortex_ensure!(
327        matches!(dtype, DType::Primitive(PType::F32, _)),
328        Serde: "expected F32 dtype for F32Value, got {dtype}"
329    );
330
331    Ok(ScalarValue::Primitive(PValue::F32(v)))
332}
333
334/// Deserialize a [`ScalarValue::Primitive`] from a protobuf `F64Value`.
335fn f64_from_proto(v: f64, dtype: &DType) -> VortexResult<ScalarValue> {
336    vortex_ensure!(
337        matches!(dtype, DType::Primitive(PType::F64, _)),
338        Serde: "expected F64 dtype for F64Value, got {dtype}"
339    );
340
341    Ok(ScalarValue::Primitive(PValue::F64(v)))
342}
343
344/// Deserialize a [`ScalarValue::Utf8`] or [`ScalarValue::Binary`] from a protobuf
345/// `StringValue`.
346fn string_from_proto(s: &str, dtype: &DType) -> VortexResult<ScalarValue> {
347    match dtype {
348        DType::Utf8(_) => Ok(ScalarValue::Utf8(BufferString::from(s))),
349        DType::Binary(_) => Ok(ScalarValue::Binary(ByteBuffer::copy_from(s.as_bytes()))),
350        _ => vortex_bail!(
351            Serde: "expected Utf8 or Binary dtype for StringValue, got {dtype}"
352        ),
353    }
354}
355
356/// Deserialize a [`ScalarValue`] from a protobuf bytes and a `DType`.
357///
358/// Handles [`Utf8`](ScalarValue::Utf8), [`Binary`](ScalarValue::Binary), and
359/// [`Decimal`](ScalarValue::Decimal) dtypes.
360fn bytes_from_proto(bytes: &[u8], dtype: &DType) -> VortexResult<ScalarValue> {
361    match dtype {
362        DType::Utf8(_) => Ok(ScalarValue::Utf8(BufferString::try_from(bytes)?)),
363        DType::Binary(_) => Ok(ScalarValue::Binary(ByteBuffer::copy_from(bytes))),
364        // TODO(connor): This is incorrect, we need to verify this matches the `dtype`.
365        DType::Decimal(..) => Ok(ScalarValue::Decimal(match bytes.len() {
366            1 => DecimalValue::I8(bytes[0] as i8),
367            2 => DecimalValue::I16(i16::from_le_bytes(
368                bytes
369                    .try_into()
370                    .ok()
371                    .vortex_expect("Buffer has invalid number of bytes"),
372            )),
373            4 => DecimalValue::I32(i32::from_le_bytes(
374                bytes
375                    .try_into()
376                    .ok()
377                    .vortex_expect("Buffer has invalid number of bytes"),
378            )),
379            8 => DecimalValue::I64(i64::from_le_bytes(
380                bytes
381                    .try_into()
382                    .ok()
383                    .vortex_expect("Buffer has invalid number of bytes"),
384            )),
385            16 => DecimalValue::I128(i128::from_le_bytes(
386                bytes
387                    .try_into()
388                    .ok()
389                    .vortex_expect("Buffer has invalid number of bytes"),
390            )),
391            32 => DecimalValue::I256(i256::from_le_bytes(
392                bytes
393                    .try_into()
394                    .ok()
395                    .vortex_expect("Buffer has invalid number of bytes"),
396            )),
397            l => vortex_bail!(Serde: "invalid decimal byte length: {l}"),
398        })),
399        _ => vortex_bail!(
400            Serde: "expected Utf8, Binary, or Decimal dtype for BytesValue, got {dtype}"
401        ),
402    }
403}
404
405/// Deserialize a [`ScalarValue::List`] from a protobuf `ListValue`.
406fn list_from_proto(v: &ListValue, dtype: &DType) -> VortexResult<ScalarValue> {
407    let element_dtype = dtype
408        .as_list_element_opt()
409        .ok_or_else(|| vortex_err!(Serde: "expected List dtype for ListValue, got {dtype}"))?;
410
411    let mut values = Vec::with_capacity(v.values.len());
412    for elem in v.values.iter() {
413        values.push(ScalarValue::from_proto(elem, element_dtype.as_ref())?);
414    }
415
416    Ok(ScalarValue::List(values))
417}
418
419#[cfg(test)]
420mod tests {
421    use std::sync::Arc;
422
423    use vortex_buffer::BufferString;
424    use vortex_dtype::DType;
425    use vortex_dtype::DecimalDType;
426    use vortex_dtype::Nullability;
427    use vortex_dtype::PType;
428    use vortex_dtype::half::f16;
429    use vortex_error::vortex_panic;
430    use vortex_proto::scalar as pb;
431    use vortex_session::VortexSession;
432
433    use super::*;
434    use crate::scalar::DecimalValue;
435    use crate::scalar::Scalar;
436    use crate::scalar::ScalarValue;
437
438    fn session() -> VortexSession {
439        VortexSession::empty()
440    }
441
442    fn round_trip(scalar: Scalar) {
443        assert_eq!(
444            scalar,
445            Scalar::from_proto(&pb::Scalar::from(&scalar), &session()).unwrap(),
446        );
447    }
448
449    #[test]
450    fn test_null() {
451        round_trip(Scalar::null(DType::Null));
452    }
453
454    #[test]
455    fn test_bool() {
456        round_trip(Scalar::new(
457            DType::Bool(Nullability::Nullable),
458            Some(ScalarValue::Bool(true)),
459        ));
460    }
461
462    #[test]
463    fn test_primitive() {
464        round_trip(Scalar::new(
465            DType::Primitive(PType::I32, Nullability::Nullable),
466            Some(ScalarValue::Primitive(42i32.into())),
467        ));
468    }
469
470    #[test]
471    fn test_buffer() {
472        round_trip(Scalar::new(
473            DType::Binary(Nullability::Nullable),
474            Some(ScalarValue::Binary(vec![1, 2, 3].into())),
475        ));
476    }
477
478    #[test]
479    fn test_buffer_string() {
480        round_trip(Scalar::new(
481            DType::Utf8(Nullability::Nullable),
482            Some(ScalarValue::Utf8(BufferString::from("hello".to_string()))),
483        ));
484    }
485
486    #[test]
487    fn test_list() {
488        round_trip(Scalar::new(
489            DType::List(
490                Arc::new(DType::Primitive(PType::I32, Nullability::Nullable)),
491                Nullability::Nullable,
492            ),
493            Some(ScalarValue::List(vec![
494                Some(ScalarValue::Primitive(42i32.into())),
495                Some(ScalarValue::Primitive(43i32.into())),
496            ])),
497        ));
498    }
499
500    #[test]
501    fn test_f16() {
502        round_trip(Scalar::primitive(
503            f16::from_f32(0.42),
504            Nullability::Nullable,
505        ));
506    }
507
508    #[test]
509    fn test_i8() {
510        round_trip(Scalar::new(
511            DType::Primitive(PType::I8, Nullability::Nullable),
512            Some(ScalarValue::Primitive(i8::MIN.into())),
513        ));
514
515        round_trip(Scalar::new(
516            DType::Primitive(PType::I8, Nullability::Nullable),
517            Some(ScalarValue::Primitive(0i8.into())),
518        ));
519
520        round_trip(Scalar::new(
521            DType::Primitive(PType::I8, Nullability::Nullable),
522            Some(ScalarValue::Primitive(i8::MAX.into())),
523        ));
524    }
525
526    #[test]
527    fn test_decimal_i32_roundtrip() {
528        // A typical decimal with moderate precision and scale.
529        round_trip(Scalar::decimal(
530            DecimalValue::I32(123_456),
531            DecimalDType::new(10, 2),
532            Nullability::NonNullable,
533        ));
534    }
535
536    #[test]
537    fn test_decimal_i128_roundtrip() {
538        // A large decimal value that requires i128 storage.
539        round_trip(Scalar::decimal(
540            DecimalValue::I128(99_999_999_999_999_999_999),
541            DecimalDType::new(38, 6),
542            Nullability::Nullable,
543        ));
544    }
545
546    #[test]
547    fn test_decimal_null_roundtrip() {
548        round_trip(Scalar::null(DType::Decimal(
549            DecimalDType::new(10, 2),
550            Nullability::Nullable,
551        )));
552    }
553
554    #[test]
555    fn test_scalar_value_serde_roundtrip_binary() {
556        round_trip(Scalar::binary(
557            ByteBuffer::copy_from(b"hello"),
558            Nullability::NonNullable,
559        ));
560    }
561
562    #[test]
563    fn test_scalar_value_serde_roundtrip_utf8() {
564        round_trip(Scalar::utf8("hello", Nullability::NonNullable));
565    }
566
567    #[test]
568    fn test_backcompat_f16_serialized_as_u64() {
569        // Backwards compatibility test for the legacy f16 serialization format.
570        //
571        // Previously, f16 ScalarValues were serialized as `Uint64Value(v.to_bits() as u64)` because
572        // the proto schema only had 64-bit integer types, and f16's underlying representation is
573        // u16 which got widened to u64.
574        //
575        // The current implementation uses a dedicated `F16Value` proto field, but we must still be
576        // able to deserialize the old format. This test verifies that:
577        //
578        // 1. A `Uint64Value` containing f16 bits can be read as a U64 primitive (the raw bits).
579        // 2. When wrapped in a Scalar with F16 dtype, the value is correctly interpreted as f16.
580        //
581        // This ensures data written with the old serialization format remains readable.
582
583        // Simulate the old serialization: f16(0.42) stored as Uint64Value with its bit pattern.
584        let f16_value = f16::from_f32(0.42);
585        let f16_bits_as_u64 = f16_value.to_bits() as u64; // 14008
586
587        let pb_scalar_value = pb::ScalarValue {
588            kind: Some(Kind::Uint64Value(f16_bits_as_u64)),
589        };
590
591        // Step 1: Verify the normal U64 scalar.
592        let scalar_value = ScalarValue::from_proto(
593            &pb_scalar_value,
594            &DType::Primitive(PType::U64, Nullability::NonNullable),
595        )
596        .unwrap();
597        assert_eq!(
598            scalar_value.as_ref().map(|v| v.as_primitive()),
599            Some(&PValue::U64(14008u64)),
600        );
601
602        // Step 2: Verify that when we use F16 dtype, the Uint64Value is correctly interpreted.
603        let scalar_value_f16 = ScalarValue::from_proto(
604            &pb_scalar_value,
605            &DType::Primitive(PType::F16, Nullability::Nullable),
606        )
607        .unwrap();
608
609        let scalar = Scalar::new(
610            DType::Primitive(PType::F16, Nullability::Nullable),
611            scalar_value_f16,
612        );
613
614        assert_eq!(
615            scalar.as_primitive().pvalue().unwrap(),
616            PValue::F16(f16::from_f32(0.42)),
617            "Uint64Value should be correctly interpreted as f16 when dtype is F16"
618        );
619    }
620
621    #[test]
622    fn test_scalar_value_direct_roundtrip_f16() {
623        // Test that ScalarValue with f16 roundtrips correctly without going through Scalar.
624        let f16_values = vec![
625            f16::from_f32(0.0),
626            f16::from_f32(1.0),
627            f16::from_f32(-1.0),
628            f16::from_f32(0.42),
629            f16::from_f32(5.722046e-6),
630            f16::from_f32(std::f32::consts::PI),
631            f16::INFINITY,
632            f16::NEG_INFINITY,
633            f16::NAN,
634        ];
635
636        for f16_val in f16_values {
637            let scalar_value = ScalarValue::Primitive(PValue::F16(f16_val));
638            let pb_value = ScalarValue::to_proto(Some(&scalar_value));
639            let read_back = ScalarValue::from_proto(
640                &pb_value,
641                &DType::Primitive(PType::F16, Nullability::NonNullable),
642            )
643            .unwrap();
644
645            match (&scalar_value, read_back.as_ref()) {
646                (
647                    ScalarValue::Primitive(PValue::F16(original)),
648                    Some(ScalarValue::Primitive(PValue::F16(roundtripped))),
649                ) => {
650                    if original.is_nan() && roundtripped.is_nan() {
651                        // NaN values are equal for our purposes.
652                        continue;
653                    }
654                    assert_eq!(
655                        original, roundtripped,
656                        "F16 value {original:?} did not roundtrip correctly"
657                    );
658                }
659                _ => {
660                    vortex_panic!(
661                        "Expected f16 primitive values, got {scalar_value:?} and {read_back:?}"
662                    )
663                }
664            }
665        }
666    }
667
668    #[test]
669    fn test_scalar_value_direct_roundtrip_preserves_values() {
670        // Test that ScalarValue roundtripping preserves values (but not necessarily exact types).
671        // Note: Proto encoding consolidates integer types (u8/u16/u32 → u64, i8/i16/i32 → i64).
672
673        // Test cases that should roundtrip exactly.
674        let exact_roundtrip_cases: Vec<(&str, Option<ScalarValue>, DType)> = vec![
675            ("null", None, DType::Null),
676            (
677                "bool_true",
678                Some(ScalarValue::Bool(true)),
679                DType::Bool(Nullability::Nullable),
680            ),
681            (
682                "bool_false",
683                Some(ScalarValue::Bool(false)),
684                DType::Bool(Nullability::Nullable),
685            ),
686            (
687                "u64",
688                Some(ScalarValue::Primitive(PValue::U64(18446744073709551615))),
689                DType::Primitive(PType::U64, Nullability::Nullable),
690            ),
691            (
692                "i64",
693                Some(ScalarValue::Primitive(PValue::I64(-9223372036854775808))),
694                DType::Primitive(PType::I64, Nullability::Nullable),
695            ),
696            (
697                "f32",
698                Some(ScalarValue::Primitive(PValue::F32(std::f32::consts::E))),
699                DType::Primitive(PType::F32, Nullability::Nullable),
700            ),
701            (
702                "f64",
703                Some(ScalarValue::Primitive(PValue::F64(std::f64::consts::PI))),
704                DType::Primitive(PType::F64, Nullability::Nullable),
705            ),
706            (
707                "string",
708                Some(ScalarValue::Utf8(BufferString::from("test"))),
709                DType::Utf8(Nullability::Nullable),
710            ),
711            (
712                "bytes",
713                Some(ScalarValue::Binary(vec![1, 2, 3, 4, 5].into())),
714                DType::Binary(Nullability::Nullable),
715            ),
716        ];
717
718        for (name, value, dtype) in exact_roundtrip_cases {
719            let pb_value = ScalarValue::to_proto(value.as_ref());
720            let read_back = ScalarValue::from_proto(&pb_value, &dtype).unwrap();
721
722            let original_debug = format!("{value:?}");
723            let roundtrip_debug = format!("{read_back:?}");
724            assert_eq!(
725                original_debug, roundtrip_debug,
726                "ScalarValue {name} did not roundtrip exactly"
727            );
728        }
729
730        // Test cases where type changes but value is preserved.
731        // Unsigned integers consolidate to U64.
732        let unsigned_cases = vec![
733            (
734                "u8",
735                ScalarValue::Primitive(PValue::U8(255)),
736                DType::Primitive(PType::U8, Nullability::Nullable),
737                255u64,
738            ),
739            (
740                "u16",
741                ScalarValue::Primitive(PValue::U16(65535)),
742                DType::Primitive(PType::U16, Nullability::Nullable),
743                65535u64,
744            ),
745            (
746                "u32",
747                ScalarValue::Primitive(PValue::U32(4294967295)),
748                DType::Primitive(PType::U32, Nullability::Nullable),
749                4294967295u64,
750            ),
751        ];
752
753        for (name, value, dtype, expected) in unsigned_cases {
754            let pb_value = ScalarValue::to_proto(Some(&value));
755            let read_back = ScalarValue::from_proto(&pb_value, &dtype).unwrap();
756
757            match read_back.as_ref() {
758                Some(ScalarValue::Primitive(pv)) => {
759                    let v = match pv {
760                        PValue::U8(v) => *v as u64,
761                        PValue::U16(v) => *v as u64,
762                        PValue::U32(v) => *v as u64,
763                        PValue::U64(v) => *v,
764                        _ => vortex_panic!("Unexpected primitive type for {name}: {pv:?}"),
765                    };
766                    assert_eq!(
767                        v, expected,
768                        "ScalarValue {name} value not preserved: expected {expected}, got {v}"
769                    );
770                }
771                _ => vortex_panic!("Unexpected type after roundtrip for {name}: {read_back:?}"),
772            }
773        }
774
775        // Signed integers consolidate to I64.
776        let signed_cases = vec![
777            (
778                "i8",
779                ScalarValue::Primitive(PValue::I8(-128)),
780                DType::Primitive(PType::I8, Nullability::Nullable),
781                -128i64,
782            ),
783            (
784                "i16",
785                ScalarValue::Primitive(PValue::I16(-32768)),
786                DType::Primitive(PType::I16, Nullability::Nullable),
787                -32768i64,
788            ),
789            (
790                "i32",
791                ScalarValue::Primitive(PValue::I32(-2147483648)),
792                DType::Primitive(PType::I32, Nullability::Nullable),
793                -2147483648i64,
794            ),
795        ];
796
797        for (name, value, dtype, expected) in signed_cases {
798            let pb_value = ScalarValue::to_proto(Some(&value));
799            let read_back = ScalarValue::from_proto(&pb_value, &dtype).unwrap();
800
801            match read_back.as_ref() {
802                Some(ScalarValue::Primitive(pv)) => {
803                    let v = match pv {
804                        PValue::I8(v) => *v as i64,
805                        PValue::I16(v) => *v as i64,
806                        PValue::I32(v) => *v as i64,
807                        PValue::I64(v) => *v,
808                        _ => vortex_panic!("Unexpected primitive type for {name}: {pv:?}"),
809                    };
810                    assert_eq!(
811                        v, expected,
812                        "ScalarValue {name} value not preserved: expected {expected}, got {v}"
813                    );
814                }
815                _ => vortex_panic!("Unexpected type after roundtrip for {name}: {read_back:?}"),
816            }
817        }
818    }
819}