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