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

CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-dotnet.git


The following commit(s) were added to refs/heads/main by this push:
     new d952f30  fix: Read large_binary and binary_view storage in shredded 
variant readers (#449)
d952f30 is described below

commit d952f30b5226dc5929867924793f4ce57f3f0aba
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Sun Sep 27 09:40:33 2026 -0700

    fix: Read large_binary and binary_view storage in shredded variant readers 
(#449)
    
    ## What's Changed
    
    `VariantArray` accepts `binary`, `large_binary` or `binary_view` for
    `metadata` and `value` at every nesting level, and
    `ShredSchema.FromArrowType` maps `large_utf8` / `large_binary`
    `typed_value` columns. The shredded readers (`ShreddedVariant`,
    `ShreddedObject`, `ShreddedArray`) cast those columns to `BinaryArray` /
    `StringArray`, so any other representation threw `InvalidCastException`.
    This even affected `GetLogicalVariantValue` on unshredded `large_binary`
    columns.
    
    This replaces the casts with two small helpers in `ShreddingHelpers`,
    `GetBytes` and `GetString`, that read from any binary or string
    representation. (`IBinaryArray` would have been the natural abstraction,
    but it's internal to `Apache.Arrow`.)
    
    The new tests shred a column that has residuals at the top level, in a
    partially shredded object, in an object field, and in a list element.
    They then convert every binary field, recursively, to `large_binary` or
    `binary_view` (and string/binary `typed_value` columns to `large_utf8` /
    `large_binary`), and read everything back, both directly and through the
    `Shred` / `Reassemble` entry points added in #447. All of these tests
    fail without the fix.
    
    Performance: `GetLogicalVariantValue` over 100k rows (BenchmarkDotNet,
    medium job, two runs) is indistinguishable from the old cast for both
    shredded and unshredded `binary` columns. An inlinable `BinaryArray`
    fast path was also tried and measured slower, so it isn't included.
    
    Closes #448.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../Shredding/ShreddedArray.cs                     |   5 +-
 .../Shredding/ShreddedObject.cs                    |   8 +-
 .../Shredding/ShreddedVariant.cs                   |  10 +-
 .../Shredding/ShreddingHelpers.cs                  |  34 +++
 .../Shredding/ShreddedVariantStorageTypeTests.cs   | 234 +++++++++++++++++++++
 5 files changed, 277 insertions(+), 14 deletions(-)

diff --git a/src/Apache.Arrow.Operations/Shredding/ShreddedArray.cs 
b/src/Apache.Arrow.Operations/Shredding/ShreddedArray.cs
index 2dcea00..e1d5956 100644
--- a/src/Apache.Arrow.Operations/Shredding/ShreddedArray.cs
+++ b/src/Apache.Arrow.Operations/Shredding/ShreddedArray.cs
@@ -106,7 +106,7 @@ namespace Apache.Arrow.Operations.Shredding
                 reader = default;
                 return false;
             }
-            ReadOnlySpan<byte> bytes = ((BinaryArray)_residual).GetBytes(_row, 
out _);
+            ReadOnlySpan<byte> bytes = ShreddingHelpers.GetBytes(_residual, 
_row);
             reader = new VariantReader(_metadata, bytes);
             return true;
         }
@@ -145,8 +145,7 @@ namespace Apache.Arrow.Operations.Shredding
             {
                 return VariantValue.Null;
             }
-            BinaryArray residualBinary = (BinaryArray)_residual;
-            ReadOnlySpan<byte> bytes = residualBinary.GetBytes(_row, out _);
+            ReadOnlySpan<byte> bytes = ShreddingHelpers.GetBytes(_residual, 
_row);
             return new VariantReader(_metadata, bytes).ToVariantValue();
         }
     }
diff --git a/src/Apache.Arrow.Operations/Shredding/ShreddedObject.cs 
b/src/Apache.Arrow.Operations/Shredding/ShreddedObject.cs
index 07e2381..98a0651 100644
--- a/src/Apache.Arrow.Operations/Shredding/ShreddedObject.cs
+++ b/src/Apache.Arrow.Operations/Shredding/ShreddedObject.cs
@@ -104,7 +104,7 @@ namespace Apache.Arrow.Operations.Shredding
                 reader = default;
                 return false;
             }
-            ReadOnlySpan<byte> bytes = 
((BinaryArray)_residual).GetBytes(_index, out _);
+            ReadOnlySpan<byte> bytes = ShreddingHelpers.GetBytes(_residual, 
_index);
             reader = new VariantReader(_metadata, bytes);
             return true;
         }
@@ -128,8 +128,7 @@ namespace Apache.Arrow.Operations.Shredding
             // No shredded fields at this row — whatever is in the residual IS 
the value.
             if (!typedPopulated)
             {
-                BinaryArray binary = (BinaryArray)_residual;
-                ReadOnlySpan<byte> bytes = binary.GetBytes(_index, out _);
+                ReadOnlySpan<byte> bytes = 
ShreddingHelpers.GetBytes(_residual, _index);
                 return new VariantReader(_metadata, bytes).ToVariantValue();
             }
 
@@ -151,8 +150,7 @@ namespace Apache.Arrow.Operations.Shredding
             // Partially shredded object — merge residual unshredded fields.
             if (residualPopulated)
             {
-                BinaryArray residualBinary = (BinaryArray)_residual;
-                ReadOnlySpan<byte> residualBytes = 
residualBinary.GetBytes(_index, out _);
+                ReadOnlySpan<byte> residualBytes = 
ShreddingHelpers.GetBytes(_residual, _index);
                 VariantReader residualReader = new VariantReader(_metadata, 
residualBytes);
                 if (!residualReader.IsObject)
                 {
diff --git a/src/Apache.Arrow.Operations/Shredding/ShreddedVariant.cs 
b/src/Apache.Arrow.Operations/Shredding/ShreddedVariant.cs
index 5ef3575..3b58a75 100644
--- a/src/Apache.Arrow.Operations/Shredding/ShreddedVariant.cs
+++ b/src/Apache.Arrow.Operations/Shredding/ShreddedVariant.cs
@@ -125,8 +125,7 @@ namespace Apache.Arrow.Operations.Shredding
         {
             if (HasResidual)
             {
-                BinaryArray binary = (BinaryArray)_valueArray;
-                ReadOnlySpan<byte> bytes = binary.GetBytes(_index, out _);
+                ReadOnlySpan<byte> bytes = 
ShreddingHelpers.GetBytes(_valueArray, _index);
                 reader = new VariantReader(_metadata, bytes);
                 return true;
             }
@@ -168,8 +167,7 @@ namespace Apache.Arrow.Operations.Shredding
             {
                 throw new InvalidOperationException("No residual value to 
read.");
             }
-            BinaryArray binary = (BinaryArray)_valueArray;
-            ReadOnlySpan<byte> bytes = binary.GetBytes(_index, out _);
+            ReadOnlySpan<byte> bytes = ShreddingHelpers.GetBytes(_valueArray, 
_index);
             return new VariantReader(_metadata, bytes).ToVariantValue();
         }
 
@@ -266,10 +264,10 @@ namespace Apache.Arrow.Operations.Shredding
         public long GetTimestampNtzNanos() => 
((TimestampArray)RequireTyped(ShredType.TimestampNtzNanos)).GetValue(_index).Value;
 
         /// <summary>Reads the shredded string value at this slot.</summary>
-        public string GetString() => 
((StringArray)RequireTyped(ShredType.String)).GetString(_index);
+        public string GetString() => 
ShreddingHelpers.GetString(RequireTyped(ShredType.String), _index);
 
         /// <summary>Reads the shredded binary value at this slot as a byte 
span.</summary>
-        public ReadOnlySpan<byte> GetBinaryBytes() => 
((BinaryArray)RequireTyped(ShredType.Binary)).GetBytes(_index);
+        public ReadOnlySpan<byte> GetBinaryBytes() => 
ShreddingHelpers.GetBytes(RequireTyped(ShredType.Binary), _index);
 
         /// <summary>Reads the shredded UUID at this slot.</summary>
         public Guid GetUuid()
diff --git a/src/Apache.Arrow.Operations/Shredding/ShreddingHelpers.cs 
b/src/Apache.Arrow.Operations/Shredding/ShreddingHelpers.cs
index f7be85e..36b628e 100644
--- a/src/Apache.Arrow.Operations/Shredding/ShreddingHelpers.cs
+++ b/src/Apache.Arrow.Operations/Shredding/ShreddingHelpers.cs
@@ -44,5 +44,39 @@ namespace Apache.Arrow.Operations.Shredding
 
             return new ShreddedVariant(slotSchema, metadata, valueArr, 
typedArr, index);
         }
+
+        /// <summary>
+        /// Reads the bytes at <paramref name="index"/> from any binary 
representation a
+        /// variant column may use (binary, large_binary or binary_view).
+        /// </summary>
+        public static ReadOnlySpan<byte> GetBytes(IArrowArray array, int index)
+        {
+            switch (array)
+            {
+                case BinaryArray binary: return binary.GetBytes(index);
+                case LargeBinaryArray largeBinary: return 
largeBinary.GetBytes(index);
+                case BinaryViewArray binaryView: return 
binaryView.GetBytes(index);
+                default:
+                    throw new InvalidOperationException(
+                        $"Cannot read variant bytes from an array of type 
{array.Data.DataType.TypeId}.");
+            }
+        }
+
+        /// <summary>
+        /// Reads the string at <paramref name="index"/> from any string 
representation
+        /// (utf8, large_utf8 or utf8_view).
+        /// </summary>
+        public static string GetString(IArrowArray array, int index)
+        {
+            switch (array)
+            {
+                case StringArray str: return str.GetString(index);
+                case LargeStringArray largeStr: return 
largeStr.GetString(index);
+                case StringViewArray strView: return strView.GetString(index);
+                default:
+                    throw new InvalidOperationException(
+                        $"Cannot read a string from an array of type 
{array.Data.DataType.TypeId}.");
+            }
+        }
     }
 }
diff --git 
a/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantStorageTypeTests.cs
 
b/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantStorageTypeTests.cs
new file mode 100644
index 0000000..0843b78
--- /dev/null
+++ 
b/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantStorageTypeTests.cs
@@ -0,0 +1,234 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using Apache.Arrow;
+using Apache.Arrow.Operations.Shredding;
+using Apache.Arrow.Scalars.Variant;
+using Apache.Arrow.Types;
+using Xunit;
+
+namespace Apache.Arrow.Operations.Tests.Shredding
+{
+    /// <summary>
+    /// The shredded readers must accept every binary representation a variant 
column
+    /// may use for its <c>metadata</c> and <c>value</c> fields (binary, 
large_binary,
+    /// binary_view), at every nesting level, as well as large_utf8 / 
large_binary
+    /// <c>typed_value</c> columns.
+    /// </summary>
+    public class ShreddedVariantStorageTypeTests
+    {
+        public enum Storage { LargeBinary, BinaryView }
+
+        private static readonly ShredSchema Schema = ShredSchema.ForObject(new 
Dictionary<string, ShredSchema>
+        {
+            ["a"] = ShredSchema.Primitive(ShredType.Int32),
+            ["s"] = ShredSchema.Primitive(ShredType.String),
+            ["bin"] = ShredSchema.Primitive(ShredType.Binary),
+            ["tags"] = 
ShredSchema.ForArray(ShredSchema.Primitive(ShredType.Int32)),
+        });
+
+        private static VariantValue Obj(params (string Name, VariantValue 
Value)[] fields)
+        {
+            var dict = new Dictionary<string, VariantValue>();
+            foreach (var (name, value) in fields) dict[name] = value;
+            return VariantValue.FromObject(dict);
+        }
+
+        // Exercises residuals at the top level, inside a partially shredded 
object,
+        // inside an object field element group, and inside a list element 
group.
+        private static readonly List<VariantValue?> Rows = new 
List<VariantValue?>
+        {
+            Obj(("a", VariantValue.FromInt32(1)),
+                ("s", VariantValue.FromString("hello")),
+                ("bin", VariantValue.FromBinary(new byte[] { 1, 2, 3 })),
+                ("tags", VariantValue.FromArray(VariantValue.FromInt32(7), 
VariantValue.FromString("residual element"))),
+                ("extra", VariantValue.FromBoolean(true))),
+            null,
+            VariantValue.FromString("not an object"),
+            Obj(("a", VariantValue.FromString("residual field")),
+                ("tags", VariantValue.FromString("not an array"))),
+        };
+
+        private static VariantArray BuildShredded()
+        {
+            (byte[] metadata, IReadOnlyList<ShredResult> rows) = 
VariantShredder.Shred(Rows, Schema);
+            return ShreddedVariantArrayBuilder.Build(Schema, metadata, rows);
+        }
+
+        [Theory]
+        [InlineData(Storage.LargeBinary)]
+        [InlineData(Storage.BinaryView)]
+        public void GetLogicalVariantValue_ReadsAlternateStorage(Storage 
storage)
+        {
+            VariantArray array = Convert(BuildShredded(), storage);
+
+            Assert.Equal(Rows.Count, array.Length);
+            for (int i = 0; i < Rows.Count; i++)
+            {
+                if (Rows[i].HasValue)
+                    Assert.Equal(Rows[i].Value, 
array.GetLogicalVariantValue(i));
+                else
+                    Assert.True(array.IsNull(i));
+            }
+        }
+
+        [Theory]
+        [InlineData(Storage.LargeBinary)]
+        [InlineData(Storage.BinaryView)]
+        public void 
GetLogicalVariantValue_ReadsAlternateStorage_Unshredded(Storage storage)
+        {
+            var builder = new VariantArray.Builder();
+            builder.AppendRange(Rows);
+            VariantArray array = Convert(builder.Build(), storage);
+
+            Assert.Equal(Rows[0].Value, array.GetLogicalVariantValue(0));
+        }
+
+        [Fact]
+        public void TypedAccessors_ReadLargeTypedColumns()
+        {
+            VariantArray array = Convert(BuildShredded(), Storage.LargeBinary);
+            ShreddedObject obj = array.GetShreddedVariant(0).GetObject();
+
+            Assert.True(obj.TryGetField("s", out ShreddedVariant s));
+            Assert.Equal("hello", s.GetString());
+            Assert.True(obj.TryGetField("bin", out ShreddedVariant bin));
+            Assert.Equal(new byte[] { 1, 2, 3 }, 
bin.GetBinaryBytes().ToArray());
+        }
+
+        [Theory]
+        [InlineData(Storage.LargeBinary, true)]
+        [InlineData(Storage.LargeBinary, false)]
+        [InlineData(Storage.BinaryView, true)]
+        [InlineData(Storage.BinaryView, false)]
+        public void Shred_ReadsAlternateStorage(Storage storage, bool 
shreddedInput)
+        {
+            VariantArray input = shreddedInput ? BuildShredded() : 
BuildUnshredded();
+
+            AssertRows(Convert(input, storage).Shred(Schema));
+        }
+
+        [Theory]
+        [InlineData(Storage.LargeBinary)]
+        [InlineData(Storage.BinaryView)]
+        public void Reassemble_ReadsAlternateStorage(Storage storage)
+        {
+            VariantArray reassembled = Convert(BuildShredded(), 
storage).Reassemble();
+
+            Assert.False(reassembled.IsShredded);
+            AssertRows(reassembled);
+        }
+
+        private static VariantArray BuildUnshredded()
+        {
+            var builder = new VariantArray.Builder();
+            builder.AppendRange(Rows);
+            return builder.Build();
+        }
+
+        private static void AssertRows(VariantArray array)
+        {
+            Assert.Equal(Rows.Count, array.Length);
+            for (int i = 0; i < Rows.Count; i++)
+            {
+                if (Rows[i].HasValue)
+                    Assert.Equal(Rows[i].Value, 
array.GetLogicalVariantValue(i));
+                else
+                    Assert.True(array.IsNull(i));
+            }
+        }
+
+        // ---------------------------------------------------------------
+        // Storage conversion
+        // ---------------------------------------------------------------
+
+        private static VariantArray Convert(VariantArray array, Storage 
storage)
+        {
+            return new 
VariantArray((StructArray)ConvertArray(array.StorageArray, null, storage));
+        }
+
+        /// <summary>
+        /// Recursively rewrites <c>metadata</c> / <c>value</c> binary fields 
to the target
+        /// storage. For <see cref="Storage.LargeBinary"/> it also rewrites 
string and binary
+        /// <c>typed_value</c> columns to large_utf8 / large_binary; there is 
no view
+        /// counterpart because the shredding schema doesn't map view types 
for typed_value.
+        /// </summary>
+        private static IArrowArray ConvertArray(IArrowArray array, string 
fieldName, Storage storage)
+        {
+            bool isVariantBinary = fieldName == "metadata" || fieldName == 
"value";
+            switch (array)
+            {
+                case StringArray str when !isVariantBinary && storage == 
Storage.LargeBinary:
+                    {
+                        var b = new LargeStringArray.Builder();
+                        for (int i = 0; i < str.Length; i++)
+                        {
+                            if (str.IsNull(i)) b.AppendNull(); else 
b.Append(str.GetString(i));
+                        }
+                        return b.Build();
+                    }
+                case StringArray str:
+                    return str;
+                case BinaryArray bin when isVariantBinary || storage == 
Storage.LargeBinary:
+                    return storage == Storage.LargeBinary ? ToLargeBinary(bin) 
: (IArrowArray)ToBinaryView(bin);
+                case StructArray st:
+                    {
+                        var type = (StructType)st.Data.DataType;
+                        var fields = new List<Field>();
+                        var children = new List<IArrowArray>();
+                        for (int f = 0; f < type.Fields.Count; f++)
+                        {
+                            Field field = type.Fields[f];
+                            IArrowArray child = ConvertArray(st.Fields[f], 
field.Name, storage);
+                            children.Add(child);
+                            fields.Add(new Field(field.Name, 
child.Data.DataType, field.IsNullable));
+                        }
+                        return new StructArray(new StructType(fields), 
st.Length, children, st.NullBitmapBuffer, st.NullCount);
+                    }
+                case ListArray list:
+                    {
+                        Field element = 
((ListType)list.Data.DataType).ValueField;
+                        IArrowArray values = ConvertArray(list.Values, 
element.Name, storage);
+                        var listType = new ListType(new Field(element.Name, 
values.Data.DataType, element.IsNullable));
+                        return new ListArray(listType, list.Length, 
list.ValueOffsetsBuffer, values, list.NullBitmapBuffer, list.NullCount);
+                    }
+                default:
+                    return array;
+            }
+        }
+
+        private static LargeBinaryArray ToLargeBinary(BinaryArray bin)
+        {
+            var b = new LargeBinaryArray.Builder();
+            for (int i = 0; i < bin.Length; i++)
+            {
+                if (bin.IsNull(i)) b.AppendNull(); else 
b.Append(bin.GetBytes(i));
+            }
+            return b.Build();
+        }
+
+        private static BinaryViewArray ToBinaryView(BinaryArray bin)
+        {
+            var b = new BinaryViewArray.Builder();
+            for (int i = 0; i < bin.Length; i++)
+            {
+                if (bin.IsNull(i)) b.AppendNull(); else 
b.Append(bin.GetBytes(i));
+            }
+            return b.Build();
+        }
+    }
+}

Reply via email to