Skip to main content

vortex_array/extension/uuid/
vtable.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use uuid;
5use vortex_error::VortexResult;
6use vortex_error::vortex_bail;
7use vortex_error::vortex_ensure;
8use vortex_error::vortex_ensure_eq;
9use vortex_error::vortex_err;
10
11use crate::dtype::DType;
12use crate::dtype::PType;
13use crate::dtype::extension::ExtDType;
14use crate::dtype::extension::ExtId;
15use crate::dtype::extension::ExtVTable;
16use crate::extension::uuid::Uuid;
17use crate::extension::uuid::UuidMetadata;
18use crate::extension::uuid::metadata::u8_to_version;
19use crate::scalar::PValue;
20use crate::scalar::ScalarValue;
21
22/// The number of bytes in a UUID.
23pub(crate) const UUID_BYTE_LEN: usize = 16;
24
25impl ExtVTable for Uuid {
26    type Metadata = UuidMetadata;
27    type NativeValue<'a> = uuid::Uuid;
28
29    fn id(&self) -> ExtId {
30        ExtId::new_ref("vortex.uuid")
31    }
32
33    fn serialize_metadata(&self, metadata: &Self::Metadata) -> VortexResult<Vec<u8>> {
34        match metadata.version {
35            None => Ok(Vec::new()),
36            Some(v) => Ok(vec![v as u8]),
37        }
38    }
39
40    fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult<Self::Metadata> {
41        let version = match metadata.len() {
42            0 => None,
43            1 => Some(u8_to_version(metadata[0])?),
44            other => vortex_bail!("UUID metadata must be 0 or 1 bytes, got {other}"),
45        };
46
47        Ok(UuidMetadata { version })
48    }
49
50    fn validate_dtype(&self, ext_dtype: &ExtDType<Self>) -> VortexResult<()> {
51        let storage_dtype = ext_dtype.storage_dtype();
52        let DType::FixedSizeList(element_dtype, list_size, _nullability) = storage_dtype else {
53            vortex_bail!("UUID storage dtype must be a FixedSizeList, got {storage_dtype}");
54        };
55
56        vortex_ensure_eq!(
57            *list_size as usize,
58            UUID_BYTE_LEN,
59            "UUID storage FixedSizeList must have size {UUID_BYTE_LEN}, got {list_size}"
60        );
61
62        let DType::Primitive(ptype, elem_nullability) = element_dtype.as_ref() else {
63            vortex_bail!("UUID element dtype must be Primitive(U8), got {element_dtype}");
64        };
65
66        vortex_ensure_eq!(
67            *ptype,
68            PType::U8,
69            "UUID element dtype must be U8, got {ptype}"
70        );
71        vortex_ensure!(
72            !elem_nullability.is_nullable(),
73            "UUID element dtype must be non-nullable"
74        );
75
76        Ok(())
77    }
78
79    fn unpack_native<'a>(
80        &self,
81        ext_dtype: &ExtDType<Self>,
82        storage_value: &'a ScalarValue,
83    ) -> VortexResult<Self::NativeValue<'a>> {
84        let elements = storage_value.as_list();
85        vortex_ensure_eq!(
86            elements.len(),
87            UUID_BYTE_LEN,
88            "UUID scalar must have exactly {UUID_BYTE_LEN} bytes, got {}",
89            elements.len()
90        );
91
92        let mut bytes = [0u8; UUID_BYTE_LEN];
93        for (i, elem) in elements.iter().enumerate() {
94            let Some(scalar_value) = elem else {
95                vortex_bail!("UUID byte at index {i} must not be null");
96            };
97            let PValue::U8(b) = scalar_value.as_primitive() else {
98                vortex_bail!("UUID byte at index {i} must be U8");
99            };
100            bytes[i] = *b;
101        }
102
103        let parsed = uuid::Uuid::from_bytes(bytes);
104
105        // Verify the parsed UUID matches the expected version, if one is set.
106        if let Some(expected) = ext_dtype.metadata().version {
107            let expected = expected as u8;
108            let actual = parsed
109                .get_version()
110                .ok_or_else(|| vortex_err!("UUID has unrecognized version nibble"))?
111                as u8;
112
113            vortex_ensure_eq!(
114                expected,
115                actual,
116                "UUID version mismatch: expected v{expected}, got v{actual}",
117            );
118        }
119
120        Ok(parsed)
121    }
122}
123
124#[expect(
125    clippy::cast_possible_truncation,
126    reason = "UUID_BYTE_LEN always fits both usize and u32"
127)]
128#[cfg(test)]
129mod tests {
130    use std::sync::Arc;
131
132    use rstest::rstest;
133    use uuid::Version;
134    use vortex_error::VortexResult;
135
136    use crate::dtype::DType;
137    use crate::dtype::Nullability;
138    use crate::dtype::PType;
139    use crate::dtype::extension::ExtDType;
140    use crate::dtype::extension::ExtVTable;
141    use crate::extension::uuid::Uuid;
142    use crate::extension::uuid::UuidMetadata;
143    use crate::extension::uuid::vtable::UUID_BYTE_LEN;
144    use crate::scalar::Scalar;
145    use crate::scalar::ScalarValue;
146
147    #[rstest]
148    #[case::no_version(None)]
149    #[case::v4_random(Some(Version::Random))]
150    #[case::v7_sort_rand(Some(Version::SortRand))]
151    #[case::nil(Some(Version::Nil))]
152    #[case::max(Some(Version::Max))]
153    fn roundtrip_metadata(#[case] version: Option<Version>) -> VortexResult<()> {
154        let metadata = UuidMetadata { version };
155        let bytes = Uuid.serialize_metadata(&metadata)?;
156        let expected_len = if version.is_none() { 0 } else { 1 };
157        assert_eq!(bytes.len(), expected_len);
158        let deserialized = Uuid.deserialize_metadata(&bytes)?;
159        assert_eq!(deserialized, metadata);
160        Ok(())
161    }
162
163    #[test]
164    fn metadata_display_no_version() {
165        let metadata = UuidMetadata { version: None };
166        assert_eq!(metadata.to_string(), "");
167    }
168
169    #[test]
170    fn metadata_display_with_version() {
171        let metadata = UuidMetadata {
172            version: Some(Version::Random),
173        };
174        assert_eq!(metadata.to_string(), "v4");
175
176        let metadata = UuidMetadata {
177            version: Some(Version::SortRand),
178        };
179        assert_eq!(metadata.to_string(), "v7");
180    }
181
182    #[rstest]
183    #[case::non_nullable(Nullability::NonNullable)]
184    #[case::nullable(Nullability::Nullable)]
185    fn validate_correct_storage_dtype(#[case] nullability: Nullability) -> VortexResult<()> {
186        let metadata = UuidMetadata::default();
187        let storage_dtype = uuid_storage_dtype(nullability);
188        ExtDType::try_with_vtable(Uuid, metadata, storage_dtype)?;
189        Ok(())
190    }
191
192    #[test]
193    fn validate_rejects_wrong_list_size() {
194        let storage_dtype = DType::FixedSizeList(
195            Arc::new(DType::Primitive(PType::U8, Nullability::NonNullable)),
196            8,
197            Nullability::NonNullable,
198        );
199        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
200    }
201
202    #[test]
203    fn validate_rejects_wrong_element_type() {
204        let storage_dtype = DType::FixedSizeList(
205            Arc::new(DType::Primitive(PType::U64, Nullability::NonNullable)),
206            UUID_BYTE_LEN as u32,
207            Nullability::NonNullable,
208        );
209        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
210    }
211
212    #[test]
213    fn validate_rejects_nullable_elements() {
214        let storage_dtype = DType::FixedSizeList(
215            Arc::new(DType::Primitive(PType::U8, Nullability::Nullable)),
216            UUID_BYTE_LEN as u32,
217            Nullability::NonNullable,
218        );
219        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
220    }
221
222    #[test]
223    fn validate_rejects_non_fsl() {
224        let storage_dtype = DType::Primitive(PType::U8, Nullability::NonNullable);
225        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
226    }
227
228    #[test]
229    fn unpack_native_uuid() -> VortexResult<()> {
230        let expected = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
231            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
232
233        let ext_dtype = ExtDType::try_new(
234            UuidMetadata::default(),
235            uuid_storage_dtype(Nullability::NonNullable),
236        )?;
237        let children: Vec<Scalar> = expected
238            .as_bytes()
239            .iter()
240            .map(|&b| Scalar::primitive(b, Nullability::NonNullable))
241            .collect();
242        let storage_scalar = Scalar::fixed_size_list(
243            DType::Primitive(PType::U8, Nullability::NonNullable),
244            children,
245            Nullability::NonNullable,
246        );
247
248        let storage_value = storage_scalar
249            .value()
250            .ok_or_else(|| vortex_error::vortex_err!("expected non-null scalar"))?;
251        let result = Uuid.unpack_native(&ext_dtype, storage_value)?;
252        assert_eq!(result, expected);
253        assert_eq!(result.to_string(), "550e8400-e29b-41d4-a716-446655440000");
254        Ok(())
255    }
256
257    #[test]
258    fn unpack_native_rejects_version_mismatch() -> VortexResult<()> {
259        // This is a v4 UUID.
260        let v4_uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
261            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
262        assert_eq!(v4_uuid.get_version(), Some(Version::Random));
263
264        // Metadata says v7, but the UUID is v4.
265        let ext_dtype = ExtDType::try_with_vtable(
266            Uuid,
267            UuidMetadata {
268                version: Some(Version::SortRand),
269            },
270            uuid_storage_dtype(Nullability::NonNullable),
271        )?;
272        let children: Vec<Scalar> = v4_uuid
273            .as_bytes()
274            .iter()
275            .map(|&b| Scalar::primitive(b, Nullability::NonNullable))
276            .collect();
277        let storage_scalar = Scalar::fixed_size_list(
278            DType::Primitive(PType::U8, Nullability::NonNullable),
279            children,
280            Nullability::NonNullable,
281        );
282
283        let storage_value = storage_scalar
284            .value()
285            .ok_or_else(|| vortex_error::vortex_err!("expected non-null scalar"))?;
286        assert!(Uuid.unpack_native(&ext_dtype, storage_value).is_err());
287        Ok(())
288    }
289
290    /// Builds a [`ScalarValue`] for a UUID's 16 bytes, suitable for passing to `unpack_native`.
291    fn uuid_storage_scalar(uuid: &uuid::Uuid) -> ScalarValue {
292        let children: Vec<Scalar> = uuid
293            .as_bytes()
294            .iter()
295            .map(|&b| Scalar::primitive(b, Nullability::NonNullable))
296            .collect();
297        let scalar = Scalar::fixed_size_list(
298            DType::Primitive(PType::U8, Nullability::NonNullable),
299            children,
300            Nullability::NonNullable,
301        );
302        scalar.value().unwrap().clone()
303    }
304
305    #[test]
306    fn unpack_native_accepts_matching_version() -> VortexResult<()> {
307        // This is a v4 UUID.
308        let v4_uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
309            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
310
311        let ext_dtype = ExtDType::try_new(
312            UuidMetadata {
313                version: Some(Version::Random),
314            },
315            uuid_storage_dtype(Nullability::NonNullable),
316        )
317        .unwrap();
318        let storage_value = uuid_storage_scalar(&v4_uuid);
319
320        let result = Uuid.unpack_native(&ext_dtype, &storage_value)?;
321        assert_eq!(result, v4_uuid);
322        Ok(())
323    }
324
325    #[test]
326    fn unpack_native_any_version_accepts_all() -> VortexResult<()> {
327        // A v4 UUID should be accepted when metadata has no version constraint.
328        let v4_uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
329            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
330
331        let ext_dtype = ExtDType::try_new(
332            UuidMetadata::default(),
333            uuid_storage_dtype(Nullability::NonNullable),
334        )
335        .unwrap();
336        let storage_value = uuid_storage_scalar(&v4_uuid);
337
338        let result = Uuid.unpack_native(&ext_dtype, &storage_value)?;
339        assert_eq!(result, v4_uuid);
340        Ok(())
341    }
342
343    fn uuid_storage_dtype(nullability: Nullability) -> DType {
344        DType::FixedSizeList(
345            Arc::new(DType::Primitive(PType::U8, Nullability::NonNullable)),
346            UUID_BYTE_LEN as u32,
347            nullability,
348        )
349    }
350}