feat(spec): read partition and sort fields with multi-argument transforms (source-ids) - #2802
moomindani wants to merge 4 commits into
Conversation
|
Gentle ping — open for ~2.5 weeks, CI green, no review yet. This one is a correctness gap rather than a feature: the v3 spec serializes partition and sort fields whose transform takes multiple arguments with a Note it touches @blackmwk @CTTY would either of you have time for a review? Closes #2801, part of #2411. |
c68a8f5 to
cbe2556
Compare
|
Still waiting for review, opened 9 July. Mergeable and green on current This is the read side of multi-argument transforms: v3 writes partition and sort fields with @CTTY @kevinjqliu a review would be appreciated. |
Stefan-Dienst
left a comment
There was a problem hiding this comment.
Hi @moomindani ,
I am new to the iceberg-rust repo, so if my comments maybe confusing just feel free to ignore them. But I have used your PR to become a bit more familiar with the spec module and had a few thoughts while doing so that I wanted to share with you.
There was a problem hiding this comment.
Do you think it maybe worth to add tests for the table metadata that use multiple arguments for partition and sort?
| #[serde( | ||
| try_from = "_serde_partition_field::PartitionFieldSerde", | ||
| into = "_serde_partition_field::PartitionFieldSerde" | ||
| )] | ||
| pub struct PartitionField { | ||
| /// A source column id from the table’s schema | ||
| pub source_id: i32, | ||
| /// Source column ids when the transform takes multiple arguments (v3 multi-argument | ||
| /// transforms). `None` for single-argument transforms, where `source_id` is used instead. | ||
| /// When set, `source_id` holds the first id so that existing consumers keep working. | ||
| #[builder(default)] | ||
| pub source_ids: Option<Vec<i32>>, |
There was a problem hiding this comment.
Here I am unsure if implementing the serde for PartitionField is the best approach. The first thing I stumbled over when reading this, was that the spec version was not explicit. See for example Schema, where the spec version is handled explicitly in the serde: https://github.com/apache/iceberg-rust/blob/main/crates/iceberg/src/spec/schema/_serde.rs
I don't know if it is worth doing here, because the differences between the specs are rather minor, but it may be worth to consider.
(Same argument for SortField)
There was a problem hiding this comment.
I don't think we should add such a filed, for in memory data structures, we should just use source_ids: Vec<i32>
There was a problem hiding this comment.
Just to clarify: I did not mean to add a version field, but have a different struct for the different spec versions. For example something like:
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "kebab-case")]
pub(super) struct PartitionFieldV3 {
#[serde(default, skip_serializing_if = "Option::is_none")]
source_id: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
source_ids: Option<Vec<i32>>,
field_id: i32,
name: String,
transform: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "kebab-case")]
pub(super) struct PartitionFieldV2 {
source_id: i32,
field_id: i32,
name: String,
transform: Option<String>
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "kebab-case")]
pub(super) struct PartitionFieldV1 {
source_id: i32,
name: String,
transform: Option<String>
}and then use this in the table metadata serdes like TableMetadataV2.
Then the in memory structure could be changed to (taking #3172 into account)
pub struct PartitionField {
source_ids: Vec<i32>,
[...]and the impl TryFrom and impl From handle the different specs versions.
Does this make sense?
There was a problem hiding this comment.
the datafusion integration will be migrated, see #3029. I don't know if these changes still need to be part of the PR.
| /// transforms). `None` for single-argument transforms, where `source_id` is used instead. | ||
| /// When set, `source_id` holds the first id so that existing consumers keep working. | ||
| #[builder(default)] | ||
| pub source_ids: Option<Vec<i32>>, |
There was a problem hiding this comment.
With the addition of the source_ids the equivalent_ignoring_names function here, maybe needs revisiting.
| /// transforms). `None` for single-argument transforms, where `source_id` is used instead. | ||
| /// When set, `source_id` holds the first id so that existing consumers keep working. | ||
| #[builder(default)] | ||
| pub source_ids: Option<Vec<i32>>, |
There was a problem hiding this comment.
The PartitionSpecBuilder & UnboundPartitionSpecBuilder have methods like add_partition_field and add_partition_fields, which do not support adding multi-arguments fields yet.
Maybe worth also updating in this PR.
|
Thanks for going through this — two of these were real, and I've pushed fixes for both.
The builders: this turned out to be worse than an ergonomics gap. Table metadata tests: added, via a
Spec-version-explicit serde: I left this as is for now. |
UnboundPartitionField had no source-ids, so converting a bound spec to an unbound one dropped the extra source ids and left source-id, the first id, behind. UnboundPartitionSpec is the wire type for table creation and AddSpec updates, so a v3 spec with a multi-argument transform silently degraded to a single-argument field on the way back out. Add source-ids to UnboundPartitionField and thread it through both conversions. Its serde now shares normalize_transform_sources with PartitionField, so an unbound field written the way the spec requires -- source-ids only, no source-id -- also deserializes. Adds a table metadata fixture with multi-argument partition and sort fields to cover both surfaces end to end.
equivalent_ignoring_names compared only source-id, which for a multi-argument field holds just the first source id. Two fields sharing a field id and a first source id but reading different columns compared equal, so the cross-spec compatibility check accepted them as the same field. Compare the effective source ids instead: source-ids for a multi-argument transform, otherwise the single source-id.
75d6423 to
a14f0d3
Compare
|
Update after rebasing onto current |
blackmwk
left a comment
There was a problem hiding this comment.
Thanks @moomindani for this pr. While this pr is valuable, I think the correct direction would be like:
- Split them into three prs, one for each struct.
- For each struct, we should remove all pub filed accessors as mentioned in #3172, and then replace the new ids.
| } | ||
| } | ||
|
|
||
| mod _serde_partition_field { |
There was a problem hiding this comment.
| mod _serde_partition_field { | |
| mod _serde { |
Following others convention.
| try_from = "_serde_partition_field::PartitionFieldSerde", | ||
| into = "_serde_partition_field::PartitionFieldSerde" | ||
| )] | ||
| pub struct PartitionField { |
There was a problem hiding this comment.
I prefer to do what's mentioned in #3172 first.
| #[serde( | ||
| try_from = "_serde_partition_field::PartitionFieldSerde", | ||
| into = "_serde_partition_field::PartitionFieldSerde" | ||
| )] | ||
| pub struct PartitionField { | ||
| /// A source column id from the table’s schema | ||
| pub source_id: i32, | ||
| /// Source column ids when the transform takes multiple arguments (v3 multi-argument | ||
| /// transforms). `None` for single-argument transforms, where `source_id` is used instead. | ||
| /// When set, `source_id` holds the first id so that existing consumers keep working. | ||
| #[builder(default)] | ||
| pub source_ids: Option<Vec<i32>>, |
There was a problem hiding this comment.
I don't think we should add such a filed, for in memory data structures, we should just use source_ids: Vec<i32>
| try_from = "_serde_unbound_partition_field::UnboundPartitionFieldSerde", | ||
| into = "_serde_unbound_partition_field::UnboundPartitionFieldSerde" | ||
| )] | ||
| pub struct UnboundPartitionField { |
There was a problem hiding this comment.
Simiarly for PartitionField
| pub transform: Transform, | ||
| } | ||
|
|
||
| mod _serde_unbound_partition_field { |
|
Thanks — happy to follow that direction. Before I start the wide mechanical part I'd like to confirm the target shape, since nothing references #3172 yet and this would be the first struct to get it. Plan, if it matches what you have in mind:
Two questions where guessing would be expensive:
One note on sequencing: with the split, the bound-to-unbound conversion keeps dropping the extra ids until step 3 lands, because The two smaller points are agreed and will be in the re-cut: |
We should still keep the derived TypedBuilder, but limit it's visibility to mod private.
I prefer to keep two methods:
We could add UnboundPartitionField refactoring first. |
|
Split this out as #3267, starting with Your answers are applied there: |
Which issue does this PR close?
What changes are included in this PR?
The v3 spec serializes partition fields and sort fields whose transform takes multiple arguments with a
source-idslist instead of the singlesource-id. Deserializing such metadata failed withmissing field source-id, so a v3 table containing any multi-argument transform could not be read at all.This adds read tolerance, mirroring PyIceberg (apache/iceberg-python#3630):
PartitionFieldandSortFieldacceptsource-ids: a single-element list is normalized ontosource-id; a multi-element list is kept in the newsource_idsfield and the transform maps toTransform::Unknown— multi-argument transforms cannot be evaluated, and the spec requires v3 readers to read such tables ignoring them.source_idholds the first id so existing consumers keep working.source-id, multi-argument transforms write onlysource-ids, so the field round-trips.PartitionSpec::is_compatible_withalso comparessource_ids, so two multi-arg fields sharing the first id no longer compare as identical.SortField'sDisplayrenders all ids for multi-argument fields.PartitionSpec::partition_typeworks unchanged:Transform::Unknownproducesstringper the spec.Note: this adds a public
source_idsfield toPartitionFieldandSortField(will be flagged by cargo-public-api). Known limitation shared with #2790:Transform::Unknowndoes not retain the original transform name, so round-tripping writes"unknown"; name preservation stays tracked in #2789.Are these changes tested?
Yes — new unit tests in
spec/partition.rsandspec/sort.rs: multi-arg deserialization and round-trip, single-element normalization, emptysource-idsrejection, missing-transform rejection, andpartition_typeover a multi-arg field.cargo test -p iceberg --lib(1399 tests),cargo clippy -p iceberg --lib --tests, andcargo check --workspace --testspass locally.This pull request and its description were written by Claude Fable 5.