-
Notifications
You must be signed in to change notification settings - Fork 233
fix(table): allocate partition IDs from history #1988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
abc35da
ebf0a16
e01dbb2
c9bae41
96d47a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ import ( | |
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/apache/iceberg-go" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
@@ -85,6 +86,55 @@ func TestParseMetadataBytesAssignsMissingPartitionFieldIDs(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestParseMetadataBytesPreservesStaleLastPartitionIDForCommit(t *testing.T) { | ||
| data := strings.Replace(ExampleTableMetadataV2, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor — New early-return condition adds three branches, only one is tested The condition at metadata.go:2047 introduces distinct branches: counter below max field ID (tested), counter above max field ID (must stay untouched), counter below the 999 floor with no field IDs, and stale counter combined with a missing field-id. Only the first has a test. I verified the untested ones behave as follows -- add cases for them so the condition is pinned: counter-above-max stays untouched, and stale-counter-plus-missing-field-id assigns correctly.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for identifying the missing branches. Added coverage in ebf0a16 for sub-999 counters with no assigned fields, counters above the greatest assigned field ID, and stale counters combined with a missing field ID. The first two cases also verify the unchanged-byte fast path.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — End-to-end allocation half of the issue's suggested regression coverage is not asserted Issue #1987 asks for coverage that asserts both that the parsed counter becomes 1000 and that the next distinct partition field receives 1001. TestParseMetadataBytesNormalizesStaleLastPartitionID asserts only the former. I verified the latter holds today, so this is purely about locking in the user-visible symptom (the cross-spec ID collision) rather than only its parse-level cause.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only exercises the fix if the Replace actually lands. If the fixture spacing ever changes and the replace silently no-ops, last-partition-id stays 1000 and both assertions pass without touching the fix. I'd add a require.Contains right after, asserting the "last-partition-id": 999 substring is present in data, so a missed replace fails loudly.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good call—added explicit fixture guards so silent replacement failures go red. |
||
| `"last-partition-id": 1000`, `"last-partition-id": 999`, 1) | ||
| data = strings.Replace(data, | ||
| `"default-spec-id": 0,`, `"default-spec-id": 1,`, 1) | ||
| data = strings.Replace(data, | ||
| `"partition-specs": [{"spec-id": 0, "fields": [{"name": "x", "transform": "identity", "source-id": 1, "field-id": 1000}]}],`, | ||
| `"partition-specs": [{"spec-id": 0, "fields": [{"name": "x", "transform": "identity", "source-id": 1, "field-id": 1000}]}, {"spec-id": 1, "fields": []}],`, 1) | ||
| require.Contains(t, data, `"last-partition-id": 999`) | ||
| require.Contains(t, data, `"spec-id": 1`) | ||
|
|
||
| parsed, err := ParseMetadataBytes([]byte(data)) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, parsed.LastPartitionSpecID()) | ||
| assert.Equal(t, 999, *parsed.LastPartitionSpecID()) | ||
|
|
||
| update := NewUpdateSpec(New(nil, parsed, "", nil, nil).NewTransaction(), false). | ||
| AddField("x", iceberg.BucketTransform{NumBuckets: 16}, "x_bucket") | ||
| _, requirements, err := update.BuildUpdates() | ||
| require.NoError(t, err) | ||
| assert.Equal(t, []int{999}, lastAssignedPartitionAssertions(requirements)) | ||
| updated, err := update.Apply() | ||
| require.NoError(t, err) | ||
| require.Equal(t, 1, updated.NumFields()) | ||
| assert.Equal(t, 1001, updated.Field(0).FieldID) | ||
| } | ||
|
|
||
| func TestAssignMissingPartitionFieldIDsPreservesConsistentMetadata(t *testing.T) { | ||
| for _, tt := range []struct { | ||
| name string | ||
| input string | ||
| }{ | ||
| { | ||
| name: "counter below assignment floor with no fields", | ||
| input: `{"last-updated-ms":0,"last-partition-id":0,"partition-specs":[{"spec-id":0,"fields":[]}]}`, | ||
| }, | ||
| { | ||
| name: "counter above greatest field ID", | ||
| input: `{"last-updated-ms":0,"last-partition-id":1001,"partition-specs":[{"spec-id":0,"fields":[{"field-id":1000}]}]}`, | ||
| }, | ||
| } { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| normalized, err := assignMissingPartitionFieldIDs([]byte(tt.input)) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, tt.input, string(normalized)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestParseMetadataBytesRejectsCaseFoldedFormatVersionCollision(t *testing.T) { | ||
| data := strings.Replace( | ||
| ExampleTableMetadataV2, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The floor made it into
BindToSchema, but theb.lastPartitionIDwrite-back a few lines down still usesmax(maxFieldID, prev)withprev = *b.lastPartitionID. For a non-empty spec that's fine,maxFieldIDheals it. For an empty spec (unpartition)maxFieldIDis 0 andprevis the stale 999, so the counter we serialize stays stale, and a reader that trusts it without scanning specs can still allocate 1000 and collide, the exact case this PR is closing on theNewUpdateSpecpath.fieldIDFlooris already in scope and always>= prev, so:The
prevblock and its nil-guard fall out as dead code, and the assertion value is unaffected since that's drawn from the baseMetadata. wdyt?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Write-back now uses
max(maxFieldID, fieldIDFloor), with an empty-spec regression test. Thanks!