mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-11 14:28:33 +01:00
build(deps): bump github.com/twmb/avro from 1.7.2 to 1.8.0 (#11210)
* build(deps): bump github.com/twmb/avro from 1.7.2 to 1.8.0 Bumps [github.com/twmb/avro](https://github.com/twmb/avro) from 1.7.2 to 1.8.0. - [Commits](https://github.com/twmb/avro/compare/v1.7.2...v1.8.0) --- updated-dependencies: - dependency-name: github.com/twmb/avro dependency-version: 1.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * iceberg: adapt to twmb/avro v1.8.0 and iceberg-go defensive copies avro v1.8.0 changes Schema.Root() to return *SchemaNode, which breaks iceberg-go v0.6.0's internal avro_schemas.go. The fix (apache/iceberg-go#1843) is only on iceberg-go's main branch, unreleased, so bump iceberg-go to that commit (c210509) alongside the avro bump. That iceberg-go revision also changes two behaviors seaweedfs worked around: - It now infers a manifest list's format version from the embedded writer schema, so a list missing the "format-version" header entry (DuckDB's shape is read as v2, not v1. ReadManifestList's header patching is now a redundant safety net; tests updated to expect v2. - It returns defensive copies from DataFile.Partition(), so the ReadManifest shim's in-place partition normalization was silently discarded. Rebuild the entry through NewDataFileBuilder when any partition value is normalized, copying every other DataFile field so manifest round-trips are preserved. - It converts day-transform partitions to iceberg.Date on read (applyDayTransformDates), so the day-partition cases the shim and tests guarded now convert without help; tests updated to expect iceberg.Date from the raw read. EOF ) * iceberg: accept assert-ref-snapshot-id without snapshot-id iceberg-go's new nullableInt64 parser rejects an assert-ref-snapshot-id requirement whose "snapshot-id" field is absent from the JSON, even though the Iceberg REST spec makes it optional (null means the ref must not already exist). v0.6.0 used a plain *int64, so absent was nil and accepted. ClickHouse sends the requirement without snapshot-id when asserting a branch does not yet exist, so its writes fail with "missing required field \"snapshot-id\"". normalizeRequirements splices an explicit null into any assert-ref-snapshot-id requirement missing the field before handing the JSON to iceberg-go's parser, restoring the v0.6.0 behavior across both iceberg-go versions. * iceberg: fix v1 block_size_in_bytes default in rebuilt manifest entries rebuildManifestEntry set block_size_in_bytes to 0, but the v1 manifest schema requires the default of 64 MiB ("Always write default in v1"). The original value is not exposed on the DataFile interface, so use the spec default. Also clarify the fallback comment to note that empty (zero-record / zero-byte) files also trigger it, not just a nil spec. Add a round-trip test that writes a rebuilt entry as v1 and verifies block_size_in_bytes is 64 MiB via Avro decoding. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
1 parent
005012edcf
commit
2d4b730a2f
11 files changed
+504
-151
No files matched your search
@@ -51,7 +51,12 @@ func (s *Server) handleUpdateTable(w http.ResponseWriter, r *http.Request) {
|
||||
req.Identifier = raw.Identifier
|
||||
var statisticsUpdates []statisticsUpdate
|
||||
if len(raw.Requirements) > 0 {
|
||||
if err := json.Unmarshal(raw.Requirements, &req.Requirements); err != nil {
|
||||
normalized, err := normalizeRequirements(raw.Requirements)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BadRequestException", "Invalid requirements: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(normalized, &req.Requirements); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BadRequestException", "Invalid requirements: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -246,7 +246,11 @@ func cloneTableMetadata(m *s3tables.TableMetadata) *s3tables.TableMetadata {
|
||||
func parseTableChange(change tableChangeRequest) (table.Requirements, table.Updates, []statisticsUpdate, *icebergRequestError) {
|
||||
var requirements table.Requirements
|
||||
if len(change.Requirements) > 0 {
|
||||
if err := json.Unmarshal(change.Requirements, &requirements); err != nil {
|
||||
normalized, err := normalizeRequirements(change.Requirements)
|
||||
if err != nil {
|
||||
return nil, nil, nil, &icebergRequestError{http.StatusBadRequest, "BadRequestException", "Invalid requirements: " + err.Error()}
|
||||
}
|
||||
if err := json.Unmarshal(normalized, &requirements); err != nil {
|
||||
return nil, nil, nil, &icebergRequestError{http.StatusBadRequest, "BadRequestException", "Invalid requirements: " + err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package iceberg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// normalizeRequirements repairs requirement JSON that iceberg-go's parser
|
||||
// rejects but the Iceberg REST spec permits.
|
||||
//
|
||||
// iceberg-go's assert-ref-snapshot-id parser uses a nullableInt64 that tracks
|
||||
// whether the "snapshot-id" field was present in the JSON. An absent field
|
||||
// (set == false) is rejected as "missing required field", even though the spec
|
||||
// makes snapshot-id optional — null means the ref must not already exist.
|
||||
// iceberg-go v0.6.0 used a plain *int64, so absent was nil and accepted.
|
||||
//
|
||||
// Clients like ClickHouse send assert-ref-snapshot-id without snapshot-id when
|
||||
// asserting a branch does not yet exist. Splicing in an explicit null before
|
||||
// unmarshaling restores the v0.6.0 behavior across both iceberg-go versions.
|
||||
func normalizeRequirements(raw json.RawMessage) (json.RawMessage, error) {
|
||||
if len(raw) == 0 {
|
||||
return raw, nil
|
||||
}
|
||||
var reqs []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &reqs); err != nil {
|
||||
return raw, err
|
||||
}
|
||||
changed := false
|
||||
for i, req := range reqs {
|
||||
var obj map[string]json.RawMessage
|
||||
if err := json.Unmarshal(req, &obj); err != nil {
|
||||
continue // let the downstream parser report the error
|
||||
}
|
||||
var typ string
|
||||
if rawType, ok := obj["type"]; ok {
|
||||
_ = json.Unmarshal(rawType, &typ)
|
||||
}
|
||||
if typ != "assert-ref-snapshot-id" {
|
||||
continue
|
||||
}
|
||||
if _, hasSnapshotID := obj["snapshot-id"]; hasSnapshotID {
|
||||
continue
|
||||
}
|
||||
obj["snapshot-id"] = json.RawMessage("null")
|
||||
fixed, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
reqs[i] = fixed
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return raw, nil
|
||||
}
|
||||
return json.Marshal(reqs)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package iceberg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/apache/iceberg-go/table"
|
||||
)
|
||||
|
||||
func TestNormalizeRequirementsAddsNullSnapshotID(t *testing.T) {
|
||||
// ClickHouse sends assert-ref-snapshot-id without snapshot-id when
|
||||
// asserting a branch does not yet exist. iceberg-go's parser rejects
|
||||
// an absent field; normalizeRequirements splices in an explicit null.
|
||||
raw := json.RawMessage(`[{"type":"assert-ref-snapshot-id","ref":"main"}]`)
|
||||
|
||||
// Without normalization, iceberg-go rejects the requirement.
|
||||
var reqs table.Requirements
|
||||
if err := json.Unmarshal(raw, &reqs); err == nil {
|
||||
t.Fatal("iceberg-go accepted an assert-ref-snapshot-id without snapshot-id; expected an error")
|
||||
}
|
||||
|
||||
normalized, err := normalizeRequirements(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeRequirements: %v", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(normalized, &reqs); err != nil {
|
||||
t.Fatalf("unmarshal normalized requirements: %v", err)
|
||||
}
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("got %d requirements, want 1", len(reqs))
|
||||
}
|
||||
if reqs[0].GetType() != "assert-ref-snapshot-id" {
|
||||
t.Fatalf("requirement type = %s, want assert-ref-snapshot-id", reqs[0].GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequirementsPreservesPresentSnapshotID(t *testing.T) {
|
||||
raw := json.RawMessage(`[{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":42}]`)
|
||||
normalized, err := normalizeRequirements(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeRequirements: %v", err)
|
||||
}
|
||||
|
||||
var reqs table.Requirements
|
||||
if err := json.Unmarshal(normalized, &reqs); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("got %d requirements, want 1", len(reqs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequirementsPreservesExplicitNullSnapshotID(t *testing.T) {
|
||||
raw := json.RawMessage(`[{"type":"assert-ref-snapshot-id","ref":"main","snapshot-id":null}]`)
|
||||
normalized, err := normalizeRequirements(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeRequirements: %v", err)
|
||||
}
|
||||
|
||||
var reqs table.Requirements
|
||||
if err := json.Unmarshal(normalized, &reqs); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("got %d requirements, want 1", len(reqs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequirementsLeavesOtherTypesUnchanged(t *testing.T) {
|
||||
raw := json.RawMessage(`[{"type":"assert-create"},{"type":"assert-table-uuid","uuid":"00000000-0000-0000-0000-000000000000"}]`)
|
||||
normalized, err := normalizeRequirements(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeRequirements: %v", err)
|
||||
}
|
||||
|
||||
var reqs table.Requirements
|
||||
if err := json.Unmarshal(normalized, &reqs); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(reqs) != 2 {
|
||||
t.Fatalf("got %d requirements, want 2", len(reqs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequirementsEmptyInput(t *testing.T) {
|
||||
normalized, err := normalizeRequirements(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeRequirements(nil): %v", err)
|
||||
}
|
||||
if normalized != nil {
|
||||
t.Fatalf("expected nil output for nil input, got %s", normalized)
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,20 @@ func ReadManifest(m iceberg.ManifestFile, manifest []byte, discardDeleted bool,
|
||||
}
|
||||
|
||||
var partitionFields []iceberg.NestedField
|
||||
if spec, found := specs[int(m.PartitionSpecID())]; found && schema != nil {
|
||||
if partitionType := spec.PartitionType(schema); partitionType != nil {
|
||||
partitionFields = partitionType.FieldList
|
||||
var spec iceberg.PartitionSpec
|
||||
var specFound bool
|
||||
if schema != nil {
|
||||
spec, specFound = specs[int(m.PartitionSpecID())]
|
||||
if specFound {
|
||||
if partitionType := spec.PartitionType(schema); partitionType != nil {
|
||||
partitionFields = partitionType.FieldList
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, entry := range entries {
|
||||
|
||||
for i, entry := range entries {
|
||||
partition := entry.DataFile().Partition()
|
||||
changed := false
|
||||
for _, field := range partitionFields {
|
||||
value, ok := partition[field.ID]
|
||||
if !ok {
|
||||
@@ -54,12 +61,107 @@ func ReadManifest(m iceberg.ManifestFile, manifest []byte, discardDeleted bool,
|
||||
}
|
||||
if normalized, ok := normalizePartitionValue(value, field.Type); ok {
|
||||
partition[field.ID] = normalized
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
// iceberg-go returns a defensive copy from Partition(), so the
|
||||
// in-place edits above never reach the underlying DataFile.
|
||||
// Rebuild the entry with a DataFile that carries the normalized
|
||||
// partition so a subsequent write encodes the Iceberg values.
|
||||
entries[i] = rebuildManifestEntry(entry, spec, partition)
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// rebuildManifestEntry returns a copy of entry whose DataFile carries the
|
||||
// given partition map. It is used to persist partition values normalized
|
||||
// after a read, since iceberg-go's Partition() getter hands back a clone
|
||||
// rather than the live map. Only the partition changes; every other DataFile
|
||||
// field is copied through the builder so manifest round-trips are preserved.
|
||||
func rebuildManifestEntry(entry iceberg.ManifestEntry, spec iceberg.PartitionSpec, partition map[int]any) iceberg.ManifestEntry {
|
||||
df := entry.DataFile()
|
||||
builder, err := iceberg.NewDataFileBuilder(
|
||||
spec,
|
||||
df.ContentType(),
|
||||
df.FilePath(),
|
||||
df.FileFormat(),
|
||||
partition,
|
||||
nil, // logical types are derived from the spec at write time
|
||||
nil,
|
||||
df.Count(),
|
||||
df.FileSizeBytes(),
|
||||
)
|
||||
if err != nil {
|
||||
// The original DataFile already validated these fields, so the only
|
||||
// ways to get here are a nil spec (which rebuildManifestEntry's caller
|
||||
// guards against) or a zero-record / zero-byte file, which the builder
|
||||
// rejects with recordCount <= 0 or fileSize <= 0. Fall back to the
|
||||
// original entry rather than panic; the unnormalized partition survives
|
||||
// but an empty file is unlikely to carry a logical partition value.
|
||||
return entry
|
||||
}
|
||||
|
||||
builder.BlockSizeInBytes(64 * 1024 * 1024) // v1 schema default; deprecated and omitted in v2+
|
||||
if sizes := df.ColumnSizes(); sizes != nil {
|
||||
builder.ColumnSizes(sizes)
|
||||
}
|
||||
if counts := df.ValueCounts(); counts != nil {
|
||||
builder.ValueCounts(counts)
|
||||
}
|
||||
if counts := df.NullValueCounts(); counts != nil {
|
||||
builder.NullValueCounts(counts)
|
||||
}
|
||||
if counts := df.NaNValueCounts(); counts != nil {
|
||||
builder.NaNValueCounts(counts)
|
||||
}
|
||||
if counts := df.DistinctValueCounts(); counts != nil {
|
||||
builder.DistinctValueCounts(counts)
|
||||
}
|
||||
if bounds := df.LowerBoundValues(); bounds != nil {
|
||||
builder.LowerBoundValues(bounds)
|
||||
}
|
||||
if bounds := df.UpperBoundValues(); bounds != nil {
|
||||
builder.UpperBoundValues(bounds)
|
||||
}
|
||||
if key := df.KeyMetadata(); key != nil {
|
||||
builder.KeyMetadata(key)
|
||||
}
|
||||
if offsets := df.SplitOffsets(); offsets != nil {
|
||||
builder.SplitOffsets(offsets)
|
||||
}
|
||||
if ids := df.EqualityFieldIDs(); ids != nil {
|
||||
builder.EqualityFieldIDs(ids)
|
||||
}
|
||||
if id := df.SortOrderID(); id != nil {
|
||||
builder.SortOrderID(*id)
|
||||
}
|
||||
if id := df.FirstRowID(); id != nil {
|
||||
builder.FirstRowID(*id)
|
||||
}
|
||||
if ref := df.ReferencedDataFile(); ref != nil {
|
||||
builder.ReferencedDataFile(*ref)
|
||||
}
|
||||
if off := df.ContentOffset(); off != nil {
|
||||
builder.ContentOffset(*off)
|
||||
}
|
||||
if size := df.ContentSizeInBytes(); size != nil {
|
||||
builder.ContentSizeInBytes(*size)
|
||||
}
|
||||
|
||||
newEntry := iceberg.NewManifestEntryBuilder(entry.Status(), ptrInt64(entry.SnapshotID()), builder.Build())
|
||||
if seq := entry.SequenceNum(); seq != -1 {
|
||||
newEntry.SequenceNum(seq)
|
||||
}
|
||||
if fileSeq := entry.FileSequenceNum(); fileSeq != nil {
|
||||
newEntry.FileSequenceNum(*fileSeq)
|
||||
}
|
||||
return newEntry.Build()
|
||||
}
|
||||
|
||||
func ptrInt64(v int64) *int64 { return &v }
|
||||
|
||||
// normalizePartitionValue converts one value the Avro decoder returned for a
|
||||
// logical type to the Iceberg representation the manifest writer expects,
|
||||
// mirroring what iceberg-go does itself for the unions it does recognize.
|
||||
|
||||
@@ -69,19 +69,22 @@ func stripFormatVersion(t *testing.T, data []byte) []byte {
|
||||
return append(stripped, data[terminator:]...)
|
||||
}
|
||||
|
||||
// DuckDB writes manifest lists with no Avro header metadata, so iceberg-go
|
||||
// falls back to v1 and every v2 manifest below it fails to parse.
|
||||
// DuckDB writes manifest lists with no Avro header metadata. iceberg-go now
|
||||
// infers the format version from the embedded writer schema on its own, so a
|
||||
// stripped v2 list is read as v2 even without the header entry. ReadManifestList
|
||||
// still patches the header for older iceberg-go releases and for writers whose
|
||||
// schema carries none of the version-distinguishing fields.
|
||||
func TestReadManifestListWithoutFormatVersion(t *testing.T) {
|
||||
stripped := stripFormatVersion(t, writeManifestList(t, 2))
|
||||
|
||||
// Baseline: iceberg-go alone reads the list as v1, which both mislabels
|
||||
// the delete manifest and makes ReadManifest reject the v2 manifests.
|
||||
// Baseline: iceberg-go reads the list as v2 by inferring the version from
|
||||
// the record schema's fields (content / sequence_number were added in v2).
|
||||
unpatched, err := iceberg.ReadManifestList(bytes.NewReader(stripped))
|
||||
if err != nil {
|
||||
t.Fatalf("iceberg.ReadManifestList: %v", err)
|
||||
}
|
||||
if got := unpatched[0].Version(); got != 1 {
|
||||
t.Fatalf("expected iceberg-go to default to v1, got v%d", got)
|
||||
if got := unpatched[0].Version(); got != 2 {
|
||||
t.Fatalf("expected iceberg-go to infer v2, got v%d", got)
|
||||
}
|
||||
|
||||
manifests, err := ReadManifestList(stripped)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/apache/iceberg-go"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
|
||||
"github.com/twmb/avro/ocf"
|
||||
)
|
||||
|
||||
// A manifest whose partition union puts the value branch first hides the
|
||||
@@ -30,6 +31,9 @@ func TestReadManifestNormalizesForeignPartitions(t *testing.T) {
|
||||
// nullFirst spells the union the way Java and iceberg-rust do, which
|
||||
// iceberg-go converts on its own once the partition is read.
|
||||
nullFirst bool
|
||||
// rawIsNormalized marks transforms iceberg-go now converts itself on
|
||||
// read (day transforms), so the raw value already matches want.
|
||||
rawIsNormalized bool
|
||||
}{
|
||||
{
|
||||
name: "day transform written null-first converts on read",
|
||||
@@ -47,6 +51,10 @@ func TestReadManifestNormalizesForeignPartitions(t *testing.T) {
|
||||
logicalType: "date",
|
||||
decoded: day,
|
||||
want: iceberg.Date(20737),
|
||||
// iceberg-go now converts day-transform partitions to iceberg.Date
|
||||
// itself (applyDayTransformDates), so the raw read already yields
|
||||
// the Iceberg value regardless of union branch ordering.
|
||||
rawIsNormalized: true,
|
||||
},
|
||||
{
|
||||
name: "identity transform on a date column",
|
||||
@@ -99,7 +107,7 @@ func TestReadManifestNormalizesForeignPartitions(t *testing.T) {
|
||||
// the ordering decides whether the entry arrives as an Iceberg value
|
||||
// or as whatever the Avro decoder produced.
|
||||
rawWant := any(c.decoded)
|
||||
if c.nullFirst {
|
||||
if c.nullFirst || c.rawIsNormalized {
|
||||
rawWant = c.want
|
||||
}
|
||||
raw, err := iceberg.ReadManifest(foreignManifest, bytes.NewReader(foreignBytes), true)
|
||||
@@ -155,12 +163,14 @@ func TestReadManifestUnknownSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// iceberg-go converts a partition value on the first Partition() call, with
|
||||
// whatever logical types are installed then, and ManifestWriter.addEntry
|
||||
// rebinds them to the manifest it is about to write before it makes that call.
|
||||
// A day partition has no date logical type there, so an entry nobody looked at
|
||||
// between reading and writing never converts at all -- the failure Doris and
|
||||
// iceberg-rust tables hit, whose manifests spell the union null-first.
|
||||
// iceberg-go now converts day-transform partitions to iceberg.Date during read
|
||||
// (applyDayTransformDates), so an entry nobody looked at between reading and
|
||||
// writing still carries an Iceberg value and writes cleanly. This used to fail
|
||||
// because conversion was lazy and ManifestWriter.addEntry rebound the logical
|
||||
// types to the output manifest — whose day transform carries no date logical
|
||||
// type — before the first Partition() call, leaving a time.Time that the
|
||||
// encoder rejected. The shim keeps the round-trip working for the identity
|
||||
// transforms iceberg-go still does not convert (see TestReadManifestNormalizesForeignPartitions).
|
||||
func TestReadManifestConvertsBeforeTheWriterRebindsLogicalTypes(t *testing.T) {
|
||||
schema := iceberg.NewSchema(0,
|
||||
iceberg.NestedField{ID: 1, Name: "event_time", Type: iceberg.PrimitiveTypes.Timestamp, Required: true},
|
||||
@@ -183,9 +193,13 @@ func TestReadManifestConvertsBeforeTheWriterRebindsLogicalTypes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read foreign manifest: %v", err)
|
||||
}
|
||||
got := untouched[0].DataFile().Partition()[1000]
|
||||
if _, ok := got.(iceberg.Date); !ok {
|
||||
t.Fatalf("iceberg-go now converts day partitions on read; got %T, want iceberg.Date", got)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if _, err := iceberg.WriteManifest("metadata/manifest.avro", &buf, 2, spec, schema, snapshotID, untouched); err == nil {
|
||||
t.Fatal("writing an entry whose partition was never read should still hold a time.Time")
|
||||
if _, err := iceberg.WriteManifest("metadata/manifest.avro", &buf, 2, spec, schema, snapshotID, untouched); err != nil {
|
||||
t.Fatalf("write manifest read directly through iceberg-go: %v", err)
|
||||
}
|
||||
|
||||
entries, err := ReadManifest(foreignManifest, foreignBytes, true, specs, schema)
|
||||
@@ -197,3 +211,65 @@ func TestReadManifestConvertsBeforeTheWriterRebindsLogicalTypes(t *testing.T) {
|
||||
t.Fatalf("write manifest read through the shim: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// rebuildManifestEntry rebuilds the DataFile through NewDataFileBuilder, which
|
||||
// has no setter for the original block_size_in_bytes (the field is not exposed
|
||||
// on DataFile). The v1 manifest schema includes block_size_in_bytes with a
|
||||
// default of 64 MiB and the spec says "Always write default in v1", so a
|
||||
// rebuilt entry written as v1 must carry that default — not 0.
|
||||
func TestReadManifestRebuiltEntryPreservesV1BlockSizeDefault(t *testing.T) {
|
||||
schema := iceberg.NewSchema(0,
|
||||
iceberg.NestedField{ID: 1, Name: "event_date", Type: iceberg.PrimitiveTypes.Date, Required: true},
|
||||
)
|
||||
spec := iceberg.NewPartitionSpec(iceberg.PartitionField{
|
||||
SourceIDs: []int{1}, FieldID: 1000, Name: "part", Transform: iceberg.IdentityTransform{},
|
||||
})
|
||||
specs := map[int]iceberg.PartitionSpec{spec.ID(): spec}
|
||||
dfBuilder, err := iceberg.NewDataFileBuilder(spec, iceberg.EntryContentData, "data/file.parquet",
|
||||
iceberg.ParquetFile, map[int]any{1000: iceberg.Date(20737)}, nil, nil, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("build data file: %v", err)
|
||||
}
|
||||
snapshotID := int64(1)
|
||||
entry := iceberg.NewManifestEntry(iceberg.EntryStatusADDED, &snapshotID, nil, nil, dfBuilder.Build())
|
||||
// valueFirst=true spells the union [<type>, null], hiding the logical type
|
||||
// from iceberg-go so the partition arrives as a time.Time. That triggers
|
||||
// normalizePartitionValue → rebuildManifestEntry, which is the path that
|
||||
// sets block_size_in_bytes.
|
||||
foreignBytes, foreignManifest := s3tablestest.ForeignPartitionManifest(t, schema, spec, entry,
|
||||
"metadata/foreign-manifest.avro", "date", time.Date(2026, time.October, 11, 0, 0, 0, 0, time.UTC), true)
|
||||
|
||||
entries, err := ReadManifest(foreignManifest, foreignBytes, true, specs, schema)
|
||||
if err != nil {
|
||||
t.Fatalf("read foreign manifest: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := iceberg.WriteManifest("metadata/manifest.avro", &buf, 1, spec, schema, snapshotID, entries); err != nil {
|
||||
t.Fatalf("write v1 manifest: %v", err)
|
||||
}
|
||||
|
||||
reader, err := ocf.NewReader(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatalf("open v1 manifest: %v", err)
|
||||
}
|
||||
var record map[string]any
|
||||
if err := reader.Decode(&record); err != nil {
|
||||
t.Fatalf("decode v1 manifest: %v", err)
|
||||
}
|
||||
if err := reader.Close(); err != nil {
|
||||
t.Fatalf("close v1 manifest: %v", err)
|
||||
}
|
||||
dataFile, ok := record["data_file"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("data_file = %T, want record", record["data_file"])
|
||||
}
|
||||
got, ok := dataFile["block_size_in_bytes"]
|
||||
if !ok {
|
||||
t.Fatal("block_size_in_bytes missing from v1 manifest")
|
||||
}
|
||||
const want = int64(64 * 1024 * 1024)
|
||||
if got != want {
|
||||
t.Fatalf("block_size_in_bytes = %v, want %d", got, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user