Skip to content

Commit 93c9353

Browse files
committed
Let tpchgen-cli shard the data instead of resharding it
`tpchgen-cli` has `--parts N`, which writes a table as N Parquet files into a directory named for it. That is exactly the shape the storage library's table provider wants -- it scans `*.parquet` under a directory, one partition per file -- so `run_tpch.py` can generate what it needs in one call instead of reading a 220 MB single-file `lineitem` and slicing it into shards itself. Drops `reshard`, the column list it needed, and the `--data`/`--rows` arguments, along with the prerequisite that a reader generate a 1 GB dataset by hand before the script will run at all. `--scale` replaces `--rows` as the size knob, defaulting to a tenth of scale factor 1. The CI step installs `tpchgen-cli` itself rather than inheriting it from the TPC-H dataset step, since it no longer reads that dataset.
1 parent 9c16b97 commit 93c9353

3 files changed

Lines changed: 65 additions & 76 deletions

File tree

.github/workflows/test.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,15 @@ jobs:
164164
cd examples/tpch
165165
uv run --no-project pytest _tests.py
166166
167-
# The script the distributed example's README advertises, against the
168-
# dataset generated above -- so the exact entry point a reader will
169-
# copy-paste cannot rot while its test suite stays green. The row cap
170-
# keeps the reshard and the worker round trip to seconds; correctness
171-
# on the full shape is the pytest suites' job, this is the script.
167+
# The script the distributed example's README advertises, run exactly as
168+
# a reader would copy-paste it -- so that entry point cannot rot while
169+
# its test suite stays green. It generates its own `lineitem` rather than
170+
# reading the dataset above, which is why `tpchgen-cli` is installed here
171+
# too: the step is meant to stand on its own if the TPC-H steps move or
172+
# go away. The scale factor keeps generation and the worker round trip to
173+
# seconds; correctness on the full shape is the pytest suites' job.
172174
- name: Run distributed TPC-H example
173175
if: matrix.wheel-tag == 'abi3'
174-
run: uv run --no-project python examples/distributed/run_tpch.py --rows 400000
176+
run: |
177+
uv pip install tpchgen-cli
178+
uv run --no-project python examples/distributed/run_tpch.py --scale 0.1

examples/distributed/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,19 @@ Each library's tests can be run from its own directory the same way, without
6666
the sibling installs. `storage-library` and `udf-library` need only
6767
`pytest maturin ../../..`.
6868

69-
Against the real TPC-H data — generate it as
70-
[`examples/tpch`](../tpch/README.md) describes, then:
69+
Against real TPC-H data, which the script generates for itself:
7170

7271
```console
72+
$ uv pip install tpchgen-cli
7373
$ uv run python ../run_tpch.py --partitions 4
7474
```
7575

76+
`tpchgen-cli` shards natively, so `--partitions 4` asks it for four Parquet
77+
files and tells the engine to use four partitions — the two are the same
78+
number because a distributed engine can only spread work as widely as the data
79+
is split. `--scale` sets how much data; it defaults to a tenth of TPC-H scale
80+
factor 1, and `--scale 1` is the full ~6M row `lineitem`.
81+
7682
## What actually happens
7783

7884
The engine's planner splits the plan at the partial aggregate, which is where

examples/distributed/run_tpch.py

Lines changed: 47 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,31 @@
1717

1818
"""Run TPC-H Q1 across worker processes, and compare against one process.
1919
20+
uv pip install tpchgen-cli
2021
python examples/distributed/run_tpch.py --partitions 4
2122
22-
Needs the TPC-H data the repository's other examples use::
23-
24-
mkdir -p examples/tpch/data && cd examples/tpch/data
25-
uv pip install tpchgen-cli && uv run --no-project tpchgen-cli -s 1 --format=parquet
26-
27-
`tpchgen-cli` writes one file per table, so `lineitem.parquet` is a single
28-
220 MB file -- one partition, and nothing to fan out. This script re-shards
29-
the columns Q1 needs into `--partitions` files first, which is also a fair
30-
illustration of the real constraint: a distributed engine can only spread work
31-
as widely as the data is split.
23+
Generates its own `lineitem` with `tpchgen-cli`, which shards natively: one
24+
`tpchgen-cli parquet --parts N` call writes N Parquet files. That is also a
25+
fair illustration of the real constraint -- a distributed engine can only
26+
spread work as widely as the data is split -- so the number of files is the
27+
same `--partitions` the engine is told to use.
3228
"""
3329

3430
from __future__ import annotations
3531

3632
import argparse
3733
import pathlib
3834
import shutil
39-
import sys
35+
import subprocess
4036
import tempfile
4137
import time
4238

4339
import pyarrow as pa
44-
import pyarrow.parquet as pq
4540
from dfx_engine.driver import run_distributed, run_local
4641
from dfx_engine.session import SessionSpec
4742

48-
# Q1 without the `l_shipdate` filter and the `avg` columns, so the shard below
49-
# stays small. The shape that matters is unchanged: group by two low-cardinality
50-
# columns, aggregate, order.
43+
# Q1 without the `l_shipdate` filter and the `avg` columns. The shape that
44+
# matters is unchanged: group by two low-cardinality columns, aggregate, order.
5145
Q1 = """
5246
select l_returnflag,
5347
l_linestatus,
@@ -61,35 +55,37 @@
6155
order by l_returnflag, l_linestatus
6256
"""
6357

64-
COLUMNS = [
65-
"l_returnflag",
66-
"l_linestatus",
67-
"l_quantity",
68-
"l_extendedprice",
69-
"l_discount",
70-
"l_tax",
71-
]
72-
73-
74-
def reshard(
75-
source: pathlib.Path, into: pathlib.Path, partitions: int, rows: int
76-
) -> int:
77-
"""Write the first `rows` rows of `source` as `partitions` Parquet files."""
78-
into.mkdir(parents=True, exist_ok=True)
79-
table = pq.read_table(source, columns=COLUMNS)
80-
if rows:
81-
table = table.slice(0, rows)
82-
83-
per_file = max(1, table.num_rows // partitions)
84-
written = 0
85-
for index in range(partitions):
86-
offset = index * per_file
87-
length = table.num_rows - offset if index == partitions - 1 else per_file
88-
if length <= 0:
89-
break
90-
pq.write_table(table.slice(offset, length), into / f"part-{index}.parquet")
91-
written += 1
92-
return written
58+
59+
def generate(into: pathlib.Path, partitions: int, scale: float) -> pathlib.Path:
60+
"""Write `lineitem` as `partitions` Parquet files, and return their directory.
61+
62+
`tpchgen-cli` puts a sharded table in a subdirectory named for it, so the
63+
directory this returns is `into/lineitem` -- which is what the storage
64+
library's table provider wants, since it scans `*.parquet` under a
65+
directory and makes one partition per file.
66+
"""
67+
executable = shutil.which("tpchgen-cli")
68+
if executable is None:
69+
message = (
70+
"tpchgen-cli not found on PATH; install it with "
71+
"`uv pip install tpchgen-cli`"
72+
)
73+
raise RuntimeError(message)
74+
75+
subprocess.run( # noqa: S603
76+
[
77+
executable,
78+
"parquet",
79+
f"--scale-factor={scale}",
80+
"--tables=lineitem",
81+
f"--parts={partitions}",
82+
f"--output-dir={into}",
83+
"--no-progress",
84+
"--quiet",
85+
],
86+
check=True,
87+
)
88+
return into / "lineitem"
9389

9490

9591
def compare(table: pa.Table, reference: pa.Table) -> None:
@@ -131,37 +127,20 @@ def compare(table: pa.Table, reference: pa.Table) -> None:
131127

132128
def main(argv: list[str] | None = None) -> int:
133129
parser = argparse.ArgumentParser(description=__doc__)
134-
parser.add_argument(
135-
"--data",
136-
type=pathlib.Path,
137-
default=pathlib.Path(__file__).resolve().parents[1]
138-
/ "tpch"
139-
/ "data"
140-
/ "lineitem.parquet",
141-
)
142130
parser.add_argument("--partitions", type=int, default=4)
143131
parser.add_argument(
144-
"--rows",
145-
type=int,
146-
default=2_000_000,
147-
help="rows to use; 0 for all of them (SF 1 lineitem is ~6M)",
132+
"--scale",
133+
type=float,
134+
default=0.1,
135+
help="TPC-H scale factor; 1 is the full ~6M row lineitem",
148136
)
149137
args = parser.parse_args(argv)
150138

151-
if not args.data.exists():
152-
sys.stderr.write(
153-
f"{args.data} not found. Generate it with:\n"
154-
" mkdir -p examples/tpch/data && cd examples/tpch/data\n"
155-
" uv pip install tpchgen-cli\n"
156-
" uv run --no-project tpchgen-cli -s 1 --format=parquet\n"
157-
)
158-
return 2
159-
160139
workspace = pathlib.Path(tempfile.mkdtemp(prefix="dfx-tpch-"))
161140
try:
162-
data = workspace / "lineitem"
163-
count = reshard(args.data, data, args.partitions, args.rows)
164-
print(f"resharded into {count} file(s) under {data}")
141+
data = generate(workspace, args.partitions, args.scale)
142+
count = len(list(data.glob("*.parquet")))
143+
print(f"generated {count} file(s) under {data}")
165144

166145
spec = SessionSpec(
167146
tables={"lineitem": str(data)},

0 commit comments

Comments
 (0)