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

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


The following commit(s) were added to refs/heads/main by this push:
     new 100025739 fix(table): write statistics blob metadata Java can read 
(#2358)
100025739 is described below

commit 10002573905efc80336d4cce8fb49d51f2bf117a
Author: Andrei Tserakhau <[email protected]>
AuthorDate: Thu Nov 5 18:36:55 2026 +0100

    fix(table): write statistics blob metadata Java can read (#2358)
    
    * fix(table): write statistics blob metadata Java can read
    
    A statistics blob with no properties was written as "properties": null,
    and one with no fields as "fields": null, in table metadata and
    set-statistics updates. Iceberg Java 1.12 rejects both in
    TableMetadataParser and MetadataUpdateParser, so the whole metadata load
    fails. Metadata Java wrote hit it too: Java omits empty properties, Go
    decoded that to a nil map, and the next Go write emitted null.
    
    Omit empty properties and write nil fields as [], which is what Java
    writes for the same blobs.
    
    * fix(table): write empty statistics blob metadata as []
    
    A StatisticsFile with no blobs was written as "blob-metadata": null,
    which Java's StatisticsFileParser rejects.
---
 table/statistics.go      | 28 ++++++++++++++++++++++++++--
 table/statistics_test.go | 28 ++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 2 deletions(-)

diff --git a/table/statistics.go b/table/statistics.go
index 46a429c4d..9368c7b12 100644
--- a/table/statistics.go
+++ b/table/statistics.go
@@ -17,7 +17,11 @@
 
 package table
 
-import "github.com/apache/iceberg-go/puffin"
+import (
+       "encoding/json"
+
+       "github.com/apache/iceberg-go/puffin"
+)
 
 // StatisticsFile represents a statistics file in the Puffin format, that can 
be used to read table data more
 // efficiently.
@@ -33,6 +37,16 @@ type StatisticsFile struct {
        BlobMetadata          []BlobMetadata `json:"blob-metadata"`
 }
 
+// MarshalJSON writes nil BlobMetadata as [], since the spec requires the 
field.
+func (s StatisticsFile) MarshalJSON() ([]byte, error) {
+       type statisticsFile StatisticsFile
+       if s.BlobMetadata == nil {
+               s.BlobMetadata = []BlobMetadata{}
+       }
+
+       return json.Marshal(statisticsFile(s))
+}
+
 // BlobMetadata is the metadata of a statistics or indices blob.
 // Its type uses the same Puffin blob identifiers as the file reader and 
writer.
 type BlobMetadata struct {
@@ -40,7 +54,17 @@ type BlobMetadata struct {
        SnapshotID     int64             `json:"snapshot-id"`
        SequenceNumber int64             `json:"sequence-number"`
        Fields         []int32           `json:"fields"`
-       Properties     map[string]string `json:"properties"`
+       Properties     map[string]string `json:"properties,omitempty"`
+}
+
+// MarshalJSON writes nil Fields as [], since the spec requires the field.
+func (b BlobMetadata) MarshalJSON() ([]byte, error) {
+       type blobMetadata BlobMetadata
+       if b.Fields == nil {
+               b.Fields = []int32{}
+       }
+
+       return json.Marshal(blobMetadata(b))
 }
 
 // PartitionStatisticsFile represents a partition statistics file that can be 
used to read table data more efficiently.
diff --git a/table/statistics_test.go b/table/statistics_test.go
index d9796bde6..aa8abe40d 100644
--- a/table/statistics_test.go
+++ b/table/statistics_test.go
@@ -66,3 +66,31 @@ func TestBlobMetadataRejectsNullAndEmptyType(t *testing.T) {
                })
        }
 }
+
+func TestStatisticsJSONMatchesJava(t *testing.T) {
+       for _, tt := range []struct {
+               value any
+               want  string
+       }{
+               {
+                       value: BlobMetadata{Type: 
puffin.BlobTypeDataSketchesTheta, SnapshotID: 1, SequenceNumber: 2, Fields: 
[]int32{1}},
+                       want:  
`{"type":"apache-datasketches-theta-v1","snapshot-id":1,"sequence-number":2,"fields":[1]}`,
+               },
+               {
+                       value: BlobMetadata{Type: 
puffin.BlobTypeDataSketchesTheta, SnapshotID: 1, SequenceNumber: 2, Fields: 
[]int32{1}, Properties: map[string]string{}},
+                       want:  
`{"type":"apache-datasketches-theta-v1","snapshot-id":1,"sequence-number":2,"fields":[1]}`,
+               },
+               {
+                       value: BlobMetadata{Type: "vendor-sketch-v1", 
SnapshotID: 1, SequenceNumber: 2, Properties: map[string]string{"k": "v"}},
+                       want:  
`{"type":"vendor-sketch-v1","snapshot-id":1,"sequence-number":2,"fields":[],"properties":{"k":"v"}}`,
+               },
+               {
+                       value: StatisticsFile{SnapshotID: 1, StatisticsPath: 
"s3://bucket/stats.puffin", FileSizeInBytes: 10, FileFooterSizeInBytes: 2},
+                       want:  
`{"snapshot-id":1,"statistics-path":"s3://bucket/stats.puffin","file-size-in-bytes":10,"file-footer-size-in-bytes":2,"blob-metadata":[]}`,
+               },
+       } {
+               data, err := json.Marshal(tt.value)
+               require.NoError(t, err)
+               assert.JSONEq(t, tt.want, string(data))
+       }
+}

Reply via email to