vortex_array/extension/uuid/
vtable.rs1use 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
22pub(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 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 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 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 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 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 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}