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 0a28b231 fix(parquet): preserve dictionary index types (#1098)
0a28b231 is described below

commit 0a28b2316a2b456bdbb5316ea78e9169fdb70967
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 7 18:40:43 2026 +0200

    fix(parquet): preserve dictionary index types (#1098)
    
    ### Rationale for this change
    
    Parquet dictionary encoding uses int32 indexes, but stored Arrow schema
    metadata can specify a different dictionary index type. The schema
    restoration path currently hardcodes int32 and changes the dictionary
    type and index buffer width on read.
    
    ### What changes are included in this PR?
    
    Restore the stored index type and safely cast the reader indexes before
    rebuilding each dictionary array. Preserve dictionary values and the
    ordered flag.
    
    ### Are these changes tested?
    
    - `go test ./parquet/pqarrow -run
    TestArrowDictionaryTypePreservesIndexType -count=1`
    
    ### Are there any user-facing changes?
    
    No API changes. This corrects the reported behavior while preserving the
    existing ownership and compatibility contracts.
---
 parquet/pqarrow/column_readers.go            | 47 +++++++++++++++++++-----
 parquet/pqarrow/dictionary_multipage_test.go | 55 ++++++++++++++++++++++++++++
 parquet/pqarrow/schema.go                    |  2 +-
 3 files changed, 93 insertions(+), 11 deletions(-)

diff --git a/parquet/pqarrow/column_readers.go 
b/parquet/pqarrow/column_readers.go
index 5c216946..9064d018 100644
--- a/parquet/pqarrow/column_readers.go
+++ b/parquet/pqarrow/column_readers.go
@@ -17,6 +17,7 @@
 package pqarrow
 
 import (
+       "context"
        "encoding/binary"
        "errors"
        "fmt"
@@ -29,6 +30,7 @@ import (
        "github.com/apache/arrow-go/v18/arrow"
        "github.com/apache/arrow-go/v18/arrow/array"
        "github.com/apache/arrow-go/v18/arrow/bitutil"
+       "github.com/apache/arrow-go/v18/arrow/compute"
        "github.com/apache/arrow-go/v18/arrow/decimal128"
        "github.com/apache/arrow-go/v18/arrow/decimal256"
        "github.com/apache/arrow-go/v18/arrow/memory"
@@ -120,7 +122,7 @@ func (lr *leafReader) LoadBatch(nrecords int64) (err error) 
{
                        }
                }
        }
-       lr.out, err = transferColumnData(lr.recordRdr, lr.field.Type, lr.descr)
+       lr.out, err = transferColumnData(lr.recordRdr, lr.field.Type, lr.descr, 
lr.rctx.mem)
        return
 }
 
@@ -587,7 +589,7 @@ func chunksToSingle(chunked *arrow.Chunked, mem 
memory.Allocator) (arrow.ArrayDa
 }
 
 // create a chunked arrow array from the raw record data
-func transferColumnData(rdr file.RecordReader, valueType arrow.DataType, descr 
*schema.Column) (*arrow.Chunked, error) {
+func transferColumnData(rdr file.RecordReader, valueType arrow.DataType, descr 
*schema.Column, mem memory.Allocator) (*arrow.Chunked, error) {
        dt := valueType
        if valueType.ID() == arrow.EXTENSION {
                dt = valueType.(arrow.ExtensionType).StorageType()
@@ -596,7 +598,7 @@ func transferColumnData(rdr file.RecordReader, valueType 
arrow.DataType, descr *
        var data arrow.ArrayData
        switch dt.ID() {
        case arrow.DICTIONARY:
-               return transferDictionary(rdr, valueType), nil
+               return transferDictionary(rdr, valueType, mem)
        case arrow.NULL:
                return arrow.NewChunked(arrow.Null, 
[]arrow.Array{array.NewNull(rdr.ValuesWritten())}), nil
        case arrow.INT32, arrow.INT64, arrow.FLOAT32, arrow.FLOAT64:
@@ -616,7 +618,7 @@ func transferColumnData(rdr file.RecordReader, valueType 
arrow.DataType, descr *
        case arrow.DATE64:
                data = transferDate64(rdr, valueType)
        case arrow.FIXED_SIZE_BINARY, arrow.BINARY, arrow.STRING, 
arrow.LARGE_BINARY, arrow.LARGE_STRING:
-               return transferBinary(rdr, valueType), nil
+               return transferBinary(rdr, valueType, mem)
        case arrow.DECIMAL, arrow.DECIMAL256:
                switch descr.PhysicalType() {
                case parquet.Types.Int32, parquet.Types.Int64:
@@ -647,7 +649,7 @@ func transferColumnData(rdr file.RecordReader, valueType 
arrow.DataType, descr *
                if len := arrow.Float16SizeBytes; descr.TypeLength() != len {
                        return nil, fmt.Errorf("fixed len byte array length for 
float16 must be %d", len)
                }
-               return transferBinary(rdr, valueType), nil
+               return transferBinary(rdr, valueType, mem)
        default:
                return nil, fmt.Errorf("no support for reading columns of type: 
%s", valueType.Name())
        }
@@ -675,10 +677,10 @@ func transferZeroCopy(rdr file.RecordReader, dt 
arrow.DataType) arrow.ArrayData
                nil, int(rdr.NullCount()), 0)
 }
 
-func transferBinary(rdr file.RecordReader, dt arrow.DataType) *arrow.Chunked {
+func transferBinary(rdr file.RecordReader, dt arrow.DataType, mem 
memory.Allocator) (*arrow.Chunked, error) {
        brdr := rdr.(file.BinaryRecordReader)
        if brdr.ReadDictionary() {
-               return transferDictionary(brdr, 
&arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Int32, ValueType: dt})
+               return transferDictionary(brdr, 
&arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Int32, ValueType: dt}, 
mem)
        }
        chunks := brdr.GetBuilderChunks()
        defer releaseArrays(chunks)
@@ -703,7 +705,7 @@ func transferBinary(rdr file.RecordReader, dt 
arrow.DataType) *arrow.Chunked {
                        chunk.Release()
                }
        }
-       return arrow.NewChunked(dt, chunks)
+       return arrow.NewChunked(dt, chunks), nil
 }
 
 func transferInt(rdr file.RecordReader, dt arrow.DataType) arrow.ArrayData {
@@ -1080,9 +1082,34 @@ func transferDecimalBytes(rdr file.BinaryRecordReader, 
dt arrow.DataType) (*arro
        return arrow.NewChunked(dt, chunks), nil
 }
 
-func transferDictionary(rdr file.RecordReader, logicalValueType 
arrow.DataType) *arrow.Chunked {
+func transferDictionary(rdr file.RecordReader, logicalValueType 
arrow.DataType, mem memory.Allocator) (*arrow.Chunked, error) {
        brdr := rdr.(file.BinaryRecordReader)
        chunks := brdr.GetBuilderChunks()
        defer releaseArrays(chunks)
-       return arrow.NewChunked(logicalValueType, chunks)
+
+       dictType, ok := logicalValueType.(*arrow.DictionaryType)
+       if !ok || dictType.IndexType.ID() == arrow.INT32 {
+               return arrow.NewChunked(logicalValueType, chunks), nil
+       }
+
+       ctx := compute.WithAllocator(context.Background(), mem)
+       for idx, chunk := range chunks {
+               dictArr, ok := chunk.(*array.Dictionary)
+               if !ok {
+                       return nil, fmt.Errorf("expected dictionary array, got 
%T", chunk)
+               }
+
+               indices, err := compute.CastArray(ctx, dictArr.Indices(), 
compute.SafeCastOptions(dictType.IndexType))
+               if err != nil {
+                       return nil, err
+               }
+               converted, err := array.NewValidatedDictionaryArray(dictType, 
indices, dictArr.Dictionary())
+               indices.Release()
+               if err != nil {
+                       return nil, err
+               }
+               chunk.Release()
+               chunks[idx] = converted
+       }
+       return arrow.NewChunked(logicalValueType, chunks), nil
 }
diff --git a/parquet/pqarrow/dictionary_multipage_test.go 
b/parquet/pqarrow/dictionary_multipage_test.go
index 8acd72f2..b6e9db5c 100644
--- a/parquet/pqarrow/dictionary_multipage_test.go
+++ b/parquet/pqarrow/dictionary_multipage_test.go
@@ -145,3 +145,58 @@ func TestArrowDictionaryTypeMultiplePages(t *testing.T) {
        require.Equal(t, int64(numRows), totalRows, "Should read all rows")
        t.Logf("Successfully read %d rows", totalRows)
 }
+
+func TestArrowDictionaryTypePreservesIndexType(t *testing.T) {
+       for _, indexType := range []arrow.DataType{
+               arrow.PrimitiveTypes.Int8,
+               arrow.PrimitiveTypes.Int16,
+               arrow.PrimitiveTypes.Int32,
+               arrow.PrimitiveTypes.Int64,
+               arrow.PrimitiveTypes.Uint8,
+               arrow.PrimitiveTypes.Uint16,
+               arrow.PrimitiveTypes.Uint32,
+               arrow.PrimitiveTypes.Uint64,
+       } {
+               t.Run(indexType.Name(), func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       defer mem.AssertSize(t, 0)
+
+                       dictType := &arrow.DictionaryType{IndexType: indexType, 
ValueType: arrow.BinaryTypes.String}
+                       sc := arrow.NewSchema([]arrow.Field{{Name: "dict_col", 
Type: dictType}}, nil)
+                       builder := array.NewDictionaryBuilder(mem, 
dictType).(*array.BinaryDictionaryBuilder)
+                       defer builder.Release()
+                       for _, value := range []string{"a", "b", "a", "c"} {
+                               require.NoError(t, builder.AppendString(value))
+                       }
+                       values := builder.NewDictionaryArray()
+                       defer values.Release()
+
+                       var buf bytes.Buffer
+                       writer, err := pqarrow.NewFileWriter(sc, &buf,
+                               
parquet.NewWriterProperties(parquet.WithAllocator(mem)),
+                               
pqarrow.NewArrowWriterProperties(pqarrow.WithStoreSchema(), 
pqarrow.WithAllocator(mem)))
+                       require.NoError(t, err)
+                       rec := array.NewRecordBatch(sc, []arrow.Array{values}, 
int64(values.Len()))
+                       require.NoError(t, writer.Write(rec))
+                       require.NoError(t, writer.Close())
+                       rec.Release()
+
+                       pf, err := 
file.NewParquetReader(bytes.NewReader(buf.Bytes()),
+                               
file.WithReadProps(parquet.NewReaderProperties(mem)))
+                       require.NoError(t, err)
+                       defer pf.Close()
+                       reader, err := pqarrow.NewFileReader(pf, 
pqarrow.ArrowReadProperties{}, mem)
+                       require.NoError(t, err)
+
+                       readSchema, err := reader.Schema()
+                       require.NoError(t, err)
+                       require.True(t, arrow.TypeEqual(dictType, 
readSchema.Field(0).Type))
+
+                       tbl, err := reader.ReadTable(context.Background())
+                       require.NoError(t, err)
+                       defer tbl.Release()
+                       require.True(t, arrow.TypeEqual(dictType, 
tbl.Column(0).DataType()))
+                       require.True(t, array.Equal(values, 
tbl.Column(0).Data().Chunk(0)))
+               })
+       }
+}
diff --git a/parquet/pqarrow/schema.go b/parquet/pqarrow/schema.go
index ef87e663..d2a97559 100644
--- a/parquet/pqarrow/schema.go
+++ b/parquet/pqarrow/schema.go
@@ -1240,7 +1240,7 @@ func applyOriginalStorageMetadata(origin arrow.Field, 
inferred *SchemaField) (mo
                // direct dictionary reads are only supported for a few 
primitive types
                // so no need to recurse on value types
                dictOriginType := origin.Type.(*arrow.DictionaryType)
-               inferred.Field.Type = &arrow.DictionaryType{IndexType: 
arrow.PrimitiveTypes.Int32,
+               inferred.Field.Type = &arrow.DictionaryType{IndexType: 
dictOriginType.IndexType,
                        ValueType: inferred.Field.Type, Ordered: 
dictOriginType.Ordered}
                modified = true
        case arrow.DECIMAL256:

Reply via email to