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;
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
23pub(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 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 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 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 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 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 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}