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
+}