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(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 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 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 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 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 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 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}