hanke580 opened a new issue, #26492:
URL: https://github.com/apache/datafusion/issues/26492

   ### Describe the bug
   
   
   `COPY … TO 'dir/' PARTITIONED BY (p0)` names the directory `p0=<value>`. The 
value is not escaped with Hive's
   escaping rules. It goes through `object_store::path::Path::join`, which 
percent-encodes only the characters that
   object stores advise against (`/`, `%`, `#`, `?`, `*`, `\`, `"`, `[`, `]`, 
`{`, `}`, `^`, `|`, `<`, `>`, `~`, `` ` ``, control
   and non-ASCII characters). The key/value separator `=` is written as is, and 
so are `:` and `'`, which Hive's
   `escapePathName` escapes, and spaces, which pyarrow, DuckDB and Polars 
escape.
   A value `x=y` gives the directory `p0=x=y`.
   
   DataFusion and pyarrow split at the first `=` and read the dataset back. 
DuckDB and Polars do not:
   - next to other partitions, DuckDB fails with `Hive partition mismatch … key 
"p0" not found`, and Polars fails as well;
   - when `p0=x=y` is the only partition, both return the rows **without the 
`p0` column**, with no error.
   
   For the same table, pyarrow, DuckDB and Polars write `p0=x%3Dy` (and 
`p0=a%20b`), which every reader, including
   DataFusion, reads back.
   
   The path is built in `compute_hive_style_file_path` 
(`datafusion/datasource/src/write/demux.rs:636`):
   `file_path.join(format!("{}={}", partition_by[j].0, part_key[j]))`. Escaping 
the value with the Hive rules (at least
   `=` and `%`, which `ListingTable` already decodes on read since #23226) 
would make the output portable.
   
   
   ### To Reproduce
   
   
   ```python
   import pyarrow as pa, pyarrow.parquet as pq
   pq.write_table(pa.table({'rid': [1, 2, 3, 4], 'p0': ['x=y', 'a b', 'é', 
'a/b']}), 'src.parquet')
   ```
   
   ```sql
   CREATE EXTERNAL TABLE src STORED AS PARQUET LOCATION 'src.parquet';
   COPY (SELECT * FROM src) TO 'd/' STORED AS PARQUET PARTITIONED BY (p0);
   ```
   
   ```
   $ ls d
   'p0=%C3%A9'  'p0=a b'  'p0=a%2Fb'  'p0=x=y'
   ```
   
   ```python
   import duckdb, polars as pl
   duckdb.sql("SELECT rid, p0 FROM read_parquet('d/**/*.parquet', 
hive_partitioning = true)")
   # Binder Error: Hive partition mismatch between file "d/p0=%C3%A9/….parquet" 
and "d/p0=x=y/….parquet": key "p0" not found
   pl.scan_parquet('d/**/*.parquet', hive_partitioning=True, hive_schema={'p0': 
pl.String}).collect()
   # could not create a new DataFrame: height of column 'p0' (3) does not match 
DataFrame height (4)
   ```
   
   Only the `x=y` partition:
   
   ```sql
   COPY (SELECT 1 AS rid, 'x=y' AS p0) TO 'one/' STORED AS PARQUET PARTITIONED 
BY (p0);   -- one/p0=x=y/…
   ```
   
   ```python
   duckdb.sql("SELECT * FROM read_parquet('one/**/*.parquet', hive_partitioning 
= true)").columns   # ['rid'] — p0 is gone
   pl.scan_parquet('one/**/*.parquet', hive_partitioning=True).collect()        
                    # only column rid
   ```
   
   ### Expected behavior
   
   Partition values are escaped as Hive, pyarrow, DuckDB and Polars do 
(`p0=x%3Dy`; the last three also write
   `p0=a%20b`), so that DuckDB, Polars, Spark and Hive read the
   dataset back with the right `p0` values.
   
   ### Additional context
   
   - DataFusion main 290ad15 (2026-10-18), built from source; also 91cd7ffb 
(2026-10-15).
   - Related (read side, merged): #23226 "Decode Hive partition values in 
listing tables". Open: #26310 (Hive's NULL
     placeholder), #9269 (panic with `'` in partition values).
   - Values with `=` are common: key-value strings, URL query strings, base64 
with padding.
   
   
   ### Willingness to contribute
   
   None


-- 
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]

Reply via email to