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 f7cf0a3fe refactor(table): separate files metadata inspection concerns
(#2293)
f7cf0a3fe is described below
commit f7cf0a3fecc9ffa1711f75502666c87101f9c959
Author: Minh Vu <[email protected]>
AuthorDate: Sun Oct 25 10:09:48 2026 +0100
refactor(table): separate files metadata inspection concerns (#2293)
---
table/inspect_content_file_builder.go | 492 +++++++++++++++++++
table/inspect_content_file_builder_test.go | 67 +++
table/inspect_files.go | 747 -----------------------------
table/inspect_files_schema.go | 115 +++++
table/inspect_partition_builder.go | 215 +++++++++
5 files changed, 889 insertions(+), 747 deletions(-)
diff --git a/table/inspect_content_file_builder.go
b/table/inspect_content_file_builder.go
new file mode 100644
index 000000000..91fcf3abb
--- /dev/null
+++ b/table/inspect_content_file_builder.go
@@ -0,0 +1,492 @@
+// 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"
+ "math"
+ "sort"
+
+ "github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/array"
+ "github.com/apache/iceberg-go"
+)
+
+func appendContentFileRecord(bldr *array.RecordBuilder, partitionType
*iceberg.StructType, file iceberg.DataFile) error {
+ contentFileBuilder, err := newInspectContentFileBuilder(bldr,
partitionType)
+ if err != nil {
+ return err
+ }
+
+ return contentFileBuilder.append(file)
+}
+
+func newInspectContentFileAppender(partitionType *iceberg.StructType)
func(*array.RecordBuilder, iceberg.DataFile) error {
+ var current *array.RecordBuilder
+ var contentFileBuilder inspectContentFileBuilder
+ var bindErr error
+
+ return func(bldr *array.RecordBuilder, file iceberg.DataFile) error {
+ if bldr != current {
+ contentFileBuilder, bindErr =
newInspectContentFileBuilder(bldr, partitionType)
+ current = bldr
+ }
+ if bindErr != nil {
+ return bindErr
+ }
+
+ return contentFileBuilder.append(file)
+ }
+}
+
+type inspectContentFileBuilder struct {
+ content *array.Int32Builder
+ filePath *array.StringBuilder
+ fileFormat *array.StringBuilder
+ specID *array.Int32Builder
+ partition *inspectPartitionBuilder
+ recordCount *array.Int64Builder
+ fileSize *array.Int64Builder
+ columnSizes *array.MapBuilder
+ valueCounts *array.MapBuilder
+ nullValueCounts *array.MapBuilder
+ nanValueCounts *array.MapBuilder
+ lowerBounds *array.MapBuilder
+ upperBounds *array.MapBuilder
+ keyMetadata *array.BinaryBuilder
+ splitOffsets *array.ListBuilder
+ equalityIDs *array.ListBuilder
+ sortOrderID *array.Int32Builder
+ firstRowID *array.Int64Builder
+ referencedDataFile *array.StringBuilder
+ contentOffset *array.Int64Builder
+ contentSize *array.Int64Builder
+ // One sorted key-set per statistics column, scoped to this record
builder.
+ // Most files in a table use the same metric field IDs. A separate cache
+ // prevents unlike columns from evicting each other's key order.
+ columnSizeFieldIDs []int
+ valueCountFieldIDs []int
+ nullCountFieldIDs []int
+ nanCountFieldIDs []int
+ lowerBoundFieldIDs []int
+ upperBoundFieldIDs []int
+}
+
+func newInspectContentFileBuilder(bldr *array.RecordBuilder, partitionType
*iceberg.StructType) (inspectContentFileBuilder, error) {
+ return newInspectContentFileBuilderFromFields(bldr.Schema().Fields(),
bldr.Field, partitionType)
+}
+
+func newInspectContentFileStructBuilder(bldr *array.StructBuilder,
partitionType *iceberg.StructType) (inspectContentFileBuilder, error) {
+ structType, ok := bldr.Type().(*arrow.StructType)
+ if !ok {
+ return inspectContentFileBuilder{}, fmt.Errorf("content-file
builder has type %T, want struct", bldr.Type())
+ }
+
+ return newInspectContentFileBuilderFromFields(structType.Fields(),
bldr.FieldBuilder, partitionType)
+}
+
+func newInspectContentFileBuilderFromFields(
+ fields []arrow.Field,
+ builder func(int) array.Builder,
+ partitionType *iceberg.StructType,
+) (inspectContentFileBuilder, error) {
+ lookup, err := newInspectBuilderLookup("content-file", fields, builder)
+ if err != nil {
+ return inspectContentFileBuilder{}, err
+ }
+ if err := validateInspectBuilderLookup("content-file", lookup,
inspectContentFileFieldIDs(partitionType)); err != nil {
+ return inspectContentFileBuilder{}, err
+ }
+
+ var out inspectContentFileBuilder
+ if out.content, err = inspectBuilderAs[*array.Int32Builder](lookup,
inspectContentFieldIDContent, "content"); err != nil {
+ return out, err
+ }
+ if out.filePath, err = inspectBuilderAs[*array.StringBuilder](lookup,
inspectContentFieldIDFilePath, "file_path"); err != nil {
+ return out, err
+ }
+ if out.fileFormat, err = inspectBuilderAs[*array.StringBuilder](lookup,
inspectContentFieldIDFileFormat, "file_format"); err != nil {
+ return out, err
+ }
+ if out.specID, err = inspectBuilderAs[*array.Int32Builder](lookup,
inspectContentFieldIDSpecID, "spec_id"); err != nil {
+ return out, err
+ }
+ if partitionType != nil && len(partitionType.FieldList) > 0 {
+ partitionBuilder, bindErr :=
inspectBuilderAs[*array.StructBuilder](lookup, inspectContentFieldIDPartition,
"partition")
+ if bindErr != nil {
+ return out, bindErr
+ }
+ if out.partition, err =
newInspectPartitionBuilder(partitionBuilder, partitionType); err != nil {
+ return out, err
+ }
+ }
+ if out.recordCount, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDRecordCount, "record_count"); err != nil {
+ return out, err
+ }
+ if out.fileSize, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDFileSize, "file_size_in_bytes"); err != nil {
+ return out, err
+ }
+ if out.columnSizes, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDColumnSizes, "column_sizes"); err != nil {
+ return out, err
+ }
+ if out.valueCounts, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDValueCounts, "value_counts"); err != nil {
+ return out, err
+ }
+ if out.nullValueCounts, err =
inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDNullValueCounts, "null_value_counts"); err != nil {
+ return out, err
+ }
+ if out.nanValueCounts, err =
inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDNaNValueCounts, "nan_value_counts"); err != nil {
+ return out, err
+ }
+ if out.lowerBounds, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDLowerBounds, "lower_bounds"); err != nil {
+ return out, err
+ }
+ if out.upperBounds, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDUpperBounds, "upper_bounds"); err != nil {
+ return out, err
+ }
+ if out.keyMetadata, err =
inspectBuilderAs[*array.BinaryBuilder](lookup,
inspectContentFieldIDKeyMetadata, "key_metadata"); err != nil {
+ return out, err
+ }
+ if out.splitOffsets, err = inspectBuilderAs[*array.ListBuilder](lookup,
inspectContentFieldIDSplitOffsets, "split_offsets"); err != nil {
+ return out, err
+ }
+ if out.equalityIDs, err = inspectBuilderAs[*array.ListBuilder](lookup,
inspectContentFieldIDEqualityIDs, "equality_ids"); err != nil {
+ return out, err
+ }
+ if out.sortOrderID, err = inspectBuilderAs[*array.Int32Builder](lookup,
inspectContentFieldIDSortOrderID, "sort_order_id"); err != nil {
+ return out, err
+ }
+ if out.firstRowID, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDFirstRowID, "first_row_id"); err != nil {
+ return out, err
+ }
+ if out.referencedDataFile, err =
inspectBuilderAs[*array.StringBuilder](lookup,
inspectContentFieldIDReferencedDataFile, "referenced_data_file"); err != nil {
+ return out, err
+ }
+ if out.contentOffset, err =
inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDContentOffset, "content_offset"); err != nil {
+ return out, err
+ }
+ if out.contentSize, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDContentSize, "content_size_in_bytes"); err != nil {
+ return out, err
+ }
+
+ return out, nil
+}
+
+func (b *inspectContentFileBuilder) append(file iceberg.DataFile) error {
+ b.content.Append(int32(file.ContentType()))
+ b.filePath.Append(file.FilePath())
+ b.fileFormat.Append(string(file.FileFormat()))
+ b.specID.Append(file.SpecID())
+
+ if b.partition != nil {
+ if err := b.partition.append(dataFilePartition(file)); err !=
nil {
+ return err
+ }
+ }
+
+ b.recordCount.Append(file.Count())
+ b.fileSize.Append(file.FileSizeBytes())
+ columnSizes, keyMetadata, splitOffsets, equalityFieldIDs :=
dataFileCollections(file)
+ if err := appendInspectInt64Map(b.columnSizes, columnSizes,
&b.columnSizeFieldIDs); err != nil {
+ return err
+ }
+ valueCounts, nullCounts, nanCounts, lowerBounds, upperBounds :=
dataFileStats(file)
+ if err := appendInspectInt64Map(b.valueCounts, valueCounts,
&b.valueCountFieldIDs); err != nil {
+ return err
+ }
+ if err := appendInspectInt64Map(b.nullValueCounts, nullCounts,
&b.nullCountFieldIDs); err != nil {
+ return err
+ }
+ if err := appendInspectInt64Map(b.nanValueCounts, nanCounts,
&b.nanCountFieldIDs); err != nil {
+ return err
+ }
+ if err := appendInspectBinaryMap(b.lowerBounds, lowerBounds,
&b.lowerBoundFieldIDs); err != nil {
+ return err
+ }
+ if err := appendInspectBinaryMap(b.upperBounds, upperBounds,
&b.upperBoundFieldIDs); err != nil {
+ return err
+ }
+ appendInspectBytes(b.keyMetadata, keyMetadata)
+ if err := appendInspectInt64List(b.splitOffsets, splitOffsets); err !=
nil {
+ return err
+ }
+ if err := appendInspectInt32List(b.equalityIDs, equalityFieldIDs); err
!= nil {
+ return err
+ }
+ if err := appendInspectOptionalInt32(b.sortOrderID,
file.SortOrderID()); err != nil {
+ return err
+ }
+ appendInspectOptionalInt64(b.firstRowID, file.FirstRowID())
+ appendInspectOptionalString(b.referencedDataFile,
file.ReferencedDataFile())
+ appendInspectOptionalInt64(b.contentOffset, file.ContentOffset())
+ appendInspectOptionalInt64(b.contentSize, file.ContentSizeInBytes())
+
+ return nil
+}
+
+func inspectContentFileFieldIDs(partitionType *iceberg.StructType)
map[int]struct{} {
+ ids := map[int]struct{}{
+ inspectContentFieldIDContent: {},
+ inspectContentFieldIDFilePath: {},
+ inspectContentFieldIDFileFormat: {},
+ inspectContentFieldIDSpecID: {},
+ inspectContentFieldIDRecordCount: {},
+ inspectContentFieldIDFileSize: {},
+ inspectContentFieldIDColumnSizes: {},
+ inspectContentFieldIDValueCounts: {},
+ inspectContentFieldIDNullValueCounts: {},
+ inspectContentFieldIDNaNValueCounts: {},
+ inspectContentFieldIDLowerBounds: {},
+ inspectContentFieldIDUpperBounds: {},
+ inspectContentFieldIDKeyMetadata: {},
+ inspectContentFieldIDSplitOffsets: {},
+ inspectContentFieldIDEqualityIDs: {},
+ inspectContentFieldIDSortOrderID: {},
+ inspectContentFieldIDFirstRowID: {},
+ inspectContentFieldIDReferencedDataFile: {},
+ inspectContentFieldIDContentOffset: {},
+ inspectContentFieldIDContentSize: {},
+ }
+ if partitionType != nil && len(partitionType.FieldList) > 0 {
+ ids[inspectContentFieldIDPartition] = struct{}{}
+ }
+
+ return ids
+}
+
+func newInspectBuilderLookup(scope string, fields []arrow.Field, builder
func(int) array.Builder) (map[int]array.Builder, error) {
+ lookup := make(map[int]array.Builder, len(fields))
+ for idx, field := range fields {
+ id := getFieldID(field)
+ if id == nil {
+ return nil, fmt.Errorf("%s field %q is missing a valid
field ID", scope, field.Name)
+ }
+ if _, exists := lookup[*id]; exists {
+ return nil, fmt.Errorf("%s schema contains duplicate
field ID %d", scope, *id)
+ }
+ lookup[*id] = builder(idx)
+ }
+
+ return lookup, nil
+}
+
+func validateInspectBuilderLookup(scope string, lookup map[int]array.Builder,
expected map[int]struct{}) error {
+ if len(lookup) != len(expected) {
+ return fmt.Errorf("%s schema has %d fields, want %d", scope,
len(lookup), len(expected))
+ }
+ for id := range lookup {
+ if _, ok := expected[id]; !ok {
+ return fmt.Errorf("%s schema contains unexpected field
ID %d", scope, id)
+ }
+ }
+ for id := range expected {
+ if _, ok := lookup[id]; !ok {
+ return fmt.Errorf("%s schema is missing field ID %d",
scope, id)
+ }
+ }
+
+ return nil
+}
+
+func inspectBuilderAs[T any](lookup map[int]array.Builder, id int, name
string) (T, error) {
+ var zero T
+ builder, ok := lookup[id]
+ if !ok {
+ return zero, fmt.Errorf("content-file schema is missing field
%q (ID %d)", name, id)
+ }
+ typed, ok := builder.(T)
+ if !ok {
+ return zero, fmt.Errorf("content-file field %q (ID %d) has
builder type %T", name, id, builder)
+ }
+
+ return typed, nil
+}
+
+func inspectInt32Value(value int, name string) (int32, error) {
+ if value < math.MinInt32 || value > math.MaxInt32 {
+ return 0, fmt.Errorf("%s %d does not fit in int32", name, value)
+ }
+
+ return int32(value), nil
+}
+
+// cachedInspectFieldIDs returns the sorted map keys, reusing the previous
+// ordering when the next file has exactly the same key set. Length alone is
+// not sufficient: maps with different IDs can have the same number of keys.
+// Keeping only one sorted set makes memory bounded by the widest map seen.
+func cachedInspectFieldIDs[V any](values map[int]V, cached *[]int) []int {
+ ids := *cached
+ if len(ids) == len(values) {
+ sameKeys := true
+ for _, id := range ids {
+ if _, ok := values[id]; !ok {
+ sameKeys = false
+
+ break
+ }
+ }
+ if sameKeys {
+ return ids
+ }
+ }
+
+ ids = ids[:0]
+ for id := range values {
+ ids = append(ids, id)
+ }
+ sort.Ints(ids)
+ *cached = ids
+
+ return ids
+}
+
+func appendInspectInt64Map(builder *array.MapBuilder, values map[int]int64,
scratch *[]int) error {
+ if values == nil {
+ builder.AppendNull()
+
+ return nil
+ }
+ builder.Append(true)
+ keys, ok := builder.KeyBuilder().(*array.Int32Builder)
+ if !ok {
+ return fmt.Errorf("map key builder has type %T, want int32",
builder.KeyBuilder())
+ }
+ items, ok := builder.ItemBuilder().(*array.Int64Builder)
+ if !ok {
+ return fmt.Errorf("map item builder has type %T, want int64",
builder.ItemBuilder())
+ }
+ ids := cachedInspectFieldIDs(values, scratch)
+ for _, id := range ids {
+ key, err := inspectInt32Value(id, "field ID")
+ if err != nil {
+ return err
+ }
+ keys.Append(key)
+ items.Append(values[id])
+ }
+
+ return nil
+}
+
+func appendInspectBinaryMap(builder *array.MapBuilder, values map[int][]byte,
scratch *[]int) error {
+ if values == nil {
+ builder.AppendNull()
+
+ return nil
+ }
+ builder.Append(true)
+ keys, ok := builder.KeyBuilder().(*array.Int32Builder)
+ if !ok {
+ return fmt.Errorf("map key builder has type %T, want int32",
builder.KeyBuilder())
+ }
+ items, ok := builder.ItemBuilder().(*array.BinaryBuilder)
+ if !ok {
+ return fmt.Errorf("map item builder has type %T, want binary",
builder.ItemBuilder())
+ }
+ ids := cachedInspectFieldIDs(values, scratch)
+ for _, id := range ids {
+ key, err := inspectInt32Value(id, "field ID")
+ if err != nil {
+ return err
+ }
+ keys.Append(key)
+ items.Append(values[id])
+ }
+
+ return nil
+}
+
+func appendInspectBytes(builder *array.BinaryBuilder, value []byte) {
+ if value == nil {
+ builder.AppendNull()
+
+ return
+ }
+ builder.Append(value)
+}
+
+func appendInspectInt64List(builder *array.ListBuilder, values []int64) error {
+ if values == nil {
+ builder.AppendNull()
+
+ return nil
+ }
+ builder.Append(true)
+ items, ok := builder.ValueBuilder().(*array.Int64Builder)
+ if !ok {
+ return fmt.Errorf("list item builder has type %T, want int64",
builder.ValueBuilder())
+ }
+ items.AppendValues(values, nil)
+
+ return nil
+}
+
+func appendInspectInt32List(builder *array.ListBuilder, values []int) error {
+ if values == nil {
+ builder.AppendNull()
+
+ return nil
+ }
+ builder.Append(true)
+ items, ok := builder.ValueBuilder().(*array.Int32Builder)
+ if !ok {
+ return fmt.Errorf("list item builder has type %T, want int32",
builder.ValueBuilder())
+ }
+ for _, value := range values {
+ converted, err := inspectInt32Value(value, "equality field ID")
+ if err != nil {
+ return err
+ }
+ items.Append(converted)
+ }
+
+ return nil
+}
+
+func appendInspectOptionalInt32(builder *array.Int32Builder, value *int) error
{
+ if value == nil {
+ builder.AppendNull()
+
+ return nil
+ }
+ converted, err := inspectInt32Value(*value, "sort order ID")
+ if err != nil {
+ return err
+ }
+ builder.Append(converted)
+
+ return nil
+}
+
+func appendInspectOptionalInt64(builder *array.Int64Builder, value *int64) {
+ if value == nil {
+ builder.AppendNull()
+
+ return
+ }
+ builder.Append(*value)
+}
+
+func appendInspectOptionalString(builder *array.StringBuilder, value *string) {
+ if value == nil {
+ builder.AppendNull()
+
+ return
+ }
+ builder.Append(*value)
+}
diff --git a/table/inspect_content_file_builder_test.go
b/table/inspect_content_file_builder_test.go
new file mode 100644
index 000000000..6960311d0
--- /dev/null
+++ b/table/inspect_content_file_builder_test.go
@@ -0,0 +1,67 @@
+// 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/array"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+ "github.com/apache/iceberg-go"
+ "github.com/stretchr/testify/require"
+)
+
+// The cached appender must bind by field ID again when the record builder
+// changes, even when the next batch presents the fields in a different order.
+func TestInspectContentFileAppenderRebindsAcrossRecordBuilders(t *testing.T) {
+ partitionType := &iceberg.StructType{}
+ fields := inspectContentFileFields(partitionType)
+ reordered := append(append([]iceberg.NestedField(nil), fields[1:]...),
fields[0])
+ file := newTestDataFile(t, *iceberg.UnpartitionedSpec,
+ "mem://default/table-location/data/rebound.parquet", nil)
+ appendFile := newInspectContentFileAppender(partitionType)
+
+ for _, tc := range []struct {
+ name string
+ schema *iceberg.Schema
+ }{
+ {name: "normal", schema: iceberg.NewSchema(0, fields...)},
+ {name: "reordered", schema: iceberg.NewSchema(0, reordered...)},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ arrowSchema, err := SchemaToArrowSchema(tc.schema, nil,
true, false)
+ require.NoError(t, err)
+
+ builder :=
array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
+ defer builder.Release()
+ require.NoError(t, appendFile(builder, file))
+ require.NoError(t, appendFile(builder, file))
+
+ record := builder.NewRecordBatch()
+ defer record.Release()
+ require.EqualValues(t, 2, record.NumRows())
+
+ pathIdx := record.Schema().FieldIndices("file_path")[0]
+ countIdx :=
record.Schema().FieldIndices("record_count")[0]
+ for row := range 2 {
+ require.Equal(t, file.FilePath(),
record.Column(pathIdx).(*array.String).Value(row))
+ require.Equal(t, file.Count(),
record.Column(countIdx).(*array.Int64).Value(row))
+ }
+ })
+ }
+}
diff --git a/table/inspect_files.go b/table/inspect_files.go
index cd51f890e..e315372eb 100644
--- a/table/inspect_files.go
+++ b/table/inspect_files.go
@@ -21,18 +21,15 @@ import (
"context"
"fmt"
"iter"
- "math"
"slices"
"sort"
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
"github.com/apache/arrow-go/v18/arrow/memory"
- "github.com/apache/arrow-go/v18/arrow/scalar"
"github.com/apache/iceberg-go"
iceinternal "github.com/apache/iceberg-go/internal"
iceio "github.com/apache/iceberg-go/io"
- "github.com/google/uuid"
)
// DataFiles returns the live data files in the current snapshot. Deleted
@@ -373,747 +370,3 @@ func isInspectUnknownTransform(transform
iceberg.Transform) bool {
return false
}
}
-
-// DataFilesSchema returns the common content-file schema used by the files
-// metadata tables. The partition field is omitted for an unpartitioned table,
-// as required by the Iceberg metadata-table spec.
-func DataFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
- return iceberg.NewSchema(0, inspectContentFileFields(partitionType)...)
-}
-
-// FilesSchema returns the schema shared by the files metadata tables.
-func FilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
- return DataFilesSchema(partitionType)
-}
-
-// AllFilesSchema returns the schema of the all_files metadata table.
-func AllFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
- return DataFilesSchema(partitionType)
-}
-
-// AllDataFilesSchema returns the schema of the all_data_files metadata table.
-func AllDataFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
- return DataFilesSchema(partitionType)
-}
-
-// AllDeleteFilesSchema returns the schema of the all_delete_files metadata
table.
-func AllDeleteFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
- return DataFilesSchema(partitionType)
-}
-
-const (
- inspectContentFieldIDContent = 134
- inspectContentFieldIDFilePath = 100
- inspectContentFieldIDFileFormat = 101
- inspectContentFieldIDSpecID = 141
- inspectContentFieldIDPartition = 102
- inspectContentFieldIDRecordCount = 103
- inspectContentFieldIDFileSize = 104
- inspectContentFieldIDColumnSizes = 108
- inspectContentFieldIDValueCounts = 109
- inspectContentFieldIDNullValueCounts = 110
- inspectContentFieldIDNaNValueCounts = 137
- inspectContentFieldIDLowerBounds = 125
- inspectContentFieldIDUpperBounds = 128
- inspectContentFieldIDKeyMetadata = 131
- inspectContentFieldIDSplitOffsets = 132
- inspectContentFieldIDEqualityIDs = 135
- inspectContentFieldIDSortOrderID = 140
- inspectContentFieldIDFirstRowID = 142
- inspectContentFieldIDReferencedDataFile = 143
- inspectContentFieldIDContentOffset = 144
- inspectContentFieldIDContentSize = 145
-)
-
-func inspectContentFileFields(partitionType *iceberg.StructType)
[]iceberg.NestedField {
- fields := []iceberg.NestedField{
- {ID: inspectContentFieldIDContent, Name: "content", Type:
iceberg.PrimitiveTypes.Int32, Required: false},
- {ID: inspectContentFieldIDFilePath, Name: "file_path", Type:
iceberg.PrimitiveTypes.String, Required: true},
- {ID: inspectContentFieldIDFileFormat, Name: "file_format",
Type: iceberg.PrimitiveTypes.String, Required: true},
- {ID: inspectContentFieldIDSpecID, Name: "spec_id", Type:
iceberg.PrimitiveTypes.Int32, Required: false},
- }
- if partitionType != nil && len(partitionType.FieldList) > 0 {
- fields = append(fields, iceberg.NestedField{
- ID: inspectContentFieldIDPartition, Name: "partition",
Type: partitionType, Required: true,
- })
- }
- fields = append(fields,
- iceberg.NestedField{ID: inspectContentFieldIDRecordCount, Name:
"record_count", Type: iceberg.PrimitiveTypes.Int64, Required: true},
- iceberg.NestedField{ID: inspectContentFieldIDFileSize, Name:
"file_size_in_bytes", Type: iceberg.PrimitiveTypes.Int64, Required: true},
- iceberg.NestedField{ID: inspectContentFieldIDColumnSizes, Name:
"column_sizes", Type: inspectInt64MapType(117, 118), Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDValueCounts, Name:
"value_counts", Type: inspectInt64MapType(119, 120), Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDNullValueCounts,
Name: "null_value_counts", Type: inspectInt64MapType(121, 122), Required:
false},
- iceberg.NestedField{ID: inspectContentFieldIDNaNValueCounts,
Name: "nan_value_counts", Type: inspectInt64MapType(138, 139), Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDLowerBounds, Name:
"lower_bounds", Type: inspectBinaryMapType(126, 127), Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDUpperBounds, Name:
"upper_bounds", Type: inspectBinaryMapType(129, 130), Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDKeyMetadata, Name:
"key_metadata", Type: iceberg.PrimitiveTypes.Binary, Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDSplitOffsets,
Name: "split_offsets", Type: &iceberg.ListType{ElementID: 133, Element:
iceberg.PrimitiveTypes.Int64, ElementRequired: true}, Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDEqualityIDs, Name:
"equality_ids", Type: &iceberg.ListType{ElementID: 136, Element:
iceberg.PrimitiveTypes.Int32, ElementRequired: true}, Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDSortOrderID, Name:
"sort_order_id", Type: iceberg.PrimitiveTypes.Int32, Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDFirstRowID, Name:
"first_row_id", Type: iceberg.PrimitiveTypes.Int64, Required: false},
- iceberg.NestedField{ID:
inspectContentFieldIDReferencedDataFile, Name: "referenced_data_file", Type:
iceberg.PrimitiveTypes.String, Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDContentOffset,
Name: "content_offset", Type: iceberg.PrimitiveTypes.Int64, Required: false},
- iceberg.NestedField{ID: inspectContentFieldIDContentSize, Name:
"content_size_in_bytes", Type: iceberg.PrimitiveTypes.Int64, Required: false},
- )
-
- return fields
-}
-
-func inspectInt64MapType(keyID, valueID int) *iceberg.MapType {
- return &iceberg.MapType{KeyID: keyID, KeyType:
iceberg.PrimitiveTypes.Int32, ValueID: valueID, ValueType:
iceberg.PrimitiveTypes.Int64, ValueRequired: true}
-}
-
-func inspectBinaryMapType(keyID, valueID int) *iceberg.MapType {
- return &iceberg.MapType{KeyID: keyID, KeyType:
iceberg.PrimitiveTypes.Int32, ValueID: valueID, ValueType:
iceberg.PrimitiveTypes.Binary, ValueRequired: true}
-}
-
-func appendContentFileRecord(bldr *array.RecordBuilder, partitionType
*iceberg.StructType, file iceberg.DataFile) error {
- contentFileBuilder, err := newInspectContentFileBuilder(bldr,
partitionType)
- if err != nil {
- return err
- }
-
- return contentFileBuilder.append(file)
-}
-
-func newInspectContentFileAppender(partitionType *iceberg.StructType)
func(*array.RecordBuilder, iceberg.DataFile) error {
- var current *array.RecordBuilder
- var contentFileBuilder inspectContentFileBuilder
- var bindErr error
-
- return func(bldr *array.RecordBuilder, file iceberg.DataFile) error {
- if bldr != current {
- contentFileBuilder, bindErr =
newInspectContentFileBuilder(bldr, partitionType)
- current = bldr
- }
- if bindErr != nil {
- return bindErr
- }
-
- return contentFileBuilder.append(file)
- }
-}
-
-type inspectContentFileBuilder struct {
- content *array.Int32Builder
- filePath *array.StringBuilder
- fileFormat *array.StringBuilder
- specID *array.Int32Builder
- partition *inspectPartitionBuilder
- recordCount *array.Int64Builder
- fileSize *array.Int64Builder
- columnSizes *array.MapBuilder
- valueCounts *array.MapBuilder
- nullValueCounts *array.MapBuilder
- nanValueCounts *array.MapBuilder
- lowerBounds *array.MapBuilder
- upperBounds *array.MapBuilder
- keyMetadata *array.BinaryBuilder
- splitOffsets *array.ListBuilder
- equalityIDs *array.ListBuilder
- sortOrderID *array.Int32Builder
- firstRowID *array.Int64Builder
- referencedDataFile *array.StringBuilder
- contentOffset *array.Int64Builder
- contentSize *array.Int64Builder
- // One sorted key-set per statistics column, scoped to this record
builder.
- // Most files in a table use the same metric field IDs. A separate cache
- // prevents unlike columns from evicting each other's key order.
- columnSizeFieldIDs []int
- valueCountFieldIDs []int
- nullCountFieldIDs []int
- nanCountFieldIDs []int
- lowerBoundFieldIDs []int
- upperBoundFieldIDs []int
-}
-
-type inspectPartitionBuilder struct {
- builder *array.StructBuilder
- fields []inspectPartitionFieldBuilder
-}
-
-type inspectPartitionFieldBuilder struct {
- id int
- name string
- typ iceberg.Type
- arrowType arrow.DataType
- builder array.Builder
- appendValue func(any) error
-}
-
-func newInspectContentFileBuilder(bldr *array.RecordBuilder, partitionType
*iceberg.StructType) (inspectContentFileBuilder, error) {
- return newInspectContentFileBuilderFromFields(bldr.Schema().Fields(),
bldr.Field, partitionType)
-}
-
-func newInspectContentFileStructBuilder(bldr *array.StructBuilder,
partitionType *iceberg.StructType) (inspectContentFileBuilder, error) {
- structType, ok := bldr.Type().(*arrow.StructType)
- if !ok {
- return inspectContentFileBuilder{}, fmt.Errorf("content-file
builder has type %T, want struct", bldr.Type())
- }
-
- return newInspectContentFileBuilderFromFields(structType.Fields(),
bldr.FieldBuilder, partitionType)
-}
-
-func newInspectContentFileBuilderFromFields(
- fields []arrow.Field,
- builder func(int) array.Builder,
- partitionType *iceberg.StructType,
-) (inspectContentFileBuilder, error) {
- lookup, err := newInspectBuilderLookup("content-file", fields, builder)
- if err != nil {
- return inspectContentFileBuilder{}, err
- }
- if err := validateInspectBuilderLookup("content-file", lookup,
inspectContentFileFieldIDs(partitionType)); err != nil {
- return inspectContentFileBuilder{}, err
- }
-
- var out inspectContentFileBuilder
- if out.content, err = inspectBuilderAs[*array.Int32Builder](lookup,
inspectContentFieldIDContent, "content"); err != nil {
- return out, err
- }
- if out.filePath, err = inspectBuilderAs[*array.StringBuilder](lookup,
inspectContentFieldIDFilePath, "file_path"); err != nil {
- return out, err
- }
- if out.fileFormat, err = inspectBuilderAs[*array.StringBuilder](lookup,
inspectContentFieldIDFileFormat, "file_format"); err != nil {
- return out, err
- }
- if out.specID, err = inspectBuilderAs[*array.Int32Builder](lookup,
inspectContentFieldIDSpecID, "spec_id"); err != nil {
- return out, err
- }
- if partitionType != nil && len(partitionType.FieldList) > 0 {
- partitionBuilder, bindErr :=
inspectBuilderAs[*array.StructBuilder](lookup, inspectContentFieldIDPartition,
"partition")
- if bindErr != nil {
- return out, bindErr
- }
- if out.partition, err =
newInspectPartitionBuilder(partitionBuilder, partitionType); err != nil {
- return out, err
- }
- }
- if out.recordCount, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDRecordCount, "record_count"); err != nil {
- return out, err
- }
- if out.fileSize, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDFileSize, "file_size_in_bytes"); err != nil {
- return out, err
- }
- if out.columnSizes, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDColumnSizes, "column_sizes"); err != nil {
- return out, err
- }
- if out.valueCounts, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDValueCounts, "value_counts"); err != nil {
- return out, err
- }
- if out.nullValueCounts, err =
inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDNullValueCounts, "null_value_counts"); err != nil {
- return out, err
- }
- if out.nanValueCounts, err =
inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDNaNValueCounts, "nan_value_counts"); err != nil {
- return out, err
- }
- if out.lowerBounds, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDLowerBounds, "lower_bounds"); err != nil {
- return out, err
- }
- if out.upperBounds, err = inspectBuilderAs[*array.MapBuilder](lookup,
inspectContentFieldIDUpperBounds, "upper_bounds"); err != nil {
- return out, err
- }
- if out.keyMetadata, err =
inspectBuilderAs[*array.BinaryBuilder](lookup,
inspectContentFieldIDKeyMetadata, "key_metadata"); err != nil {
- return out, err
- }
- if out.splitOffsets, err = inspectBuilderAs[*array.ListBuilder](lookup,
inspectContentFieldIDSplitOffsets, "split_offsets"); err != nil {
- return out, err
- }
- if out.equalityIDs, err = inspectBuilderAs[*array.ListBuilder](lookup,
inspectContentFieldIDEqualityIDs, "equality_ids"); err != nil {
- return out, err
- }
- if out.sortOrderID, err = inspectBuilderAs[*array.Int32Builder](lookup,
inspectContentFieldIDSortOrderID, "sort_order_id"); err != nil {
- return out, err
- }
- if out.firstRowID, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDFirstRowID, "first_row_id"); err != nil {
- return out, err
- }
- if out.referencedDataFile, err =
inspectBuilderAs[*array.StringBuilder](lookup,
inspectContentFieldIDReferencedDataFile, "referenced_data_file"); err != nil {
- return out, err
- }
- if out.contentOffset, err =
inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDContentOffset, "content_offset"); err != nil {
- return out, err
- }
- if out.contentSize, err = inspectBuilderAs[*array.Int64Builder](lookup,
inspectContentFieldIDContentSize, "content_size_in_bytes"); err != nil {
- return out, err
- }
-
- return out, nil
-}
-
-func (b *inspectContentFileBuilder) append(file iceberg.DataFile) error {
- b.content.Append(int32(file.ContentType()))
- b.filePath.Append(file.FilePath())
- b.fileFormat.Append(string(file.FileFormat()))
- b.specID.Append(file.SpecID())
-
- if b.partition != nil {
- if err := b.partition.append(dataFilePartition(file)); err !=
nil {
- return err
- }
- }
-
- b.recordCount.Append(file.Count())
- b.fileSize.Append(file.FileSizeBytes())
- columnSizes, keyMetadata, splitOffsets, equalityFieldIDs :=
dataFileCollections(file)
- if err := appendInspectInt64Map(b.columnSizes, columnSizes,
&b.columnSizeFieldIDs); err != nil {
- return err
- }
- valueCounts, nullCounts, nanCounts, lowerBounds, upperBounds :=
dataFileStats(file)
- if err := appendInspectInt64Map(b.valueCounts, valueCounts,
&b.valueCountFieldIDs); err != nil {
- return err
- }
- if err := appendInspectInt64Map(b.nullValueCounts, nullCounts,
&b.nullCountFieldIDs); err != nil {
- return err
- }
- if err := appendInspectInt64Map(b.nanValueCounts, nanCounts,
&b.nanCountFieldIDs); err != nil {
- return err
- }
- if err := appendInspectBinaryMap(b.lowerBounds, lowerBounds,
&b.lowerBoundFieldIDs); err != nil {
- return err
- }
- if err := appendInspectBinaryMap(b.upperBounds, upperBounds,
&b.upperBoundFieldIDs); err != nil {
- return err
- }
- appendInspectBytes(b.keyMetadata, keyMetadata)
- if err := appendInspectInt64List(b.splitOffsets, splitOffsets); err !=
nil {
- return err
- }
- if err := appendInspectInt32List(b.equalityIDs, equalityFieldIDs); err
!= nil {
- return err
- }
- if err := appendInspectOptionalInt32(b.sortOrderID,
file.SortOrderID()); err != nil {
- return err
- }
- appendInspectOptionalInt64(b.firstRowID, file.FirstRowID())
- appendInspectOptionalString(b.referencedDataFile,
file.ReferencedDataFile())
- appendInspectOptionalInt64(b.contentOffset, file.ContentOffset())
- appendInspectOptionalInt64(b.contentSize, file.ContentSizeInBytes())
-
- return nil
-}
-
-func newInspectPartitionBuilder(
- builder *array.StructBuilder,
- partitionType *iceberg.StructType,
-) (*inspectPartitionBuilder, error) {
- arrowType, ok := builder.Type().(*arrow.StructType)
- if !ok {
- return nil, fmt.Errorf("partition builder has type %T, want
struct", builder.Type())
- }
- lookup, err := newInspectBuilderLookup("partition", arrowType.Fields(),
builder.FieldBuilder)
- if err != nil {
- return nil, err
- }
- expected := make(map[int]struct{}, len(partitionType.FieldList))
- for _, field := range partitionType.FieldList {
- expected[field.ID] = struct{}{}
- }
- if err := validateInspectBuilderLookup("partition", lookup, expected);
err != nil {
- return nil, err
- }
-
- fields := make([]inspectPartitionFieldBuilder, 0,
len(partitionType.FieldList))
- for _, field := range partitionType.FieldList {
- fieldBuilder := lookup[field.ID]
- partitionField := inspectPartitionFieldBuilder{
- id: field.ID,
- name: field.Name,
- typ: field.Type,
- arrowType: fieldBuilder.Type(),
- builder: fieldBuilder,
- }
- partitionField.appendValue =
newInspectPartitionFieldAppender(partitionField)
- fields = append(fields, partitionField)
- }
-
- return &inspectPartitionBuilder{builder: builder, fields: fields}, nil
-}
-
-func (b *inspectPartitionBuilder) append(values map[int]any) error {
- b.builder.Append(true)
- for _, field := range b.fields {
- if err := field.appendValue(values[field.id]); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func newInspectPartitionFieldAppender(field inspectPartitionFieldBuilder)
func(any) error {
- appendScalar := func(value any) error {
- sc, err := inspectValueScalar(value, field.typ, field.arrowType)
- if err != nil {
- return fmt.Errorf("partition field %q: %w", field.name,
err)
- }
-
- return scalar.Append(field.builder, sc)
- }
- appendNull := func() {
- field.builder.AppendNull()
- }
-
- switch field.typ.(type) {
- case iceberg.BooleanType:
- if builder, ok := field.builder.(*array.BooleanBuilder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- case iceberg.Int32Type:
- if builder, ok := field.builder.(*array.Int32Builder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- case iceberg.Int64Type:
- if builder, ok := field.builder.(*array.Int64Builder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- case iceberg.Float32Type:
- if builder, ok := field.builder.(*array.Float32Builder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- case iceberg.Float64Type:
- if builder, ok := field.builder.(*array.Float64Builder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- case iceberg.StringType:
- if builder, ok := field.builder.(*array.StringBuilder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- case iceberg.BinaryType:
- if builder, ok := field.builder.(*array.BinaryBuilder); ok {
- return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
- }
- }
-
- return func(value any) error {
- if value == nil {
- appendNull()
-
- return nil
- }
-
- return appendScalar(value)
- }
-}
-
-func newInspectTypedPartitionAppender[T any](
- appendValue func(T),
- appendNull func(),
- appendScalar func(any) error,
-) func(any) error {
- return func(value any) error {
- if value == nil {
- appendNull()
-
- return nil
- }
- typed, ok := value.(T)
- if !ok {
- return appendScalar(value)
- }
- appendValue(typed)
-
- return nil
- }
-}
-
-func inspectValueScalar(value any, typ iceberg.Type, arrowType arrow.DataType)
(scalar.Scalar, error) {
- switch typ.(type) {
- case iceberg.DateType:
- switch value := value.(type) {
- case iceberg.Date:
- return scalar.NewDate32Scalar(arrow.Date32(value)), nil
- case int32:
- return scalar.NewDate32Scalar(arrow.Date32(value)), nil
- default:
- return nil, fmt.Errorf("unsupported date partition
value %T", value)
- }
- case iceberg.TimeType:
- if value, ok := value.(iceberg.Time); ok {
- return scalar.NewTime64Scalar(arrow.Time64(value),
arrowType), nil
- }
-
- return nil, fmt.Errorf("unsupported time partition value %T",
value)
- case iceberg.TimestampType, iceberg.TimestampTzType:
- if value, ok := value.(iceberg.Timestamp); ok {
- return
scalar.NewTimestampScalar(arrow.Timestamp(value), arrowType), nil
- }
-
- return nil, fmt.Errorf("unsupported timestamp partition value
%T", value)
- case iceberg.TimestampNsType, iceberg.TimestampTzNsType:
- if value, ok := value.(iceberg.TimestampNano); ok {
- return
scalar.NewTimestampScalar(arrow.Timestamp(value), arrowType), nil
- }
-
- return nil, fmt.Errorf("unsupported nanosecond timestamp
partition value %T", value)
- case iceberg.UUIDType:
- if value, ok := value.(uuid.UUID); ok {
- return scalar.MakeScalarParam(value[:], arrowType)
- }
-
- return nil, fmt.Errorf("unsupported UUID partition value %T",
value)
- case iceberg.DecimalType:
- switch value := value.(type) {
- case iceberg.DecimalLiteral:
- return scalar.NewDecimal128Scalar(value.Val,
arrowType), nil
- case iceberg.Decimal:
- return scalar.NewDecimal128Scalar(value.Val,
arrowType), nil
- default:
- return nil, fmt.Errorf("unsupported decimal partition
value %T", value)
- }
- }
-
- return scalar.MakeScalarParam(value, arrowType)
-}
-
-func inspectContentFileFieldIDs(partitionType *iceberg.StructType)
map[int]struct{} {
- ids := map[int]struct{}{
- inspectContentFieldIDContent: {},
- inspectContentFieldIDFilePath: {},
- inspectContentFieldIDFileFormat: {},
- inspectContentFieldIDSpecID: {},
- inspectContentFieldIDRecordCount: {},
- inspectContentFieldIDFileSize: {},
- inspectContentFieldIDColumnSizes: {},
- inspectContentFieldIDValueCounts: {},
- inspectContentFieldIDNullValueCounts: {},
- inspectContentFieldIDNaNValueCounts: {},
- inspectContentFieldIDLowerBounds: {},
- inspectContentFieldIDUpperBounds: {},
- inspectContentFieldIDKeyMetadata: {},
- inspectContentFieldIDSplitOffsets: {},
- inspectContentFieldIDEqualityIDs: {},
- inspectContentFieldIDSortOrderID: {},
- inspectContentFieldIDFirstRowID: {},
- inspectContentFieldIDReferencedDataFile: {},
- inspectContentFieldIDContentOffset: {},
- inspectContentFieldIDContentSize: {},
- }
- if partitionType != nil && len(partitionType.FieldList) > 0 {
- ids[inspectContentFieldIDPartition] = struct{}{}
- }
-
- return ids
-}
-
-func newInspectBuilderLookup(scope string, fields []arrow.Field, builder
func(int) array.Builder) (map[int]array.Builder, error) {
- lookup := make(map[int]array.Builder, len(fields))
- for idx, field := range fields {
- id := getFieldID(field)
- if id == nil {
- return nil, fmt.Errorf("%s field %q is missing a valid
field ID", scope, field.Name)
- }
- if _, exists := lookup[*id]; exists {
- return nil, fmt.Errorf("%s schema contains duplicate
field ID %d", scope, *id)
- }
- lookup[*id] = builder(idx)
- }
-
- return lookup, nil
-}
-
-func validateInspectBuilderLookup(scope string, lookup map[int]array.Builder,
expected map[int]struct{}) error {
- if len(lookup) != len(expected) {
- return fmt.Errorf("%s schema has %d fields, want %d", scope,
len(lookup), len(expected))
- }
- for id := range lookup {
- if _, ok := expected[id]; !ok {
- return fmt.Errorf("%s schema contains unexpected field
ID %d", scope, id)
- }
- }
- for id := range expected {
- if _, ok := lookup[id]; !ok {
- return fmt.Errorf("%s schema is missing field ID %d",
scope, id)
- }
- }
-
- return nil
-}
-
-func inspectBuilderAs[T any](lookup map[int]array.Builder, id int, name
string) (T, error) {
- var zero T
- builder, ok := lookup[id]
- if !ok {
- return zero, fmt.Errorf("content-file schema is missing field
%q (ID %d)", name, id)
- }
- typed, ok := builder.(T)
- if !ok {
- return zero, fmt.Errorf("content-file field %q (ID %d) has
builder type %T", name, id, builder)
- }
-
- return typed, nil
-}
-
-func inspectInt32Value(value int, name string) (int32, error) {
- if value < math.MinInt32 || value > math.MaxInt32 {
- return 0, fmt.Errorf("%s %d does not fit in int32", name, value)
- }
-
- return int32(value), nil
-}
-
-// cachedInspectFieldIDs returns the sorted map keys, reusing the previous
-// ordering when the next file has exactly the same key set. Length alone is
-// not sufficient: maps with different IDs can have the same number of keys.
-// Keeping only one sorted set makes memory bounded by the widest map seen.
-func cachedInspectFieldIDs[V any](values map[int]V, cached *[]int) []int {
- ids := *cached
- if len(ids) == len(values) {
- sameKeys := true
- for _, id := range ids {
- if _, ok := values[id]; !ok {
- sameKeys = false
-
- break
- }
- }
- if sameKeys {
- return ids
- }
- }
-
- ids = ids[:0]
- for id := range values {
- ids = append(ids, id)
- }
- sort.Ints(ids)
- *cached = ids
-
- return ids
-}
-
-func appendInspectInt64Map(builder *array.MapBuilder, values map[int]int64,
scratch *[]int) error {
- if values == nil {
- builder.AppendNull()
-
- return nil
- }
- builder.Append(true)
- keys, ok := builder.KeyBuilder().(*array.Int32Builder)
- if !ok {
- return fmt.Errorf("map key builder has type %T, want int32",
builder.KeyBuilder())
- }
- items, ok := builder.ItemBuilder().(*array.Int64Builder)
- if !ok {
- return fmt.Errorf("map item builder has type %T, want int64",
builder.ItemBuilder())
- }
- ids := cachedInspectFieldIDs(values, scratch)
- for _, id := range ids {
- key, err := inspectInt32Value(id, "field ID")
- if err != nil {
- return err
- }
- keys.Append(key)
- items.Append(values[id])
- }
-
- return nil
-}
-
-func appendInspectBinaryMap(builder *array.MapBuilder, values map[int][]byte,
scratch *[]int) error {
- if values == nil {
- builder.AppendNull()
-
- return nil
- }
- builder.Append(true)
- keys, ok := builder.KeyBuilder().(*array.Int32Builder)
- if !ok {
- return fmt.Errorf("map key builder has type %T, want int32",
builder.KeyBuilder())
- }
- items, ok := builder.ItemBuilder().(*array.BinaryBuilder)
- if !ok {
- return fmt.Errorf("map item builder has type %T, want binary",
builder.ItemBuilder())
- }
- ids := cachedInspectFieldIDs(values, scratch)
- for _, id := range ids {
- key, err := inspectInt32Value(id, "field ID")
- if err != nil {
- return err
- }
- keys.Append(key)
- items.Append(values[id])
- }
-
- return nil
-}
-
-func appendInspectBytes(builder *array.BinaryBuilder, value []byte) {
- if value == nil {
- builder.AppendNull()
-
- return
- }
- builder.Append(value)
-}
-
-func appendInspectInt64List(builder *array.ListBuilder, values []int64) error {
- if values == nil {
- builder.AppendNull()
-
- return nil
- }
- builder.Append(true)
- items, ok := builder.ValueBuilder().(*array.Int64Builder)
- if !ok {
- return fmt.Errorf("list item builder has type %T, want int64",
builder.ValueBuilder())
- }
- items.AppendValues(values, nil)
-
- return nil
-}
-
-func appendInspectInt32List(builder *array.ListBuilder, values []int) error {
- if values == nil {
- builder.AppendNull()
-
- return nil
- }
- builder.Append(true)
- items, ok := builder.ValueBuilder().(*array.Int32Builder)
- if !ok {
- return fmt.Errorf("list item builder has type %T, want int32",
builder.ValueBuilder())
- }
- for _, value := range values {
- converted, err := inspectInt32Value(value, "equality field ID")
- if err != nil {
- return err
- }
- items.Append(converted)
- }
-
- return nil
-}
-
-func appendInspectOptionalInt32(builder *array.Int32Builder, value *int) error
{
- if value == nil {
- builder.AppendNull()
-
- return nil
- }
- converted, err := inspectInt32Value(*value, "sort order ID")
- if err != nil {
- return err
- }
- builder.Append(converted)
-
- return nil
-}
-
-func appendInspectOptionalInt64(builder *array.Int64Builder, value *int64) {
- if value == nil {
- builder.AppendNull()
-
- return
- }
- builder.Append(*value)
-}
-
-func appendInspectOptionalString(builder *array.StringBuilder, value *string) {
- if value == nil {
- builder.AppendNull()
-
- return
- }
- builder.Append(*value)
-}
diff --git a/table/inspect_files_schema.go b/table/inspect_files_schema.go
new file mode 100644
index 000000000..7f4a7608a
--- /dev/null
+++ b/table/inspect_files_schema.go
@@ -0,0 +1,115 @@
+// 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 (
+ "github.com/apache/iceberg-go"
+)
+
+// DataFilesSchema returns the common content-file schema used by the files
+// metadata tables. The partition field is omitted for an unpartitioned table,
+// as required by the Iceberg metadata-table spec.
+func DataFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
+ return iceberg.NewSchema(0, inspectContentFileFields(partitionType)...)
+}
+
+// FilesSchema returns the schema shared by the files metadata tables.
+func FilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
+ return DataFilesSchema(partitionType)
+}
+
+// AllFilesSchema returns the schema of the all_files metadata table.
+func AllFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
+ return DataFilesSchema(partitionType)
+}
+
+// AllDataFilesSchema returns the schema of the all_data_files metadata table.
+func AllDataFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
+ return DataFilesSchema(partitionType)
+}
+
+// AllDeleteFilesSchema returns the schema of the all_delete_files metadata
table.
+func AllDeleteFilesSchema(partitionType *iceberg.StructType) *iceberg.Schema {
+ return DataFilesSchema(partitionType)
+}
+
+const (
+ inspectContentFieldIDContent = 134
+ inspectContentFieldIDFilePath = 100
+ inspectContentFieldIDFileFormat = 101
+ inspectContentFieldIDSpecID = 141
+ inspectContentFieldIDPartition = 102
+ inspectContentFieldIDRecordCount = 103
+ inspectContentFieldIDFileSize = 104
+ inspectContentFieldIDColumnSizes = 108
+ inspectContentFieldIDValueCounts = 109
+ inspectContentFieldIDNullValueCounts = 110
+ inspectContentFieldIDNaNValueCounts = 137
+ inspectContentFieldIDLowerBounds = 125
+ inspectContentFieldIDUpperBounds = 128
+ inspectContentFieldIDKeyMetadata = 131
+ inspectContentFieldIDSplitOffsets = 132
+ inspectContentFieldIDEqualityIDs = 135
+ inspectContentFieldIDSortOrderID = 140
+ inspectContentFieldIDFirstRowID = 142
+ inspectContentFieldIDReferencedDataFile = 143
+ inspectContentFieldIDContentOffset = 144
+ inspectContentFieldIDContentSize = 145
+)
+
+func inspectContentFileFields(partitionType *iceberg.StructType)
[]iceberg.NestedField {
+ fields := []iceberg.NestedField{
+ {ID: inspectContentFieldIDContent, Name: "content", Type:
iceberg.PrimitiveTypes.Int32, Required: false},
+ {ID: inspectContentFieldIDFilePath, Name: "file_path", Type:
iceberg.PrimitiveTypes.String, Required: true},
+ {ID: inspectContentFieldIDFileFormat, Name: "file_format",
Type: iceberg.PrimitiveTypes.String, Required: true},
+ {ID: inspectContentFieldIDSpecID, Name: "spec_id", Type:
iceberg.PrimitiveTypes.Int32, Required: false},
+ }
+ if partitionType != nil && len(partitionType.FieldList) > 0 {
+ fields = append(fields, iceberg.NestedField{
+ ID: inspectContentFieldIDPartition, Name: "partition",
Type: partitionType, Required: true,
+ })
+ }
+ fields = append(fields,
+ iceberg.NestedField{ID: inspectContentFieldIDRecordCount, Name:
"record_count", Type: iceberg.PrimitiveTypes.Int64, Required: true},
+ iceberg.NestedField{ID: inspectContentFieldIDFileSize, Name:
"file_size_in_bytes", Type: iceberg.PrimitiveTypes.Int64, Required: true},
+ iceberg.NestedField{ID: inspectContentFieldIDColumnSizes, Name:
"column_sizes", Type: inspectInt64MapType(117, 118), Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDValueCounts, Name:
"value_counts", Type: inspectInt64MapType(119, 120), Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDNullValueCounts,
Name: "null_value_counts", Type: inspectInt64MapType(121, 122), Required:
false},
+ iceberg.NestedField{ID: inspectContentFieldIDNaNValueCounts,
Name: "nan_value_counts", Type: inspectInt64MapType(138, 139), Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDLowerBounds, Name:
"lower_bounds", Type: inspectBinaryMapType(126, 127), Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDUpperBounds, Name:
"upper_bounds", Type: inspectBinaryMapType(129, 130), Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDKeyMetadata, Name:
"key_metadata", Type: iceberg.PrimitiveTypes.Binary, Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDSplitOffsets,
Name: "split_offsets", Type: &iceberg.ListType{ElementID: 133, Element:
iceberg.PrimitiveTypes.Int64, ElementRequired: true}, Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDEqualityIDs, Name:
"equality_ids", Type: &iceberg.ListType{ElementID: 136, Element:
iceberg.PrimitiveTypes.Int32, ElementRequired: true}, Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDSortOrderID, Name:
"sort_order_id", Type: iceberg.PrimitiveTypes.Int32, Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDFirstRowID, Name:
"first_row_id", Type: iceberg.PrimitiveTypes.Int64, Required: false},
+ iceberg.NestedField{ID:
inspectContentFieldIDReferencedDataFile, Name: "referenced_data_file", Type:
iceberg.PrimitiveTypes.String, Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDContentOffset,
Name: "content_offset", Type: iceberg.PrimitiveTypes.Int64, Required: false},
+ iceberg.NestedField{ID: inspectContentFieldIDContentSize, Name:
"content_size_in_bytes", Type: iceberg.PrimitiveTypes.Int64, Required: false},
+ )
+
+ return fields
+}
+
+func inspectInt64MapType(keyID, valueID int) *iceberg.MapType {
+ return &iceberg.MapType{KeyID: keyID, KeyType:
iceberg.PrimitiveTypes.Int32, ValueID: valueID, ValueType:
iceberg.PrimitiveTypes.Int64, ValueRequired: true}
+}
+
+func inspectBinaryMapType(keyID, valueID int) *iceberg.MapType {
+ return &iceberg.MapType{KeyID: keyID, KeyType:
iceberg.PrimitiveTypes.Int32, ValueID: valueID, ValueType:
iceberg.PrimitiveTypes.Binary, ValueRequired: true}
+}
diff --git a/table/inspect_partition_builder.go
b/table/inspect_partition_builder.go
new file mode 100644
index 000000000..56253760f
--- /dev/null
+++ b/table/inspect_partition_builder.go
@@ -0,0 +1,215 @@
+// 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"
+
+ "github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/array"
+ "github.com/apache/arrow-go/v18/arrow/scalar"
+ "github.com/apache/iceberg-go"
+ "github.com/google/uuid"
+)
+
+type inspectPartitionBuilder struct {
+ builder *array.StructBuilder
+ fields []inspectPartitionFieldBuilder
+}
+
+type inspectPartitionFieldBuilder struct {
+ id int
+ name string
+ typ iceberg.Type
+ arrowType arrow.DataType
+ builder array.Builder
+ appendValue func(any) error
+}
+
+func newInspectPartitionBuilder(
+ builder *array.StructBuilder,
+ partitionType *iceberg.StructType,
+) (*inspectPartitionBuilder, error) {
+ arrowType, ok := builder.Type().(*arrow.StructType)
+ if !ok {
+ return nil, fmt.Errorf("partition builder has type %T, want
struct", builder.Type())
+ }
+ lookup, err := newInspectBuilderLookup("partition", arrowType.Fields(),
builder.FieldBuilder)
+ if err != nil {
+ return nil, err
+ }
+ expected := make(map[int]struct{}, len(partitionType.FieldList))
+ for _, field := range partitionType.FieldList {
+ expected[field.ID] = struct{}{}
+ }
+ if err := validateInspectBuilderLookup("partition", lookup, expected);
err != nil {
+ return nil, err
+ }
+
+ fields := make([]inspectPartitionFieldBuilder, 0,
len(partitionType.FieldList))
+ for _, field := range partitionType.FieldList {
+ fieldBuilder := lookup[field.ID]
+ partitionField := inspectPartitionFieldBuilder{
+ id: field.ID,
+ name: field.Name,
+ typ: field.Type,
+ arrowType: fieldBuilder.Type(),
+ builder: fieldBuilder,
+ }
+ partitionField.appendValue =
newInspectPartitionFieldAppender(partitionField)
+ fields = append(fields, partitionField)
+ }
+
+ return &inspectPartitionBuilder{builder: builder, fields: fields}, nil
+}
+
+func (b *inspectPartitionBuilder) append(values map[int]any) error {
+ b.builder.Append(true)
+ for _, field := range b.fields {
+ if err := field.appendValue(values[field.id]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func newInspectPartitionFieldAppender(field inspectPartitionFieldBuilder)
func(any) error {
+ appendScalar := func(value any) error {
+ sc, err := inspectValueScalar(value, field.typ, field.arrowType)
+ if err != nil {
+ return fmt.Errorf("partition field %q: %w", field.name,
err)
+ }
+
+ return scalar.Append(field.builder, sc)
+ }
+ appendNull := func() {
+ field.builder.AppendNull()
+ }
+
+ switch field.typ.(type) {
+ case iceberg.BooleanType:
+ if builder, ok := field.builder.(*array.BooleanBuilder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Int32Type:
+ if builder, ok := field.builder.(*array.Int32Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Int64Type:
+ if builder, ok := field.builder.(*array.Int64Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Float32Type:
+ if builder, ok := field.builder.(*array.Float32Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.Float64Type:
+ if builder, ok := field.builder.(*array.Float64Builder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.StringType:
+ if builder, ok := field.builder.(*array.StringBuilder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ case iceberg.BinaryType:
+ if builder, ok := field.builder.(*array.BinaryBuilder); ok {
+ return newInspectTypedPartitionAppender(builder.Append,
builder.AppendNull, appendScalar)
+ }
+ }
+
+ return func(value any) error {
+ if value == nil {
+ appendNull()
+
+ return nil
+ }
+
+ return appendScalar(value)
+ }
+}
+
+func newInspectTypedPartitionAppender[T any](
+ appendValue func(T),
+ appendNull func(),
+ appendScalar func(any) error,
+) func(any) error {
+ return func(value any) error {
+ if value == nil {
+ appendNull()
+
+ return nil
+ }
+ typed, ok := value.(T)
+ if !ok {
+ return appendScalar(value)
+ }
+ appendValue(typed)
+
+ return nil
+ }
+}
+
+func inspectValueScalar(value any, typ iceberg.Type, arrowType arrow.DataType)
(scalar.Scalar, error) {
+ switch typ.(type) {
+ case iceberg.DateType:
+ switch value := value.(type) {
+ case iceberg.Date:
+ return scalar.NewDate32Scalar(arrow.Date32(value)), nil
+ case int32:
+ return scalar.NewDate32Scalar(arrow.Date32(value)), nil
+ default:
+ return nil, fmt.Errorf("unsupported date partition
value %T", value)
+ }
+ case iceberg.TimeType:
+ if value, ok := value.(iceberg.Time); ok {
+ return scalar.NewTime64Scalar(arrow.Time64(value),
arrowType), nil
+ }
+
+ return nil, fmt.Errorf("unsupported time partition value %T",
value)
+ case iceberg.TimestampType, iceberg.TimestampTzType:
+ if value, ok := value.(iceberg.Timestamp); ok {
+ return
scalar.NewTimestampScalar(arrow.Timestamp(value), arrowType), nil
+ }
+
+ return nil, fmt.Errorf("unsupported timestamp partition value
%T", value)
+ case iceberg.TimestampNsType, iceberg.TimestampTzNsType:
+ if value, ok := value.(iceberg.TimestampNano); ok {
+ return
scalar.NewTimestampScalar(arrow.Timestamp(value), arrowType), nil
+ }
+
+ return nil, fmt.Errorf("unsupported nanosecond timestamp
partition value %T", value)
+ case iceberg.UUIDType:
+ if value, ok := value.(uuid.UUID); ok {
+ return scalar.MakeScalarParam(value[:], arrowType)
+ }
+
+ return nil, fmt.Errorf("unsupported UUID partition value %T",
value)
+ case iceberg.DecimalType:
+ switch value := value.(type) {
+ case iceberg.DecimalLiteral:
+ return scalar.NewDecimal128Scalar(value.Val,
arrowType), nil
+ case iceberg.Decimal:
+ return scalar.NewDecimal128Scalar(value.Val,
arrowType), nil
+ default:
+ return nil, fmt.Errorf("unsupported decimal partition
value %T", value)
+ }
+ }
+
+ return scalar.MakeScalarParam(value, arrowType)
+}