Repository navigation
refactor(spec): make PartitionField fields private and carry source-ids - #2802
moomindani wants to merge 2 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?
There was a problem hiding this comment.
Added test_table_metadata_v3_multi_argument_partition_field in 2b6554c: a v3 metadata document with a source-ids partition field reads, binds as unknown, and writes source-ids back. The sort-order half goes with the SortField PR.
| #[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.
Thanks for raising this. I looked at splitting by spec version, but for partition fields the choice between source-id and source-ids is driven by the number of arguments, not the format version: a single-argument field is still written with source-id in v3. A per-version struct would need that same check inside each variant, so I kept one _serde struct per field type, which keeps the rule in one place and matches UnboundPartitionField from #3267.
The one version-dependent rule is that v1/v2 metadata must not contain multi-argument fields. A partition field does not know which format version it belongs to, so that check fits in TableMetadata. It is outside this refactor, so I left it out; I am happy to add it here if you would like it in this PR. The same reasoning applies to SortField.
| /// 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.
There was a problem hiding this comment.
add_unbound_field / add_unbound_fields already take a multi-argument UnboundPartitionField and check every source id. I kept the name-based add_partition_field(source_name, ...) single-column: a several-column form only becomes useful once a multi-argument transform can be evaluated, and until then such a field binds as unknown.
|
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. |
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.
| #[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>
|
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: |
a14f0d3 to
a67f9c4
Compare
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
The second of the per-struct refactors after UnboundPartitionField. The four fields are private behind accessors, source_ids replaces source_id, and the builder validates at build time. A bound field can now hold several source ids, so binding a multi-argument field succeeds instead of failing. Such a transform cannot be evaluated yet, so it binds and reads as Transform::Unknown, as the spec requires of v3 readers; transform compatibility is checked for every source id. Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
a67f9c4 to
2b6554c
Compare
|
Rebased onto main (head 2b6554c). Code that main added since the last push needed the accessors too: the check of partition field names against a new current schema now skips a field if any of its source columns is gone, and allows a shared name only for a single-source field reading that column; the snapshot producer's schema lookup for removed files requires every source id. Replies to the open threads are inline. @blackmwk @Stefan-Dienst could you take another look? |
Which issue does this PR close?
Part of #3172 and #2801. Second of the three per-struct PRs @blackmwk asked for, after #3267 (
UnboundPartitionField). This PR is now narrowed toPartitionField;SortFieldfollows, and #2801 closes with it.What changes are included in this PR?
PartitionFieldgets the same treatment #3267 gaveUnboundPartitionField.source_id() -> Result<i32>,source_ids() -> &[i32],field_id(),name()andtransform()accessors. The derivedTypedBuilderis keptpub(crate), as you suggested for the bound struct, and itsbuild()returnsResult, following refactor(spec): make UnboundPartitionField fields private and carry source-ids #3267.source_ids: Vec<i32>replacessource_id: i32and always holds at least one id. The_serdemodule readssource-idorsource-ids(not both) and writes back the one that matches the field; the read and write rules are shared withUnboundPartitionField.is_compatible_with,partition_type, the field-id reuse intable_metadata_builder, and the missing-source checks all consider the whole id list.source_id()error; it never quietly takes the first id.One decision worth your call:
Transform::Unknown, andbuild()rejects a multi-argument field with any other transform. Multi-argument transforms cannot be evaluated yet (result_type, projection and the transform functions all take one input), and the spec requires v3 readers to read such tables while ignoring them.UnboundPartitionFieldkeeps the original transform, since it only carries what it was given. The cost is the one already tracked in Unrecognized transform names fail parsing instead of becoming Transform::Unknown #2789: writing the field back writesunknownrather than the original name.Are these changes tested?
Yes — five new unit tests in
spec/partition.rs, one of them replacing #3267'stest_binding_a_multi_argument_field_fails_loudly, which now fails by design:source-idsand writing back the matching spelling;source-ids, a missing id, and a field carrying both spellings;source_idsand a multi-argument field whose transform is not unknown;is_compatible_withtelling apart two fields that share a first id.cargo test -p iceberg --libpasses (2097 tests), as do the rest, sql, glue and hms catalog unit tests.cargo fmt,cargo clippy --workspace --all-targets --all-features -- -D warningsand a full workspace build are clean, andcrates/iceberg/public-api.txtis regenerated: four public fields and the publicbuilder()out, five accessors andFrom<PartitionField> for Result<PartitionField>in.AI Disclosure
This pull request and its description were written by Isaac.