ryanworl opened a new issue, #1760:
URL: https://github.com/apache/iceberg-go/issues/1760

   ### Apache Iceberg version
   
   main (development)
   
   ### Please describe the bug 🐞
   
   
   `ReadManifestList` picks its reader schema from the `format-version` key in
   the Avro file metadata, and assumes v1 when the key is missing. As the doc
   comment on the function itself notes, manifest lists aren't required to
   carry that key — but the v1 fallback means a perfectly valid v2/v3 list
   from a writer that skips it gets decoded through the v1 schema, and the
   fields v1 doesn't have are silently zeroed:
   
   - `content` reads as `data`, so delete manifests look like data manifests
   - `sequence-number` / `min-sequence-number` read as 0
   - v3 `first-row-id` is lost
   
   No error, no warning — Avro schema resolution just drops the extra writer
   fields.
   
   I hit this with real files: DuckDB's iceberg extension (v1.5.x) writes
   manifest lists without the key, so reading any DuckDB-written v3 table
   with iceberg-go makes its deletion vectors invisible and zeroes every
   sequence number. Java and PyIceberg read the same files fine since they
   project from the embedded writer schema.
   
   Observed on v0.6.0.
   
   ## Reproduction
   
   Round-trip through iceberg-go itself: write a v2 list, drop the optional
   key from the header (data blocks untouched), read it back.
   
   ```go
   package main
   
   import (
        "bytes"
        "fmt"
   
        "github.com/apache/iceberg-go"
   )
   
   func main() {
        // A v2 manifest list holding one delete manifest with sequence number 
5.
        mf := iceberg.NewManifestFile(2, 
"s3://bucket/metadata/deletes-m0.avro", 100, 0, 42).
                Content(iceberg.ManifestContentDeletes).
                SequenceNum(5, 3).
                Build()
   
        var buf bytes.Buffer
        seq := int64(5)
        if err := iceberg.WriteManifestList(2, &buf, 42, nil, &seq, 0, 
[]iceberg.ManifestFile{mf}); err != nil {
                panic(err)
        }
   
        stripped := stripMetadataKey(buf.Bytes(), "format-version")
   
        got, err := iceberg.ReadManifestList(bytes.NewReader(stripped))
        if err != nil {
                panic(err)
        }
        fmt.Printf("content             = %v  (want deletes)\n", 
got[0].ManifestContent())
        fmt.Printf("sequence-number     = %d  (want 5)\n", got[0].SequenceNum())
        fmt.Printf("min-sequence-number = %d  (want 3)\n", 
got[0].MinSequenceNum())
   }
   
   // stripMetadataKey re-encodes an Avro OCF header without one metadata key;
   // the sync marker and data blocks are copied through untouched.
   func stripMetadataKey(data []byte, key string) []byte {
        off := 4 // past "Obj\x01"
        meta := map[string][]byte{}
        for {
                n, next := readLong(data, off)
                if n == 0 {
                        off++ // consume terminator
   
                        break
                }
                if n < 0 {
                        n = -n
                        _, next = readLong(data, next) // block byte size
                }
                off = next
                for i := int64(0); i < n; i++ {
                        var k, v []byte
                        k, off = readBytes(data, off)
                        v, off = readBytes(data, off)
                        meta[string(k)] = v
                }
        }
        delete(meta, key)
   
        out := append([]byte(nil), data[:4]...)
        out = appendLong(out, int64(len(meta)))
        for k, v := range meta {
                out = appendLong(out, int64(len(k)))
                out = append(out, k...)
                out = appendLong(out, int64(len(v)))
                out = append(out, v...)
        }
        out = append(out, 0)
   
        return append(out, data[off:]...)
   }
   
   func readLong(data []byte, off int) (int64, int) {
        var u uint64
        var shift uint
        for {
                b := data[off]
                off++
                u |= uint64(b&0x7f) << shift
                if b&0x80 == 0 {
                        break
                }
                shift += 7
        }
   
        return int64(u>>1) ^ -int64(u&1), off
   }
   
   func readBytes(data []byte, off int) ([]byte, int) {
        n, off := readLong(data, off)
   
        return data[off : off+int(n)], off + int(n)
   }
   
   func appendLong(dst []byte, v int64) []byte {
        u := uint64(v<<1) ^ uint64(v>>63)
        for u >= 0x80 {
                dst = append(dst, byte(u)|0x80)
                u >>= 7
        }
   
        return append(dst, byte(u))
   }
   ```
   
   Output on v0.6.0:
   
   ```
   content             = data  (want deletes)
   sequence-number     = 0  (want 5)
   min-sequence-number = 0  (want 3)
   ```
   
   Or with real files: create a v3 table with DuckDB's iceberg extension and
   read `metadata/snap-*.avro` with `ReadManifestList` — same result.
   
   ## Suggested fix
   
   The version is recoverable from the embedded writer schema when the key
   is absent: `sequence_number`/`content` fields (ids 515/517) imply v2+,
   `first_row_id` (id 520) implies v3. Even an error would beat silently
   returning wrong values, since anything that rewrites metadata based on
   them (compaction, snapshot management) writes the corruption back out.
   
   Happy to send a PR if the schema-inference approach sounds right to you.
   


-- 
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