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

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new b8685e6ab feat(literals): convert primitive variants in 
VariantLiteral.To (#2088)
b8685e6ab is described below

commit b8685e6abc7ab405570b0213c2041365f9250d75
Author: Enes Keles <[email protected]>
AuthorDate: Fri Oct 2 18:41:14 2026 +0300

    feat(literals): convert primitive variants in VariantLiteral.To (#2088)
    
    * feat(literals): convert primitive variants in VariantLiteral.To
    
    Signed-off-by: Enes Keles <[email protected]>
    
    * document VariantLiteral.To and keep ErrBadCast on inner cast errors
    
    Signed-off-by: Enes Keles <[email protected]>
    
    ---------
    
    Signed-off-by: Enes Keles <[email protected]>
---
 literals.go      |  22 +++++++++--
 literals_test.go | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 variant_cast.go  |  55 ++++++++++++++++++++-------
 3 files changed, 174 insertions(+), 16 deletions(-)

diff --git a/literals.go b/literals.go
index fd3cac903..0a89adb40 100644
--- a/literals.go
+++ b/literals.go
@@ -1524,14 +1524,30 @@ func (v VariantLiteral) MarshalBinary() ([]byte, error) 
{
        return variant.Value(v).Bytes(), nil
 }
 
+// To returns v for VariantType. For other types, a primitive variant is
+// converted with the cast rules of the literal for its value (int8/int16 widen
+// to int32, timestamps are tz-agnostic), not the stricter CastVariantLiteral
+// rules. Null, object, array and zero-value variants return ErrBadCast.
 func (v VariantLiteral) To(typ Type) (Literal, error) {
        if _, ok := typ.(VariantType); ok {
                return v, nil
        }
 
-       // TODO: improve by getting the actual value (using .Type()) and 
attempting
-       // to convert, or returning an error if it can't.
-       return nil, fmt.Errorf("%w: VariantLiteral to %s", ErrBadCast, typ)
+       lit, ok := literalFromVariant(variant.Value(v))
+       if !ok {
+               return nil, fmt.Errorf("%w: VariantLiteral to %s", ErrBadCast, 
typ)
+       }
+
+       out, err := lit.To(typ)
+       if err != nil {
+               if !errors.Is(err, ErrBadCast) {
+                       err = fmt.Errorf("%w: %w", ErrBadCast, err)
+               }
+
+               return nil, fmt.Errorf("VariantLiteral: %w", err)
+       }
+
+       return out, nil
 }
 
 func (v VariantLiteral) Equals(other Literal) bool {
diff --git a/literals_test.go b/literals_test.go
index 080b084ec..6823033ae 100644
--- a/literals_test.go
+++ b/literals_test.go
@@ -25,6 +25,7 @@ import (
        "time"
 
        "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/decimal"
        "github.com/apache/arrow-go/v18/arrow/decimal128"
        "github.com/apache/arrow-go/v18/parquet/variant"
        "github.com/apache/iceberg-go"
@@ -831,6 +832,81 @@ func TestVariantLiteralLargeObject(t *testing.T) {
        }
 }
 
+func variantLiteralOf(t *testing.T, v any, opts ...variant.AppendOpt) 
iceberg.VariantLiteral {
+       t.Helper()
+       var b variant.Builder
+       require.NoError(t, b.Append(v, opts...))
+       val, err := b.Build()
+       require.NoError(t, err)
+
+       return iceberg.VariantLiteral(val)
+}
+
+func TestVariantLiteralConversions(t *testing.T) {
+       testUUID := uuid.New()
+       ts := arrow.Timestamp(1503066061919234)
+       dec := iceberg.Decimal{Val: decimal128.FromI64(1234), Scale: 2}
+
+       tests := []struct {
+               from iceberg.VariantLiteral
+               to   iceberg.Literal
+       }{
+               {variantLiteralOf(t, int8(5)), iceberg.NewLiteral(int32(5))},
+               {variantLiteralOf(t, int16(1000)), 
iceberg.NewLiteral(int32(1000))},
+               {variantLiteralOf(t, int32(100000)), 
iceberg.NewLiteral(int32(100000))},
+               {variantLiteralOf(t, int64(12345123456)), 
iceberg.NewLiteral(int64(12345123456))},
+               {variantLiteralOf(t, float32(1.5)), 
iceberg.NewLiteral(float32(1.5))},
+               {variantLiteralOf(t, float64(2.5)), 
iceberg.NewLiteral(float64(2.5))},
+               {variantLiteralOf(t, true), iceberg.NewLiteral(true)},
+               {variantLiteralOf(t, "abc"), iceberg.NewLiteral("abc")},
+               {variantLiteralOf(t, []byte{0x00, 0x01, 0x02}), 
iceberg.NewLiteral([]byte{0x00, 0x01, 0x02})},
+               {variantLiteralOf(t, arrow.Date32(17396)), 
iceberg.NewLiteral(iceberg.Date(17396))},
+               {variantLiteralOf(t, arrow.Time64(51661919000)), 
iceberg.NewLiteral(iceberg.Time(51661919000))},
+               {variantLiteralOf(t, ts), 
iceberg.NewLiteral(iceberg.Timestamp(ts))},
+               {variantLiteralOf(t, ts, variant.OptTimestampNano), 
iceberg.NewLiteral(iceberg.TimestampNano(ts))},
+               {variantLiteralOf(t, testUUID), iceberg.NewLiteral(testUUID)},
+               {
+                       variantLiteralOf(t, 
variant.DecimalValue[decimal.Decimal32]{Scale: 2, Value: 
decimal.Decimal32(1234)}),
+                       iceberg.NewLiteral(dec),
+               },
+               {
+                       variantLiteralOf(t, 
variant.DecimalValue[decimal.Decimal64]{Scale: 2, Value: 
decimal.Decimal64(1234)}),
+                       iceberg.NewLiteral(dec),
+               },
+               {
+                       variantLiteralOf(t, 
variant.DecimalValue[decimal.Decimal128]{Scale: 2, Value: 
decimal128.FromI64(1234)}),
+                       iceberg.NewLiteral(dec),
+               },
+               // the casts of the literal for the underlying value apply
+               {variantLiteralOf(t, int8(5)), iceberg.NewLiteral(int64(5))},
+               {variantLiteralOf(t, int8(5)), 
iceberg.NewLiteral(iceberg.Decimal{Val: decimal128.FromI64(500), Scale: 2})},
+               {variantLiteralOf(t, float32(1.5)), 
iceberg.NewLiteral(float64(1.5))},
+               {variantLiteralOf(t, "12345"), 
iceberg.NewLiteral(int32(12345))},
+               {variantLiteralOf(t, testUUID.String()), 
iceberg.NewLiteral(testUUID)},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.to.Type().String(), func(t *testing.T) {
+                       got, err := tt.from.To(tt.to.Type())
+                       require.NoError(t, err)
+                       assert.Truef(t, tt.to.Equals(got), "expected: %s, got: 
%s", tt.to, got)
+               })
+       }
+
+       lit := variantLiteralOf(t, ts, variant.OptTimestampUTC)
+       casted, err := lit.To(iceberg.PrimitiveTypes.TimestampTz)
+       require.NoError(t, err)
+       assert.Equal(t, iceberg.NewLiteral(iceberg.Timestamp(ts)), casted)
+
+       // null, object and array variants still convert to the variant type
+       for _, v := range []any{nil, []any{int64(1)}, map[string]any{"a": 
int64(1)}} {
+               lit := variantLiteralOf(t, v)
+               same, err := lit.To(iceberg.VariantType{})
+               require.NoError(t, err)
+               assert.True(t, lit.Equals(same))
+       }
+}
+
 func TestFixedLiteral(t *testing.T) {
        emptyFixed := iceberg.FixedLiteral(nil)
        assert.NotPanics(t, func() {
@@ -1150,6 +1226,43 @@ func TestInvalidBinaryLiteralConversions(t *testing.T) {
        })
 }
 
+func TestInvalidVariantLiteralConversions(t *testing.T) {
+       // null, object and array variants hold no primitive value to convert
+       for _, v := range []any{nil, []any{int64(1)}, map[string]any{"a": 
int64(1)}} {
+               testInvalidLiteralConversions(t, variantLiteralOf(t, v), 
[]iceberg.Type{
+                       iceberg.PrimitiveTypes.Bool,
+                       iceberg.PrimitiveTypes.Int32,
+                       iceberg.PrimitiveTypes.Int64,
+                       iceberg.PrimitiveTypes.Float32,
+                       iceberg.PrimitiveTypes.Float64,
+                       iceberg.PrimitiveTypes.Date,
+                       iceberg.PrimitiveTypes.Time,
+                       iceberg.PrimitiveTypes.Timestamp,
+                       iceberg.PrimitiveTypes.TimestampTz,
+                       iceberg.DecimalTypeOf(9, 2),
+                       iceberg.PrimitiveTypes.String,
+                       iceberg.PrimitiveTypes.UUID,
+                       iceberg.PrimitiveTypes.Binary,
+                       iceberg.FixedTypeOf(2),
+               })
+       }
+
+       // a zero-value variant has no value to convert
+       testInvalidLiteralConversions(t, 
iceberg.VariantLiteral(variant.Value{}), []iceberg.Type{
+               iceberg.PrimitiveTypes.Int32,
+               iceberg.PrimitiveTypes.String,
+       })
+
+       _, err := variantLiteralOf(t, int8(5)).To(iceberg.PrimitiveTypes.String)
+       assert.ErrorIs(t, err, iceberg.ErrBadCast)
+       assert.ErrorContains(t, err, "VariantLiteral: could not cast value: 
Int32Literal to string")
+
+       // a cast error from the underlying literal that lacks ErrBadCast still 
gets it
+       _, err = variantLiteralOf(t, float32(1e30)).To(iceberg.DecimalTypeOf(9, 
2))
+       assert.ErrorIs(t, err, iceberg.ErrBadCast)
+       assert.ErrorContains(t, err, "VariantLiteral: ")
+}
+
 func TestBadStringLiteralCasts(t *testing.T) {
        tests := []iceberg.Type{
                iceberg.PrimitiveTypes.Int32,
diff --git a/variant_cast.go b/variant_cast.go
index cabf44bc2..f7db8e0da 100644
--- a/variant_cast.go
+++ b/variant_cast.go
@@ -173,28 +173,25 @@ func exactVariantMatch(pt variant.Type, raw any, typ 
PrimitiveType) (any, bool)
 }
 
 func castVariantDecimal(raw any, typ DecimalType) (any, bool) {
+       d, ok := decimalFromVariant(raw)
+       if !ok || d.Scale != typ.Scale() {
+               return nil, false
+       }
+
+       return d, true
+}
+
+func decimalFromVariant(raw any) (Decimal, bool) {
        switch d := raw.(type) {
        case variant.DecimalValue[decimal.Decimal32]:
-               if int(d.Scale) != typ.Scale() {
-                       return nil, false
-               }
-
                return Decimal{Val: 
decimal128.FromI64(int64(d.Value.(decimal.Decimal32))), Scale: int(d.Scale)}, 
true
        case variant.DecimalValue[decimal.Decimal64]:
-               if int(d.Scale) != typ.Scale() {
-                       return nil, false
-               }
-
                return Decimal{Val: 
decimal128.FromI64(int64(d.Value.(decimal.Decimal64))), Scale: int(d.Scale)}, 
true
        case variant.DecimalValue[decimal.Decimal128]:
-               if int(d.Scale) != typ.Scale() {
-                       return nil, false
-               }
-
                return Decimal{Val: d.Value.(decimal.Decimal128), Scale: 
int(d.Scale)}, true
        }
 
-       return nil, false
+       return Decimal{}, false
 }
 
 // castVariantToMicros converts a nanosecond or date leaf to a microsecond 
timestamp; the source's tz-awareness must match the target's (tz).
@@ -281,3 +278,35 @@ func literalFromCastValue(result any) Literal {
 
        return nil
 }
+
+func literalFromVariant(v variant.Value) (Literal, bool) {
+       // a zero Value has no header byte to read a type from
+       if len(v.Bytes()) == 0 {
+               return nil, false
+       }
+
+       raw := v.Value()
+       switch v.Type() {
+       case variant.Int8:
+               return Int32Literal(raw.(int8)), true
+       case variant.Int16:
+               return Int32Literal(raw.(int16)), true
+       case variant.Date:
+               return DateLiteral(raw.(arrow.Date32)), true
+       case variant.Time:
+               return TimeLiteral(raw.(arrow.Time64)), true
+       case variant.TimestampMicros, variant.TimestampMicrosNTZ:
+               return TimestampLiteral(raw.(arrow.Timestamp)), true
+       case variant.TimestampNanos, variant.TimestampNanosNTZ:
+               return TimestampNsLiteral(raw.(arrow.Timestamp)), true
+       case variant.Decimal4, variant.Decimal8, variant.Decimal16:
+               d, ok := decimalFromVariant(raw)
+
+               return DecimalLiteral(d), ok
+       }
+
+       // the rest map directly; null, object and array have no literal
+       lit := literalFromCastValue(raw)
+
+       return lit, lit != nil
+}

Reply via email to