mfellner opened a new issue, #3403:
URL: https://github.com/apache/iceberg-rust/issues/3403
Related to #2411 (v3 tracking). Related to #3362 (name-based resolution of
metadata columns, a different mechanism).
### Apache Iceberg Rust version
`main` at a76678363
### Describe the bug
The spec's Reserved Field IDs section says: "Iceberg tables must not use
field ids greater than 2147483447 (`Integer.MAX_VALUE - 200`). This id range is
reserved for metadata columns that can be used in user data schemas, like the
`_file` column that holds the file path in which a row was stored." The `_file`
column has id 2147483646 (`RESERVED_FIELD_ID_FILE` in
`crates/iceberg/src/metadata_columns.rs`).
Table metadata does not enforce this limit.
`TableMetadataBuilder::add_schema` and JSON deserialization of table metadata
both accept a schema with field id 2147483448, the first id above the limit,
for format versions v2 and v3. The boundary id 2147483447 is accepted, as the
spec allows.
The reader matches columns by field id, so a user column that uses a
metadata id is read as that metadata column
(`crates/iceberg/src/arrow/reader/pipeline.rs:326`,
`crates/iceberg/src/arrow/record_batch_transformer.rs:734`). In a local probe,
a Parquet file with a column `payload` (field id 2147483646) and a table schema
with the same id returned the data file path for all three rows instead of the
stored values `a`, `b`, `c`. This state is reachable only from table metadata
that the spec forbids, which the check requested below would reject.
I would expect the check to sit at the table metadata level rather than in
`SchemaBuilder::build`, because the crate builds internal schemas that use
reserved ids, such as the position delete schema in
`crates/iceberg/src/writer/base_writer/position_delete_writer.rs`.
### To Reproduce
On `main` at a76678363, add this test module at the end of
`crates/iceberg/src/spec/table_metadata_builder.rs`:
<details>
<summary>Probe test module</summary>
```rust
#[cfg(test)]
mod reserved_id_probe {
use std::collections::HashMap;
use crate::spec::*;
fn base_metadata(format_version: FormatVersion) -> TableMetadata {
let schema = Schema::builder()
.with_fields(vec![NestedField::required(1, "x",
Type::Primitive(PrimitiveType::Long)).into()])
.build()
.unwrap();
TableMetadataBuilder::new(
schema,
UnboundPartitionSpec::builder().build(),
SortOrder::unsorted_order(),
"s3://bucket/t".to_string(),
format_version,
HashMap::new(),
)
.unwrap()
.build()
.unwrap()
.metadata
}
#[test]
fn probe_add_schema_field_ids() {
for format_version in [FormatVersion::V2, FormatVersion::V3] {
for id in [2147483447, 2147483448] {
let schema = Schema::builder()
.with_fields(vec![NestedField::optional(id, "user_col",
Type::Primitive(PrimitiveType::String)).into()])
.build()
.unwrap();
let result = base_metadata(format_version)
.into_builder(Some("s3://bucket/t/metadata/v1.json".to_string()))
.add_schema(schema)
.and_then(|b| b.build());
println!("PROBE add_schema format={format_version:?} id={id}
is_ok={}", result.is_ok());
}
}
}
#[test]
fn probe_metadata_json_field_ids() {
for format_version in [FormatVersion::V2, FormatVersion::V3] {
for id in [2147483447, 2147483448] {
let mut value =
serde_json::to_value(base_metadata(format_version)).unwrap();
value["schemas"][0]["fields"][0]["id"] =
serde_json::json!(id);
let result = serde_json::from_value::<TableMetadata>(value);
println!("PROBE metadata_json format={format_version:?}
id={id} is_ok={}", result.is_ok());
}
}
}
}
```
</details>
Run `cargo test -p iceberg --lib table_metadata_builder::reserved_id_probe
-- --nocapture`. Output:
```
PROBE add_schema format=V2 id=2147483447 is_ok=true
PROBE add_schema format=V2 id=2147483448 is_ok=true
PROBE add_schema format=V3 id=2147483447 is_ok=true
PROBE add_schema format=V3 id=2147483448 is_ok=true
PROBE metadata_json format=V2 id=2147483447 is_ok=true
PROBE metadata_json format=V2 id=2147483448 is_ok=true
PROBE metadata_json format=V3 id=2147483447 is_ok=true
PROBE metadata_json format=V3 id=2147483448 is_ok=true
```
Reader case, on the same tree: a Parquet file with `id` (field id 1, Int32)
and `payload` (Utf8, field id 2147483646) holding `a`, `b`, `c`. The table
schema has `id` (id 1) and an optional `payload` at id 2147483646. The scan
projects field ids `[1, 2147483646]`. This came from a local probe test of
about 60 lines that is not part of the upstream tree, so it is not included
here. Output:
```
PROBE_SCHEMA [("id", "Int32"), ("_file", "RunEndEncoded(\"run_ends\":
non-null Int32, \"values\": Utf8)")]
PROBE_RESULT payload_values=["<data file path>", "<data file path>", "<data
file path>"]
```
### Expected behavior
Adding a schema with a field id greater than 2147483447 to table metadata
(`add_schema`), and parsing table metadata JSON with such a schema, return an
error, as the spec requires. The boundary id 2147483447 stays valid.
### Willingness to contribute
I would be willing to contribute a fix with guidance from the Iceberg
community (any contribution would be agent-assisted and I would disclose that).
Found during an agent-assisted audit of v3 support (see #2411). The
reproduction above was run against main at a76678363.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]