laskoviymishka commented on code in PR #1697:
URL: https://github.com/apache/iceberg-go/pull/1697#discussion_r3749988029


##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)

Review Comment:
   For a read-only inspect API I'm a little wary of hard-erroring the whole 
call when we hit a single manifest with a content value we don't recognize — 
one forward-version manifest makes the entire table uninspectable through this 
method. Java/PyIceberg lean toward graceful degradation here.
   
   Not blocking, and I can see the case for failing loud. But I'd at least 
consider skip-with-warning or a wrapped sentinel the caller can pick out. Same 
thought applies to the excess-summaries reject a few lines down. wdyt?



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())

Review Comment:
   `added_snapshot_id` is declared `Required:true`, but `SnapshotID()` returns 
`-1` for a V1 manifest whose `added_snapshot_id` field was absent, and we 
append it straight through. Java returns null there, so a query joining 
`manifests.added_snapshot_id` against the snapshots table would silently miss 
instead of seeing null, and `-1` reads like a real id.
   
   I don't think any of the current constructors can actually produce the `-1` 
(`NewManifestFile` always injects a concrete id), so this may not be reachable 
today — but if the Avro read path can surface it, I'd declare the field 
`Required:false` and `AppendNull()` when `SnapshotID()==-1`. Worth confirming 
which way that goes.



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())
+               appendCount := func(builder *array.Int32Builder, name string, 
count int32) error {
+                       if err := appendManifestCount(builder, 
manifest.Version(), name, count); err != nil {
+                               return fmt.Errorf("manifest %s: %w", 
manifest.FilePath(), err)
+                       }
+
+                       return nil
+               }
+
+               switch manifestContent {
+               case iceberg.ManifestContentData:
+                       if err := appendCount(addedDataFiles, 
"added_data_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDataFiles, 
"existing_data_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDataFiles, 
"deleted_data_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       addedDeleteFiles.Append(0)
+                       existingDeleteFiles.Append(0)
+                       deletedDeleteFiles.Append(0)
+               case iceberg.ManifestContentDeletes:
+                       addedDataFiles.Append(0)
+                       existingDataFiles.Append(0)
+                       deletedDataFiles.Append(0)
+                       if err := appendCount(addedDeleteFiles, 
"added_delete_files", manifest.AddedDataFiles()); err != nil {

Review Comment:
   This routing is going to make the next reader do a double-take: for delete 
manifests we're pulling the delete-file counts out of 
`AddedDataFiles()`/`ExistingDataFiles()`/`DeletedDataFiles()`. It's correct 
today because those accessors return the generic `added_files_count` field, 
which holds delete-file counts for a delete manifest — but the names say the 
opposite.
   
   A one-line comment noting that would save the confusion, and flag it if 
iceberg-go ever grows distinct `AddedDeleteFiles()` accessors.



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())
+               appendCount := func(builder *array.Int32Builder, name string, 
count int32) error {
+                       if err := appendManifestCount(builder, 
manifest.Version(), name, count); err != nil {
+                               return fmt.Errorf("manifest %s: %w", 
manifest.FilePath(), err)
+                       }
+
+                       return nil
+               }
+
+               switch manifestContent {
+               case iceberg.ManifestContentData:
+                       if err := appendCount(addedDataFiles, 
"added_data_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDataFiles, 
"existing_data_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDataFiles, 
"deleted_data_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       addedDeleteFiles.Append(0)
+                       existingDeleteFiles.Append(0)
+                       deletedDeleteFiles.Append(0)
+               case iceberg.ManifestContentDeletes:
+                       addedDataFiles.Append(0)
+                       existingDataFiles.Append(0)
+                       deletedDataFiles.Append(0)
+                       if err := appendCount(addedDeleteFiles, 
"added_delete_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDeleteFiles, 
"existing_delete_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDeleteFiles, 
"deleted_delete_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+               }
+
+               spec := 
i.tbl.metadata.PartitionSpecByID(int(manifest.PartitionSpecID()))
+               if spec == nil {

Review Comment:
   I think there's a subtle ordering bug here. We look up the spec and error 
out if it's nil before we check `partitions == nil`, so a manifest that carries 
no partition summaries but was written under a spec that's since been removed 
(spec evolution + metadata cleanup) fails the whole `Manifests()` call.
   
   Java and PyIceberg return early on the no-summaries case and never touch the 
spec, so they succeed where we'd error. I'd move the `partitions == nil` check 
above the spec lookup — that also gives a clean home for the empty-list change 
from the other thread. wdyt?



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())
+               appendCount := func(builder *array.Int32Builder, name string, 
count int32) error {
+                       if err := appendManifestCount(builder, 
manifest.Version(), name, count); err != nil {
+                               return fmt.Errorf("manifest %s: %w", 
manifest.FilePath(), err)
+                       }
+
+                       return nil
+               }
+
+               switch manifestContent {
+               case iceberg.ManifestContentData:
+                       if err := appendCount(addedDataFiles, 
"added_data_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDataFiles, 
"existing_data_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDataFiles, 
"deleted_data_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       addedDeleteFiles.Append(0)
+                       existingDeleteFiles.Append(0)
+                       deletedDeleteFiles.Append(0)
+               case iceberg.ManifestContentDeletes:
+                       addedDataFiles.Append(0)
+                       existingDataFiles.Append(0)
+                       deletedDataFiles.Append(0)
+                       if err := appendCount(addedDeleteFiles, 
"added_delete_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDeleteFiles, 
"existing_delete_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDeleteFiles, 
"deleted_delete_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+               }
+
+               spec := 
i.tbl.metadata.PartitionSpecByID(int(manifest.PartitionSpecID()))
+               if spec == nil {
+                       return nil, fmt.Errorf("manifest %s references missing 
partition spec %d",
+                               manifest.FilePath(), manifest.PartitionSpecID())
+               }
+               partType := spec.PartitionType(i.tbl.metadata.CurrentSchema())
+               partitions := manifest.Partitions()
+               if partitions == nil {
+                       partitionSummaries.AppendNull()
+
+                       continue
+               }
+               if len(partitions) > spec.NumFields() {
+                       return nil, fmt.Errorf("manifest %s has %d partition 
summaries for partition spec %d with %d fields",
+                               manifest.FilePath(), len(partitions), 
manifest.PartitionSpecID(), spec.NumFields())
+               }
+
+               partitionSummaries.Append(true)
+               for idx, summary := range partitions {
+                       summaryStruct.Append(true)
+                       summaryContainsNull.Append(summary.ContainsNull)
+                       if summary.ContainsNaN == nil {
+                               summaryContainsNaN.AppendNull()
+                       } else {
+                               summaryContainsNaN.Append(*summary.ContainsNaN)
+                       }
+
+                       fieldType := partType.FieldList[idx].Type
+                       transform := spec.Field(idx).Transform
+                       if err := appendManifestBound(summaryLower, fieldType, 
transform, summary.LowerBound); err != nil {
+                               return nil, fmt.Errorf("manifest %s partition 
field %d lower bound: %w", manifest.FilePath(), idx, err)
+                       }
+                       if err := appendManifestBound(summaryUpper, fieldType, 
transform, summary.UpperBound); err != nil {
+                               return nil, fmt.Errorf("manifest %s partition 
field %d upper bound: %w", manifest.FilePath(), idx, err)
+                       }
+               }
+       }
+
+       rr, err := singleBatchReader(arrowSchema, bldr)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       return rr, nil
+}
+
+func appendManifestCount(builder *array.Int32Builder, version int, name 
string, count int32) error {
+       if count < 0 {
+               if version == 1 {
+                       builder.AppendNull()

Review Comment:
   This is the one I'd block on. The schema declares the six count fields 
`Required:true`, which `SchemaToArrowSchema` turns into Arrow `Nullable:false` 
— but here we `AppendNull()` for V1 manifests, so we emit a batch whose schema 
says non-nullable while the data carries nulls. Arrow-Go won't panic on that, 
but any consumer that trusts the schema (a Parquet writer, a scan engine) reads 
it as corrupt.
   
   Every reference client is internally consistent and none emit null here: 
PyIceberg and Java declare the fields and write 0 for V1 unknowns; Rust 
declares them nullable. I'd match PyIceberg/Java and `Append(int32(0))` instead 
of `AppendNull()` — that keeps `Required:true` honest. If we'd rather preserve 
the null, the fields (5, 6, 7, 15, 16, 17) have to flip to `Required:false`. 
Either is fine, but the schema and the data have to agree.



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())
+               appendCount := func(builder *array.Int32Builder, name string, 
count int32) error {
+                       if err := appendManifestCount(builder, 
manifest.Version(), name, count); err != nil {
+                               return fmt.Errorf("manifest %s: %w", 
manifest.FilePath(), err)
+                       }
+
+                       return nil
+               }
+
+               switch manifestContent {
+               case iceberg.ManifestContentData:
+                       if err := appendCount(addedDataFiles, 
"added_data_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDataFiles, 
"existing_data_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDataFiles, 
"deleted_data_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       addedDeleteFiles.Append(0)
+                       existingDeleteFiles.Append(0)
+                       deletedDeleteFiles.Append(0)
+               case iceberg.ManifestContentDeletes:
+                       addedDataFiles.Append(0)
+                       existingDataFiles.Append(0)
+                       deletedDataFiles.Append(0)
+                       if err := appendCount(addedDeleteFiles, 
"added_delete_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDeleteFiles, 
"existing_delete_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDeleteFiles, 
"deleted_delete_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+               }
+
+               spec := 
i.tbl.metadata.PartitionSpecByID(int(manifest.PartitionSpecID()))
+               if spec == nil {
+                       return nil, fmt.Errorf("manifest %s references missing 
partition spec %d",
+                               manifest.FilePath(), manifest.PartitionSpecID())
+               }
+               partType := spec.PartitionType(i.tbl.metadata.CurrentSchema())
+               partitions := manifest.Partitions()
+               if partitions == nil {
+                       partitionSummaries.AppendNull()
+
+                       continue
+               }
+               if len(partitions) > spec.NumFields() {
+                       return nil, fmt.Errorf("manifest %s has %d partition 
summaries for partition spec %d with %d fields",
+                               manifest.FilePath(), len(partitions), 
manifest.PartitionSpecID(), spec.NumFields())
+               }
+
+               partitionSummaries.Append(true)
+               for idx, summary := range partitions {
+                       summaryStruct.Append(true)
+                       summaryContainsNull.Append(summary.ContainsNull)
+                       if summary.ContainsNaN == nil {
+                               summaryContainsNaN.AppendNull()
+                       } else {
+                               summaryContainsNaN.Append(*summary.ContainsNaN)
+                       }
+
+                       fieldType := partType.FieldList[idx].Type
+                       transform := spec.Field(idx).Transform
+                       if err := appendManifestBound(summaryLower, fieldType, 
transform, summary.LowerBound); err != nil {
+                               return nil, fmt.Errorf("manifest %s partition 
field %d lower bound: %w", manifest.FilePath(), idx, err)

Review Comment:
   nit: this reports the partition field by positional index ("partition field 
3"), which makes the user count schema fields to find it. We've got 
`partType.FieldList[idx].Name` right here — worth using the name instead.



##########
table/inspect_internal_test.go:
##########
@@ -404,6 +457,308 @@ func TestInspectSnapshotsEmpty(t *testing.T) {
        require.EqualValues(t, 6, rec.NumCols())
 }
 
+func TestInspectManifestsSchema(t *testing.T) {
+       sc := ManifestsSchema()
+
+       require.Equal(t,
+               []string{
+                       "content", "path", "length", "partition_spec_id", 
"added_snapshot_id",
+                       "added_data_files_count", "existing_data_files_count", 
"deleted_data_files_count",
+                       "added_delete_files_count", 
"existing_delete_files_count", "deleted_delete_files_count",
+                       "partition_summaries",
+               },
+               testFieldNames(sc))
+
+       fields := sc.Fields()
+       require.Equal(t, 14, fields[0].ID)
+       require.Equal(t, 1, fields[1].ID)
+       require.Equal(t, 17, fields[10].ID)
+       require.Equal(t, 8, fields[11].ID)
+       require.True(t, fields[11].Required)
+       require.True(t, fields[11].Type.(*iceberg.ListType).ElementRequired)
+       require.Equal(t, 9, fields[11].Type.(*iceberg.ListType).ElementID)
+
+       partitionSummary := 
fields[11].Type.(*iceberg.ListType).Element.(*iceberg.StructType)
+       require.Equal(t,
+               []string{"contains_null", "contains_nan", "lower_bound", 
"upper_bound"},
+               testFieldNames(iceberg.NewSchema(0, 
partitionSummary.FieldList...)))
+       require.Equal(t, 10, partitionSummary.FieldList[0].ID)
+       require.Equal(t, 11, partitionSummary.FieldList[1].ID)
+       require.Equal(t, 12, partitionSummary.FieldList[2].ID)
+       require.Equal(t, 13, partitionSummary.FieldList[3].ID)
+}
+
+func TestInspectManifests(t *testing.T) {
+       const snapshotID = int64(1)
+       spec := partitionedSpec()
+       txn, memIO := createTestTransactionWithMemIO(t, spec)
+       schema := simpleSchema()
+       file := newTestDataFileWithCount(t, spec,
+               "mem://default/table-location/data/data.parquet", 
map[int]any{1000: int32(7)}, 3)
+       sequenceNumber := int64(1)
+       entry := iceberg.NewManifestEntry(
+               iceberg.EntryStatusADDED, int64Ptr(snapshotID), 
&sequenceNumber, &sequenceNumber, file)
+
+       manifestPath := 
"mem://default/table-location/metadata/data-manifest.avro"
+       manifestListPath := 
"mem://default/table-location/metadata/snap-1-manifest-list.avro"
+       var manifestBuf bytes.Buffer
+       manifest, err := iceberg.WriteManifest(manifestPath, &manifestBuf, 2, 
spec, schema, snapshotID,
+               []iceberg.ManifestEntry{entry})
+       require.NoError(t, err)
+       require.NoError(t, memIO.WriteFile(manifestPath, manifestBuf.Bytes()))
+
+       var listBuf bytes.Buffer
+       require.NoError(t, iceberg.WriteManifestList(2, &listBuf, snapshotID, 
nil, &sequenceNumber, 0,
+               []iceberg.ManifestFile{manifest}))
+       require.NoError(t, memIO.WriteFile(manifestListPath, listBuf.Bytes()))
+
+       snapID := snapshotID
+       txn.meta.snapshotList = []Snapshot{{
+               SnapshotID:     snapshotID,
+               ManifestList:   manifestListPath,
+               SequenceNumber: sequenceNumber,
+       }}
+       txn.meta.currentSnapshotID = &snapID
+       built, err := txn.meta.Build()
+       require.NoError(t, err)
+
+       tbl := New(Identifier{"db", "tbl"}, built, "metadata.json",
+               func(context.Context) (iceio.IO, error) { return memIO, nil }, 
nil)
+       rr, err := tbl.Inspect().Manifests(context.Background())
+       require.NoError(t, err)
+       defer rr.Release()
+
+       record := collectRecord(t, rr)
+       defer record.Release()
+       require.EqualValues(t, 1, record.NumRows())
+       require.EqualValues(t, 1, record.Column(5).(*array.Int32).Value(0))
+       require.EqualValues(t, 0, record.Column(8).(*array.Int32).Value(0))
+       require.Equal(t, manifestPath, 
record.Column(1).(*array.String).Value(0))
+
+       summaries := record.Column(11).(*array.List)
+       require.False(t, summaries.IsNull(0))
+       start, end := summaries.ValueOffsets(0)
+       require.EqualValues(t, 1, end-start)
+       summary := summaries.ListValues().(*array.Struct)
+       require.False(t, summary.Field(0).(*array.Boolean).Value(0))
+       require.Equal(t, "7", summary.Field(2).(*array.String).Value(0))

Review Comment:
   The non-null `contains_nan` path never gets exercised. Every test leaves 
`ContainsNaN` nil, so we only ever hit the `AppendNull` branch — the 
`Append(*summary.ContainsNaN)` side would pass just as well if it were deleted. 
This block also skips asserting `Field(1)` entirely.
   
   I'd add a case with `ContainsNaN` set to a bool and assert `Field(1)` is 
non-null with the right value.



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())
+               appendCount := func(builder *array.Int32Builder, name string, 
count int32) error {
+                       if err := appendManifestCount(builder, 
manifest.Version(), name, count); err != nil {
+                               return fmt.Errorf("manifest %s: %w", 
manifest.FilePath(), err)
+                       }
+
+                       return nil
+               }
+
+               switch manifestContent {
+               case iceberg.ManifestContentData:
+                       if err := appendCount(addedDataFiles, 
"added_data_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDataFiles, 
"existing_data_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDataFiles, 
"deleted_data_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       addedDeleteFiles.Append(0)
+                       existingDeleteFiles.Append(0)
+                       deletedDeleteFiles.Append(0)
+               case iceberg.ManifestContentDeletes:
+                       addedDataFiles.Append(0)
+                       existingDataFiles.Append(0)
+                       deletedDataFiles.Append(0)
+                       if err := appendCount(addedDeleteFiles, 
"added_delete_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDeleteFiles, 
"existing_delete_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDeleteFiles, 
"deleted_delete_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+               }
+
+               spec := 
i.tbl.metadata.PartitionSpecByID(int(manifest.PartitionSpecID()))
+               if spec == nil {
+                       return nil, fmt.Errorf("manifest %s references missing 
partition spec %d",
+                               manifest.FilePath(), manifest.PartitionSpecID())
+               }
+               partType := spec.PartitionType(i.tbl.metadata.CurrentSchema())
+               partitions := manifest.Partitions()
+               if partitions == nil {
+                       partitionSummaries.AppendNull()

Review Comment:
   Same contract problem as the count columns, one column over. 
`partition_summaries` is field 8, `Required:true` → `Nullable:false`, but when 
`partitions == nil` we `AppendNull()` into it. PyIceberg and Rust both emit an 
empty list here, never null.
   
   I'd emit `partitionSummaries.Append(true)` with no children (a zero-length 
list), then `continue`. One gotcha: `Append(false)` would still be a null slot, 
so it has to be `Append(true)` to get a non-null empty list. That matches the 
reference clients and keeps the column honestly non-nullable.



##########
table/inspect.go:
##########
@@ -247,6 +248,224 @@ func (i InspectTable) Snapshots(ctx context.Context) 
(array.RecordReader, error)
        return rr, nil
 }
 
+// Manifests returns one row for each manifest in the current snapshot.
+// Partition summaries are exposed as the human-readable values used by the
+// other Iceberg clients.
+func (i InspectTable) Manifests(ctx context.Context) (array.RecordReader, 
error) {
+       schema := ManifestsSchema()
+       arrowSchema, err := SchemaToArrowSchema(schema, nil, true, false)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: build arrow schema: 
%w", err)
+       }
+
+       manifests, err := i.currentSnapshotManifests(ctx)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       bldr := array.NewRecordBuilder(i.alloc, arrowSchema)
+       defer bldr.Release()
+
+       content := bldr.Field(0).(*array.Int32Builder)
+       path := bldr.Field(1).(*array.StringBuilder)
+       length := bldr.Field(2).(*array.Int64Builder)
+       partitionSpecID := bldr.Field(3).(*array.Int32Builder)
+       addedSnapshotID := bldr.Field(4).(*array.Int64Builder)
+       addedDataFiles := bldr.Field(5).(*array.Int32Builder)
+       existingDataFiles := bldr.Field(6).(*array.Int32Builder)
+       deletedDataFiles := bldr.Field(7).(*array.Int32Builder)
+       addedDeleteFiles := bldr.Field(8).(*array.Int32Builder)
+       existingDeleteFiles := bldr.Field(9).(*array.Int32Builder)
+       deletedDeleteFiles := bldr.Field(10).(*array.Int32Builder)
+       partitionSummaries := bldr.Field(11).(*array.ListBuilder)
+       summaryStruct := 
partitionSummaries.ValueBuilder().(*array.StructBuilder)
+       summaryContainsNull := 
summaryStruct.FieldBuilder(0).(*array.BooleanBuilder)
+       summaryContainsNaN := 
summaryStruct.FieldBuilder(1).(*array.BooleanBuilder)
+       summaryLower := summaryStruct.FieldBuilder(2).(*array.StringBuilder)
+       summaryUpper := summaryStruct.FieldBuilder(3).(*array.StringBuilder)
+
+       for _, manifest := range manifests {
+               if err := ctx.Err(); err != nil {
+                       return nil, err
+               }
+
+               manifestContent := manifest.ManifestContent()
+               switch manifestContent {
+               case iceberg.ManifestContentData, 
iceberg.ManifestContentDeletes:
+               default:
+                       return nil, fmt.Errorf("manifest %s has unknown content 
%d", manifest.FilePath(), manifestContent)
+               }
+
+               content.Append(int32(manifestContent))
+               path.Append(manifest.FilePath())
+               length.Append(manifest.Length())
+               partitionSpecID.Append(manifest.PartitionSpecID())
+               addedSnapshotID.Append(manifest.SnapshotID())
+               appendCount := func(builder *array.Int32Builder, name string, 
count int32) error {
+                       if err := appendManifestCount(builder, 
manifest.Version(), name, count); err != nil {
+                               return fmt.Errorf("manifest %s: %w", 
manifest.FilePath(), err)
+                       }
+
+                       return nil
+               }
+
+               switch manifestContent {
+               case iceberg.ManifestContentData:
+                       if err := appendCount(addedDataFiles, 
"added_data_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDataFiles, 
"existing_data_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDataFiles, 
"deleted_data_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       addedDeleteFiles.Append(0)
+                       existingDeleteFiles.Append(0)
+                       deletedDeleteFiles.Append(0)
+               case iceberg.ManifestContentDeletes:
+                       addedDataFiles.Append(0)
+                       existingDataFiles.Append(0)
+                       deletedDataFiles.Append(0)
+                       if err := appendCount(addedDeleteFiles, 
"added_delete_files", manifest.AddedDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(existingDeleteFiles, 
"existing_delete_files", manifest.ExistingDataFiles()); err != nil {
+                               return nil, err
+                       }
+                       if err := appendCount(deletedDeleteFiles, 
"deleted_delete_files", manifest.DeletedDataFiles()); err != nil {
+                               return nil, err
+                       }
+               }
+
+               spec := 
i.tbl.metadata.PartitionSpecByID(int(manifest.PartitionSpecID()))
+               if spec == nil {
+                       return nil, fmt.Errorf("manifest %s references missing 
partition spec %d",
+                               manifest.FilePath(), manifest.PartitionSpecID())
+               }
+               partType := spec.PartitionType(i.tbl.metadata.CurrentSchema())
+               partitions := manifest.Partitions()
+               if partitions == nil {
+                       partitionSummaries.AppendNull()
+
+                       continue
+               }
+               if len(partitions) > spec.NumFields() {
+                       return nil, fmt.Errorf("manifest %s has %d partition 
summaries for partition spec %d with %d fields",
+                               manifest.FilePath(), len(partitions), 
manifest.PartitionSpecID(), spec.NumFields())
+               }
+
+               partitionSummaries.Append(true)
+               for idx, summary := range partitions {
+                       summaryStruct.Append(true)
+                       summaryContainsNull.Append(summary.ContainsNull)
+                       if summary.ContainsNaN == nil {
+                               summaryContainsNaN.AppendNull()
+                       } else {
+                               summaryContainsNaN.Append(*summary.ContainsNaN)
+                       }
+
+                       fieldType := partType.FieldList[idx].Type
+                       transform := spec.Field(idx).Transform
+                       if err := appendManifestBound(summaryLower, fieldType, 
transform, summary.LowerBound); err != nil {
+                               return nil, fmt.Errorf("manifest %s partition 
field %d lower bound: %w", manifest.FilePath(), idx, err)
+                       }
+                       if err := appendManifestBound(summaryUpper, fieldType, 
transform, summary.UpperBound); err != nil {
+                               return nil, fmt.Errorf("manifest %s partition 
field %d upper bound: %w", manifest.FilePath(), idx, err)
+                       }
+               }
+       }
+
+       rr, err := singleBatchReader(arrowSchema, bldr)
+       if err != nil {
+               return nil, fmt.Errorf("inspect manifests: %w", err)
+       }
+
+       return rr, nil
+}
+
+func appendManifestCount(builder *array.Int32Builder, version int, name 
string, count int32) error {
+       if count < 0 {
+               if version == 1 {
+                       builder.AppendNull()
+
+                       return nil
+               }
+
+               return fmt.Errorf("negative %s count %d in manifest list 
version %d", name, count, version)
+       }
+
+       builder.Append(count)
+
+       return nil
+}
+
+func (i InspectTable) currentSnapshotManifests(ctx context.Context) 
([]iceberg.ManifestFile, error) {
+       snapshot := i.tbl.metadata.CurrentSnapshot()
+       if snapshot == nil {
+               return nil, nil
+       }
+       if i.tbl.fsF == nil {
+               return nil, errors.New("table file IO is not configured")
+       }
+
+       fs, err := i.tbl.fsF(ctx)
+       if err != nil {
+               return nil, err

Review Comment:
   Small thing — this `err` comes back unwrapped while everything else in the 
chain gets an `fmt.Errorf`. The outer wrap is just `"inspect manifests: %w"`, 
so a failure in the IO factory loses the context that it happened here. I'd do 
`return nil, fmt.Errorf("get file IO: %w", err)`.



##########
table/inspect_internal_test.go:
##########
@@ -404,6 +457,308 @@ func TestInspectSnapshotsEmpty(t *testing.T) {
        require.EqualValues(t, 6, rec.NumCols())
 }
 
+func TestInspectManifestsSchema(t *testing.T) {
+       sc := ManifestsSchema()
+
+       require.Equal(t,
+               []string{
+                       "content", "path", "length", "partition_spec_id", 
"added_snapshot_id",
+                       "added_data_files_count", "existing_data_files_count", 
"deleted_data_files_count",
+                       "added_delete_files_count", 
"existing_delete_files_count", "deleted_delete_files_count",
+                       "partition_summaries",
+               },
+               testFieldNames(sc))
+
+       fields := sc.Fields()
+       require.Equal(t, 14, fields[0].ID)
+       require.Equal(t, 1, fields[1].ID)
+       require.Equal(t, 17, fields[10].ID)
+       require.Equal(t, 8, fields[11].ID)
+       require.True(t, fields[11].Required)
+       require.True(t, fields[11].Type.(*iceberg.ListType).ElementRequired)
+       require.Equal(t, 9, fields[11].Type.(*iceberg.ListType).ElementID)
+
+       partitionSummary := 
fields[11].Type.(*iceberg.ListType).Element.(*iceberg.StructType)
+       require.Equal(t,
+               []string{"contains_null", "contains_nan", "lower_bound", 
"upper_bound"},
+               testFieldNames(iceberg.NewSchema(0, 
partitionSummary.FieldList...)))
+       require.Equal(t, 10, partitionSummary.FieldList[0].ID)
+       require.Equal(t, 11, partitionSummary.FieldList[1].ID)
+       require.Equal(t, 12, partitionSummary.FieldList[2].ID)
+       require.Equal(t, 13, partitionSummary.FieldList[3].ID)
+}
+
+func TestInspectManifests(t *testing.T) {
+       const snapshotID = int64(1)
+       spec := partitionedSpec()
+       txn, memIO := createTestTransactionWithMemIO(t, spec)
+       schema := simpleSchema()
+       file := newTestDataFileWithCount(t, spec,
+               "mem://default/table-location/data/data.parquet", 
map[int]any{1000: int32(7)}, 3)
+       sequenceNumber := int64(1)
+       entry := iceberg.NewManifestEntry(
+               iceberg.EntryStatusADDED, int64Ptr(snapshotID), 
&sequenceNumber, &sequenceNumber, file)
+
+       manifestPath := 
"mem://default/table-location/metadata/data-manifest.avro"
+       manifestListPath := 
"mem://default/table-location/metadata/snap-1-manifest-list.avro"
+       var manifestBuf bytes.Buffer
+       manifest, err := iceberg.WriteManifest(manifestPath, &manifestBuf, 2, 
spec, schema, snapshotID,
+               []iceberg.ManifestEntry{entry})
+       require.NoError(t, err)
+       require.NoError(t, memIO.WriteFile(manifestPath, manifestBuf.Bytes()))
+
+       var listBuf bytes.Buffer
+       require.NoError(t, iceberg.WriteManifestList(2, &listBuf, snapshotID, 
nil, &sequenceNumber, 0,
+               []iceberg.ManifestFile{manifest}))
+       require.NoError(t, memIO.WriteFile(manifestListPath, listBuf.Bytes()))
+
+       snapID := snapshotID
+       txn.meta.snapshotList = []Snapshot{{
+               SnapshotID:     snapshotID,
+               ManifestList:   manifestListPath,
+               SequenceNumber: sequenceNumber,
+       }}
+       txn.meta.currentSnapshotID = &snapID
+       built, err := txn.meta.Build()
+       require.NoError(t, err)
+
+       tbl := New(Identifier{"db", "tbl"}, built, "metadata.json",
+               func(context.Context) (iceio.IO, error) { return memIO, nil }, 
nil)
+       rr, err := tbl.Inspect().Manifests(context.Background())
+       require.NoError(t, err)
+       defer rr.Release()
+
+       record := collectRecord(t, rr)
+       defer record.Release()
+       require.EqualValues(t, 1, record.NumRows())
+       require.EqualValues(t, 1, record.Column(5).(*array.Int32).Value(0))
+       require.EqualValues(t, 0, record.Column(8).(*array.Int32).Value(0))
+       require.Equal(t, manifestPath, 
record.Column(1).(*array.String).Value(0))
+
+       summaries := record.Column(11).(*array.List)
+       require.False(t, summaries.IsNull(0))
+       start, end := summaries.ValueOffsets(0)
+       require.EqualValues(t, 1, end-start)
+       summary := summaries.ListValues().(*array.Struct)
+       require.False(t, summary.Field(0).(*array.Boolean).Value(0))
+       require.Equal(t, "7", summary.Field(2).(*array.String).Value(0))
+       require.Equal(t, "7", summary.Field(3).(*array.String).Value(0))
+}
+
+func TestInspectManifestsPromotedPartitionSummaryBounds(t *testing.T) {
+       tests := []struct {
+               name        string
+               initialType iceberg.Type
+               currentType iceberg.Type
+               literal     iceberg.Literal
+               expected    string
+       }{
+               {
+                       name:        "int to long",
+                       initialType: iceberg.PrimitiveTypes.Int32,
+                       currentType: iceberg.PrimitiveTypes.Int64,
+                       literal:     iceberg.Int32Literal(7),
+                       expected:    "7",
+               },
+               {
+                       name:        "float to double",
+                       initialType: iceberg.PrimitiveTypes.Float32,
+                       currentType: iceberg.PrimitiveTypes.Float64,
+                       literal:     iceberg.Float32Literal(1.5),
+                       expected:    "1.5",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       spec := partitionedSpec()
+                       initialSchema := iceberg.NewSchema(0, 
iceberg.NestedField{
+                               ID: 1, Name: "id", Type: tt.initialType, 
Required: true,
+                       })
+                       currentSchema := iceberg.NewSchema(1, 
iceberg.NestedField{
+                               ID: 1, Name: "id", Type: tt.currentType, 
Required: true,
+                       })
+                       bound, err := tt.literal.MarshalBinary()
+                       require.NoError(t, err)
+                       manifest := iceberg.NewManifestFile(2, 
"mem://default/table-location/metadata/promoted.avro",
+                               100, int32(spec.ID()), 1).
+                               SequenceNum(1, 1).
+                               Partitions([]iceberg.FieldSummary{{LowerBound: 
&bound, UpperBound: &bound}}).
+                               Build()
+                       tbl := inspectTableWithManifestListAndSchemas(t, 
initialSchema, currentSchema,
+                               spec, 2, []iceberg.ManifestFile{manifest})
+
+                       rr, err := tbl.Inspect().Manifests(context.Background())
+                       require.NoError(t, err)
+                       defer rr.Release()
+                       record := collectRecord(t, rr)
+                       defer record.Release()
+
+                       summaries := record.Column(11).(*array.List)
+                       start, end := summaries.ValueOffsets(0)
+                       require.EqualValues(t, 1, end-start)
+                       summary := summaries.ListValues().(*array.Struct)
+                       require.Equal(t, tt.expected, 
summary.Field(2).(*array.String).Value(0))
+                       require.Equal(t, tt.expected, 
summary.Field(3).(*array.String).Value(0))
+               })
+       }
+}
+
+func TestInspectManifestsDroppedPartitionSource(t *testing.T) {
+       spec := partitionedSpec()
+       initialSchema := simpleSchema()
+       currentSchema := iceberg.NewSchema(1)
+       bound, err := iceberg.Int32Literal(7).MarshalBinary()
+       require.NoError(t, err)
+       manifest := iceberg.NewManifestFile(2, 
"mem://default/table-location/metadata/dropped-source.avro",
+               100, int32(spec.ID()), 1).
+               SequenceNum(1, 1).
+               Partitions([]iceberg.FieldSummary{{LowerBound: &bound, 
UpperBound: &bound}}).
+               Build()
+       tbl := inspectTableWithManifestListAndSchemas(t, initialSchema, 
currentSchema,
+               spec, 2, []iceberg.ManifestFile{manifest})
+
+       rr, err := tbl.Inspect().Manifests(context.Background())
+       require.NoError(t, err)
+       defer rr.Release()
+       record := collectRecord(t, rr)
+       defer record.Release()
+
+       summaries := record.Column(11).(*array.List)
+       start, end := summaries.ValueOffsets(0)
+       require.EqualValues(t, 1, end-start)
+       summary := summaries.ListValues().(*array.Struct)
+       require.True(t, summary.Field(2).(*array.String).IsNull(0))
+       require.True(t, summary.Field(3).(*array.String).IsNull(0))
+}
+
+func TestInspectManifestsDeleteCounts(t *testing.T) {
+       spec := partitionedSpec()
+       manifest := iceberg.NewManifestFile(2, 
"mem://default/table-location/metadata/delete-manifest.avro",
+               100, int32(spec.ID()), 1).
+               Content(iceberg.ManifestContentDeletes).
+               SequenceNum(1, 1).
+               AddedFiles(2).
+               ExistingFiles(3).
+               DeletedFiles(4).
+               Build()
+       tbl := inspectTableWithManifestList(t, spec, 2, 
[]iceberg.ManifestFile{manifest})
+
+       rr, err := tbl.Inspect().Manifests(context.Background())
+       require.NoError(t, err)
+       defer rr.Release()
+       record := collectRecord(t, rr)
+       defer record.Release()
+
+       require.EqualValues(t, iceberg.ManifestContentDeletes, 
record.Column(0).(*array.Int32).Value(0))
+       for _, col := range []int{5, 6, 7} {
+               require.EqualValues(t, 0, 
record.Column(col).(*array.Int32).Value(0))
+       }
+       require.EqualValues(t, 2, record.Column(8).(*array.Int32).Value(0))
+       require.EqualValues(t, 3, record.Column(9).(*array.Int32).Value(0))
+       require.EqualValues(t, 4, record.Column(10).(*array.Int32).Value(0))
+}
+
+func TestInspectManifestsV1UnknownCounts(t *testing.T) {
+       spec := partitionedSpec()
+       manifest := iceberg.NewManifestFile(1, 
"mem://default/table-location/metadata/v1-manifest.avro",
+               100, int32(spec.ID()), 1).
+               AddedFiles(-1).
+               ExistingFiles(-1).
+               DeletedFiles(-1).
+               Build()
+       tbl := inspectTableWithManifestList(t, spec, 1, 
[]iceberg.ManifestFile{manifest})
+
+       rr, err := tbl.Inspect().Manifests(context.Background())
+       require.NoError(t, err)
+       defer rr.Release()
+       record := collectRecord(t, rr)
+       defer record.Release()
+
+       for _, col := range []int{5, 6, 7} {
+               require.True(t, record.Column(col).(*array.Int32).IsNull(0))
+       }
+       for _, col := range []int{8, 9, 10} {
+               require.EqualValues(t, 0, 
record.Column(col).(*array.Int32).Value(0))
+       }
+       require.True(t, record.Column(11).(*array.List).IsNull(0))
+}
+
+func TestInspectManifestsRejectsNegativeCountsForV2AndV3(t *testing.T) {
+       tests := []struct {
+               name                     string
+               version                  int
+               added, existing, deleted int32
+               invalidCountName         string
+       }{
+               {name: "v2 added data files", version: 2, added: -123, 
existing: 1, deleted: 1, invalidCountName: "added_data_files"},
+               {name: "v2 existing data files", version: 2, added: 1, 
existing: -123, deleted: 1, invalidCountName: "existing_data_files"},

Review Comment:
   This table only covers data manifests — every row is the default 
`ManifestContentData`. The delete-file counts go through the same 
`appendManifestCount`, so a negative delete-count on a V2/V3 delete manifest 
hits the identical error path but isn't verified. I'd add a few 
`ManifestContentDeletes` rows with negative counts to cover the symmetry.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to