Repository navigation
refactor(spec): make PartitionField fields private and carry source-ids - #2802
moomindani wants to merge 1 commit 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. |
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: |
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>
a14f0d3 to
a67f9c4
Compare
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 (1801 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.