From 83aed27acae645be925e035760710e7cbaef8221 Mon Sep 17 00:00:00 2001 From: Digvijay Date: Fri, 28 Aug 2026 00:14:57 -0500 Subject: [PATCH] fix(arrow/extensions): reject Null Variant typed_value A Null typed_value column is not a valid shredded type. Variant nulls belong in the value column. Fixes #1205 Signed-off-by: Digvijay --- arrow/extensions/variant.go | 11 +++++++++-- arrow/extensions/variant_test.go | 10 ++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/arrow/extensions/variant.go b/arrow/extensions/variant.go index fee2e046a..555946961 100644 --- a/arrow/extensions/variant.go +++ b/arrow/extensions/variant.go @@ -225,6 +225,10 @@ func NewVariantType(storage arrow.DataType) (*VariantType, error) { dt = dt.(arrow.ExtensionType).StorageType() } + 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 { if !validNestedType(nt) { return nil, fmt.Errorf("%w: typed_value field must be a valid nested type, got %s", arrow.ErrInvalid, typedValueField.Type) @@ -295,7 +299,10 @@ 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" && f.Type.ID() != arrow.NULL case 2: valField, ok := s.FieldByName("value") if !ok || !valField.Nullable || !isBinary(valField.Type) { @@ -311,7 +318,7 @@ func validStruct(s *arrow.StructType) bool { return validNestedType(nt) } - return true + return typedField.Type.ID() != arrow.NULL default: return false } diff --git a/arrow/extensions/variant_test.go b/arrow/extensions/variant_test.go index a39fd5131..e49177465 100644 --- a/arrow/extensions/variant_test.go +++ b/arrow/extensions/variant_test.go @@ -83,6 +83,11 @@ 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"}, } for _, tt := range tests { @@ -113,6 +118,11 @@ 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})}, {"non-nullable two elem struct", arrow.StructOf( arrow.Field{Name: "foobar", Type: arrow.StructOf( arrow.Field{Name: "value", Type: arrow.BinaryTypes.Binary, Nullable: true},