Skip to main content

vortex_array/scalar/
validate.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexError;
5use vortex_error::VortexExpect;
6use vortex_error::VortexResult;
7use vortex_error::vortex_bail;
8use vortex_error::vortex_ensure;
9use vortex_error::vortex_ensure_eq;
10use vortex_error::vortex_err;
11
12use crate::dtype::DType;
13use crate::dtype::PType;
14use crate::scalar::PValue;
15use crate::scalar::Scalar;
16use crate::scalar::ScalarValue;
17
18impl Scalar {
19    /// Validate that the given [`ScalarValue`] is compatible with the given [`DType`].
20    #[inline]
21    pub fn validate(dtype: &DType, value: Option<&ScalarValue>) -> VortexResult<()> {
22        if Self::is_compatible(dtype, value) {
23            return Ok(());
24        }
25
26        Err(Self::incompatible_error(dtype, value))
27    }
28
29    /// Returns `true` if the given [`ScalarValue`] is compatible with the given [`DType`].
30    ///
31    /// This is the fast path of [`Self::validate`]. It must accept exactly the values that
32    /// [`Self::validate_detailed`] accepts, but it builds no error messages.
33    ///
34    /// Null values and leaf types are checked inline, so that callers with a known dtype fold the
35    /// check away. Other types go to [`Self::is_compatible_nested`].
36    #[inline]
37    pub(super) fn is_compatible(dtype: &DType, value: Option<&ScalarValue>) -> bool {
38        let Some(value) = value else {
39            return dtype.is_nullable();
40        };
41
42        match (dtype, value) {
43            (DType::Bool(_), ScalarValue::Bool(_))
44            | (DType::Utf8(_), ScalarValue::Utf8(_))
45            | (DType::Binary(_), ScalarValue::Binary(_)) => true,
46            (DType::Primitive(ptype, _), ScalarValue::Primitive(pvalue)) => {
47                // See `validate_detailed` for the `f16` backwards compatibility case.
48                pvalue.ptype() == *ptype
49                    || (matches!(ptype, PType::F16) && matches!(pvalue, PValue::U64(_)))
50            }
51            _ => Self::is_compatible_nested(dtype, value),
52        }
53    }
54
55    /// The out-of-line part of [`Self::is_compatible`] for non-null values of all other types.
56    fn is_compatible_nested(dtype: &DType, value: &ScalarValue) -> bool {
57        match (dtype, value) {
58            (DType::Decimal(dec_dtype, _), ScalarValue::Decimal(dvalue)) => {
59                dvalue.fits_in_precision(*dec_dtype)
60            }
61            (DType::List(elem_dtype, _), ScalarValue::Tuple(elements)) => elements
62                .iter()
63                .all(|element| Self::is_compatible(elem_dtype, element.as_ref())),
64            (DType::FixedSizeList(elem_dtype, size, _), ScalarValue::Tuple(elements)) => {
65                elements.len() == *size as usize
66                    && elements
67                        .iter()
68                        .all(|element| Self::is_compatible(elem_dtype, element.as_ref()))
69            }
70            (DType::Map(map, _), ScalarValue::Tuple(entries)) => {
71                let key_dtype = map.key_dtype();
72                let value_dtype = map.value_dtype();
73
74                entries.iter().all(|entry| match entry {
75                    Some(ScalarValue::Tuple(values)) => {
76                        values.len() == 2
77                            && Self::is_compatible(&key_dtype, values[0].as_ref())
78                            && Self::is_compatible(&value_dtype, values[1].as_ref())
79                    }
80                    _ => false,
81                })
82            }
83            (DType::Struct(fields, _), ScalarValue::Tuple(values)) => {
84                values.len() == fields.nfields()
85                    && fields
86                        .field_dtypes()
87                        .zip(values.iter())
88                        .all(|(field, value)| Self::is_compatible(field, value.as_ref()))
89            }
90            (DType::Union(variants, _), ScalarValue::Union(union_value)) => variants
91                .tag_to_child_index(union_value.type_id())
92                .and_then(|child_index| variants.variant_by_index(child_index))
93                .is_some_and(|child_dtype| {
94                    Self::is_compatible(&child_dtype, union_value.child_value())
95                }),
96            (DType::Variant(_), ScalarValue::Variant(inner)) => {
97                Self::is_compatible(inner.dtype(), inner.value())
98                    && (!inner.is_null() || matches!(inner.dtype(), DType::Null))
99            }
100            (DType::Extension(ext_dtype), value) => ext_dtype.validate_storage_value(value).is_ok(),
101            _ => false,
102        }
103    }
104
105    /// Builds the error for a [`ScalarValue`] that is not compatible with the [`DType`].
106    ///
107    /// Kept out of line so that the formatting code does not count against the hot callers when
108    /// the inliner sizes them.
109    #[cold]
110    #[inline(never)]
111    pub(super) fn incompatible_error(dtype: &DType, value: Option<&ScalarValue>) -> VortexError {
112        match Self::validate_detailed(dtype, value) {
113            Err(err) => err,
114            Ok(()) => vortex_err!(
115                "scalar validation fast path rejected value {value:?} for dtype {dtype}, but the \
116                 detailed check accepted it"
117            ),
118        }
119    }
120
121    /// Validates the [`ScalarValue`] against the [`DType`], with a detailed error message.
122    fn validate_detailed(dtype: &DType, value: Option<&ScalarValue>) -> VortexResult<()> {
123        let Some(value) = value else {
124            vortex_ensure!(
125                dtype.is_nullable(),
126                "non-nullable dtype {dtype} cannot hold a null value",
127            );
128            return Ok(());
129        };
130
131        // From here onwards, we know that the value is not null.
132        match dtype {
133            DType::Null => {
134                vortex_bail!("null dtype cannot hold a non-null value {value}");
135            }
136            DType::Bool(_) => {
137                vortex_ensure!(
138                    matches!(value, ScalarValue::Bool(_)),
139                    "bool dtype expected Bool value, got {value}",
140                );
141            }
142            DType::Primitive(ptype, _) => {
143                let ScalarValue::Primitive(pvalue) = value else {
144                    vortex_bail!("primitive dtype {ptype} expected Primitive value, got {value}",);
145                };
146
147                // Note that this is a backwards compatibility check for poor design in the
148                // previous implementation. `f16` `ScalarValue`s used to be serialized as
149                // `pb::ScalarValue::Uint64Value(v.to_bits() as u64)`, so we need to ensure
150                // that we can still represent them as such.
151                let f16_backcompat_still_works =
152                    matches!(ptype, &PType::F16) && matches!(pvalue, PValue::U64(_));
153
154                vortex_ensure!(
155                    f16_backcompat_still_works || pvalue.ptype() == *ptype,
156                    "primitive dtype {ptype} is not compatible with value {pvalue}",
157                );
158            }
159            DType::Decimal(dec_dtype, _) => {
160                let ScalarValue::Decimal(dvalue) = value else {
161                    vortex_bail!("decimal dtype expected Decimal value, got {value}");
162                };
163
164                vortex_ensure!(
165                    dvalue.fits_in_precision(*dec_dtype),
166                    "decimal value {dvalue} does not fit in precision of {dec_dtype}",
167                );
168            }
169            DType::Utf8(_) => {
170                vortex_ensure!(
171                    matches!(value, ScalarValue::Utf8(_)),
172                    "utf8 dtype expected Utf8 value, got {value}",
173                );
174            }
175            DType::Binary(_) => {
176                vortex_ensure!(
177                    matches!(value, ScalarValue::Binary(_)),
178                    "binary dtype expected Binary value, got {value}",
179                );
180            }
181            DType::List(elem_dtype, _) => {
182                let ScalarValue::Tuple(elements) = value else {
183                    vortex_bail!("list dtype expected Tuple value, got {value}");
184                };
185
186                for (i, element) in elements.iter().enumerate() {
187                    Self::validate_detailed(elem_dtype.as_ref(), element.as_ref())
188                        .map_err(|e| vortex_error::vortex_err!("list element at index {i}: {e}"))?;
189                }
190            }
191            DType::FixedSizeList(elem_dtype, size, _) => {
192                let ScalarValue::Tuple(elements) = value else {
193                    vortex_bail!("fixed-size list dtype expected Tuple value, got {value}",);
194                };
195
196                let len = elements.len();
197                vortex_ensure_eq!(
198                    len,
199                    *size as usize,
200                    "fixed-size list scalar has the wrong number of elements",
201                );
202
203                for (i, element) in elements.iter().enumerate() {
204                    Self::validate_detailed(elem_dtype.as_ref(), element.as_ref()).map_err(
205                        |e| vortex_error::vortex_err!("fixed-size list element at index {i}: {e}",),
206                    )?;
207                }
208            }
209            DType::Map(map, _) => {
210                let ScalarValue::Tuple(entries) = value else {
211                    vortex_bail!("map dtype expected Tuple value, got {value}");
212                };
213                let key_dtype = map.key_dtype();
214                let value_dtype = map.value_dtype();
215
216                for (index, entry) in entries.iter().enumerate() {
217                    let entry = entry.as_ref().ok_or_else(|| {
218                        vortex_error::vortex_err!("map entry at index {index} cannot be null")
219                    })?;
220                    let ScalarValue::Tuple(values) = entry else {
221                        vortex_bail!(
222                            "map entry at index {index} expected Tuple value, got {entry}"
223                        );
224                    };
225                    vortex_ensure_eq!(
226                        values.len(),
227                        2,
228                        "map entry at index {index} has the wrong number of values",
229                    );
230
231                    Self::validate_detailed(&key_dtype, values[0].as_ref()).map_err(|error| {
232                        vortex_error::vortex_err!("map key at entry {index}: {error}")
233                    })?;
234                    Self::validate_detailed(&value_dtype, values[1].as_ref()).map_err(|error| {
235                        vortex_error::vortex_err!("map value at entry {index}: {error}")
236                    })?;
237                }
238            }
239            DType::Struct(fields, _) => {
240                let ScalarValue::Tuple(values) = value else {
241                    vortex_bail!("struct dtype expected Tuple value, got {value}");
242                };
243
244                let nfields = fields.nfields();
245                let nvalues = values.len();
246                vortex_ensure_eq!(
247                    nvalues,
248                    nfields,
249                    "struct scalar has the wrong number of fields",
250                );
251
252                for (field, field_value) in fields.field_dtypes().zip(values.iter()) {
253                    Self::validate_detailed(field, field_value.as_ref())?;
254                }
255            }
256            DType::Union(variants, _) => {
257                let ScalarValue::Union(union_value) = value else {
258                    vortex_bail!("union dtype expected Union value, got {value}");
259                };
260
261                let type_id = union_value.type_id();
262                let Some(child_index) = variants.tag_to_child_index(type_id) else {
263                    vortex_bail!(
264                        "union value has unknown type ID {type_id}; expected one of {:?}",
265                        variants.type_ids()
266                    );
267                };
268
269                let child_dtype = variants
270                    .variant_by_index(child_index)
271                    .vortex_expect("resolved union child index must be valid");
272
273                Self::validate_detailed(&child_dtype, union_value.child_value()).map_err(
274                    |error| {
275                        vortex_error::vortex_err!(
276                            "union value for type ID {type_id} is invalid for dtype {child_dtype}: \
277                         {error}"
278                        )
279                    },
280                )?;
281            }
282            DType::Variant(_) => {
283                let ScalarValue::Variant(inner) = value else {
284                    vortex_bail!("variant dtype expected Variant value, got {value}");
285                };
286
287                Self::validate_detailed(inner.dtype(), inner.value())?;
288                vortex_ensure!(
289                    !inner.is_null() || matches!(inner.dtype(), DType::Null),
290                    "variant nulls must use a nested null scalar, got {}",
291                    inner.dtype(),
292                );
293            }
294            DType::Extension(ext_dtype) => ext_dtype.validate_storage_value(value)?,
295        }
296
297        Ok(())
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use std::sync::Arc;
304
305    use vortex_error::VortexResult;
306
307    use crate::dtype::DType;
308    use crate::dtype::DecimalDType;
309    use crate::dtype::FieldNames;
310    use crate::dtype::MapDType;
311    use crate::dtype::Nullability;
312    use crate::dtype::PType;
313    use crate::dtype::StructFields;
314    use crate::dtype::UnionVariants;
315    use crate::scalar::DecimalValue;
316    use crate::scalar::PValue;
317    use crate::scalar::Scalar;
318    use crate::scalar::ScalarValue;
319    use crate::scalar::UnionValue;
320
321    /// The fast check in `is_compatible` must accept exactly what `validate_detailed` accepts.
322    #[test]
323    fn fast_and_detailed_checks_agree() -> VortexResult<()> {
324        use Nullability::NonNullable;
325        use Nullability::Nullable;
326
327        let i32_value = || Some(ScalarValue::Primitive(PValue::I32(1)));
328        let i32_dtype = DType::Primitive(PType::I32, NonNullable);
329        let struct_dtype = DType::Struct(
330            StructFields::new(
331                FieldNames::from(["a", "b"]),
332                vec![i32_dtype.clone(), DType::Utf8(Nullable)],
333            ),
334            NonNullable,
335        );
336        let map_dtype = DType::Map(
337            MapDType::try_new(i32_dtype.clone(), DType::Bool(Nullable), false)?,
338            NonNullable,
339        );
340        let union_dtype = DType::Union(
341            UnionVariants::try_new(["int"].into(), vec![i32_dtype.clone()], vec![3])?,
342            NonNullable,
343        );
344        let list_dtype = DType::List(Arc::new(i32_dtype.clone()), NonNullable);
345        let fsl_dtype = DType::FixedSizeList(Arc::new(i32_dtype.clone()), 2, NonNullable);
346        let decimal_dtype = DType::Decimal(DecimalDType::new(3, 0), NonNullable);
347        let tuple = |values: Vec<Option<ScalarValue>>| Some(ScalarValue::Tuple(values));
348
349        let cases = [
350            (DType::Null, None),
351            (DType::Null, Some(ScalarValue::Bool(true))),
352            (DType::Bool(Nullable), None),
353            (DType::Bool(NonNullable), None),
354            (DType::Bool(NonNullable), Some(ScalarValue::Bool(true))),
355            (DType::Bool(NonNullable), i32_value()),
356            (i32_dtype.clone(), i32_value()),
357            (i32_dtype, Some(ScalarValue::Primitive(PValue::I64(1)))),
358            (
359                DType::Primitive(PType::F16, NonNullable),
360                Some(ScalarValue::Primitive(PValue::U64(1))),
361            ),
362            (
363                decimal_dtype.clone(),
364                Some(ScalarValue::Decimal(DecimalValue::I32(999))),
365            ),
366            (
367                decimal_dtype,
368                Some(ScalarValue::Decimal(DecimalValue::I32(1000))),
369            ),
370            (
371                DType::Utf8(NonNullable),
372                Some(ScalarValue::Utf8("a".into())),
373            ),
374            (DType::Utf8(NonNullable), Some(ScalarValue::Bool(true))),
375            (
376                DType::Binary(NonNullable),
377                Some(ScalarValue::Utf8("a".into())),
378            ),
379            (list_dtype.clone(), tuple(vec![i32_value(), i32_value()])),
380            (list_dtype.clone(), tuple(vec![i32_value(), None])),
381            (list_dtype, i32_value()),
382            (fsl_dtype.clone(), tuple(vec![i32_value(), i32_value()])),
383            (fsl_dtype, tuple(vec![i32_value()])),
384            (
385                map_dtype.clone(),
386                tuple(vec![tuple(vec![i32_value(), None])]),
387            ),
388            (map_dtype.clone(), tuple(vec![None])),
389            (map_dtype.clone(), tuple(vec![tuple(vec![None, None])])),
390            (map_dtype, tuple(vec![tuple(vec![i32_value()])])),
391            (
392                struct_dtype.clone(),
393                tuple(vec![i32_value(), Some(ScalarValue::Utf8("a".into()))]),
394            ),
395            (struct_dtype.clone(), tuple(vec![i32_value(), None])),
396            (struct_dtype.clone(), tuple(vec![None, None])),
397            (struct_dtype, tuple(vec![i32_value()])),
398            (
399                union_dtype.clone(),
400                Some(ScalarValue::Union(UnionValue::new(3, i32_value()))),
401            ),
402            (
403                union_dtype.clone(),
404                Some(ScalarValue::Union(UnionValue::new(4, i32_value()))),
405            ),
406            (
407                union_dtype,
408                Some(ScalarValue::Union(UnionValue::new(3, None))),
409            ),
410            (
411                DType::Variant(NonNullable),
412                Some(ScalarValue::Variant(Box::new(Scalar::null(DType::Null)))),
413            ),
414            (
415                DType::Variant(NonNullable),
416                Some(ScalarValue::Variant(Box::new(Scalar::null(DType::Bool(
417                    Nullable,
418                ))))),
419            ),
420            (
421                DType::Variant(NonNullable),
422                Some(ScalarValue::Variant(Box::new(Scalar::from(true)))),
423            ),
424        ];
425
426        for (dtype, value) in &cases {
427            assert_eq!(
428                Scalar::is_compatible(dtype, value.as_ref()),
429                Scalar::validate_detailed(dtype, value.as_ref()).is_ok(),
430                "fast and detailed checks disagree for {dtype} and {value:?}",
431            );
432        }
433
434        Ok(())
435    }
436
437    #[test]
438    fn union_rejects_unknown_tag_and_wrong_value() -> VortexResult<()> {
439        let variants = UnionVariants::try_new(
440            ["int", "string"].into(),
441            vec![
442                DType::Primitive(PType::I32, Nullability::Nullable),
443                DType::Utf8(Nullability::NonNullable),
444            ],
445            vec![5, 9],
446        )?;
447        let dtype = DType::Union(variants, Nullability::NonNullable);
448
449        assert!(
450            Scalar::try_new(
451                dtype.clone(),
452                Some(ScalarValue::Union(UnionValue::new(
453                    7,
454                    Scalar::primitive(42_i32, Nullability::Nullable).into_value(),
455                ))),
456            )
457            .is_err()
458        );
459
460        assert!(
461            Scalar::try_new(
462                dtype,
463                Some(ScalarValue::Union(UnionValue::new(
464                    5,
465                    Scalar::utf8("wrong", Nullability::NonNullable).into_value(),
466                ))),
467            )
468            .is_err()
469        );
470
471        Ok(())
472    }
473}