feat: add support for _spec_id metadata column#2695
Conversation
9279b1c to
0a25d45
Compare
| if task | ||
| .project_field_ids() | ||
| .contains(&RESERVED_FIELD_ID_SPEC_ID) | ||
| && let Some(partition_spec) = &task.partition_spec |
There was a problem hiding this comment.
I think we should always add spec_id into the constant map if it's selected/projected.
The task.partition_spec should be present even for unpartitioned table(spec_id = 0).
There was a problem hiding this comment.
+1. If the invariant is that task.partition_spec is always present (spec_id = 0 even for unpartitioned tables), then gating the column on let Some(partition_spec) means a projected _spec_id can silently produce no column when that invariant is ever violated, which would be a confusing 'column vanished' bug rather than a clean error. Adding _spec_id to the constant map whenever it's projected (per @advancedxy) is the safer contract. Worth a test on an unpartitioned table to lock in spec_id = 0.
There was a problem hiding this comment.
@mbutrovich Thanks for the suggestion, I think it makes sense and I have a test_select_with_spec_id_column_from_unpartitioned_table for this case.
| partition_spec_id: manifest_file.partition_spec_id, | ||
| bound_predicates: bound_predicates.clone(), | ||
| snapshot_schema: snapshot_schema.clone(), | ||
| delete_file_index: delete_file_index.clone(), | ||
| name_mapping: name_mapping.clone(), | ||
| case_sensitive, | ||
| partition_spec: partition_spec.clone(), |
There was a problem hiding this comment.
the partition spec is already in mainifest_file, like the partition_spec_id. maybe we could just use that and avoid embedding partition_spec into ManifestFileContext
|
@hsiang-c thanks, I think it's in good shape, just two minor comments. |
mbutrovich
left a comment
There was a problem hiding this comment.
This is a clean, well-scoped PR. Reusing with_constant is exactly how Java treats _spec_id (a per-file constant in PartitionUtil.constantsMap), and the REE-vs-schema-type split in record_batch_transformer.rs looks like a genuine fix the other two PRs will want. Two questions before I'd feel good approving, both about test confidence rather than the implementation:
- Both
_spec_idtests assert0on single-spec fixtures. Could we add one where two files have different spec ids, since that's the case the column exists for? - The change edits the shared
setup_manifest_filesfixture (and itsinspect/manifests.rssnapshot). Was that necessary, or could a dedicated fixture avoid weakening coverage for other tests?
(I've read @advancedxy's review. The "spec_id should always be added when projected / partition_spec is always present" point is already covered there, so I've turned mine into a +1 rather than a separate comment. The items below are the ones I didn't see raised yet.)
There is also a cross-PR shape question (below): I think the synthesized metadata-column mechanisms should converge, but the enum is better introduced in #2668 than here. This PR just needs to keep its scalar handling foldable.
Three sibling PRs each add a metadata column, and each currently reaches for a different mechanism: this PR (_spec_id) reuses the existing constant_fields: HashMap<i32, Datum> map; #2668 (_partition) adds partition_column: Option<PartitionColumnConstant>; #2746 (_pos) adds virtual_fields: HashSet<i32>.
Two of these are the same kind of thing and one isn't:
_file/_spec_id/identity partitions and_partitionall synthesize a constant column (they already converge onColumnSource::Add/AddStructConstant)._posis a different axis: arrow-rsRowNumbermakes it a real column in the source batch, so it is passed through, not synthesized. It probably should stay its own small mechanism.
So the unification worth making is scalar-constant plus struct-constant (this PR's path plus #2668's), not all three. The natural place to introduce it is #2668, where the genuine second synthesis variant (StructConstant) arrives and both arms are test-covered at introduction (the existing _file test plus #2668's _partition tests). There's no good way to land that enum as a standalone PR first: the StructConstant arm has no producer or consumer until #2668, so it would be untested speculative code.
The only ask on this PR: keep the scalar-constant handling (the REE-vs-schema-type branch) in a shape that #2668 can fold into a MetadataColumnSource::ScalarConstant arm cleanly. Since this merges first in the suggested order (#2695 -> #2746 -> #2668), it sets the scalar baseline and #2668 generalizes on top. The full sketch is in the #2668 notes (a comment on its partition_column field; the author can move it to #2699 if that's a better home). cc the #2668 / #2746 authors.
| .file_size_in_bytes(parquet_file_size) | ||
| .record_count(1) | ||
| .partition(Struct::from_iter([Some(Literal::long(100))])) | ||
| .partition(Struct::from_iter([Some(Literal::long(1))])) |
There was a problem hiding this comment.
Collapsing three distinct partition values to 1/1/1 reduces distinctness for every test on setup_manifest_files and forces the inspect/manifests.rs bound edits below. Was there a failure with the original values that motivated this? If the new constant-type handling needs specific values, a separate fixture might keep the shared one intact. But if the old values actually broke, that itself sounds like a test worth keeping. What did you hit here?
There was a problem hiding this comment.
Thanks @mbutrovich, good catch. It is actually a bug in my prior commits b/c I didn't distinguish field ids that are not present in a data file. This is now fixed.
| StringArray | ||
| [ | ||
| "100", | ||
| "1", |
There was a problem hiding this comment.
This expected-output edit only exists because the fixture changed; if the fixture stays, this revert isn't needed. Flagging so reviewers know it's coupled to the above, not an independent behavior change.
There was a problem hiding this comment.
This unnecessary change is reverted.
| assert!(result.is_ok(), "Scan timed out - deadlock detected"); | ||
| } | ||
|
|
||
| #[tokio::test] |
There was a problem hiding this comment.
Both new tests assert 0 on a single-spec table. The whole point of _spec_id (https://iceberg.apache.org/spec/#reserved-field-ids) is distinguishing files written under different specs. A partition-evolved fixture where two files carry different spec ids would validate that directly. Would that be easy to add on top of the existing fixtures?
There was a problem hiding this comment.
I added a new test fixture with evolved table partitions.
| if task | ||
| .project_field_ids() | ||
| .contains(&RESERVED_FIELD_ID_SPEC_ID) | ||
| && let Some(partition_spec) = &task.partition_spec |
There was a problem hiding this comment.
+1. If the invariant is that task.partition_spec is always present (spec_id = 0 even for unpartitioned tables), then gating the column on let Some(partition_spec) means a projected _spec_id can silently produce no column when that invariant is ever violated, which would be a confusing 'column vanished' bug rather than a clean error. Adding _spec_id to the constant map whenever it's projected (per @advancedxy) is the safer contract. Worth a test on an unpartitioned table to lock in spec_id = 0.
| && let Some(partition_spec) = &task.partition_spec | ||
| { | ||
| let spec_id_datum = Datum::int(partition_spec.spec_id()); | ||
| record_batch_transformer_builder = record_batch_transformer_builder |
There was a problem hiding this comment.
Routing _spec_id through with_constant makes it RunEndEncoded (metadata-field constants get datum_to_arrow_type_with_ree), same as _file. The spec types _spec_id as a plain int (https://iceberg.apache.org/spec/#reserved-field-ids). For the Comet/Spark consumer, does REE get decoded cheaply downstream, or would a plain Int32 avoid a cast? Not necessarily a change, just confirming the consumer is happy with REE here as it is for _file.
| datum_to_arrow_type_with_ree(datum) | ||
| } else { | ||
| field_id_to_mapped_schema_map | ||
| .get(field_id) |
There was a problem hiding this comment.
Separating "metadata field becomes REE" from "identity-partition field becomes schema type" looks like a real correctness improvement: identity-partition constants shouldn't be forced to RunEndEncoded. Worth calling out to the #2668/#2746 authors so they rebase on top rather than reintroducing the old derivation. This branch is the scalar arm that #2668 would fold into a MetadataColumnSource::ScalarConstant; keeping it in this shape makes that easy.
5ecd879 to
bf527a6
Compare
bf527a6 to
aee857e
Compare
Which issue does this PR close?
What changes are included in this PR?
_spec_id, which is a constant like the_filemetadata column for all rows, add it toRecordBatchTransformerBuilderAre these changes tested?
Sample plan