You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by at...@apache.org on 2026/08/27 17:14:56 UTC

(iceberg-go) branch main updated: perf(table): borrow DataFile stats in metadata tables (#1896)

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

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 8748cbd89 perf(table): borrow DataFile stats in metadata tables (#1896)
8748cbd89 is described below

commit 8748cbd896551145610ff9d59ec952948b010e4b
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Thu Aug 27 19:14:51 2026 +0200

    perf(table): borrow DataFile stats in metadata tables (#1896)
---
 table/inspect_files.go                  |  13 +--
 table/inspect_files_stats_bench_test.go | 166 ++++++++++++++++++++++++++++++++
 table/inspect_files_stats_test.go       | 117 ++++++++++++++++++++++
 3 files changed, 290 insertions(+), 6 deletions(-)

diff --git a/table/inspect_files.go b/table/inspect_files.go
index fe80b4699..d1fc4c7b2 100644
--- a/table/inspect_files.go
+++ b/table/inspect_files.go
@@ -655,7 +655,7 @@ func (b inspectContentFileBuilder) append(file iceberg.DataFile) error {
 	b.specID.Append(file.SpecID())
 
 	if b.partition != nil {
-		if err := b.partition.append(file.Partition()); err != nil {
+		if err := b.partition.append(dataFilePartition(file)); err != nil {
 			return err
 		}
 	}
@@ -665,19 +665,20 @@ func (b inspectContentFileBuilder) append(file iceberg.DataFile) error {
 	if err := appendInspectInt64Map(b.columnSizes, file.ColumnSizes()); err != nil {
 		return err
 	}
-	if err := appendInspectInt64Map(b.valueCounts, file.ValueCounts()); err != nil {
+	valueCounts, nullCounts, nanCounts, lowerBounds, upperBounds := dataFileStats(file)
+	if err := appendInspectInt64Map(b.valueCounts, valueCounts); err != nil {
 		return err
 	}
-	if err := appendInspectInt64Map(b.nullValueCounts, file.NullValueCounts()); err != nil {
+	if err := appendInspectInt64Map(b.nullValueCounts, nullCounts); err != nil {
 		return err
 	}
-	if err := appendInspectInt64Map(b.nanValueCounts, file.NaNValueCounts()); err != nil {
+	if err := appendInspectInt64Map(b.nanValueCounts, nanCounts); err != nil {
 		return err
 	}
-	if err := appendInspectBinaryMap(b.lowerBounds, file.LowerBoundValues()); err != nil {
+	if err := appendInspectBinaryMap(b.lowerBounds, lowerBounds); err != nil {
 		return err
 	}
-	if err := appendInspectBinaryMap(b.upperBounds, file.UpperBoundValues()); err != nil {
+	if err := appendInspectBinaryMap(b.upperBounds, upperBounds); err != nil {
 		return err
 	}
 	appendInspectBytes(b.keyMetadata, file.KeyMetadata())
diff --git a/table/inspect_files_stats_bench_test.go b/table/inspect_files_stats_bench_test.go
new file mode 100644
index 000000000..429954f6d
--- /dev/null
+++ b/table/inspect_files_stats_bench_test.go
@@ -0,0 +1,166 @@
+// 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.
+
+package table
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/apache/arrow-go/v18/arrow/array"
+	"github.com/apache/arrow-go/v18/arrow/memory"
+	"github.com/apache/iceberg-go"
+)
+
+type inspectPublicStatsDataFile struct {
+	iceberg.DataFile
+}
+
+func (f *inspectPublicStatsDataFile) Partition() map[int]any {
+	return f.DataFile.Partition()
+}
+
+func (f *inspectPublicStatsDataFile) ValueCounts() map[int]int64 {
+	return f.DataFile.ValueCounts()
+}
+
+func (f *inspectPublicStatsDataFile) NullValueCounts() map[int]int64 {
+	return f.DataFile.NullValueCounts()
+}
+
+func (f *inspectPublicStatsDataFile) NaNValueCounts() map[int]int64 {
+	return f.DataFile.NaNValueCounts()
+}
+
+func (f *inspectPublicStatsDataFile) LowerBoundValues() map[int][]byte {
+	return f.DataFile.LowerBoundValues()
+}
+
+func (f *inspectPublicStatsDataFile) UpperBoundValues() map[int][]byte {
+	return f.DataFile.UpperBoundValues()
+}
+
+func BenchmarkInspectContentFileAppenderDataFileStats(b *testing.B) {
+	for _, fileCount := range []int{4096, 16384} {
+		for _, statsWidth := range []int{0, 8, 32, 128} {
+			b.Run(fmt.Sprintf("files=%d/stats=%d", fileCount, statsWidth), func(b *testing.B) {
+				partitionType, files := benchmarkInspectContentFilesWithStats(b, statsWidth, fileCount)
+				publicFiles := make([]iceberg.DataFile, len(files))
+				for i, file := range files {
+					publicFiles[i] = &inspectPublicStatsDataFile{DataFile: file}
+				}
+
+				b.Run("borrowed", func(b *testing.B) {
+					benchmarkAppendInspectContentFiles(b, partitionType, files)
+				})
+				b.Run("public", func(b *testing.B) {
+					benchmarkAppendInspectContentFiles(b, partitionType, publicFiles)
+				})
+			})
+		}
+	}
+}
+
+func benchmarkAppendInspectContentFiles(
+	b *testing.B,
+	partitionType *iceberg.StructType,
+	files []iceberg.DataFile,
+) {
+	b.Helper()
+
+	arrowSchema, err := SchemaToArrowSchema(DataFilesSchema(partitionType), nil, true, false)
+	if err != nil {
+		b.Fatal(err)
+	}
+	builder := array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
+	defer builder.Release()
+	appendFile := newInspectContentFileAppender(partitionType)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	b.ReportMetric(float64(len(files)), "files/op")
+	for range b.N {
+		for _, file := range files {
+			if err := appendFile(builder, file); err != nil {
+				b.Fatal(err)
+			}
+		}
+		record := builder.NewRecordBatch()
+		record.Release()
+	}
+}
+
+func benchmarkInspectContentFilesWithStats(
+	b *testing.B,
+	statsWidth int,
+	fileCount int,
+) (*iceberg.StructType, []iceberg.DataFile) {
+	b.Helper()
+
+	schemaWidth := max(statsWidth, 1)
+	schemaFields := make([]iceberg.NestedField, schemaWidth)
+	for i := range schemaFields {
+		fieldID := i + 1
+		schemaFields[i] = iceberg.NestedField{
+			ID: fieldID, Name: fmt.Sprintf("field_%d", fieldID),
+			Type: iceberg.PrimitiveTypes.Int32, Required: true,
+		}
+	}
+	schema := iceberg.NewSchema(0, schemaFields...)
+	spec := iceberg.NewPartitionSpec(iceberg.PartitionField{
+		SourceIDs: []int{1}, FieldID: 1000, Name: "partition", Transform: iceberg.IdentityTransform{},
+	})
+	partitionType := spec.PartitionType(schema)
+
+	files := make([]iceberg.DataFile, fileCount)
+	for i := range files {
+		builder, err := iceberg.NewDataFileBuilder(
+			spec, iceberg.EntryContentData, fmt.Sprintf("file-%d.parquet", i),
+			iceberg.ParquetFile, map[int]any{1000: int32(i)}, nil, nil, 1, 1,
+		)
+		if err != nil {
+			b.Fatal(err)
+		}
+		if statsWidth > 0 {
+			columnSizes := make(map[int]int64, statsWidth)
+			valueCounts := make(map[int]int64, statsWidth)
+			nullCounts := make(map[int]int64, statsWidth)
+			nanCounts := make(map[int]int64, statsWidth)
+			lowerBounds := make(map[int][]byte, statsWidth)
+			upperBounds := make(map[int][]byte, statsWidth)
+			for field := range statsWidth {
+				fieldID := field + 1
+				columnSizes[fieldID] = int64(field + 1)
+				valueCounts[fieldID] = int64(i + 1)
+				nullCounts[fieldID] = int64(field)
+				nanCounts[fieldID] = 0
+				lowerBounds[fieldID] = []byte{byte(field), byte(i)}
+				upperBounds[fieldID] = []byte{byte(field + 1), byte(i)}
+			}
+			builder = builder.
+				ColumnSizes(columnSizes).
+				ValueCounts(valueCounts).
+				NullValueCounts(nullCounts).
+				NaNValueCounts(nanCounts).
+				LowerBoundValues(lowerBounds).
+				UpperBoundValues(upperBounds)
+		}
+		files[i] = builder.Build()
+	}
+
+	return partitionType, files
+}
diff --git a/table/inspect_files_stats_test.go b/table/inspect_files_stats_test.go
new file mode 100644
index 000000000..9fa224e1c
--- /dev/null
+++ b/table/inspect_files_stats_test.go
@@ -0,0 +1,117 @@
+// 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.
+
+package table
+
+import (
+	"testing"
+
+	"github.com/apache/arrow-go/v18/arrow"
+	"github.com/apache/arrow-go/v18/arrow/array"
+	"github.com/apache/arrow-go/v18/arrow/memory"
+	iceberg "github.com/apache/iceberg-go"
+	"github.com/apache/iceberg-go/internal"
+	"github.com/stretchr/testify/require"
+)
+
+type borrowedInspectDataFile struct {
+	iceberg.DataFile
+}
+
+func (f *borrowedInspectDataFile) Partition() map[int]any {
+	panic("metadata appender called the public Partition getter")
+}
+
+func (f *borrowedInspectDataFile) ValueCounts() map[int]int64 {
+	panic("metadata appender called the public ValueCounts getter")
+}
+
+func (f *borrowedInspectDataFile) NullValueCounts() map[int]int64 {
+	panic("metadata appender called the public NullValueCounts getter")
+}
+
+func (f *borrowedInspectDataFile) NaNValueCounts() map[int]int64 {
+	panic("metadata appender called the public NaNValueCounts getter")
+}
+
+func (f *borrowedInspectDataFile) LowerBoundValues() map[int][]byte {
+	panic("metadata appender called the public LowerBoundValues getter")
+}
+
+func (f *borrowedInspectDataFile) UpperBoundValues() map[int][]byte {
+	panic("metadata appender called the public UpperBoundValues getter")
+}
+
+func (f *borrowedInspectDataFile) DataFileStatsRef(_ internal.DataFileRef) (
+	map[int]int64, map[int]int64, map[int]int64, map[int][]byte, map[int][]byte,
+) {
+	return internal.BorrowedDataFileStats(f.DataFile)
+}
+
+func (f *borrowedInspectDataFile) DataFilePartitionRef(_ internal.DataFileRef) map[int]any {
+	return internal.BorrowedDataFilePartition(f.DataFile)
+}
+
+func TestInspectContentFileBuilderUsesBorrowedDataFileMetadata(t *testing.T) {
+	file := &borrowedInspectDataFile{DataFile: testDataFileWithStats(t)}
+	partitionType := &iceberg.StructType{FieldList: []iceberg.NestedField{
+		{ID: 1000, Name: "part", Type: iceberg.PrimitiveTypes.String},
+	}}
+
+	withInspectContentFileRecord(t, partitionType, file, func(record arrow.RecordBatch) {
+		require.EqualValues(t, 1, record.NumRows())
+		partition := record.Column(4).(*array.Struct)
+		require.Equal(t, "partition", partition.Field(0).(*array.String).Value(0))
+
+		valueCounts := record.Column(8).(*array.Map)
+		require.False(t, valueCounts.IsNull(0))
+		start, end := valueCounts.ValueOffsets(0)
+		require.EqualValues(t, 1, end-start)
+		require.EqualValues(t, 2, valueCounts.Items().(*array.Int64).Value(int(start)))
+	})
+}
+
+func TestInspectContentFileBuilderFallsBackToPublicDataFileMetadata(t *testing.T) {
+	file := &publicStatsDataFile{DataFile: testDataFileWithStats(t)}
+	partitionType := &iceberg.StructType{FieldList: []iceberg.NestedField{
+		{ID: 1000, Name: "part", Type: iceberg.PrimitiveTypes.String},
+	}}
+
+	withInspectContentFileRecord(t, partitionType, file, func(record arrow.RecordBatch) {
+		require.EqualValues(t, 1, record.NumRows())
+	})
+	require.Equal(t, 5, file.getterCalls)
+}
+
+func withInspectContentFileRecord(
+	t *testing.T,
+	partitionType *iceberg.StructType,
+	file iceberg.DataFile,
+	check func(arrow.RecordBatch),
+) {
+	t.Helper()
+
+	arrowSchema, err := SchemaToArrowSchema(DataFilesSchema(partitionType), nil, true, false)
+	require.NoError(t, err)
+	builder := array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
+	defer builder.Release()
+	require.NoError(t, appendContentFileRecord(builder, partitionType, file))
+
+	record := builder.NewRecordBatch()
+	defer record.Release()
+	check(record)
+}