Skip to content

Commit 11538b5

Browse files
committed
nice cast
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 30185d6 commit 11538b5

1 file changed

Lines changed: 146 additions & 37 deletions

File tree

parquet-variant-compute/src/variant_get.rs

Lines changed: 146 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
use arrow::{
1818
array::{self, Array, ArrayRef, StructArray, make_array},
1919
buffer::NullBuffer,
20-
compute::CastOptions,
20+
compute::{CastOptions, can_cast_types, cast_with_options},
2121
datatypes::Field,
2222
error::Result,
2323
};
24-
use arrow_schema::{ArrowError, DataType, FieldRef};
24+
use arrow_schema::{ArrowError, DataType, FieldRef, TimeUnit};
2525
use parquet_variant::{VariantPath, VariantPathElement};
2626

2727
use crate::VariantArray;
@@ -140,11 +140,12 @@ fn shredded_get_path(
140140
let value = target.try_value(i)?;
141141
builder.append_value(value)?;
142142
} 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+
}
145147
Err(_) => {
146148
builder.append_null()?;
147-
false // add this to make match arms have the same return type
148149
}
149150
};
150151
}
@@ -201,7 +202,7 @@ fn shredded_get_path(
201202
};
202203

203204
// 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)? {
205206
return Ok(shredded);
206207
}
207208

@@ -244,44 +245,131 @@ fn shredded_get_path(
244245
shred_basic_variant(target, VariantPath::default(), Some(as_field))
245246
}
246247

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>> {
248253
// Try to return the typed value directly when we have a perfect shredding match.
249254
if matches!(as_field.data_type(), DataType::Struct(_)) {
250-
return None;
255+
return Ok(None);
251256
}
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+
};
253260

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())
258264
{
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+
}
280294

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);
282297
}
283298

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+
)
285373
}
286374

287375
/// Returns an array with the specified path extracted from the variant values.
@@ -353,7 +441,7 @@ mod test {
353441
use std::str::FromStr;
354442
use std::sync::Arc;
355443

356-
use super::{GetOptions, variant_get};
444+
use super::{GetOptions, try_perfect_shredding, variant_get};
357445
use crate::variant_array::{ShreddedVariantFieldArray, StructArrayBuilder};
358446
use crate::{
359447
VariantArray, VariantArrayBuilder, cast_to_variant, json_to_variant, shred_variant,
@@ -3974,6 +4062,27 @@ mod test {
39744062
assert!(Arc::ptr_eq(&typed_value_arc, &result));
39754063
}
39764064

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+
39774086
#[test]
39784087
fn test_perfect_shredding_three_typed_value_columns() {
39794088
// Column 1: perfectly shredded primitive with all nulls

0 commit comments

Comments
 (0)