|
| 1 | +<!-- Draft to file at apache/datafusion. Title below; body is everything under it. --> |
| 2 | + |
| 3 | +# Title |
| 4 | + |
| 5 | +`date_part` over a `Dictionary(Timestamp)` returns a type that violates its declared `Int32` (crashes GROUP BY / JOIN) |
| 6 | + |
| 7 | +# Body |
| 8 | + |
| 9 | +> Filed with AI assistance, per the project's AI policy. The reproduction was written and verified by running it against `datafusion 54.0.0` (`pyarrow 23.0.0`, arrow-rs 58.3.0); a human reviewed it before filing. |
| 10 | +
|
| 11 | +## Describe the bug |
| 12 | + |
| 13 | +`date_part('hour', t)` where `t` is `Dictionary(Int32, Timestamp)` preserves the dictionary encoding and returns a `Dictionary(Int32, Int32)`, but the expression's **declared logical type is plain `Int32`**. The physical/logical mismatch surfaces as soon as the column is materialized: |
| 14 | + |
| 15 | +``` |
| 16 | +Arrow error: Invalid argument error: column types must match schema types, |
| 17 | +expected Int32 but found Dictionary(Int32, Int32) at column index 0 |
| 18 | +``` |
| 19 | + |
| 20 | +A `GROUP BY` / `JOIN` on the same expression is worse — it **panics a worker thread**: |
| 21 | + |
| 22 | +``` |
| 23 | +thread 'tokio-rt-worker' panicked at arrow-array-58.3.0/src/cast.rs:849: primitive array |
| 24 | +``` |
| 25 | + |
| 26 | +(a failed downcast of the dictionary array to a primitive array). `GROUP BY date_part('hour', time)` is the canonical way to write a diurnal climatology, so this is not an edge case for time-series workloads. |
| 27 | + |
| 28 | +## To Reproduce |
| 29 | + |
| 30 | +Pure `datafusion` + `pyarrow`, no other deps, no network: |
| 31 | + |
| 32 | +```python |
| 33 | +import numpy as np, pyarrow as pa |
| 34 | +from datafusion import SessionContext |
| 35 | + |
| 36 | +N = 240 |
| 37 | +stamps = (np.datetime64("2020-01-01") + np.arange(N) * np.timedelta64(1, "h")).astype("datetime64[us]") |
| 38 | +distinct = np.unique(stamps) |
| 39 | +values = pa.array(distinct, type=pa.timestamp("us")) |
| 40 | +indices = pa.array(np.searchsorted(distinct, stamps).astype(np.int32)) |
| 41 | +t = pa.DictionaryArray.from_arrays(indices, values) # Dictionary(Int32, Timestamp) |
| 42 | + |
| 43 | +ctx = SessionContext() |
| 44 | +ctx.register_record_batches("x", [pa.table({"t": t}).to_batches()]) |
| 45 | +ctx.sql("SELECT date_part('hour', t) AS h FROM x").to_arrow_table() |
| 46 | +# -> expected Int32 but found Dictionary(Int32, Int32) at column index 0 |
| 47 | +# and: SELECT date_part('hour', t), COUNT(*) FROM x GROUP BY 1 -> panics ("primitive array") |
| 48 | +``` |
| 49 | + |
| 50 | +Rust equivalent (paste into a `datafusion` test): |
| 51 | + |
| 52 | +```rust |
| 53 | +use std::sync::Arc; |
| 54 | +use arrow::array::{DictionaryArray, Int32Array, TimestampMicrosecondArray}; |
| 55 | +use arrow::record_batch::RecordBatch; |
| 56 | +use datafusion::prelude::*; |
| 57 | + |
| 58 | +#[tokio::test] |
| 59 | +async fn date_part_over_dictionary_timestamp() -> datafusion::error::Result<()> { |
| 60 | + let ctx = SessionContext::new(); |
| 61 | + let values = TimestampMicrosecondArray::from(vec![0, 3_600_000_000, 7_200_000_000]); |
| 62 | + let keys = Int32Array::from(vec![0, 1, 2, 0, 1, 2]); |
| 63 | + let t = DictionaryArray::new(keys, Arc::new(values)); |
| 64 | + ctx.register_batch("x", RecordBatch::try_from_iter([("t", Arc::new(t) as _)])?)?; |
| 65 | + ctx.sql("SELECT date_part('hour', t) AS h FROM x").await?.collect().await?; |
| 66 | + Ok(()) |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +## Expected behavior |
| 71 | + |
| 72 | +`date_part` (and temporal scalar functions generally) over a dictionary-encoded input should return a plain `Int32` (decode the dictionary), or declare the `Dictionary(Int32, Int32)` it actually produces — logical and physical types must agree, and a downstream `GROUP BY` / `JOIN` must never panic on the result. |
| 73 | + |
| 74 | +## Additional context |
| 75 | + |
| 76 | +Version: datafusion 54.0.0 / pyarrow 23.0.0 (arrow-rs 58.3.0). |
| 77 | + |
| 78 | +Downstream tracking issue with the same repro (xarray-sql): xqlsystems/xarray-sql#221. It blocks dictionary-encoding the time coordinate there; because time is where `date_part`/`date_trunc`/`extract` are applied most, encoding it breaks climatology and anomaly queries. |
0 commit comments