feat(operator): read a Parquet file as a source - #8512
Conversation
Texera read CSV, JSONL, Arrow and plain text off disk but not Parquet, the format most tables in a data-science workflow are already stored in. Converting one to CSV first loses what the file knew: the column written as an INTEGER came back as text for the schema to guess at again. The file states its own types in a footer, so this source infers nothing. It reads a row group at a time rather than the whole file, which is the thing a columnar format is chosen to avoid, and a column that is a group, a list or a map is refused by name instead of being dropped in silence. A timestamp is read with UTC arithmetic, matching what ArrowUtils means by a Texera TIMESTAMP: the count from the epoch lands on a wall clock, and no zone of the machine's own enters it. Reading it any other way would move the value and part the engine from the script the export writes. No new dependency: parquet-hadoop and parquet-column are already on the classpath under iceberg-parquet, and the reader takes a LocalInputFile so none of Hadoop's own file plumbing is involved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Automated Reviewer SuggestionsBased on the
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8512 +/- ##
============================================
+ Coverage 93.62% 95.51% +1.89%
+ Complexity 4857 459 -4398
============================================
Files 1212 586 -626
Lines 50037 33449 -16588
Branches 6132 4151 -1981
============================================
- Hits 46847 31950 -14897
+ Misses 1676 936 -740
+ Partials 1514 563 -951
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 526 | 0.321 | 18,782/26,446/26,446 us | 🔴 +17.7% / 🔴 +69.0% |
| 🟢 | bs=100 sw=10 sl=64 | 1,186 | 0.724 | 82,959/94,849/94,849 us | 🟢 -36.1% / 🟢 -19.5% |
| ⚪ | bs=1000 sw=10 sl=64 | 1,347 | 0.822 | 738,829/788,919/788,919 us | ⚪ within ±5% / 🟢 +26.4% |
Baseline details
Latest main 75c85aa from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 526 tuples/sec | 583 tuples/sec | 794.98 tuples/sec | -9.8% | -33.8% |
| bs=10 sw=10 sl=64 | MB/s | 0.321 MB/s | 0.356 MB/s | 0.485 MB/s | -9.8% | -33.8% |
| bs=10 sw=10 sl=64 | p50 | 18,782 us | 15,963 us | 12,443 us | +17.7% | +50.9% |
| bs=10 sw=10 sl=64 | p95 | 26,446 us | 25,755 us | 15,648 us | +2.7% | +69.0% |
| bs=10 sw=10 sl=64 | p99 | 26,446 us | 25,755 us | 19,794 us | +2.7% | +33.6% |
| bs=100 sw=10 sl=64 | throughput | 1,186 tuples/sec | 1,066 tuples/sec | 1,034 tuples/sec | +11.3% | +14.7% |
| bs=100 sw=10 sl=64 | MB/s | 0.724 MB/s | 0.65 MB/s | 0.631 MB/s | +11.4% | +14.7% |
| bs=100 sw=10 sl=64 | p50 | 82,959 us | 89,637 us | 97,860 us | -7.4% | -15.2% |
| bs=100 sw=10 sl=64 | p95 | 94,849 us | 148,502 us | 104,570 us | -36.1% | -9.3% |
| bs=100 sw=10 sl=64 | p99 | 94,849 us | 148,502 us | 117,811 us | -36.1% | -19.5% |
| bs=1000 sw=10 sl=64 | throughput | 1,347 tuples/sec | 1,351 tuples/sec | 1,066 tuples/sec | -0.3% | +26.4% |
| bs=1000 sw=10 sl=64 | MB/s | 0.822 MB/s | 0.824 MB/s | 0.651 MB/s | -0.2% | +26.3% |
| bs=1000 sw=10 sl=64 | p50 | 738,829 us | 735,998 us | 958,266 us | +0.4% | -22.9% |
| bs=1000 sw=10 sl=64 | p95 | 788,919 us | 773,147 us | 998,734 us | +2.0% | -21.0% |
| bs=1000 sw=10 sl=64 | p99 | 788,919 us | 773,147 us | 1,032,953 us | +2.0% | -23.6% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,379.93,200,128000,526,0.321,18781.91,26445.59,26445.59
1,100,10,64,20,1686.64,2000,1280000,1186,0.724,82959.09,94849.40,94849.40
2,1000,10,64,20,14848.28,20000,12800000,1347,0.822,738829.22,788919.14,788919.14|
@carloea2 a new source, still a draft like the other two new operators: it reads a Parquet file, taking the column types from the file's own footer rather than inferring them. Would you take a look when you have a moment? |
carloea2
left a comment
There was a problem hiding this comment.
Two data-loss cases reproduced with real Parquet files and the compiled native reader.
A DECIMAL column was read as the integer it is stored in, so a file meaning 12.34 arrived as 1234. It is read as the number the scale makes of it, in each of the three storages Parquet allows for one, and the exported script casts the column pandas fills with Decimal objects so both sides hold the same type. A timestamp counted in microseconds or nanoseconds was rounded to the millisecond on the way into a java.sql.Timestamp, which holds nanos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What changes were proposed in this PR?
A source that reads a Parquet file. Texera read CSV, JSONL, Arrow and plain text off disk but not Parquet, and converting one to CSV first loses what the file knew: the column written as an INTEGER came back as text for the schema to guess at again.
The format states its own types in a footer, so this source infers nothing. It reads a row group at a time rather than the whole file, which is the thing a columnar format is chosen to avoid, and a column that is a group, a list or a map is refused by name instead of being dropped in silence.
A timestamp is read with UTC arithmetic, matching what
ArrowUtilsmeans by a Texera TIMESTAMP: the count from the epoch lands on a wall clock, and no zone of the machine's own enters it. Reading it any other way would move the value and part the engine from the script the export writes.No new dependency.
parquet-hadoopandparquet-columnare already on the classpath undericeberg-parquet, and the reader takes aLocalInputFile, so none of Hadoop's own file plumbing is involved.Any related issues, documentation, discussions?
Not part of #8325: nothing here makes a workflow exportable. It does ship standalone code, so it reads the trait that issue introduced.
It stays a draft until the set in #8325 has merged, together with the two other new operators, rather than because anything here is unfinished: the trait it reads landed with #8502, and this branch builds and tests green on
maintoday.Closes #8511, the task this change is the whole of.
How was this PR tested?
Thirteen tests in the operator's own spec, over a Parquet file the spec writes: the columns the footer states, the values read back, a row that wrote no field at all, the timestamp that a zone could have moved, a nested column refused by name, a file that is not Parquet, and the Python the export emits with each scan window.
The parity harness needs no per-operator code for it. A scan source is fixtured by the format it declares, so an encoder for
"Parquet"is the whole of what it asks for, and that entry lands with the harness in #8364. With both in place the operator runs on the canonical table both ways and the two answers match.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)
🤖 Generated with Claude Code