This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 5fa2c331 [core] Fix VARIANT timestamp projection and scalar 
conversions (#1151)
5fa2c331 is described below

commit 5fa2c331120d7065c94df7951e166d7596b12275
Author: XiaoHongbo <[email protected]>
AuthorDate: Sun Oct 18 09:23:40 2026 +0800

    [core] Fix VARIANT timestamp projection and scalar conversions (#1151)
---
 crates/paimon/src/arrow/shredding/variant.rs | 171 ++++++++++++++++++++++++++-
 crates/paimon/src/variant.rs                 |  30 +++--
 2 files changed, 193 insertions(+), 8 deletions(-)

diff --git a/crates/paimon/src/arrow/shredding/variant.rs 
b/crates/paimon/src/arrow/shredding/variant.rs
index 519510f4..c0e5201d 100644
--- a/crates/paimon/src/arrow/shredding/variant.rs
+++ b/crates/paimon/src/arrow/shredding/variant.rs
@@ -1546,7 +1546,12 @@ fn timestamp_array(
     let values = values
         .iter()
         .map(|value| match value {
-            Some(ShreddedValue::Timestamp(v)) => Some(*v),
+            Some(ShreddedValue::Timestamp(v)) => match precision {
+                0 => Some(v.div_euclid(1_000_000)),
+                1..=3 => Some(v.div_euclid(1_000)),
+                4..=6 => Some(*v),
+                _ => v.checked_mul(1_000),
+            },
             _ => None,
         })
         .collect::<Vec<_>>();
@@ -2757,4 +2762,168 @@ mod tests {
                 .data_type()
         );
     }
+    fn parquet_roundtrip(array: ArrayRef) -> ArrayRef {
+        use parquet::arrow::{arrow_reader::ParquetRecordBatchReaderBuilder, 
ArrowWriter};
+        let schema = Arc::new(arrow_schema::Schema::new(vec![ArrowField::new(
+            "v",
+            array.data_type().clone(),
+            true,
+        )]));
+        let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
+        let mut output = Vec::new();
+        let mut writer = ArrowWriter::try_new(&mut output, schema, 
None).unwrap();
+        writer.write(&batch).unwrap();
+        writer.close().unwrap();
+        ParquetRecordBatchReaderBuilder::try_new(bytes::Bytes::from(output))
+            .unwrap()
+            .build()
+            .unwrap()
+            .next()
+            .unwrap()
+            .unwrap()
+            .column(0)
+            .clone()
+    }
+
+    fn extract_root(array: &dyn Array, data_type: DataType, strict: bool) -> 
Result<ArrayRef> {
+        let field = DataField::new(0, "result".into(), 
data_type).with_description(Some(
+            crate::spec::build_variant_metadata("$", strict, "UTC").unwrap(),
+        ));
+        let result = assemble_variant_extraction_array(array, 
&RowType::new(vec![field]))?;
+        Ok(result
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap()
+            .column(0)
+            .clone())
+    }
+
+    #[test]
+    fn variant_timestamp_projection_units_and_overflow() {
+        use crate::spec::{LocalZonedTimestampType, TimestampType};
+        let micros = [1_234_567_890_123_i64, -1, -1_234_567, 0, i64::MAX, 
i64::MIN];
+        for zoned in [false, true] {
+            let dtype = |precision| {
+                if zoned {
+                    
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(precision).unwrap())
+                } else {
+                    DataType::Timestamp(TimestampType::new(precision).unwrap())
+                }
+            };
+            let values = micros
+                .iter()
+                .map(|value| {
+                    let mut raw = vec![if zoned { 12 << 2 } else { 13 << 2 }];
+                    raw.extend_from_slice(&value.to_le_bytes());
+                    Some(GenericVariant::from_parts(raw, vec![1, 0, 
0]).unwrap())
+                })
+                .chain([Some(GenericVariant::parse_json("null").unwrap()), 
None])
+                .collect();
+            let plain = variant_array(values).unwrap();
+            let physical = variant_shredding_type(&dtype(3)).unwrap();
+            let shredded = shred_variant_array(plain.as_ref(), 
&physical).unwrap();
+            for input in [plain, shredded] {
+                let input = parquet_roundtrip(input);
+                for precision in [0, 3, 6, 9] {
+                    let result = extract_root(input.as_ref(), 
dtype(precision), false).unwrap();
+                    assert_eq!(
+                        result.data_type(),
+                        &paimon_type_to_arrow(&dtype(precision)).unwrap()
+                    );
+                    for (row, value) in micros.iter().enumerate() {
+                        let expected = match precision {
+                            0 => Some(value.div_euclid(1_000_000)),
+                            3 => Some(value.div_euclid(1_000)),
+                            6 => Some(*value),
+                            _ => value.checked_mul(1_000),
+                        };
+                        match expected {
+                            Some(value) => {
+                                assert!(!result.is_null(row));
+                                assert_eq!(
+                                    timestamp_value_at(result.as_ref(), 
row).unwrap(),
+                                    value
+                                );
+                            }
+                            None => assert!(result.is_null(row)),
+                        }
+                    }
+                    assert!(result.is_null(micros.len()));
+                    assert!(result.is_null(micros.len() + 1));
+                    let strict = extract_root(input.as_ref(), 
dtype(precision), true);
+                    assert_eq!(strict.is_err(), precision == 9);
+                    let valid = input.slice(0, 4);
+                    assert!(extract_root(valid.as_ref(), dtype(precision), 
true).is_ok());
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn variant_boolean_numeric_projection() {
+        use crate::spec::{
+            BigIntType, BooleanType, DecimalType, DoubleType, SmallIntType, 
TinyIntType,
+        };
+        let values = ["true", "false", "null"]
+            .iter()
+            .map(|value| Some(GenericVariant::parse_json(value).unwrap()))
+            .chain([None])
+            .collect();
+        let plain = variant_array(values).unwrap();
+        let physical = 
variant_shredding_type(&DataType::Boolean(BooleanType::new())).unwrap();
+        let shredded = shred_variant_array(plain.as_ref(), &physical).unwrap();
+        for input in [plain, shredded] {
+            let input = parquet_roundtrip(input);
+            for dtype in [
+                DataType::TinyInt(TinyIntType::new()),
+                DataType::SmallInt(SmallIntType::new()),
+                DataType::Int(IntType::new()),
+                DataType::BigInt(BigIntType::new()),
+                DataType::Float(FloatType::new()),
+                DataType::Double(DoubleType::new()),
+                DataType::Decimal(DecimalType::new(10, 2).unwrap()),
+            ] {
+                for strict in [false, true] {
+                    let output = extract_root(input.as_ref(), dtype.clone(), 
strict).unwrap();
+                    assert_eq!(output.null_count(), 2);
+                    assert!(output.is_null(2));
+                    assert!(output.is_null(3));
+                    let numeric =
+                        arrow_cast::cast(output.as_ref(), 
&ArrowDataType::Float64).unwrap();
+                    let numeric = 
numeric.as_any().downcast_ref::<Float64Array>().unwrap();
+                    assert_eq!(numeric.value(0), 1.0);
+                    assert_eq!(numeric.value(1), 0.0);
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn variant_decimal_overflow_preserves_residual() {
+        use crate::spec::DecimalType;
+        let values = ["10", "-10", "9223372036854775807", 
"-9223372036854775808"];
+        let input = variant_array(
+            values
+                .iter()
+                .map(|v| Some(GenericVariant::parse_json(v).unwrap()))
+                .collect(),
+        )
+        .unwrap();
+        let physical =
+            variant_shredding_type(&DataType::Decimal(DecimalType::new(38, 
38).unwrap())).unwrap();
+        let output = shred_variant_array(input.as_ref(), &physical).unwrap();
+        let output = parquet_roundtrip(output);
+        let storage = output.as_any().downcast_ref::<StructArray>().unwrap();
+        assert_eq!(
+            storage.column_by_name("typed_value").unwrap().null_count(),
+            values.len()
+        );
+        assert_eq!(storage.column_by_name("value").unwrap().null_count(), 0);
+        let restored = 
assemble_shredded_variant_array(output.as_ref()).unwrap();
+        for row in 0..values.len() {
+            let expected = variant_from_array(input.as_ref(), 
row).unwrap().unwrap();
+            let actual = variant_from_array(restored.as_ref(), 
row).unwrap().unwrap();
+            assert_eq!(actual, expected);
+        }
+    }
 }
diff --git a/crates/paimon/src/variant.rs b/crates/paimon/src/variant.rs
index d9616315..20aa42b2 100644
--- a/crates/paimon/src/variant.rs
+++ b/crates/paimon/src/variant.rs
@@ -2774,7 +2774,20 @@ pub(crate) fn cast_variant_to_shredded_value(
             message: format!("Unsupported Variant extraction target type: 
{data_type:?}"),
         });
     };
-    match cast_variant_to_extraction_value(variant, &scalar) {
+    let value = cast_variant_to_extraction_value(variant, 
&scalar).filter(|value| {
+        let precision = match data_type {
+            DataType::Timestamp(ts) => ts.precision(),
+            DataType::LocalZonedTimestamp(ts) => ts.precision(),
+            _ => return true,
+        };
+        match value {
+            ShreddedValue::Timestamp(micros) if precision > 6 => {
+                micros.checked_mul(1_000).is_some()
+            }
+            _ => true,
+        }
+    });
+    match value {
         Some(value) => Ok(Some(value)),
         None if fail_on_error => Err(Error::DataInvalid {
             message: format!("Cannot cast Variant value to {data_type:?}"),
@@ -2827,6 +2840,7 @@ fn cast_variant_to_bool(variant: VariantRef<'_>) -> 
Option<bool> {
 
 fn cast_variant_to_i64(variant: VariantRef<'_>) -> Option<i64> {
     match variant.kind().ok()? {
+        VariantKind::Boolean => Some(i64::from(variant.get_boolean().ok()?)),
         VariantKind::Long
         | VariantKind::Date
         | VariantKind::Timestamp
@@ -2873,6 +2887,7 @@ fn f64_to_i64_truncating(value: f64) -> Option<i64> {
 
 fn cast_variant_to_f64(variant: VariantRef<'_>) -> Option<f64> {
     match variant.kind().ok()? {
+        VariantKind::Boolean => Some(u8::from(variant.get_boolean().ok()?) as 
f64),
         VariantKind::Long
         | VariantKind::Date
         | VariantKind::Timestamp
@@ -2931,6 +2946,9 @@ fn cast_variant_to_string(variant: VariantRef<'_>) -> 
Option<String> {
 
 fn cast_variant_to_decimal(variant: VariantRef<'_>, precision: u8, scale: i8) 
-> Option<i128> {
     let unscaled = match variant.kind().ok()? {
+        VariantKind::Boolean => {
+            rescale_decimal_exact(i128::from(variant.get_boolean().ok()?), 0, 
scale)?
+        }
         VariantKind::Long
         | VariantKind::Date
         | VariantKind::Timestamp
@@ -3153,12 +3171,10 @@ fn try_typed_shred(
             Some(ShreddedValue::Int64(variant.get_long()?))
         }
         (VariantKind::Long, VariantScalarSchema::Decimal { precision, scale }) 
=> {
-            let unscaled = rescale_decimal(variant.get_long()? as i128, 0, 
*scale)?;
-            if decimal_precision(unscaled) <= *precision {
-                Some(ShreddedValue::Decimal128(unscaled))
-            } else {
-                None
-            }
+            rescale_decimal_exact(variant.get_long()? as i128, 0, 
*scale).and_then(|unscaled| {
+                (decimal_precision(unscaled) <= *precision)
+                    .then_some(ShreddedValue::Decimal128(unscaled))
+            })
         }
         (VariantKind::Decimal, VariantScalarSchema::Decimal { precision, scale 
}) => {
             let decimal = variant.get_decimal()?;

Reply via email to