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(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        ext_dtype: &ExtDType<Self>,
81        storage_value: &'a ScalarValue,
82    ) -> VortexResult<Self::NativeValue<'a>> {
83        let elements = storage_value.as_list();
84        vortex_ensure_eq!(
85            elements.len(),
86            UUID_BYTE_LEN,
87            "UUID scalar must have exactly {UUID_BYTE_LEN} bytes, got {}",
88            elements.len()
89        );
90
91        let mut bytes = [0u8; UUID_BYTE_LEN];
92        for (i, elem) in elements.iter().enumerate() {
93            let Some(scalar_value) = elem else {
94                vortex_bail!("UUID byte at index {i} must not be null");
95            };
96            let PValue::U8(b) = scalar_value.as_primitive() else {
97                vortex_bail!("UUID byte at index {i} must be U8");
98            };
99            bytes[i] = *b;
100        }
101
102        let parsed = uuid::Uuid::from_bytes(bytes);
103
104        // Verify the parsed UUID matches the expected version, if one is set.
105        if let Some(expected) = ext_dtype.metadata().version {
106            let expected = expected as u8;
107            let actual = parsed
108                .get_version()
109                .ok_or_else(|| vortex_err!("UUID has unrecognized version nibble"))?
110                as u8;
111
112            vortex_ensure_eq!(
113                expected,
114                actual,
115                "UUID version mismatch: expected v{expected}, got v{actual}",
116            );
117        }
118
119        Ok(parsed)
120    }
121}
122
123#[expect(
124    clippy::cast_possible_truncation,
125    reason = "UUID_BYTE_LEN always fits both usize and u32"
126)]
127#[cfg(test)]
128mod tests {
129    use std::sync::Arc;
130
131    use rstest::rstest;
132    use uuid::Version;
133    use vortex_error::VortexResult;
134
135    use crate::dtype::DType;
136    use crate::dtype::Nullability;
137    use crate::dtype::PType;
138    use crate::dtype::extension::ExtDType;
139    use crate::dtype::extension::ExtVTable;
140    use crate::extension::uuid::Uuid;
141    use crate::extension::uuid::UuidMetadata;
142    use crate::extension::uuid::vtable::UUID_BYTE_LEN;
143    use crate::scalar::Scalar;
144    use crate::scalar::ScalarValue;
145
146    #[rstest]
147    #[case::no_version(None)]
148    #[case::v4_random(Some(Version::Random))]
149    #[case::v7_sort_rand(Some(Version::SortRand))]
150    #[case::nil(Some(Version::Nil))]
151    #[case::max(Some(Version::Max))]
152    fn roundtrip_metadata(#[case] version: Option<Version>) -> VortexResult<()> {
153        let metadata = UuidMetadata { version };
154        let bytes = Uuid.serialize_metadata(&metadata)?;
155        let expected_len = if version.is_none() { 0 } else { 1 };
156        assert_eq!(bytes.len(), expected_len);
157        let deserialized = Uuid.deserialize_metadata(&bytes)?;
158        assert_eq!(deserialized, metadata);
159        Ok(())
160    }
161
162    #[test]
163    fn metadata_display_no_version() {
164        let metadata = UuidMetadata { version: None };
165        assert_eq!(metadata.to_string(), "");
166    }
167
168    #[test]
169    fn metadata_display_with_version() {
170        let metadata = UuidMetadata {
171            version: Some(Version::Random),
172        };
173        assert_eq!(metadata.to_string(), "v4");
174
175        let metadata = UuidMetadata {
176            version: Some(Version::SortRand),
177        };
178        assert_eq!(metadata.to_string(), "v7");
179    }
180
181    #[rstest]
182    #[case::non_nullable(Nullability::NonNullable)]
183    #[case::nullable(Nullability::Nullable)]
184    fn validate_correct_storage_dtype(#[case] nullability: Nullability) -> VortexResult<()> {
185        let metadata = UuidMetadata::default();
186        let storage_dtype = uuid_storage_dtype(nullability);
187        ExtDType::try_with_vtable(Uuid, metadata, storage_dtype)?;
188        Ok(())
189    }
190
191    #[test]
192    fn validate_rejects_wrong_list_size() {
193        let storage_dtype = DType::FixedSizeList(
194            Arc::new(DType::Primitive(PType::U8, Nullability::NonNullable)),
195            8,
196            Nullability::NonNullable,
197        );
198        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
199    }
200
201    #[test]
202    fn validate_rejects_wrong_element_type() {
203        let storage_dtype = DType::FixedSizeList(
204            Arc::new(DType::Primitive(PType::U64, Nullability::NonNullable)),
205            UUID_BYTE_LEN as u32,
206            Nullability::NonNullable,
207        );
208        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
209    }
210
211    #[test]
212    fn validate_rejects_nullable_elements() {
213        let storage_dtype = DType::FixedSizeList(
214            Arc::new(DType::Primitive(PType::U8, Nullability::Nullable)),
215            UUID_BYTE_LEN as u32,
216            Nullability::NonNullable,
217        );
218        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
219    }
220
221    #[test]
222    fn validate_rejects_non_fsl() {
223        let storage_dtype = DType::Primitive(PType::U8, Nullability::NonNullable);
224        assert!(ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage_dtype).is_err());
225    }
226
227    #[test]
228    fn unpack_native_uuid() -> VortexResult<()> {
229        let expected = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
230            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
231
232        let ext_dtype = ExtDType::try_new(
233            UuidMetadata::default(),
234            uuid_storage_dtype(Nullability::NonNullable),
235        )?;
236        let children: Vec<Scalar> = expected
237            .as_bytes()
238            .iter()
239            .map(|&b| Scalar::primitive(b, Nullability::NonNullable))
240            .collect();
241        let storage_scalar = Scalar::fixed_size_list(
242            DType::Primitive(PType::U8, Nullability::NonNullable),
243            children,
244            Nullability::NonNullable,
245        );
246
247        let storage_value = storage_scalar
248            .value()
249            .ok_or_else(|| vortex_error::vortex_err!("expected non-null scalar"))?;
250        let result = Uuid::unpack_native(&ext_dtype, storage_value)?;
251        assert_eq!(result, expected);
252        assert_eq!(result.to_string(), "550e8400-e29b-41d4-a716-446655440000");
253        Ok(())
254    }
255
256    #[test]
257    fn unpack_native_rejects_version_mismatch() -> VortexResult<()> {
258        // This is a v4 UUID.
259        let v4_uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
260            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
261        assert_eq!(v4_uuid.get_version(), Some(Version::Random));
262
263        // Metadata says v7, but the UUID is v4.
264        let ext_dtype = ExtDType::try_with_vtable(
265            Uuid,
266            UuidMetadata {
267                version: Some(Version::SortRand),
268            },
269            uuid_storage_dtype(Nullability::NonNullable),
270        )?;
271        let children: Vec<Scalar> = v4_uuid
272            .as_bytes()
273            .iter()
274            .map(|&b| Scalar::primitive(b, Nullability::NonNullable))
275            .collect();
276        let storage_scalar = Scalar::fixed_size_list(
277            DType::Primitive(PType::U8, Nullability::NonNullable),
278            children,
279            Nullability::NonNullable,
280        );
281
282        let storage_value = storage_scalar
283            .value()
284            .ok_or_else(|| vortex_error::vortex_err!("expected non-null scalar"))?;
285        assert!(Uuid::unpack_native(&ext_dtype, storage_value).is_err());
286        Ok(())
287    }
288
289    /// Builds a [`ScalarValue`] for a UUID's 16 bytes, suitable for passing to `unpack_native`.
290    fn uuid_storage_scalar(uuid: &uuid::Uuid) -> ScalarValue {
291        let children: Vec<Scalar> = uuid
292            .as_bytes()
293            .iter()
294            .map(|&b| Scalar::primitive(b, Nullability::NonNullable))
295            .collect();
296        let scalar = Scalar::fixed_size_list(
297            DType::Primitive(PType::U8, Nullability::NonNullable),
298            children,
299            Nullability::NonNullable,
300        );
301        scalar.value().unwrap().clone()
302    }
303
304    #[test]
305    fn unpack_native_accepts_matching_version() -> VortexResult<()> {
306        // This is a v4 UUID.
307        let v4_uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
308            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
309
310        let ext_dtype = ExtDType::try_new(
311            UuidMetadata {
312                version: Some(Version::Random),
313            },
314            uuid_storage_dtype(Nullability::NonNullable),
315        )
316        .unwrap();
317        let storage_value = uuid_storage_scalar(&v4_uuid);
318
319        let result = Uuid::unpack_native(&ext_dtype, &storage_value)?;
320        assert_eq!(result, v4_uuid);
321        Ok(())
322    }
323
324    #[test]
325    fn unpack_native_any_version_accepts_all() -> VortexResult<()> {
326        // A v4 UUID should be accepted when metadata has no version constraint.
327        let v4_uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")
328            .map_err(|e| vortex_error::vortex_err!("{e}"))?;
329
330        let ext_dtype = ExtDType::try_new(
331            UuidMetadata::default(),
332            uuid_storage_dtype(Nullability::NonNullable),
333        )
334        .unwrap();
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}