Wrong results and panics after a column is dropped and added back under the same name
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 32/100
Research direction
Start in crates/datafusion/src/table_provider/catalog_table_provider.rs, where the Arrow schema is cached when the provider is built, and in crates/datafusion/src/read/scan.rs, where columns are selected by name. Reproduce by creating a table, inserting a row, dropping and re-adding a column with a different type, then querying through a provider built before the change. Done means the stale-provider query either matches a fresh session or fails with a clear schema-changed error, with no wrong counts or panics. The issue leaves the fix open, so agree on the approach with maintainers before coding.
Written by the indexing model from the issue text.
Description
Describe the bug
IcebergCatalogTableProvider caches the table's Arrow schema when it is built (catalog_table_provider.rs#L81-L84), and IcebergCatalogProvider keeps each provider for its lifetime (schema.rs#L64-L94). Scans select columns by name from the snapshot they read (read/scan.rs#L93-L97).
Dropping a column and adding one back under the same name is how a column's type is changed when the change isn't an allowed promotion. After that, a provider built before the change reads the new column but still declares the old column's type and nullability. DataFusion trusts the declared schema:
- It folds
price IS NULLtofalsebecause the cachedpriceis NOT NULL. The plan replaces the scan withEmptyRelation, so the count is 0 without reading the table. - Operators that rely on the declared
Float64panic on the decimal arrays. SELECT *fails because a column declared non-nullable contains nulls.
To Reproduce
- Create
ns.t(id INT NOT NULL, price DOUBLE NOT NULL), register anIcebergCatalogProviderascin session S, and runINSERT INTO c.ns.t VALUES (1, 1.5). - Outside S, change
priceto a decimal: drop it withupdate_schema().delete_column("price"), then add it back withupdate_schema().add_column(AddColumn::optional("price", Type::Primitive(PrimitiveType::Decimal { precision: 10, scale: 2 }))). From Spark, that isALTER TABLE t DROP COLUMN pricefollowed byALTER TABLE t ADD COLUMN price DECIMAL(10, 2). - Insert
(2, 2.25)from a session that registers the catalog after step 2. - Run these queries in S and, for comparison, in a new session:
| Query | Session S | New session |
|---|---|---|
SELECT count(*) FROM c.ns.t WHERE price IS NULL |
0 |
1 |
SELECT count(price) FROM c.ns.t |
panics: primitive array |
1 |
SELECT id, price * 2 FROM c.ns.t |
panics: primitive array |
(1, NULL), (2, 4.50) |
SELECT * FROM c.ns.t |
error: Column 'price' is declared as non-nullable but contains null values |
(1, NULL), (2, 2.25) |
The panics come from arrow-array's as_primitive (cast.rs:849) and reach the caller as a Join Error.
If price is added back as DOUBLE instead, S still counts 0 and still fails SELECT *. Only the panics need the type change.
Expected behavior
S returns the same results as the new session, or fails every query on t with an error saying that the table's schema changed after the provider was built. It never returns a wrong count or panics.
Additional context
- This is another concrete case of #24 §2, on the read path. #56 is the write-path counterpart.
- #42 is related but different: there the provider declares the table's current schema while the scan binds to an older snapshot's schema.
- #48 lets Ballista rebuild a provider with the schema the client planned against, so a rebuilt provider behaves the same way.
- Possible fixes:
- in
scan, check each declared field against the field of the same name in the scanned snapshot's schema (field id fromPARQUET:field_id, type and nullability), and return an error on a mismatch - resolve the table, and with it the schema, per query (#24)
- in
- Dominant language
- Rust
- Stars
- 30
- Forks
- 30
- Avg merge
- 3d 19h
- Merged PRs (30d)
- 15
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from apache/datafusion-iceberg
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
apache/datafusion-iceberg#100 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
apache/datafusion-iceberg#85 · 1 comment · 5 reactions ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
apache/datafusion-iceberg#84 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
apache/datafusion-iceberg#75 ·
Maintainers usually reply within 1 day
-
Slow CI runnersOpen
Difficulty 4/5 3-5 days Newbie friendliness 18/100
apache/datafusion-iceberg#74 ·
Maintainers usually reply within 1 day
All issues in apache/datafusion-iceberg
Similar issues
-
bug i18n priority: medium ui
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
storytold/craft-anything#983 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Dinnerbone/Rascal#91 · 2 reactions ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
higress-group/HiRoute#405 ·
Maintainers usually reply within 1 day
-
rust_analyzer flycheck: no CLI way to enable clippy from `check.overrideCommand`Possibly taken @TrapsterDK claimed this today. Openneeds-triage
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
bazelbuild/rules_rust#4392 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day