Skip to content

fix(table): prevent index out of range error in buildManifestEvaluator#692

Merged
zeroshade merged 5 commits intoapache:mainfrom
ErenDursun:fix-index-out-of-range
Jan 28, 2026
Merged

fix(table): prevent index out of range error in buildManifestEvaluator#692
zeroshade merged 5 commits intoapache:mainfrom
ErenDursun:fix-index-out-of-range

Conversation

@ErenDursun
Copy link
Copy Markdown
Contributor

fixes #691

Previously, the buildManifestEvaluator method directly accessed the PartitionSpecs array by index without validating that the requested specID actually exists in the partition specs. This could cause an index out of bounds error.

This change adds a proper lookup using slices.IndexFunc to find the partition spec by its ID and returns an error if the spec is not found, improving error handling and robustness.

Copy link
Copy Markdown
Member

@zeroshade zeroshade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be relatively easy to construct a contrived test that would have triggered the out of range error? Just to ensure we don't introduce this back later

@ErenDursun ErenDursun force-pushed the fix-index-out-of-range branch 2 times, most recently from 0737398 to 50e72f9 Compare January 23, 2026 09:39
@ErenDursun
Copy link
Copy Markdown
Contributor Author

ErenDursun commented Jan 23, 2026

@zeroshade I've added a short unit test.
We also received an answer on apache/iceberg#15108 confirming it is not required that metadata has to contain the complete history of all partition specs. Therefore I also removed the wrapped ErrInvalidMetadata from the error I added - it might have been misleading. Hope you agree.

@ErenDursun ErenDursun force-pushed the fix-index-out-of-range branch from 4f3e817 to 6fb1100 Compare January 23, 2026 10:19
@ErenDursun
Copy link
Copy Markdown
Contributor Author

Also apache/iceberg#15108 (comment) suggests the partition specs should be tracked in a map<spec id, spec>. But that would necessitate a change in the Metadata interface, which is user-facing and thus a breaking change.

PartitionSpecs() []iceberg.PartitionSpec

@zeroshade
Copy link
Copy Markdown
Member

But that would necessitate a change in the Metadata interface, which is user-facing and thus a breaking change.

We are still pre-version 1. Thus I'm okay with a breaking change here. These sort of situations are precisely why we haven't hit version 1.0 and called it stable yet. Just make sure we're very clear in the commit notice about the breaking change (so the release notes make it clear)

@zeroshade
Copy link
Copy Markdown
Member

Alternately, we could store it as a map internally and just construct the slice in the function on the fly (maps.Values) which might make sense to avoid the breaking change and ensure that a user can't change the internal map on us

@ErenDursun
Copy link
Copy Markdown
Contributor Author

ErenDursun commented Jan 23, 2026

Just had a closer look into it and it seems the field Specs is a list

Specs []iceberg.PartitionSpec `json:"partition-specs"`

because it is defined as such in the Iceberg docs:

partition-specs: A list of partition specs, stored as full partition spec objects.

Changing its type in the commonMetadata struct would interfere with the (un)marshalling logic and changing it in the (user facing) Metadata interface would let it diverge from the Iceberg specs. I could instead add an internal field specsByID map[int]iceberg.PartitionSpec and an internal method partitionSpecsByID() map[int]iceberg.PartitionSpec with lazy-loading logic. That way the methods

iceberg-go/table/metadata.go

Lines 1316 to 1324 in 3ce3b4b

func (c *commonMetadata) PartitionSpec() iceberg.PartitionSpec {
for _, s := range c.Specs {
if s.ID() == c.DefaultSpecID {
return s
}
}
return *iceberg.UnpartitionedSpec
}

and

iceberg-go/table/metadata.go

Lines 1430 to 1439 in 3ce3b4b

func (c *commonMetadata) checkPartitionSpecs() error {
for _, spec := range c.Specs {
if spec.ID() == c.DefaultSpecID {
return nil
}
}
return fmt.Errorf("%w: default-spec-id %d can't be found",
ErrInvalidMetadata, c.DefaultSpecID)
}

could use the map for better performance.

If we'd also add the new internal lazy-loading method to the Metadata interface, users wouldn't be affected but all these methods could use the map instead:

iceberg-go/table/scanner.go

Lines 244 to 249 in 8ff2e91

func (scan *Scan) buildPartitionProjection(specID int) (iceberg.BooleanExpression, error) {
project := newInclusiveProjection(scan.metadata.CurrentSchema(),
scan.metadata.PartitionSpecs()[specID], true)
return project(scan.rowFilter)
}

iceberg-go/table/scanner.go

Lines 251 to 256 in 8ff2e91

func (scan *Scan) buildManifestEvaluator(specID int) (func(iceberg.ManifestFile) (bool, error), error) {
spec := scan.metadata.PartitionSpecs()[specID]
return newManifestEvaluator(spec, scan.metadata.CurrentSchema(),
scan.partitionFilters.Get(specID), scan.caseSensitive)
}

iceberg-go/table/scanner.go

Lines 258 to 272 in 8ff2e91

func (scan *Scan) buildPartitionEvaluator(specID int) func(iceberg.DataFile) (bool, error) {
spec := scan.metadata.PartitionSpecs()[specID]
partType := spec.PartitionType(scan.metadata.CurrentSchema())
partSchema := iceberg.NewSchema(0, partType.FieldList...)
partExpr := scan.partitionFilters.Get(specID)
return func(d iceberg.DataFile) (bool, error) {
fn, err := iceberg.ExpressionEvaluator(partSchema, partExpr, scan.caseSensitive)
if err != nil {
return false, err
}
return fn(getPartitionRecord(d, partType))
}
}

func (us *UpdateSpec) isNewPartitionSpec(newSpecId int) bool {
return !slices.ContainsFunc(us.txn.tbl.Metadata().PartitionSpecs(), func(s iceberg.PartitionSpec) bool {
return s.ID() == newSpecId
})
}

Buuuut... I'm not sure if the minor performance boost justifies the additional lazy-loading complexity 😅 there's the risk of future changes to the code leading to diverging states between the partition-spec list and map. Should I give it a try or do we keep the lists and just prevent the out of range errors for now?

@ErenDursun ErenDursun force-pushed the fix-index-out-of-range branch 4 times, most recently from 1a14f1f to b649746 Compare January 27, 2026 10:54
@ErenDursun
Copy link
Copy Markdown
Contributor Author

ErenDursun commented Jan 27, 2026

@zeroshade the methods buildPartitionProjection and buildPartitionEvaluator had the same issue. Added a fix and unit tests for those two as well.

Copy link
Copy Markdown
Member

@zeroshade zeroshade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, looks good!

Buuuut... I'm not sure if the minor performance boost justifies the additional lazy-loading complexity 😅 there's the risk of future changes to the code leading to diverging states between the partition-spec list and map. Should I give it a try or do we keep the lists and just prevent the out of range errors for now?

I think we should keep the lists and prevent the out of range errors for now, and we can look into switching to the proper map approach/lazy-loading/etc. as a separate PR.

I've got just one nitpick question for you, otherwise this is good to go!

Comment thread table/utils.go Outdated
…ion, buildManifestEvaluator, buildPartitionEvaluator

fixes apache#691

Previously, these builders directly accessed the PartitionSpecs array by index without validating that the requested specID actually exists in the partition specs. This could cause an index out of bounds error.

This change adds a proper lookup using slices.IndexFunc to find the partition spec by its ID and returns an ErrPartitionSpecNotFound error if the spec is not found, improving error handling and robustness. The lookup logic is implemented once as a helper method and is reused.
@ErenDursun ErenDursun force-pushed the fix-index-out-of-range branch from b649746 to 50b0260 Compare January 27, 2026 18:18
Comment thread table/metadata.go
Comment thread table/utils.go Outdated
Comment thread table/scanner.go Outdated
Comment thread table/utils.go Outdated
Replace generic error message with ErrPartitionSpecNotFound sentinel error in GetSpecByID() for improved error handling and type safety.
Introduce PartitionSpecByID method for cleaner partition spec lookups,
replacing ad-hoc filtering patterns throughout the codebase with
a standardized interface method.
Move evaluator creation and error handling outside the closure to
ensure errors are caught during builder construction time.
@ErenDursun ErenDursun force-pushed the fix-index-out-of-range branch from 9799d2d to de8797b Compare January 28, 2026 18:57
@zeroshade
Copy link
Copy Markdown
Member

Thanks for reviewing @ferhatelmas, any further comments/issues on this?

@ferhatelmas
Copy link
Copy Markdown
Contributor

My pleasure. Overall looks good to me. #692 (comment) is the only concern but not a blocker to merge this PR I think

@zeroshade
Copy link
Copy Markdown
Member

I agree with your statement there, it'd be better to address all at once and for now let's keep the consistency

@zeroshade zeroshade merged commit 564c5e6 into apache:main Jan 28, 2026
11 checks passed
@ErenDursun ErenDursun deleted the fix-index-out-of-range branch January 29, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: index out of range in buildManifestEvaluator

3 participants