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 0cd57944 feat(parquet/variant): add Metadata.SizeBytes to split a 
concatenated variant (#939)
0cd57944 is described below

commit 0cd579447b8c1a56e67e9ae1bd766d5c84e20c04
Author: Neelesh Salian <[email protected]>
AuthorDate: Wed Jul 15 08:17:22 2026 -0700

    feat(parquet/variant): add Metadata.SizeBytes to split a concatenated 
variant (#939)
    
    ### Rationale for this change
    
    A variant bound is stored on disk as the variant metadata bytes
    concatenated with the value bytes (metadata ++ value), as Iceberg does
    for variant `lower_bound` / `upper_bound`. `Metadata.Bytes()` returns
    the backing slice as-is, so a caller that reads such a concatenated
    buffer has no way to know where the metadata ends and the
    value begins. `NewMetadata` accepts a buffer with trailing bytes but
    does not expose the metadata's own length.
    
    iceberg-go needs this to read variant bounds back for scan pruning
    (apache/iceberg-go#1016): it must split a stored bound into metadata and
    value to reconstruct the variant. Filing it now so it is released ahead
    of that work.
    
    ### What changes are included in this PR?
    
    - Adds `Metadata.SizeBytes()`, which computes the metadata's own byte
    length from its header and offset table (the final offset gives the
    string-region length past `valuesStart`). A caller can then split a
    buffer at `data[m.SizeBytes():]`.
    
    ### Are these changes tested?
    
    Yes. `TestMetadataSizeBytes` covers empty metadata, a keyed object, and
    splitting a concatenated metadata++value buffer with a round-trip
    through `New`.`TestMetadataSizeBytesMultiByteOffset` covers the
    multi-byte offset-width path.
    
    ### Are there any user-facing changes?
    
    One new exported method. No change to existing behavior.
---
 parquet/variant/variant.go      | 19 ++++++++++++
 parquet/variant/variant_test.go | 66 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 85 insertions(+)

diff --git a/parquet/variant/variant.go b/parquet/variant/variant.go
index 1fe44cc5..0e8a904b 100644
--- a/parquet/variant/variant.go
+++ b/parquet/variant/variant.go
@@ -231,6 +231,25 @@ func (m Metadata) OffsetSize() uint8 {
 // DictionarySize returns the number of keys in the metadata dictionary.
 func (m Metadata) DictionarySize() uint32 { return uint32(len(m.keys)) }
 
+// SizeBytes returns the metadata's own byte length, so a caller can split a
+// buffer that stores metadata concatenated with a value at 
data[m.SizeBytes():].
+func (m Metadata) SizeBytes() int {
+       offsetSz := uint32(m.OffsetSize())
+       if uint32(len(m.data)) < uint32(hdrSizeBytes)+offsetSz {
+               return len(m.data)
+       }
+
+       dictSize := readLEU32(m.data[hdrSizeBytes : 
uint32(hdrSizeBytes)+offsetSz])
+       // Final offset (index dictSize) is the string-region length; it starts 
at valuesStart.
+       lastOffsetPos := uint32(hdrSizeBytes) + offsetSz*(1+dictSize)
+       valuesStart := lastOffsetPos + offsetSz
+       if valuesStart > uint32(len(m.data)) {
+               return len(m.data)
+       }
+
+       return int(valuesStart + readLEU32(m.data[lastOffsetPos:valuesStart]))
+}
+
 // KeyAt returns the string key at the given dictionary ID.
 // Returns an error if the ID is out of range.
 func (m Metadata) KeyAt(id uint32) (string, error) {
diff --git a/parquet/variant/variant_test.go b/parquet/variant/variant_test.go
index 5b76fd3a..b2f55339 100644
--- a/parquet/variant/variant_test.go
+++ b/parquet/variant/variant_test.go
@@ -22,6 +22,7 @@ import (
        "math"
        "os"
        "path/filepath"
+       "strings"
        "testing"
        "time"
 
@@ -833,3 +834,68 @@ func TestObjectBinarySearch(t *testing.T) {
                assert.Equal(t, int8(i), field.Value.Value())
        }
 }
+
+func TestMetadataSizeBytes(t *testing.T) {
+       t.Run("empty metadata", func(t *testing.T) {
+               var b variant.Builder
+               require.NoError(t, b.AppendNull())
+               v, err := b.Build()
+               require.NoError(t, err)
+               m := v.Metadata()
+               assert.Equal(t, len(m.Bytes()), m.SizeBytes())
+               assert.Equal(t, len(variant.EmptyMetadataBytes), m.SizeBytes())
+       })
+
+       t.Run("object with keys", func(t *testing.T) {
+               var b variant.Builder
+               start := b.Offset()
+               fields := []variant.FieldEntry{
+                       b.NextField(start, "a"),
+               }
+               require.NoError(t, b.AppendInt(1))
+               fields = append(fields, b.NextField(start, "longer_key"))
+               require.NoError(t, b.AppendString("hello"))
+               require.NoError(t, b.FinishObject(start, fields))
+               v, err := b.Build()
+               require.NoError(t, err)
+
+               m := v.Metadata()
+               assert.Equal(t, len(m.Bytes()), m.SizeBytes())
+
+               // Split a concatenated metadata++value buffer using SizeBytes.
+               concat := append(append([]byte{}, m.Bytes()...), v.Bytes()...)
+               parsed, err := variant.NewMetadata(concat)
+               require.NoError(t, err)
+               split := parsed.SizeBytes()
+               assert.Equal(t, m.Bytes(), concat[:split])
+               assert.Equal(t, v.Bytes(), concat[split:])
+
+               rt, err := variant.New(concat[:split], concat[split:])
+               require.NoError(t, err)
+               assert.Equal(t, v.Bytes(), rt.Bytes())
+       })
+}
+
+func TestMetadataSizeBytesMultiByteOffset(t *testing.T) {
+       // A long key forces the metadata offset width above 1 byte, exercising 
the
+       // offsetSize>1 arithmetic in SizeBytes.
+       longKey := strings.Repeat("k", 300)
+       var b variant.Builder
+       start := b.Offset()
+       fields := []variant.FieldEntry{b.NextField(start, longKey)}
+       require.NoError(t, b.AppendInt(7))
+       require.NoError(t, b.FinishObject(start, fields))
+       v, err := b.Build()
+       require.NoError(t, err)
+
+       m := v.Metadata()
+       require.Greater(t, m.OffsetSize(), uint8(1), "test must exercise 
multi-byte offsets")
+       assert.Equal(t, len(m.Bytes()), m.SizeBytes())
+
+       concat := append(append([]byte{}, m.Bytes()...), v.Bytes()...)
+       parsed, err := variant.NewMetadata(concat)
+       require.NoError(t, err)
+       split := parsed.SizeBytes()
+       assert.Equal(t, m.Bytes(), concat[:split])
+       assert.Equal(t, v.Bytes(), concat[split:])
+}

Reply via email to