|
17 | 17 | use arrow::{ |
18 | 18 | array::{self, Array, ArrayRef, StructArray, make_array}, |
19 | 19 | buffer::NullBuffer, |
20 | | - compute::CastOptions, |
| 20 | + compute::{CastOptions, can_cast_types, cast_with_options}, |
21 | 21 | datatypes::Field, |
22 | 22 | error::Result, |
23 | 23 | }; |
24 | | -use arrow_schema::{ArrowError, DataType, FieldRef}; |
| 24 | +use arrow_schema::{ArrowError, DataType, FieldRef, TimeUnit}; |
25 | 25 | use parquet_variant::{VariantPath, VariantPathElement}; |
26 | 26 |
|
27 | 27 | use crate::VariantArray; |
@@ -140,11 +140,12 @@ fn shredded_get_path( |
140 | 140 | let value = target.try_value(i)?; |
141 | 141 | builder.append_value(value)?; |
142 | 142 | } else { |
143 | | - let _ = match target.try_value(i) { |
144 | | - Ok(v) => builder.append_value(v)?, |
| 143 | + match target.try_value(i) { |
| 144 | + Ok(v) => { |
| 145 | + builder.append_value(v)?; |
| 146 | + } |
145 | 147 | Err(_) => { |
146 | 148 | builder.append_null()?; |
147 | | - false // add this to make match arms have the same return type |
148 | 149 | } |
149 | 150 | }; |
150 | 151 | } |
@@ -201,7 +202,7 @@ fn shredded_get_path( |
201 | 202 | }; |
202 | 203 |
|
203 | 204 | // Try to return the typed value directly when we have a perfect shredding match. |
204 | | - if let Some(shredded) = try_perfect_shredding(&target, as_field) { |
| 205 | + if let Some(shredded) = try_perfect_shredding(&target, as_field, cast_options)? { |
205 | 206 | return Ok(shredded); |
206 | 207 | } |
207 | 208 |
|
@@ -244,44 +245,131 @@ fn shredded_get_path( |
244 | 245 | shred_basic_variant(target, VariantPath::default(), Some(as_field)) |
245 | 246 | } |
246 | 247 |
|
247 | | -fn try_perfect_shredding(variant_array: &VariantArray, as_field: &Field) -> Option<ArrayRef> { |
| 248 | +fn try_perfect_shredding( |
| 249 | + variant_array: &VariantArray, |
| 250 | + as_field: &Field, |
| 251 | + cast_options: &CastOptions, |
| 252 | +) -> Result<Option<ArrayRef>> { |
248 | 253 | // Try to return the typed value directly when we have a perfect shredding match. |
249 | 254 | if matches!(as_field.data_type(), DataType::Struct(_)) { |
250 | | - return None; |
| 255 | + return Ok(None); |
251 | 256 | } |
252 | | - let typed_value = variant_array.typed_value_field()?; |
| 257 | + let Some(typed_value) = variant_array.typed_value_field() else { |
| 258 | + return Ok(None); |
| 259 | + }; |
253 | 260 |
|
254 | | - if typed_value.data_type() == as_field.data_type() |
255 | | - && variant_array |
256 | | - .value_field() |
257 | | - .is_none_or(|v| v.null_count() == v.len()) |
| 261 | + if variant_array |
| 262 | + .value_field() |
| 263 | + .is_some_and(|v| v.null_count() != v.len()) |
258 | 264 | { |
259 | | - // Here we need to gate against the case where the `typed_value` is null but data is in the `value` column. |
260 | | - // 1. If the `value` column is null, or |
261 | | - // 2. If every row in the `value` column is null |
262 | | - |
263 | | - // This is a perfect shredding, where the value is entirely shredded out, |
264 | | - // so we can just return the typed value after merging the accumulated nulls. |
265 | | - let parent_nulls = variant_array.nulls(); |
266 | | - |
267 | | - // If we have no nulls OR the shredded array is `Null`, which doesn't support external nulls. |
268 | | - let target_array = if parent_nulls.is_none() || typed_value.data_type().is_null() { |
269 | | - typed_value.clone() |
270 | | - } else { |
271 | | - let merged_nulls = NullBuffer::union(parent_nulls, typed_value.nulls()); |
272 | | - let data = typed_value |
273 | | - .to_data() |
274 | | - .into_builder() |
275 | | - .nulls(merged_nulls) |
276 | | - .build() |
277 | | - .ok()?; |
278 | | - make_array(data) |
279 | | - }; |
| 265 | + // We can only use the perfect-shredding fast path when the value is entirely shredded out. |
| 266 | + return Ok(None); |
| 267 | + } |
| 268 | + |
| 269 | + // Here we need to gate against the case where the `typed_value` is null but data is in the |
| 270 | + // `value` column: |
| 271 | + // 1. If the `value` column is null, or |
| 272 | + // 2. If every row in the `value` column is null |
| 273 | + |
| 274 | + // This is a perfect shredding, where the value is entirely shredded out, so we can reuse the |
| 275 | + // typed value after merging the accumulated nulls from the traversed parent fields. |
| 276 | + let parent_nulls = variant_array.nulls(); |
| 277 | + |
| 278 | + // If we have no nulls OR the shredded array is `Null`, which doesn't support external nulls. |
| 279 | + let target_array = if parent_nulls.is_none() || typed_value.data_type().is_null() { |
| 280 | + typed_value.clone() |
| 281 | + } else { |
| 282 | + let merged_nulls = NullBuffer::union(parent_nulls, typed_value.nulls()); |
| 283 | + let data = typed_value |
| 284 | + .to_data() |
| 285 | + .into_builder() |
| 286 | + .nulls(merged_nulls) |
| 287 | + .build()?; |
| 288 | + make_array(data) |
| 289 | + }; |
| 290 | + |
| 291 | + if target_array.data_type() == as_field.data_type() { |
| 292 | + return Ok(Some(target_array)); |
| 293 | + } |
280 | 294 |
|
281 | | - return Some(target_array); |
| 295 | + if !can_use_perfect_shredding_arrow_cast(target_array.data_type(), as_field.data_type()) { |
| 296 | + return Ok(None); |
282 | 297 | } |
283 | 298 |
|
284 | | - None |
| 299 | + // Use Arrow's vectorized cast when it cleanly matches the shredded representation. If not, |
| 300 | + // fall back to row-wise extraction to preserve the existing variant-specific semantics. |
| 301 | + Ok(cast_with_options(target_array.as_ref(), as_field.data_type(), cast_options).ok()) |
| 302 | +} |
| 303 | + |
| 304 | +fn can_use_perfect_shredding_arrow_cast(from_type: &DataType, to_type: &DataType) -> bool { |
| 305 | + use DataType::*; |
| 306 | + |
| 307 | + if !can_cast_types(from_type, to_type) { |
| 308 | + return false; |
| 309 | + } |
| 310 | + |
| 311 | + match from_type { |
| 312 | + Null => true, |
| 313 | + Boolean => is_non_decimal_numeric_or_bool(to_type), |
| 314 | + Int8 | Int16 | Int32 | Int64 => { |
| 315 | + is_non_decimal_numeric_or_bool(to_type) |
| 316 | + || matches!( |
| 317 | + to_type, |
| 318 | + Decimal32(..) | Decimal64(..) | Decimal128(..) | Decimal256(..) |
| 319 | + ) |
| 320 | + } |
| 321 | + Float32 | Float64 => is_non_decimal_numeric_or_bool(to_type), |
| 322 | + Decimal32(..) | Decimal64(..) | Decimal128(..) | Decimal256(..) => { |
| 323 | + matches!( |
| 324 | + to_type, |
| 325 | + Decimal32(..) | Decimal64(..) | Decimal128(..) | Decimal256(..) |
| 326 | + ) |
| 327 | + } |
| 328 | + Date32 => matches!(to_type, Date32 | Date64), |
| 329 | + Time64(TimeUnit::Microsecond) => matches!( |
| 330 | + to_type, |
| 331 | + Time64(TimeUnit::Microsecond) | Time64(TimeUnit::Nanosecond) |
| 332 | + ), |
| 333 | + Timestamp(TimeUnit::Microsecond, from_tz) => matches!( |
| 334 | + to_type, |
| 335 | + Timestamp(TimeUnit::Microsecond | TimeUnit::Nanosecond, to_tz) |
| 336 | + if from_tz.is_some() == to_tz.is_some() |
| 337 | + ), |
| 338 | + Timestamp(TimeUnit::Nanosecond, from_tz) => matches!( |
| 339 | + to_type, |
| 340 | + Timestamp(TimeUnit::Nanosecond, to_tz) if from_tz.is_some() == to_tz.is_some() |
| 341 | + ), |
| 342 | + Utf8 | LargeUtf8 | Utf8View => matches!(to_type, Utf8 | LargeUtf8 | Utf8View), |
| 343 | + Binary | LargeBinary | BinaryView => matches!(to_type, Binary | LargeBinary | BinaryView), |
| 344 | + FixedSizeBinary(16) => matches!(to_type, FixedSizeBinary(16)), |
| 345 | + List(_) | LargeList(_) | ListView(_) | LargeListView(_) => { |
| 346 | + matches!( |
| 347 | + to_type, |
| 348 | + List(_) | LargeList(_) | ListView(_) | LargeListView(_) |
| 349 | + ) |
| 350 | + } |
| 351 | + _ => false, |
| 352 | + } |
| 353 | +} |
| 354 | + |
| 355 | +fn is_non_decimal_numeric_or_bool(data_type: &DataType) -> bool { |
| 356 | + use DataType::*; |
| 357 | + |
| 358 | + matches!( |
| 359 | + data_type, |
| 360 | + Boolean |
| 361 | + | Int8 |
| 362 | + | Int16 |
| 363 | + | Int32 |
| 364 | + | Int64 |
| 365 | + | UInt8 |
| 366 | + | UInt16 |
| 367 | + | UInt32 |
| 368 | + | UInt64 |
| 369 | + | Float16 |
| 370 | + | Float32 |
| 371 | + | Float64 |
| 372 | + ) |
285 | 373 | } |
286 | 374 |
|
287 | 375 | /// Returns an array with the specified path extracted from the variant values. |
@@ -353,7 +441,7 @@ mod test { |
353 | 441 | use std::str::FromStr; |
354 | 442 | use std::sync::Arc; |
355 | 443 |
|
356 | | - use super::{GetOptions, variant_get}; |
| 444 | + use super::{GetOptions, try_perfect_shredding, variant_get}; |
357 | 445 | use crate::variant_array::{ShreddedVariantFieldArray, StructArrayBuilder}; |
358 | 446 | use crate::{ |
359 | 447 | VariantArray, VariantArrayBuilder, cast_to_variant, json_to_variant, shred_variant, |
@@ -3974,6 +4062,27 @@ mod test { |
3974 | 4062 | assert!(Arc::ptr_eq(&typed_value_arc, &result)); |
3975 | 4063 | } |
3976 | 4064 |
|
| 4065 | + #[test] |
| 4066 | + fn test_try_perfect_shredding_casts_typed_value_and_preserves_parent_nulls() { |
| 4067 | + let typed_value: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), Some(2), Some(3)])); |
| 4068 | + let metadata = Arc::new(BinaryViewArray::from_iter_values(std::iter::repeat_n( |
| 4069 | + EMPTY_VARIANT_METADATA_BYTES, |
| 4070 | + typed_value.len(), |
| 4071 | + ))); |
| 4072 | + let parent_nulls = Some(NullBuffer::from(vec![true, false, true])); |
| 4073 | + let variant_array = |
| 4074 | + VariantArray::from_parts(metadata, None, Some(typed_value), parent_nulls); |
| 4075 | + let requested_field = Field::new("result", DataType::Int64, true); |
| 4076 | + |
| 4077 | + let result = |
| 4078 | + try_perfect_shredding(&variant_array, &requested_field, &CastOptions::default()) |
| 4079 | + .unwrap() |
| 4080 | + .unwrap(); |
| 4081 | + let expected: ArrayRef = Arc::new(Int64Array::from(vec![Some(1), None, Some(3)])); |
| 4082 | + |
| 4083 | + assert_eq!(&result, &expected); |
| 4084 | + } |
| 4085 | + |
3977 | 4086 | #[test] |
3978 | 4087 | fn test_perfect_shredding_three_typed_value_columns() { |
3979 | 4088 | // Column 1: perfectly shredded primitive with all nulls |
|
0 commit comments