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/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new d24b83eb fix(arrow/extensions): reject Null Variant typed_value (#1243)
d24b83eb is described below

commit d24b83ebd28d2bb2568d0a2ea2e1f10d0240694b
Author: Digvijay <[email protected]>
AuthorDate: Wed Sep 2 13:27:16 2026 -0500

    fix(arrow/extensions): reject Null Variant typed_value (#1243)
    
    ### Rationale for this change
    
    A Null `typed_value` column is not a valid shredded Variant type. Nulls
    are encoded in the `value` column as Variant null (`00`). A dedicated
    Null `typed_value` would also make “field present and null”
    indistinguishable from “field missing” for shredded objects.
    
    See apache/arrow#50622 and apache/arrow#50810.
    
    Fixes #1205
    
    ### What changes are included in this PR?
    
    - `NewVariantType` rejects a Null `typed_value` field
    - Nested shredded fields with a Null `typed_value` are also rejected
    
    ### Are these changes tested?
    
    - `go test ./arrow/extensions`
    
    ### Are there any user-facing changes?
    
    Yes — constructing a Variant extension type with a Null `typed_value`
    now returns `arrow.ErrInvalid`.
    
    ---------
    
    Signed-off-by: Digvijay <[email protected]>
---
 arrow/extensions/variant.go      | 43 +++++++++++++++++++++++++------
 arrow/extensions/variant_test.go | 55 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 90 insertions(+), 8 deletions(-)

diff --git a/arrow/extensions/variant.go b/arrow/extensions/variant.go
index a0cc74ed..5222d5fb 100644
--- a/arrow/extensions/variant.go
+++ b/arrow/extensions/variant.go
@@ -122,10 +122,10 @@ func createShreddedField(dt arrow.DataType) 
arrow.DataType {
 //      ), Nullable: true})
 //
 // This is intended to be a convenient way to create a shredded variant type 
from a definition
-// of the fields to shred. If the provided data type is nil, it will create a 
default
-// variant type.
+// of the fields to shred. If the provided data type is nil or Null (including 
an
+// extension whose storage is Null), it will create a default variant type.
 func NewShreddedVariantType(dt arrow.DataType) *VariantType {
-       if dt == nil {
+       if dt == nil || isNullType(dt) {
                return NewDefaultVariantType()
        }
 
@@ -220,9 +220,12 @@ func NewVariantType(storage arrow.DataType) (*VariantType, 
error) {
                return nil, fmt.Errorf("%w: typed_value field must be nullable, 
got %s", arrow.ErrInvalid, typedValueField.Type)
        }
 
-       dt := typedValueField.Type
-       if dt.ID() == arrow.EXTENSION {
-               dt = dt.(arrow.ExtensionType).StorageType()
+       dt := storageType(typedValueField.Type)
+       if dt == nil {
+               return nil, fmt.Errorf("%w: typed_value field has invalid 
storage type", arrow.ErrInvalid)
+       }
+       if dt.ID() == arrow.NULL {
+               return nil, fmt.Errorf("%w: typed_value field must not be null 
type", arrow.ErrInvalid)
        }
 
        if nt, ok := dt.(arrow.NestedType); ok {
@@ -291,11 +294,35 @@ func isBinary(dt arrow.DataType) bool {
                dt.ID() == arrow.BINARY_VIEW
 }
 
+func storageType(dt arrow.DataType) arrow.DataType {
+       seen := make(map[arrow.DataType]struct{})
+       for dt != nil {
+               ext, ok := dt.(arrow.ExtensionType)
+               if !ok {
+                       return dt
+               }
+               if _, dup := seen[dt]; dup {
+                       return nil
+               }
+               seen[dt] = struct{}{}
+               dt = ext.StorageType()
+       }
+       return nil
+}
+
+func isNullType(dt arrow.DataType) bool {
+       st := storageType(dt)
+       return st == nil || st.ID() == arrow.NULL
+}
+
 func validStruct(s *arrow.StructType) bool {
        switch s.NumFields() {
        case 1:
                f := s.Field(0)
-               return (f.Name == "value" && isBinary(f.Type)) || f.Name == 
"typed_value"
+               if f.Name == "value" {
+                       return isBinary(f.Type)
+               }
+               return f.Name == "typed_value" && !isNullType(f.Type)
        case 2:
                valField, ok := s.FieldByName("value")
                if !ok || !valField.Nullable || !isBinary(valField.Type) {
@@ -311,7 +338,7 @@ func validStruct(s *arrow.StructType) bool {
                        return validNestedType(nt)
                }
 
-               return true
+               return !isNullType(typedField.Type)
        default:
                return false
        }
diff --git a/arrow/extensions/variant_test.go b/arrow/extensions/variant_test.go
index a39fd513..905fc5d9 100644
--- a/arrow/extensions/variant_test.go
+++ b/arrow/extensions/variant_test.go
@@ -83,6 +83,27 @@ func TestVariantExtensionType(t *testing.T) {
                        arrow.Field{Name: "metadata", Type: 
arrow.BinaryTypes.String, Nullable: false},
                        arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: false}),
                        "metadata field must be non-nullable binary type, got 
utf8"},
+               {arrow.StructOf(
+                       arrow.Field{Name: "metadata", Type: 
arrow.BinaryTypes.Binary, Nullable: false},
+                       arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                       arrow.Field{Name: "typed_value", Type: arrow.Null, 
Nullable: true}),
+                       "typed_value field must not be null type"},
+               {arrow.StructOf(
+                       arrow.Field{Name: "metadata", Type: 
arrow.BinaryTypes.Binary, Nullable: false},
+                       arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                       arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(arrow.Null, "null", "test"), Nullable: true}),
+                       "typed_value field must not be null type"},
+               {arrow.StructOf(
+                       arrow.Field{Name: "metadata", Type: 
arrow.BinaryTypes.Binary, Nullable: false},
+                       arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                       arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(
+                               extensions.NewOpaqueType(arrow.Null, "null", 
"test"), "null", "test"), Nullable: true}),
+                       "typed_value field must not be null type"},
+               {arrow.StructOf(
+                       arrow.Field{Name: "metadata", Type: 
arrow.BinaryTypes.Binary, Nullable: false},
+                       arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                       arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(nil, "null", "test"), Nullable: true}),
+                       "typed_value field has invalid storage type"},
        }
 
        for _, tt := range tests {
@@ -113,6 +134,31 @@ func TestVariantExtensionBadNestedTypes(t *testing.T) {
                        ), Nullable: false})},
                {"empty struct elem", arrow.StructOf(
                        arrow.Field{Name: "foobar", Type: arrow.StructOf(), 
Nullable: false})},
+               {"null typed_value in shredded field", arrow.StructOf(
+                       arrow.Field{Name: "foobar", Type: arrow.StructOf(
+                               arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                               arrow.Field{Name: "typed_value", Type: 
arrow.Null, Nullable: true},
+                       ), Nullable: false})},
+               {"null typed_value extension in one-field shredded field", 
arrow.StructOf(
+                       arrow.Field{Name: "foobar", Type: arrow.StructOf(
+                               arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(arrow.Null, "null", "test"), Nullable: true},
+                       ), Nullable: false})},
+               {"null typed_value extension in two-field shredded field", 
arrow.StructOf(
+                       arrow.Field{Name: "foobar", Type: arrow.StructOf(
+                               arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                               arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(arrow.Null, "null", "test"), Nullable: true},
+                       ), Nullable: false})},
+               {"double-wrapped null typed_value in one-field shredded field", 
arrow.StructOf(
+                       arrow.Field{Name: "foobar", Type: arrow.StructOf(
+                               arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(
+                                       extensions.NewOpaqueType(arrow.Null, 
"null", "test"), "null", "test"), Nullable: true},
+                       ), Nullable: false})},
+               {"double-wrapped null typed_value in two-field shredded field", 
arrow.StructOf(
+                       arrow.Field{Name: "foobar", Type: arrow.StructOf(
+                               arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                               arrow.Field{Name: "typed_value", Type: 
extensions.NewOpaqueType(
+                                       extensions.NewOpaqueType(arrow.Null, 
"null", "test"), "null", "test"), Nullable: true},
+                       ), Nullable: false})},
                {"non-nullable two elem struct", arrow.StructOf(
                        arrow.Field{Name: "foobar", Type: arrow.StructOf(
                                arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
@@ -1560,6 +1606,15 @@ func TestVariantBuilderUnmarshalJSON(t *testing.T) {
 func TestNewSimpleShreddedVariantType(t *testing.T) {
        assert.True(t, arrow.TypeEqual(extensions.NewDefaultVariantType(),
                extensions.NewShreddedVariantType(nil)))
+       assert.True(t, arrow.TypeEqual(extensions.NewDefaultVariantType(),
+               extensions.NewShreddedVariantType(arrow.Null)))
+       assert.True(t, arrow.TypeEqual(extensions.NewDefaultVariantType(),
+               
extensions.NewShreddedVariantType(extensions.NewOpaqueType(arrow.Null, "null", 
"test"))))
+       assert.True(t, arrow.TypeEqual(extensions.NewDefaultVariantType(),
+               extensions.NewShreddedVariantType(extensions.NewOpaqueType(
+                       extensions.NewOpaqueType(arrow.Null, "null", "test"), 
"null", "test"))))
+       assert.True(t, arrow.TypeEqual(extensions.NewDefaultVariantType(),
+               extensions.NewShreddedVariantType(extensions.NewOpaqueType(nil, 
"null", "test"))))
 
        vt := extensions.NewShreddedVariantType(arrow.PrimitiveTypes.Float32)
        s := arrow.StructOf(

Reply via email to