This is an automated email from the ASF dual-hosted git repository.
laskoviymishka 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 569a427a4 refactor(partitions,sorting)!: make first-source access
explicit (#2304)
569a427a4 is described below
commit 569a427a472be4fb555375240cd56b44c0e5cd08
Author: Minh Vu <[email protected]>
AuthorDate: Sun Oct 25 10:10:53 2026 +0100
refactor(partitions,sorting)!: make first-source access explicit (#2304)
---
catalog/rest/scan_task_decoder.go | 2 +-
cmd/iceberg/utils_test.go | 4 ++--
data_file_codec.go | 2 +-
partitions.go | 24 +++++++++++-----------
partitions_test.go | 37 +++++++++++++++++++++++++---------
table/conflict_validation.go | 4 ++--
table/equality_delete_writer.go | 8 ++++----
table/internal/utils.go | 10 ++++-----
table/metadata.go | 4 ++--
table/metadata_internal_test.go | 8 ++++----
table/partition_conflict_bench_test.go | 4 ++--
table/partitioned_fanout_writer.go | 10 ++++-----
table/sort_keys.go | 4 ++--
table/sort_keys_bench_test.go | 4 ++--
table/sorting.go | 15 +++++++-------
table/sorting_test.go | 23 ++++++++++++++++++---
table/update_spec.go | 14 ++++++-------
table/update_spec_test.go | 20 +++++++++---------
website/src/releases.md | 2 ++
19 files changed, 118 insertions(+), 81 deletions(-)
diff --git a/catalog/rest/scan_task_decoder.go
b/catalog/rest/scan_task_decoder.go
index f61c667d3..0d852837d 100644
--- a/catalog/rest/scan_task_decoder.go
+++ b/catalog/rest/scan_task_decoder.go
@@ -608,7 +608,7 @@ func newPartitionDecodePlan(spec *iceberg.PartitionSpec,
metadata table.ScanPlan
fieldPlan := partitionDecodeField{
fieldID: field.FieldID,
fieldName: field.Name,
- sourceID: field.SourceID(),
+ sourceID: field.FirstSourceID(),
}
sourceType, ok := findFieldType(fieldPlan.sourceID, metadata)
fieldPlan.sourceTypeFound = ok
diff --git a/cmd/iceberg/utils_test.go b/cmd/iceberg/utils_test.go
index 69f7c4929..0d935f9dd 100644
--- a/cmd/iceberg/utils_test.go
+++ b/cmd/iceberg/utils_test.go
@@ -344,8 +344,8 @@ func TestParseSortOrder(t *testing.T) {
// Validate sort directions and null orders
i := 0
for _, field := range got.Fields() {
- if i < len(tt.expectedSourceIDs) &&
field.SourceID() != tt.expectedSourceIDs[i] {
- t.Errorf("parseSortOrder()
field %d source ID = %v, expected %v", i, field.SourceID(),
tt.expectedSourceIDs[i])
+ if i < len(tt.expectedSourceIDs) &&
field.FirstSourceID() != tt.expectedSourceIDs[i] {
+ t.Errorf("parseSortOrder()
field %d source ID = %v, expected %v", i, field.FirstSourceID(),
tt.expectedSourceIDs[i])
}
if i < len(tt.expectedDirections) &&
field.Direction != tt.expectedDirections[i] {
t.Errorf("parseSortOrder()
field %d direction = %v, expected %v", i, field.Direction,
tt.expectedDirections[i])
diff --git a/data_file_codec.go b/data_file_codec.go
index b11fec574..f9897c030 100644
--- a/data_file_codec.go
+++ b/data_file_codec.go
@@ -333,7 +333,7 @@ func partitionSchemaFingerprint(spec PartitionSpec, schema
*Schema) (string, err
sourceType := Type(UnknownType{})
// ResultType only inspects the source type here; borrow it to
avoid
// cloning nested source types on every schema-cache hit.
- if sourceField, ok := schema.FindFieldByIDRef(field.SourceID(),
internal.SchemaRef{}); ok {
+ if sourceField, ok :=
schema.FindFieldByIDRef(field.FirstSourceID(), internal.SchemaRef{}); ok {
sourceType = sourceField.Type
}
resultType := field.Transform.ResultType(sourceType)
diff --git a/partitions.go b/partitions.go
index 38867e267..396b5e16f 100644
--- a/partitions.go
+++ b/partitions.go
@@ -69,10 +69,10 @@ type PartitionField struct {
escapedName string
}
-// SourceID returns the first source column id. For single-argument transforms
-// this is the only source column. For multi-argument transforms this is the
-// first source column.
-func (p PartitionField) SourceID() int {
+// FirstSourceID returns the first source column id, or 0 when SourceIDs is
+// empty. For single-argument transforms this is the only source column; for
+// multi-argument transforms the other ids are only in SourceIDs.
+func (p PartitionField) FirstSourceID() int {
if len(p.SourceIDs) == 0 {
return 0
}
@@ -114,7 +114,7 @@ func (p PartitionField) MarshalJSON() ([]byte, error) {
FieldID int `json:"field-id"`
Name string `json:"name"`
Transform Transform `json:"transform"`
- }{p.SourceID(), p.FieldID, p.Name, p.Transform})
+ }{p.FirstSourceID(), p.FieldID, p.Name, p.Transform})
}
func (p PartitionField) Equals(other PartitionField) bool {
@@ -129,7 +129,7 @@ func (p *PartitionField) String() string {
return fmt.Sprintf("%d: %s: %s(%v)", p.FieldID, p.Name,
p.Transform, p.SourceIDs)
}
- return fmt.Sprintf("%d: %s: %s(%d)", p.FieldID, p.Name, p.Transform,
p.SourceID())
+ return fmt.Sprintf("%d: %s: %s(%d)", p.FieldID, p.Name, p.Transform,
p.FirstSourceID())
}
func (p *PartitionField) UnmarshalJSON(b []byte) error {
@@ -313,7 +313,7 @@ func (p *PartitionSpec) BindToSchema(schema *Schema,
lastPartitionID *int, newSp
continue
}
}
- opts = append(opts,
AddPartitionFieldBySourceID(field.SourceID(), field.Name, field.Transform,
schema, &field.FieldID))
+ opts = append(opts,
AddPartitionFieldBySourceID(field.FirstSourceID(), field.Name, field.Transform,
schema, &field.FieldID))
}
// Replay, not authoring: the spec may come from another client, so it
only
@@ -760,7 +760,7 @@ func (ps *PartitionSpec) initialize() {
for i := range ps.fields {
ps.fields[i].escapedName = url.QueryEscape(ps.fields[i].Name)
- ps.sourceIdToFields[ps.fields[i].SourceID()] =
append(ps.sourceIdToFields[ps.fields[i].SourceID()], ps.fields[i])
+ ps.sourceIdToFields[ps.fields[i].FirstSourceID()] =
append(ps.sourceIdToFields[ps.fields[i].FirstSourceID()], ps.fields[i])
}
}
@@ -867,7 +867,7 @@ func (ps *PartitionSpec) resolvedPartitionFields(schema
*Schema) []resolvedParti
fields := make([]resolvedPartitionField, 0, len(ps.fields))
for _, field := range ps.fields {
sourceType := Type(UnknownType{})
- if sourceField, ok := schema.FindFieldByIDRef(field.SourceID(),
internal.SchemaRef{}); ok {
+ if sourceField, ok :=
schema.FindFieldByIDRef(field.FirstSourceID(), internal.SchemaRef{}); ok {
sourceType = sourceField.Type
}
@@ -899,7 +899,7 @@ func (ps *PartitionSpec) PartitionType(schema *Schema)
*StructType {
nestedFields := make([]NestedField, len(ps.fields))
for i, field := range ps.fields {
sourceType := Type(UnknownType{})
- if typ, ok := schema.FindTypeByID(field.SourceID()); ok {
+ if typ, ok := schema.FindTypeByID(field.FirstSourceID()); ok {
sourceType = typ
}
@@ -963,9 +963,9 @@ func GeneratePartitionFieldName(schema *Schema, field
PartitionField) (string, e
return field.Name, nil
}
- sourceName, exists := schema.FindColumnName(field.SourceID())
+ sourceName, exists := schema.FindColumnName(field.FirstSourceID())
if !exists {
- return "", fmt.Errorf("could not find field with id %d",
field.SourceID())
+ return "", fmt.Errorf("could not find field with id %d",
field.FirstSourceID())
}
transform := field.Transform
diff --git a/partitions_test.go b/partitions_test.go
index 542b3e87d..274dfe042 100644
--- a/partitions_test.go
+++ b/partitions_test.go
@@ -772,7 +772,7 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
var field iceberg.PartitionField
err := json.Unmarshal([]byte(jsonData), &field)
require.NoError(t, err)
- assert.Equal(t, 1, field.SourceID())
+ assert.Equal(t, 1, field.FirstSourceID())
assert.Equal(t, 1000, field.FieldID)
assert.Equal(t, "str_truncate", field.Name)
assert.Equal(t, iceberg.TruncateTransform{Width: 19},
field.Transform)
@@ -789,7 +789,7 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
var field iceberg.PartitionField
err := json.Unmarshal([]byte(jsonData), &field)
require.NoError(t, err)
- assert.Equal(t, 2, field.SourceID())
+ assert.Equal(t, 2, field.FirstSourceID())
assert.Equal(t, 1001, field.FieldID)
assert.Equal(t, "int_bucket", field.Name)
assert.Equal(t, iceberg.BucketTransform{NumBuckets: 25},
field.Transform)
@@ -806,7 +806,7 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
var field iceberg.PartitionField
err := json.Unmarshal([]byte(jsonData), &field)
require.NoError(t, err)
- assert.Equal(t, 2, field.SourceID(), "SourceID should be first
element")
+ assert.Equal(t, 2, field.FirstSourceID(), "FirstSourceID should
return the first element")
assert.Equal(t, []int{2, 3}, field.SourceIDs, "SourceIDs should
contain all elements")
assert.Equal(t, 1001, field.FieldID)
assert.Equal(t, "int_bucket", field.Name)
@@ -827,7 +827,7 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
var decoded iceberg.PartitionField
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
- assert.Equal(t, 2, decoded.SourceID())
+ assert.Equal(t, 2, decoded.FirstSourceID())
assert.Equal(t, []int{2, 3}, decoded.SourceIDs)
})
@@ -886,7 +886,7 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
}`
var field iceberg.PartitionField
require.NoError(t, json.Unmarshal([]byte(jsonData), &field))
- assert.Equal(t, 0, field.SourceID())
+ assert.Equal(t, 0, field.FirstSourceID())
assert.Equal(t, []int{0}, field.SourceIDs)
})
@@ -895,7 +895,7 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
require.NoError(t, json.Unmarshal([]byte(`{
"source-id": 1, "field-id": 1003, "transform": "void",
"name": "void"
}`), &field))
- assert.Equal(t, 1, field.SourceID())
+ assert.Equal(t, 1, field.FirstSourceID())
})
}
@@ -1172,7 +1172,7 @@ func
TestUnboundPartitionSpecUnmarshalAcceptsZeroSourceID(t *testing.T) {
var spec iceberg.UnboundPartitionSpec
require.NoError(t, json.Unmarshal([]byte(data), &spec))
- assert.Equal(t, 0, spec.Field(0).SourceID())
+ assert.Equal(t, 0, spec.Field(0).FirstSourceID())
roundTripped, err := json.Marshal(spec)
require.NoError(t, err)
@@ -1191,8 +1191,8 @@ func
TestUnboundPartitionSpecUnmarshalAcceptsRepeatedZeroSourceID(t *testing.T)
var spec iceberg.UnboundPartitionSpec
require.NoError(t, json.Unmarshal([]byte(data), &spec))
require.Equal(t, 2, spec.NumFields())
- assert.Equal(t, 0, spec.Field(0).SourceID())
- assert.Equal(t, 0, spec.Field(1).SourceID())
+ assert.Equal(t, 0, spec.Field(0).FirstSourceID())
+ assert.Equal(t, 0, spec.Field(1).FirstSourceID())
}
func TestUnboundPartitionSpecUnmarshalRejectsInvalidStructure(t *testing.T) {
@@ -1294,7 +1294,7 @@ func TestUnboundPartitionSpecBindToSchema(t *testing.T) {
bound, err := spec.BindToSchema(requestSchema, nil, nil)
require.NoError(t, err)
require.Equal(t, 1, bound.NumFields())
- source, ok := requestSchema.FindFieldByID(bound.Field(0).SourceID())
+ source, ok :=
requestSchema.FindFieldByID(bound.Field(0).FirstSourceID())
require.True(t, ok)
assert.Equal(t, "floats", source.Name)
@@ -1692,3 +1692,20 @@ func TestNewPartitionSpecOptsTimeTransformMembership(t
*testing.T) {
})
}
}
+
+func TestPartitionFieldFirstSourceID(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ sourceIDs []int
+ want int
+ }{
+ {name: "empty", sourceIDs: nil, want: 0},
+ {name: "single", sourceIDs: []int{7}, want: 7},
+ {name: "multiple", sourceIDs: []int{7, 9}, want: 7},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ field := iceberg.PartitionField{SourceIDs: tc.sourceIDs}
+ assert.Equal(t, tc.want, field.FirstSourceID())
+ })
+ }
+}
diff --git a/table/conflict_validation.go b/table/conflict_validation.go
index e8b56a159..c47eee777 100644
--- a/table/conflict_validation.go
+++ b/table/conflict_validation.go
@@ -974,12 +974,12 @@ func eqDeletePartitionsToFilter(files []iceberg.DataFile,
meta Metadata) (iceber
if currentSchema == nil {
currentSchema = meta.CurrentSchema()
}
- field, found :=
currentSchema.FindFieldByID(pf.SourceID())
+ field, found :=
currentSchema.FindFieldByID(pf.FirstSourceID())
sourceField =
equalityDeleteSourceFieldInfo{name: field.Name, found: found}
specInfo.sourceFields[partFieldID] = sourceField
}
if !sourceField.found {
- return nil, fmt.Errorf("source field ID %d
(partition field %q) not found in schema", pf.SourceID(), pf.Name)
+ return nil, fmt.Errorf("source field ID %d
(partition field %q) not found in schema", pf.FirstSourceID(), pf.Name)
}
value := p[partFieldID]
diff --git a/table/equality_delete_writer.go b/table/equality_delete_writer.go
index d35fcf493..852149354 100644
--- a/table/equality_delete_writer.go
+++ b/table/equality_delete_writer.go
@@ -316,14 +316,14 @@ func equalityDeleteWriteSchema(tableSchema
*iceberg.Schema, equalityFieldIDs []i
// Partition source columns not already in the equality key.
for _, f := range spec.Fields() {
- if _, ok := seen[f.SourceID()]; ok {
+ if _, ok := seen[f.FirstSourceID()]; ok {
continue
}
- name, ok := tableSchema.FindColumnName(f.SourceID())
+ name, ok := tableSchema.FindColumnName(f.FirstSourceID())
if !ok {
- return nil, fmt.Errorf("%w: partition source field ID
%d not found in table schema", iceberg.ErrInvalidSchema, f.SourceID())
+ return nil, fmt.Errorf("%w: partition source field ID
%d not found in table schema", iceberg.ErrInvalidSchema, f.FirstSourceID())
}
- seen[f.SourceID()] = struct{}{}
+ seen[f.FirstSourceID()] = struct{}{}
names = append(names, name)
}
diff --git a/table/internal/utils.go b/table/internal/utils.go
index 8fb7f1e22..ca52adf4e 100644
--- a/table/internal/utils.go
+++ b/table/internal/utils.go
@@ -177,7 +177,7 @@ func PartitionRecordValue(field iceberg.PartitionField, val
iceberg.Literal, sch
return ret, nil
}
- f, ok := schema.FindFieldByIDRef(field.SourceID(),
iceberginternal.SchemaRef{})
+ f, ok := schema.FindFieldByIDRef(field.FirstSourceID(),
iceberginternal.SchemaRef{})
if !ok {
return ret, fmt.Errorf("%w: could not find source field in
schema for %s",
iceberg.ErrInvalidSchema, field.String())
@@ -239,7 +239,7 @@ type DataFileStatistics struct {
}
func (d *DataFileStatistics) PartitionValue(field iceberg.PartitionField, sc
*iceberg.Schema) any {
- agg, ok := d.ColAggs[field.SourceID()]
+ agg, ok := d.ColAggs[field.FirstSourceID()]
if !ok {
return nil
}
@@ -248,7 +248,7 @@ func (d *DataFileStatistics) PartitionValue(field
iceberg.PartitionField, sc *ic
return nil
}
- sourceField, ok := sc.FindFieldByIDRef(field.SourceID(),
iceberginternal.SchemaRef{})
+ sourceField, ok := sc.FindFieldByIDRef(field.FirstSourceID(),
iceberginternal.SchemaRef{})
if !ok {
panic(fmt.Errorf("%w: could not find source field in schema for
%s",
iceberg.ErrInvalidSchema, field.String()))
@@ -312,7 +312,7 @@ func (d *DataFileStatistics) ToDataFile(opts DataFileOpts)
iceberg.DataFile {
fieldIDToPartitionData = make(map[int]any,
opts.Spec.NumFields())
for _, field := range opts.Spec.Fields() {
sourceField, sourceFieldFound :=
opts.Schema.FindFieldByIDRef(
- field.SourceID(), iceberginternal.SchemaRef{})
+ field.FirstSourceID(),
iceberginternal.SchemaRef{})
partitionVal := opts.PartitionValues[field.FieldID]
switch {
case partitionVal != nil:
@@ -320,7 +320,7 @@ func (d *DataFileStatistics) ToDataFile(opts DataFileOpts)
iceberg.DataFile {
fieldIDToPartitionData[field.FieldID] =
partitionVal
case field.Transform.PreservesOrder() &&
sourceFieldFound:
fieldIDToPartitionData[field.FieldID] =
d.partitionValueWithSourceType(
- field, d.ColAggs[field.SourceID()],
sourceField.Type)
+ field,
d.ColAggs[field.FirstSourceID()], sourceField.Type)
case field.Transform.PreservesOrder():
// Preserve the existing error path for an
invalid schema. This branch
// is not part of the valid hot path because
sourceFieldFound is false.
diff --git a/table/metadata.go b/table/metadata.go
index 3caf45d71..6c0843afc 100644
--- a/table/metadata.go
+++ b/table/metadata.go
@@ -3727,8 +3727,8 @@ func reassignIDs(sc *iceberg.Schema, partitions
*iceberg.PartitionSpec, sortOrde
for _, f := range partitions.Fields() {
var s string
var ok bool
- if s, ok = previousMapFn(f.SourceID()); !ok {
- return nil, fmt.Errorf("%w: partition source field %d
not found in schema", ErrInvalidMetadata, f.SourceID())
+ if s, ok = previousMapFn(f.FirstSourceID()); !ok {
+ return nil, fmt.Errorf("%w: partition source field %d
not found in schema", ErrInvalidMetadata, f.FirstSourceID())
}
opts = append(opts, iceberg.AddPartitionFieldByName(s, f.Name,
f.Transform, freshSc, nil))
}
diff --git a/table/metadata_internal_test.go b/table/metadata_internal_test.go
index 7e6de78ee..9c9e3d3c2 100644
--- a/table/metadata_internal_test.go
+++ b/table/metadata_internal_test.go
@@ -1168,14 +1168,14 @@ func TestNewMetadataFromOrdinalNumberedRequest(t
*testing.T) {
gotSpec := meta.PartitionSpec()
require.Equal(t, len(tt.wantSourceIDs),
gotSpec.NumFields())
for i, wantSourceID := range tt.wantSourceIDs {
- assert.Equal(t, wantSourceID,
gotSpec.Field(i).SourceID())
+ assert.Equal(t, wantSourceID,
gotSpec.Field(i).FirstSourceID())
assert.Equal(t, tt.wantNames[i],
gotSpec.Field(i).Name)
}
// The sort order references the first column, so it
remaps to 1.
require.Equal(t, 1, meta.SortOrder().Len())
for _, field := range meta.SortOrder().Fields() {
- assert.Equal(t, wantSortID, field.SourceID())
+ assert.Equal(t, wantSortID,
field.FirstSourceID())
}
// Written metadata carries bound IDs only, so it
parses back as
@@ -1186,7 +1186,7 @@ func TestNewMetadataFromOrdinalNumberedRequest(t
*testing.T) {
require.NoError(t, err)
reparsedSpec := reparsed.PartitionSpec()
for i, wantSourceID := range tt.wantSourceIDs {
- assert.Equal(t, wantSourceID,
reparsedSpec.Field(i).SourceID())
+ assert.Equal(t, wantSourceID,
reparsedSpec.Field(i).FirstSourceID())
}
})
}
@@ -2102,7 +2102,7 @@ func TestTableMetadataV1PartitionSpecsWithoutDefaultId(t
*testing.T) {
require.Equal(t, spec.NumFields(), 1)
require.Equal(t, spec.Field(0).Name, "y")
require.Equal(t, spec.Field(0).Transform, iceberg.IdentityTransform{})
- require.Equal(t, spec.Field(0).SourceID(), 2)
+ require.Equal(t, spec.Field(0).FirstSourceID(), 2)
}
func TestTableMetadataV2MissingPartitionSpecs(t *testing.T) {
diff --git a/table/partition_conflict_bench_test.go
b/table/partition_conflict_bench_test.go
index 9e5cc06c8..9ec32ff17 100644
--- a/table/partition_conflict_bench_test.go
+++ b/table/partition_conflict_bench_test.go
@@ -201,9 +201,9 @@ func eqDeletePartitionsToFilterBeforeDedup(
return nil, fmt.Errorf("partition field ID %d
not found in spec %d", partFieldID, f.SpecID())
}
- sourceField, ok :=
meta.CurrentSchema().FindFieldByID(pf.SourceID())
+ sourceField, ok :=
meta.CurrentSchema().FindFieldByID(pf.FirstSourceID())
if !ok {
- return nil, fmt.Errorf("source field ID %d
(partition field %q) not found in schema", pf.SourceID(), pf.Name)
+ return nil, fmt.Errorf("source field ID %d
(partition field %q) not found in schema", pf.FirstSourceID(), pf.Name)
}
value := p[partFieldID]
diff --git a/table/partitioned_fanout_writer.go
b/table/partitioned_fanout_writer.go
index 6583758a0..686786213 100644
--- a/table/partitioned_fanout_writer.go
+++ b/table/partitioned_fanout_writer.go
@@ -377,7 +377,7 @@ func newPartitionExtractionPlan(spec iceberg.PartitionSpec,
schema *iceberg.Sche
pathPlan.fields[i] = pathField
pathPlan.estimatedSize += len(pathField.escapedName) + 20
partitionFieldsInfo[i].fieldID = partitionField.ID
- colName, ok := schema.FindColumnName(sourceField.SourceID())
+ colName, ok :=
schema.FindColumnName(sourceField.FirstSourceID())
if !ok {
continue
}
@@ -385,9 +385,9 @@ func newPartitionExtractionPlan(spec iceberg.PartitionSpec,
schema *iceberg.Sche
if !ok {
return nil, fmt.Errorf("failed to find source column %q
in record schema", colName)
}
- sourceType, ok := schema.FindTypeByID(sourceField.SourceID())
+ sourceType, ok :=
schema.FindTypeByID(sourceField.FirstSourceID())
if !ok {
- return nil, fmt.Errorf("failed to find type for source
field ID %d in schema", sourceField.SourceID())
+ return nil, fmt.Errorf("failed to find type for source
field ID %d in schema", sourceField.FirstSourceID())
}
partitionFieldsInfo[i] = partitionFieldInfo{
sourceField: sourceField,
@@ -554,7 +554,7 @@ func (p *partitionExtractionPlan)
getRecordPartitions(record arrow.RecordBatch)
chain, err := resolveColumnChain(record,
fieldInfo.columnPath)
if err != nil {
return nil, fmt.Errorf("failed to resolve
source column %q (field ID %d): %w",
- fieldInfo.sourceName,
fieldInfo.sourceField.SourceID(), err)
+ fieldInfo.sourceName,
fieldInfo.sourceField.FirstSourceID(), err)
}
columnChains[i] = chain
}
@@ -589,7 +589,7 @@ func (p *partitionExtractionPlan)
getRecordPartitions(record arrow.RecordBatch)
return nil, fmt.Errorf(
"failed to convert source
column %q (field ID %d) from Arrow type %s to Iceberg type %s: %w",
fieldInfo.sourceName,
-
fieldInfo.sourceField.SourceID(),
+
fieldInfo.sourceField.FirstSourceID(),
col.DataType(),
fieldInfo.sourceType,
err,
diff --git a/table/sort_keys.go b/table/sort_keys.go
index 0088a93dc..16ee150f1 100644
--- a/table/sort_keys.go
+++ b/table/sort_keys.go
@@ -50,10 +50,10 @@ func resolveSortKeys(order SortOrder, fileSchema
*iceberg.Schema) ([]compute.Sor
fields := fileSchema.Fields()
keys := make([]compute.SortKey, 0, order.Len())
for _, field := range order.fields {
- idx, ok := topLevelFieldIndex(fields, field.SourceID())
+ idx, ok := topLevelFieldIndex(fields, field.FirstSourceID())
if !ok {
return nil, fmt.Errorf("sort order %d: source id %d is
not a top-level column in schema",
- order.OrderID(), field.SourceID())
+ order.OrderID(), field.FirstSourceID())
}
key := compute.SortKey{ColumnIndex: idx, Order:
compute.SortOrderAscending, NullPlacement: compute.SortNullsAtEnd}
diff --git a/table/sort_keys_bench_test.go b/table/sort_keys_bench_test.go
index 01d231048..785b5ae86 100644
--- a/table/sort_keys_bench_test.go
+++ b/table/sort_keys_bench_test.go
@@ -144,10 +144,10 @@ func resolveSortKeysBefore(order SortOrder, fileSchema
*iceberg.Schema) ([]compu
keys := make([]compute.SortKey, 0, order.Len())
for _, field := range order.fields {
- idx, ok := topLevelFieldIndexBefore(fileSchema,
field.SourceID())
+ idx, ok := topLevelFieldIndexBefore(fileSchema,
field.FirstSourceID())
if !ok {
return nil, fmt.Errorf("sort order %d: source id %d is
not a top-level column in schema",
- order.OrderID(), field.SourceID())
+ order.OrderID(), field.FirstSourceID())
}
key := compute.SortKey{ColumnIndex: idx, Order:
compute.SortOrderAscending, NullPlacement: compute.SortNullsAtEnd}
diff --git a/table/sorting.go b/table/sorting.go
index 80c288740..83e17b12f 100644
--- a/table/sorting.go
+++ b/table/sorting.go
@@ -76,8 +76,9 @@ type SortField struct {
NullOrder NullOrder `json:"null-order"`
}
-// SourceID returns the first source column id.
-func (s SortField) SourceID() int {
+// FirstSourceID returns the first source column id, or 0 when SourceIDs is
+// empty. For multi-argument transforms the other ids are only in SourceIDs.
+func (s SortField) FirstSourceID() int {
if len(s.SourceIDs) == 0 {
return 0
}
@@ -98,14 +99,14 @@ func (s *SortField) String() string {
return fmt.Sprintf("%v %s %s", s.SourceIDs,
s.Direction, s.NullOrder)
}
- return fmt.Sprintf("%d %s %s", s.SourceID(), s.Direction,
s.NullOrder)
+ return fmt.Sprintf("%d %s %s", s.FirstSourceID(), s.Direction,
s.NullOrder)
}
if len(s.SourceIDs) > 1 {
return fmt.Sprintf("%s(%v) %s %s", s.Transform, s.SourceIDs,
s.Direction, s.NullOrder)
}
- return fmt.Sprintf("%s(%d) %s %s", s.Transform, s.SourceID(),
s.Direction, s.NullOrder)
+ return fmt.Sprintf("%s(%d) %s %s", s.Transform, s.FirstSourceID(),
s.Direction, s.NullOrder)
}
func (s SortField) MarshalJSON() ([]byte, error) {
@@ -137,7 +138,7 @@ func (s SortField) MarshalJSON() ([]byte, error) {
Transform iceberg.Transform `json:"transform"`
Direction SortDirection `json:"direction"`
NullOrder NullOrder `json:"null-order"`
- }{s.SourceID(), s.Transform, direction, nullOrder})
+ }{s.FirstSourceID(), s.Transform, direction, nullOrder})
}
func (s *SortField) UnmarshalJSON(b []byte) error {
@@ -482,7 +483,7 @@ func (s *SortOrder) CheckCompatibility(schema
*iceberg.Schema) error {
for _, field := range s.fields {
if field.Transform == nil {
- return fmt.Errorf("%w: sort field with source id %d has
no transform", ErrInvalidTransform, field.SourceID())
+ return fmt.Errorf("%w: sort field with source id %d has
no transform", ErrInvalidTransform, field.FirstSourceID())
}
if err := validateSortSourceIDs(field.SourceIDs, boundOrder);
err != nil {
@@ -552,7 +553,7 @@ func AssignFreshSortOrderIDsWithID(sortOrder SortOrder,
old, fresh *iceberg.Sche
fields := make([]SortField, 0, len(sortOrder.fields))
for _, field := range sortOrder.fields {
- originalField, ok := old.FindColumnName(field.SourceID())
+ originalField, ok := old.FindColumnName(field.FirstSourceID())
if !ok {
return SortOrder{}, fmt.Errorf("cannot find source
column id %s in old schema", field.String())
}
diff --git a/table/sorting_test.go b/table/sorting_test.go
index 06b14ac6f..e8ff271f3 100644
--- a/table/sorting_test.go
+++ b/table/sorting_test.go
@@ -231,7 +231,7 @@ func TestUnboundSortOrderUnmarshalAcceptsZeroSourceID(t
*testing.T) {
require.NoError(t, json.Unmarshal([]byte(`{"order-id": 1, "fields":
[{"source-id": 0, "transform": "identity", "direction": "asc", "null-order":
"nulls-first"}]}`), &sortOrder))
require.Equal(t, 1, sortOrder.Len())
for _, field := range sortOrder.Fields() {
- assert.Equal(t, 0, field.SourceID())
+ assert.Equal(t, 0, field.FirstSourceID())
}
roundTripped, err := json.Marshal(sortOrder)
@@ -631,7 +631,7 @@ func TestSortFieldMultiArgSourceIDs(t *testing.T) {
var field table.SortField
err := json.Unmarshal([]byte(jsonData), &field)
require.NoError(t, err)
- assert.Equal(t, 2, field.SourceID())
+ assert.Equal(t, 2, field.FirstSourceID())
assert.Equal(t, []int{2, 3}, field.SourceIDs)
})
@@ -700,7 +700,7 @@ func TestSortFieldMultiArgSourceIDs(t *testing.T) {
var decoded table.SortField
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
- assert.Equal(t, 2, decoded.SourceID())
+ assert.Equal(t, 2, decoded.FirstSourceID())
assert.Equal(t, []int{2, 3}, decoded.SourceIDs)
})
@@ -747,3 +747,20 @@ func
TestSortFieldMarshalAppliesDefaultsWithoutMutatingReceiver(t *testing.T) {
assert.Empty(t, field.Direction)
assert.Empty(t, field.NullOrder)
}
+
+func TestSortFieldFirstSourceID(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ sourceIDs []int
+ want int
+ }{
+ {name: "empty", sourceIDs: nil, want: 0},
+ {name: "single", sourceIDs: []int{7}, want: 7},
+ {name: "multiple", sourceIDs: []int{7, 9}, want: 7},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ field := table.SortField{SourceIDs: tc.sourceIDs}
+ assert.Equal(t, tc.want, field.FirstSourceID())
+ })
+ }
+}
diff --git a/table/update_spec.go b/table/update_spec.go
index dae04d0e1..678a77f92 100644
--- a/table/update_spec.go
+++ b/table/update_spec.go
@@ -116,7 +116,7 @@ func NewUpdateSpec(t *Transaction, caseSensitive bool)
*UpdateSpec {
return us
}
transformToField[transformKey{
- SourceId: partitionField.SourceID(),
+ SourceId: partitionField.FirstSourceID(),
Transform: partitionField.Transform.String(),
}] = partitionField
nameToField[partitionField.Name] = partitionField
@@ -215,9 +215,9 @@ func (us *UpdateSpec) Apply() (iceberg.PartitionSpec,
error) {
var err error
if _, deleted := us.deletes[field.FieldID]; !deleted {
if rename, renamed := us.renames[field.Name]; renamed {
- newField, err = us.addNewField(us.schema,
field.SourceID(), field.FieldID, rename, field.Transform, partitionNames)
+ newField, err = us.addNewField(us.schema,
field.FirstSourceID(), field.FieldID, rename, field.Transform, partitionNames)
} else {
- newField, err = us.addNewField(us.schema,
field.SourceID(), field.FieldID, field.Name, field.Transform, partitionNames)
+ newField, err = us.addNewField(us.schema,
field.FirstSourceID(), field.FieldID, field.Name, field.Transform,
partitionNames)
}
if err != nil {
return iceberg.PartitionSpec{}, err
@@ -225,9 +225,9 @@ func (us *UpdateSpec) Apply() (iceberg.PartitionSpec,
error) {
partitionFields = append(partitionFields, newField)
} else if us.meta.Version() == 1 {
if rename, renamed := us.renames[field.Name]; renamed {
- newField, err = us.addNewField(us.schema,
field.SourceID(), field.FieldID, rename, iceberg.VoidTransform{},
partitionNames)
+ newField, err = us.addNewField(us.schema,
field.FirstSourceID(), field.FieldID, rename, iceberg.VoidTransform{},
partitionNames)
} else {
- newField, err = us.addNewField(us.schema,
field.SourceID(), field.FieldID, field.Name, iceberg.VoidTransform{},
partitionNames)
+ newField, err = us.addNewField(us.schema,
field.FirstSourceID(), field.FieldID, field.Name, iceberg.VoidTransform{},
partitionNames)
}
if err != nil {
return iceberg.PartitionSpec{}, err
@@ -275,7 +275,7 @@ func validateNoRedundantTimeFields(fields
[]iceberg.PartitionField) error {
if _, isTime := field.Transform.(iceberg.TimeTransform);
!isTime {
continue
}
- source := field.SourceID()
+ source := field.FirstSourceID()
if existing, exists := timeFields[source]; exists {
return fmt.Errorf("%w: redundant partition field for
source ID %d: %s (%s) conflicts with %s (%s); remove one of them in this
update",
iceberg.ErrInvalidPartitionSpec, source,
@@ -419,7 +419,7 @@ func (us *UpdateSpec) historicalPartitionFields(key
transformKey) []iceberg.Part
for _, spec := range us.meta.PartitionSpecs() {
for _, field := range spec.Fields() {
historicalKey := transformKey{
- SourceId: field.SourceID(),
+ SourceId: field.FirstSourceID(),
Transform: field.Transform.String(),
}
us.historicalFields[historicalKey] =
append(us.historicalFields[historicalKey], field)
diff --git a/table/update_spec_test.go b/table/update_spec_test.go
index 92727b0bf..65bae0be4 100644
--- a/table/update_spec_test.go
+++ b/table/update_spec_test.go
@@ -118,7 +118,7 @@ func TestUpdateSpecPreservesSourceLessVoidTombstone(t
*testing.T) {
assert.Equal(t, 1000, newSpec.Field(0).FieldID)
assert.Equal(t, "id", newSpec.Field(0).Name)
assert.Equal(t, iceberg.VoidTransform{}, newSpec.Field(0).Transform)
- assert.Equal(t, 2, newSpec.Field(1).SourceID())
+ assert.Equal(t, 2, newSpec.Field(1).FirstSourceID())
assert.Equal(t, "name_identity", newSpec.Field(1).Name)
}
@@ -169,7 +169,7 @@ func
TestUpdateSpecAllowsReplacingTimeTransformOnSameSource(t *testing.T) {
require.Equal(t, 1, newSpec.NumFields())
assert.Equal(t, "ts_month", newSpec.Field(0).Name)
assert.Equal(t, iceberg.MonthTransform{},
newSpec.Field(0).Transform)
- assert.Equal(t, 3, newSpec.Field(0).SourceID())
+ assert.Equal(t, 3, newSpec.Field(0).FirstSourceID())
}
t.Run("remove then add", func(t *testing.T) {
@@ -211,10 +211,10 @@ func
TestUpdateSpecReplacesTimeTransformOverV1VoidTombstone(t *testing.T) {
require.Equal(t, 2, newSpec.NumFields())
assert.Equal(t, "ts_year", newSpec.Field(0).Name)
assert.Equal(t, iceberg.VoidTransform{}, newSpec.Field(0).Transform)
- assert.Equal(t, 3, newSpec.Field(0).SourceID())
+ assert.Equal(t, 3, newSpec.Field(0).FirstSourceID())
assert.Equal(t, "ts_month", newSpec.Field(1).Name)
assert.Equal(t, iceberg.MonthTransform{}, newSpec.Field(1).Transform)
- assert.Equal(t, 3, newSpec.Field(1).SourceID())
+ assert.Equal(t, 3, newSpec.Field(1).FirstSourceID())
}
// Iceberg-Java writes this metadata today, so loading it has to keep working:
the
@@ -328,13 +328,13 @@ func TestUpdateSpecAddField(t *testing.T) {
assert.Equal(t, "street_void",
newSpec.FieldsBySourceID(5)[0].Name)
addedField := newSpec.FieldsBySourceID(3)[0]
- assert.Equal(t, 3, addedField.SourceID())
+ assert.Equal(t, 3, addedField.FirstSourceID())
assert.Equal(t, 1002, addedField.FieldID)
assert.Equal(t, "year_transform", addedField.Name)
assert.Equal(t, iceberg.YearTransform{}, addedField.Transform)
addedField = newSpec.FieldsBySourceID(7)[0]
- assert.Equal(t, 7, addedField.SourceID())
+ assert.Equal(t, 7, addedField.FirstSourceID())
assert.Equal(t, 1003, addedField.FieldID)
assert.Equal(t, "zipcode_bucket", addedField.Name)
assert.Equal(t, iceberg.BucketTransform{NumBuckets: 5},
addedField.Transform)
@@ -643,13 +643,13 @@ func TestUpdateSpecAddIdentityField(t *testing.T) {
assert.Equal(t, "street_void",
newSpec.FieldsBySourceID(5)[0].Name)
addedField := newSpec.FieldsBySourceID(3)[0]
- assert.Equal(t, 3, addedField.SourceID())
+ assert.Equal(t, 3, addedField.FirstSourceID())
assert.Equal(t, 1002, addedField.FieldID)
assert.Equal(t, "ts", addedField.Name)
assert.Equal(t, iceberg.IdentityTransform{},
addedField.Transform)
addedField = newSpec.FieldsBySourceID(2)[0]
- assert.Equal(t, 2, addedField.SourceID())
+ assert.Equal(t, 2, addedField.FirstSourceID())
assert.Equal(t, 1003, addedField.FieldID)
assert.Equal(t, "name", addedField.Name)
assert.Equal(t, iceberg.IdentityTransform{},
addedField.Transform)
@@ -910,13 +910,13 @@ func TestUpdateSpecCommit(t *testing.T) {
assert.Equal(t, []iceberg.PartitionField(nil),
currSpec.FieldsBySourceID(1))
addedField := currSpec.FieldsBySourceID(6)[0]
- assert.Equal(t, 6, addedField.SourceID())
+ assert.Equal(t, 6, addedField.FirstSourceID())
assert.Equal(t, 1002, addedField.FieldID)
assert.Equal(t, "address.city_trunc_3", addedField.Name)
assert.Equal(t, iceberg.TruncateTransform{Width: 3},
addedField.Transform)
addedIdentity := currSpec.FieldsBySourceID(7)[0]
- assert.Equal(t, 7, addedIdentity.SourceID())
+ assert.Equal(t, 7, addedIdentity.FirstSourceID())
assert.Equal(t, 1003, addedIdentity.FieldID)
assert.Equal(t, "address.zip_code", addedIdentity.Name)
assert.Equal(t, iceberg.IdentityTransform{},
addedIdentity.Transform)
diff --git a/website/src/releases.md b/website/src/releases.md
index ba5859370..61ae67689 100644
--- a/website/src/releases.md
+++ b/website/src/releases.md
@@ -61,6 +61,8 @@ Per-release notes (highlights, breaking changes,
contributors) are published on
Relatedly, the previously unexported `isGCEnabled` is now
`table.IsGCEnabled`, so the new package can gate destructive operations on the
`gc.enabled` table property.
- `maintenance.PurgeFiles` (previously `Table.PurgeFiles`) now calls
`IO.Remove` concurrently, with up to 32 calls in flight, when the filesystem
does not implement `BulkRemovableIO`. Custom IO implementations, including
those registered with `io.Register`, must make `Remove` safe for concurrent use
and synchronize any shared mutable state. The bulk deletion path is unchanged.
+- **Partition and sort source accessors renamed.** Replace
`PartitionField.SourceID()` and `SortField.SourceID()` with `FirstSourceID()`.
Their behavior and serialized fields are unchanged.
+
## Verifying and producing releases
If you are validating an RC or cutting a new release, see: