pqbench 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pqbench-0.0.2/Cargo.toml +42 -0
- pqbench-0.0.2/PKG-INFO +188 -0
- pqbench-0.0.2/README.md +169 -0
- pqbench-0.0.2/crates/pqbench/Cargo.toml +53 -0
- pqbench-0.0.2/crates/pqbench/src/bench.rs +112 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/aggregate.rs +137 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/analytics.rs +121 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/api.rs +207 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/collection.rs +213 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/json.rs +61 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/mod.rs +30 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/pages.rs +89 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/raw.rs +75 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/remote.rs +143 -0
- pqbench-0.0.2/crates/pqbench/src/bytemass/text.rs +90 -0
- pqbench-0.0.2/crates/pqbench/src/capability.rs +63 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/get/api.rs +38 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/get/impl.rs +58 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/get/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/list/api.rs +37 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/list/impl.rs +78 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/list/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/catalog/mod.rs +7 -0
- pqbench-0.0.2/crates/pqbench/src/codecs.rs +475 -0
- pqbench-0.0.2/crates/pqbench/src/compression/analytics.rs +90 -0
- pqbench-0.0.2/crates/pqbench/src/compression/api.rs +59 -0
- pqbench-0.0.2/crates/pqbench/src/compression/json.rs +13 -0
- pqbench-0.0.2/crates/pqbench/src/compression/mod.rs +106 -0
- pqbench-0.0.2/crates/pqbench/src/compression/raw.rs +104 -0
- pqbench-0.0.2/crates/pqbench/src/compression/text.rs +52 -0
- pqbench-0.0.2/crates/pqbench/src/config.rs +89 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/check/api.rs +96 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/check/impl.rs +140 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/check/mod.rs +6 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/get/api.rs +52 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/get/impl.rs +292 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/get/mod.rs +6 -0
- pqbench-0.0.2/crates/pqbench/src/credentials/mod.rs +6 -0
- pqbench-0.0.2/crates/pqbench/src/dialect.rs +88 -0
- pqbench-0.0.2/crates/pqbench/src/diff.rs +110 -0
- pqbench-0.0.2/crates/pqbench/src/experiment.rs +1113 -0
- pqbench-0.0.2/crates/pqbench/src/file/list/api.rs +54 -0
- pqbench-0.0.2/crates/pqbench/src/file/list/impl.rs +29 -0
- pqbench-0.0.2/crates/pqbench/src/file/list/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/file/mod.rs +7 -0
- pqbench-0.0.2/crates/pqbench/src/lib.rs +32 -0
- pqbench-0.0.2/crates/pqbench/src/lz/analytics.rs +25 -0
- pqbench-0.0.2/crates/pqbench/src/lz/api.rs +46 -0
- pqbench-0.0.2/crates/pqbench/src/lz/json.rs +13 -0
- pqbench-0.0.2/crates/pqbench/src/lz/mod.rs +121 -0
- pqbench-0.0.2/crates/pqbench/src/lz/raw.rs +42 -0
- pqbench-0.0.2/crates/pqbench/src/lz/text.rs +12 -0
- pqbench-0.0.2/crates/pqbench/src/metastore/get/api.rs +37 -0
- pqbench-0.0.2/crates/pqbench/src/metastore/get/impl.rs +64 -0
- pqbench-0.0.2/crates/pqbench/src/metastore/get/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/metastore/mod.rs +5 -0
- pqbench-0.0.2/crates/pqbench/src/partition/list/api.rs +61 -0
- pqbench-0.0.2/crates/pqbench/src/partition/list/impl.rs +143 -0
- pqbench-0.0.2/crates/pqbench/src/partition/list/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/partition/mod.rs +8 -0
- pqbench-0.0.2/crates/pqbench/src/profile.rs +538 -0
- pqbench-0.0.2/crates/pqbench/src/ratelimit.rs +75 -0
- pqbench-0.0.2/crates/pqbench/src/report.rs +136 -0
- pqbench-0.0.2/crates/pqbench/src/schema/get/api.rs +39 -0
- pqbench-0.0.2/crates/pqbench/src/schema/get/impl.rs +104 -0
- pqbench-0.0.2/crates/pqbench/src/schema/get/mod.rs +11 -0
- pqbench-0.0.2/crates/pqbench/src/schema/list/api.rs +39 -0
- pqbench-0.0.2/crates/pqbench/src/schema/list/impl.rs +140 -0
- pqbench-0.0.2/crates/pqbench/src/schema/list/mod.rs +12 -0
- pqbench-0.0.2/crates/pqbench/src/schema/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/skill.rs +129 -0
- pqbench-0.0.2/crates/pqbench/src/stats.rs +217 -0
- pqbench-0.0.2/crates/pqbench/src/table/get/api.rs +57 -0
- pqbench-0.0.2/crates/pqbench/src/table/get/impl.rs +312 -0
- pqbench-0.0.2/crates/pqbench/src/table/get/mod.rs +11 -0
- pqbench-0.0.2/crates/pqbench/src/table/list/api.rs +42 -0
- pqbench-0.0.2/crates/pqbench/src/table/list/impl.rs +170 -0
- pqbench-0.0.2/crates/pqbench/src/table/list/mod.rs +11 -0
- pqbench-0.0.2/crates/pqbench/src/table/mod.rs +689 -0
- pqbench-0.0.2/crates/pqbench/src/table/selection.rs +88 -0
- pqbench-0.0.2/crates/pqbench/src/text.rs +34 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/avro/api.rs +41 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/avro/impl.rs +38 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/avro/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/delta/api.rs +49 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/delta/impl/load.rs +671 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/delta/impl.rs +77 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/delta/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/flate2/api.rs +32 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/flate2/impl.rs +29 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/flate2/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/api.rs +51 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/impl/load.rs +689 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/impl.rs +77 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/lz4/api.rs +32 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/lz4/impl.rs +17 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/lz4/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/mod.rs +15 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/object_store/api.rs +339 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/object_store/impl/remote.rs +542 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/object_store/impl.rs +77 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/object_store/mod.rs +10 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/parquet/api.rs +533 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/parquet/impl.rs +807 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/parquet/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/reqwest/api.rs +96 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/reqwest/impl.rs +76 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/reqwest/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/snappy/api.rs +32 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/snappy/impl.rs +45 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/snappy/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/zstd/api.rs +32 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/zstd/impl.rs +16 -0
- pqbench-0.0.2/crates/pqbench/src/third_party/zstd/mod.rs +9 -0
- pqbench-0.0.2/crates/pqbench/src/v1.rs +863 -0
- pqbench-0.0.2/crates/pqbench/src/viz/html.rs +165 -0
- pqbench-0.0.2/crates/pqbench/src/viz/mod.rs +158 -0
- pqbench-0.0.2/crates/pqbench/tests/bytemass_e2e.rs +184 -0
- pqbench-0.0.2/crates/pqbench/tests/common/mod.rs +15 -0
- pqbench-0.0.2/crates/pqbench/tests/compression_e2e.rs +108 -0
- pqbench-0.0.2/crates/pqbench/tests/delta.rs +697 -0
- pqbench-0.0.2/crates/pqbench/tests/diff.rs +51 -0
- pqbench-0.0.2/crates/pqbench/tests/file_metadata.rs +88 -0
- pqbench-0.0.2/crates/pqbench/tests/file_selection.rs +51 -0
- pqbench-0.0.2/crates/pqbench/tests/fixtures/small_reddit_none.parquet +3 -0
- pqbench-0.0.2/crates/pqbench/tests/fixtures/small_snappy.parquet +3 -0
- pqbench-0.0.2/crates/pqbench/tests/iceberg.rs +438 -0
- pqbench-0.0.2/crates/pqbench/tests/lz_e2e.rs +104 -0
- pqbench-0.0.2/crates/pqbench/tests/object_patterns.rs +23 -0
- pqbench-0.0.2/crates/pqbench/tests/page_headers.rs +108 -0
- pqbench-0.0.2/crates/pqbench/tests/redset_e2e.rs +37 -0
- pqbench-0.0.2/crates/pqbench/tests/support/mod.rs +126 -0
- pqbench-0.0.2/pqbench/__init__.py +79 -0
- pqbench-0.0.2/pqbench/native.pyi +390 -0
- pqbench-0.0.2/pqbench/py.typed +0 -0
- pqbench-0.0.2/pqbench/walk.py +194 -0
- pqbench-0.0.2/pyproject.toml +36 -0
- pqbench-0.0.2/python/Cargo.lock +4690 -0
- pqbench-0.0.2/python/Cargo.toml +48 -0
- pqbench-0.0.2/python/README.md +169 -0
- pqbench-0.0.2/python/samples/walk.py +37 -0
- pqbench-0.0.2/python/src/bytemass.rs +32 -0
- pqbench-0.0.2/python/src/catalog.rs +50 -0
- pqbench-0.0.2/python/src/credentials.rs +55 -0
- pqbench-0.0.2/python/src/file.rs +33 -0
- pqbench-0.0.2/python/src/handles.rs +170 -0
- pqbench-0.0.2/python/src/lib.rs +89 -0
- pqbench-0.0.2/python/src/marshal.rs +160 -0
- pqbench-0.0.2/python/src/metastore.rs +25 -0
- pqbench-0.0.2/python/src/partition.rs +37 -0
- pqbench-0.0.2/python/src/prelude.rs +23 -0
- pqbench-0.0.2/python/src/ratelimit.rs +28 -0
- pqbench-0.0.2/python/src/records.rs +200 -0
- pqbench-0.0.2/python/src/requests.rs +50 -0
- pqbench-0.0.2/python/src/schema.rs +54 -0
- pqbench-0.0.2/python/src/table.rs +65 -0
- pqbench-0.0.2/python/tests/test_bindings.py +173 -0
- pqbench-0.0.2/python/tests/test_dbx_e2e.py +141 -0
- pqbench-0.0.2/python/tests/test_native_stub.py +258 -0
pqbench-0.0.2/Cargo.toml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
resolver = "2"
|
|
3
|
+
members = [
|
|
4
|
+
"crates/aipnaming",
|
|
5
|
+
"crates/aipnaming-cli",
|
|
6
|
+
"crates/docscheck",
|
|
7
|
+
"crates/docscheck-cli",
|
|
8
|
+
"crates/pqbench",
|
|
9
|
+
"crates/pqbench-cli",
|
|
10
|
+
]
|
|
11
|
+
exclude = ["python"]
|
|
12
|
+
default-members = [
|
|
13
|
+
"crates/aipnaming",
|
|
14
|
+
"crates/aipnaming-cli",
|
|
15
|
+
"crates/docscheck",
|
|
16
|
+
"crates/docscheck-cli",
|
|
17
|
+
"crates/pqbench",
|
|
18
|
+
"crates/pqbench-cli",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[workspace.package]
|
|
22
|
+
version = "0.0.2"
|
|
23
|
+
edition = "2021"
|
|
24
|
+
rust-version = "1.91.1"
|
|
25
|
+
license = "MIT OR Apache-2.0"
|
|
26
|
+
authors = ["pqbench contributors"]
|
|
27
|
+
repository = "https://github.com/pqbench/pqbench"
|
|
28
|
+
|
|
29
|
+
[workspace.dependencies]
|
|
30
|
+
aipnaming = { version = "0.0.2", path = "crates/aipnaming" }
|
|
31
|
+
docscheck = { version = "0.0.2", path = "crates/docscheck" }
|
|
32
|
+
pqbench = { version = "0.0.2", path = "crates/pqbench" }
|
|
33
|
+
|
|
34
|
+
[profile.dev]
|
|
35
|
+
opt-level = 0
|
|
36
|
+
debug = 1
|
|
37
|
+
incremental = true
|
|
38
|
+
|
|
39
|
+
# The profile that 'dist' will build with
|
|
40
|
+
[profile.dist]
|
|
41
|
+
inherits = "release"
|
|
42
|
+
lto = "thin"
|
pqbench-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pqbench
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Rust
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Summary: Python bindings for the pqbench command line.
|
|
10
|
+
Keywords: parquet,compression,benchmark
|
|
11
|
+
Author: pqbench contributors
|
|
12
|
+
License-Expression: MIT OR Apache-2.0
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
15
|
+
Project-URL: Documentation, https://github.com/pqbench/pqbench/tree/main/python
|
|
16
|
+
Project-URL: Homepage, https://github.com/pqbench/pqbench
|
|
17
|
+
Project-URL: Repository, https://github.com/pqbench/pqbench
|
|
18
|
+
|
|
19
|
+
# pqbench
|
|
20
|
+
|
|
21
|
+
PyO3 bindings for [pqbench](https://github.com/pqbench/pqbench). `pip install`
|
|
22
|
+
builds a native wheel; a separate `pqbench` binary is not required. Two
|
|
23
|
+
surfaces, one wheel:
|
|
24
|
+
|
|
25
|
+
* **`pqbench.native`** — the commands: one function per library path, with the
|
|
26
|
+
same arguments. Every name is the Rust function of the same path.
|
|
27
|
+
* **`pqbench`** — the ergonomic surface over them: the client handles every
|
|
28
|
+
command takes (`Config`, `Dialect`, `Lake`, `Layer`, `Workspace`) and `walk`,
|
|
29
|
+
the whole-metastore walk composed once.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install pqbench
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The wheel carries every storage backend (`delta-s3`, `iceberg-s3`), so `s3://`
|
|
38
|
+
reads and Delta/Iceberg tables work out of the box. A storage read
|
|
39
|
+
authenticates with the `credentials=` argument (a `Credentials` record) or,
|
|
40
|
+
for `bytemass`, with `env` on its request — or with the process environment.
|
|
41
|
+
|
|
42
|
+
From a checkout:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install maturin
|
|
46
|
+
maturin develop --manifest-path python/Cargo.toml
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## The commands
|
|
50
|
+
|
|
51
|
+
Each command is the library function of the same path, under `pqbench.native`,
|
|
52
|
+
and takes the handles and the resource's name like the Rust signature: the name
|
|
53
|
+
is the AIP resource name a parent record carries, so a record feeds the next
|
|
54
|
+
command as it is. `credentials.get` takes a table's name and returns its lease,
|
|
55
|
+
the `Credentials` record a storage read takes, and a record is its model's
|
|
56
|
+
class: `record["uri"]`, `record.uri` and `record.asdict()` agree.
|
|
57
|
+
|
|
58
|
+
They are the walk's own pipeline — metastore, catalog, schema, table,
|
|
59
|
+
credentials, partition, file, bytemass — and they stop at `bytemass`: the
|
|
60
|
+
commands downstream of a byte-mass stream (`viz`, `diff`, `profile`,
|
|
61
|
+
`experiment`), the standalone codec benchmarks (`lz`, `compression`) and
|
|
62
|
+
`skill` are separate tools, and are not bound.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
import pqbench
|
|
67
|
+
import pqbench.native
|
|
68
|
+
|
|
69
|
+
async def main():
|
|
70
|
+
workspace = pqbench.Workspace(pqbench.Config("https://…", token="…"))
|
|
71
|
+
lake = pqbench.Lake(workspace)
|
|
72
|
+
|
|
73
|
+
# The walk, one function per resource. The I/O verbs are awaitable.
|
|
74
|
+
await pqbench.native.metastore.get.get(workspace)
|
|
75
|
+
await pqbench.native.catalog.get.get(workspace, "catalogs/dbx_samples")
|
|
76
|
+
await pqbench.native.catalog.list.list(workspace)
|
|
77
|
+
await pqbench.native.schema.list.list(workspace, "catalogs/dbx_samples")
|
|
78
|
+
refs = await pqbench.native.table.list.list(
|
|
79
|
+
workspace, "catalogs/dbx_samples/schemas/nyctaxi"
|
|
80
|
+
)
|
|
81
|
+
await pqbench.native.credentials.check.check(
|
|
82
|
+
workspace, "catalogs/dbx_samples/schemas/nyctaxi/tables/trips"
|
|
83
|
+
)
|
|
84
|
+
# `credentials.get.get` is name -> lease: the lease is what a storage read
|
|
85
|
+
# takes, from the table's name alone.
|
|
86
|
+
credentials = await pqbench.native.credentials.get.get(workspace, refs[0]["name"])
|
|
87
|
+
table = await pqbench.native.table.get.get(
|
|
88
|
+
workspace,
|
|
89
|
+
lake,
|
|
90
|
+
refs[0]["name"], refs[0].get("uri"), credentials,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# `partition.list.list` and `file.list.list` stream (async iterators); the
|
|
94
|
+
# single-value verbs are awaitable.
|
|
95
|
+
async for partition in pqbench.native.partition.list.list(
|
|
96
|
+
lake, table["name"], table["uri"], table.get("credentials"), 86_400_000
|
|
97
|
+
):
|
|
98
|
+
async for file in pqbench.native.file.list.list(
|
|
99
|
+
lake, partition["uri"], partition.get("credentials"), partition["commit_versions"]
|
|
100
|
+
):
|
|
101
|
+
# The pipeline stops here: `bytemass` measures the file's bytes.
|
|
102
|
+
rows = await pqbench.native.bytemass.bytemass(
|
|
103
|
+
lake, pqbench.native.BytemassRequest(inputs=[file["uri"]])
|
|
104
|
+
)
|
|
105
|
+
print(len(rows), file["uri"])
|
|
106
|
+
|
|
107
|
+
asyncio.run(main())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### The walk
|
|
111
|
+
|
|
112
|
+
`walk` is the one composition rather than a binding: it spans the catalog
|
|
113
|
+
levels and descends every table the principal may read outside compute, with
|
|
114
|
+
the tables in flight together (bounded by `concurrency`, 32 by default), and
|
|
115
|
+
yields each table file.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
async for file in pqbench.walk(workspace):
|
|
119
|
+
print(file["uri"])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`catalog=` descends one catalog instead of every readable one. A table that
|
|
123
|
+
cannot be read is skipped (logged at `debug`), so a mixed metastore still
|
|
124
|
+
walks; records stream, so the walk holds no more than a bounded queue of them.
|
|
125
|
+
|
|
126
|
+
The Unity Catalog API limits requests per endpoint and per workspace, so its
|
|
127
|
+
calls are paced at `requests_per_second` — 15, one bucket per endpoint, and `0`
|
|
128
|
+
turns pacing off. The storage reads are not paced: object storage has no such
|
|
129
|
+
limit and retries its own. Anything skipped is counted in a `warning` when the
|
|
130
|
+
walk ends, so a rate limit shows up as a count rather than as silently missing
|
|
131
|
+
files.
|
|
132
|
+
|
|
133
|
+
`bytemass` is the last command: it reads what `file.list.list` yields — a
|
|
134
|
+
path or URI per file — and returns one `MassRow` per column chunk.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
rows = await pqbench.native.bytemass.bytemass(
|
|
138
|
+
lake, pqbench.native.BytemassRequest(inputs=["s3://…/part-00000.parquet"])
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
[`samples/walk.py`](samples/walk.py) is a runnable walk over the **whole
|
|
143
|
+
metastore**: it calls `walk`, spans every catalog and schema, descends the one
|
|
144
|
+
catalog the principal may read outside compute, and prints each table file.
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
PQB_ENDPOINT=https://<workspace>.cloud.databricks.com PQB_TOKEN=<bearer> \
|
|
148
|
+
AWS_REGION=us-east-2 python samples/walk.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Configuration
|
|
152
|
+
|
|
153
|
+
`Config(endpoint, token=None, dialect=Dialect.UNITY)` carries the endpoint, the
|
|
154
|
+
bearer, and the dialect (`UNITY` or `ICEBERG`); `Layer(endpoint=None,
|
|
155
|
+
token=None, dialect=None).resolve()` combines configuration layers into a
|
|
156
|
+
`Config`.
|
|
157
|
+
|
|
158
|
+
`credentials` is a `Credentials` record — what `credentials.get` returns — or
|
|
159
|
+
the mapping of the same fields; a read given none falls back to the process
|
|
160
|
+
environment. A request's `env` is the library's `BTreeMap<String, String>`,
|
|
161
|
+
passed straight through. A `BytemassRequest` names `inputs` (paths or glob
|
|
162
|
+
masks), `env` (the `AWS_*` options a remote read uses) and `indexes` (load the
|
|
163
|
+
parquet page indexes too, one extra range per file).
|
|
164
|
+
|
|
165
|
+
## Publish to PyPI
|
|
166
|
+
|
|
167
|
+
[`.github/workflows/publish-pypi.yml`](../.github/workflows/publish-pypi.yml)
|
|
168
|
+
builds manylinux, macOS, and Windows wheels with
|
|
169
|
+
[maturin](https://www.maturin.rs/) and uploads them with
|
|
170
|
+
[PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) — no token
|
|
171
|
+
is stored. It has its own dispatch, after the matching tag is released:
|
|
172
|
+
`gh workflow run publish-pypi.yml`, or **Publish Python package** from the
|
|
173
|
+
Actions tab. Publishing is separate because PyPI names the workflow file that
|
|
174
|
+
may upload, and its trusted publisher does not accept a job defined in a
|
|
175
|
+
workflow that another workflow calls.
|
|
176
|
+
|
|
177
|
+
Its version is the workspace's: `scripts/version.sh sync` writes
|
|
178
|
+
`[workspace.package] version` from `Cargo.toml` into `python/Cargo.toml` before
|
|
179
|
+
maturin reads it, and `make check-version` fails when the two disagree —
|
|
180
|
+
`python/` is its own crate and cannot inherit it.
|
|
181
|
+
|
|
182
|
+
Before the first upload:
|
|
183
|
+
|
|
184
|
+
1. On PyPI, add a pending publisher for this repository: owner `pqbench`,
|
|
185
|
+
project `pqbench`, workflow `publish-pypi.yml`, environment `pypi`.
|
|
186
|
+
2. In the GitHub repository settings, create an environment named `pypi`.
|
|
187
|
+
3. Run the **Publish Python package** workflow.
|
|
188
|
+
|
pqbench-0.0.2/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# pqbench
|
|
2
|
+
|
|
3
|
+
PyO3 bindings for [pqbench](https://github.com/pqbench/pqbench). `pip install`
|
|
4
|
+
builds a native wheel; a separate `pqbench` binary is not required. Two
|
|
5
|
+
surfaces, one wheel:
|
|
6
|
+
|
|
7
|
+
* **`pqbench.native`** — the commands: one function per library path, with the
|
|
8
|
+
same arguments. Every name is the Rust function of the same path.
|
|
9
|
+
* **`pqbench`** — the ergonomic surface over them: the client handles every
|
|
10
|
+
command takes (`Config`, `Dialect`, `Lake`, `Layer`, `Workspace`) and `walk`,
|
|
11
|
+
the whole-metastore walk composed once.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install pqbench
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The wheel carries every storage backend (`delta-s3`, `iceberg-s3`), so `s3://`
|
|
20
|
+
reads and Delta/Iceberg tables work out of the box. A storage read
|
|
21
|
+
authenticates with the `credentials=` argument (a `Credentials` record) or,
|
|
22
|
+
for `bytemass`, with `env` on its request — or with the process environment.
|
|
23
|
+
|
|
24
|
+
From a checkout:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install maturin
|
|
28
|
+
maturin develop --manifest-path python/Cargo.toml
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The commands
|
|
32
|
+
|
|
33
|
+
Each command is the library function of the same path, under `pqbench.native`,
|
|
34
|
+
and takes the handles and the resource's name like the Rust signature: the name
|
|
35
|
+
is the AIP resource name a parent record carries, so a record feeds the next
|
|
36
|
+
command as it is. `credentials.get` takes a table's name and returns its lease,
|
|
37
|
+
the `Credentials` record a storage read takes, and a record is its model's
|
|
38
|
+
class: `record["uri"]`, `record.uri` and `record.asdict()` agree.
|
|
39
|
+
|
|
40
|
+
They are the walk's own pipeline — metastore, catalog, schema, table,
|
|
41
|
+
credentials, partition, file, bytemass — and they stop at `bytemass`: the
|
|
42
|
+
commands downstream of a byte-mass stream (`viz`, `diff`, `profile`,
|
|
43
|
+
`experiment`), the standalone codec benchmarks (`lz`, `compression`) and
|
|
44
|
+
`skill` are separate tools, and are not bound.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
import pqbench
|
|
49
|
+
import pqbench.native
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
workspace = pqbench.Workspace(pqbench.Config("https://…", token="…"))
|
|
53
|
+
lake = pqbench.Lake(workspace)
|
|
54
|
+
|
|
55
|
+
# The walk, one function per resource. The I/O verbs are awaitable.
|
|
56
|
+
await pqbench.native.metastore.get.get(workspace)
|
|
57
|
+
await pqbench.native.catalog.get.get(workspace, "catalogs/dbx_samples")
|
|
58
|
+
await pqbench.native.catalog.list.list(workspace)
|
|
59
|
+
await pqbench.native.schema.list.list(workspace, "catalogs/dbx_samples")
|
|
60
|
+
refs = await pqbench.native.table.list.list(
|
|
61
|
+
workspace, "catalogs/dbx_samples/schemas/nyctaxi"
|
|
62
|
+
)
|
|
63
|
+
await pqbench.native.credentials.check.check(
|
|
64
|
+
workspace, "catalogs/dbx_samples/schemas/nyctaxi/tables/trips"
|
|
65
|
+
)
|
|
66
|
+
# `credentials.get.get` is name -> lease: the lease is what a storage read
|
|
67
|
+
# takes, from the table's name alone.
|
|
68
|
+
credentials = await pqbench.native.credentials.get.get(workspace, refs[0]["name"])
|
|
69
|
+
table = await pqbench.native.table.get.get(
|
|
70
|
+
workspace,
|
|
71
|
+
lake,
|
|
72
|
+
refs[0]["name"], refs[0].get("uri"), credentials,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# `partition.list.list` and `file.list.list` stream (async iterators); the
|
|
76
|
+
# single-value verbs are awaitable.
|
|
77
|
+
async for partition in pqbench.native.partition.list.list(
|
|
78
|
+
lake, table["name"], table["uri"], table.get("credentials"), 86_400_000
|
|
79
|
+
):
|
|
80
|
+
async for file in pqbench.native.file.list.list(
|
|
81
|
+
lake, partition["uri"], partition.get("credentials"), partition["commit_versions"]
|
|
82
|
+
):
|
|
83
|
+
# The pipeline stops here: `bytemass` measures the file's bytes.
|
|
84
|
+
rows = await pqbench.native.bytemass.bytemass(
|
|
85
|
+
lake, pqbench.native.BytemassRequest(inputs=[file["uri"]])
|
|
86
|
+
)
|
|
87
|
+
print(len(rows), file["uri"])
|
|
88
|
+
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### The walk
|
|
93
|
+
|
|
94
|
+
`walk` is the one composition rather than a binding: it spans the catalog
|
|
95
|
+
levels and descends every table the principal may read outside compute, with
|
|
96
|
+
the tables in flight together (bounded by `concurrency`, 32 by default), and
|
|
97
|
+
yields each table file.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
async for file in pqbench.walk(workspace):
|
|
101
|
+
print(file["uri"])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`catalog=` descends one catalog instead of every readable one. A table that
|
|
105
|
+
cannot be read is skipped (logged at `debug`), so a mixed metastore still
|
|
106
|
+
walks; records stream, so the walk holds no more than a bounded queue of them.
|
|
107
|
+
|
|
108
|
+
The Unity Catalog API limits requests per endpoint and per workspace, so its
|
|
109
|
+
calls are paced at `requests_per_second` — 15, one bucket per endpoint, and `0`
|
|
110
|
+
turns pacing off. The storage reads are not paced: object storage has no such
|
|
111
|
+
limit and retries its own. Anything skipped is counted in a `warning` when the
|
|
112
|
+
walk ends, so a rate limit shows up as a count rather than as silently missing
|
|
113
|
+
files.
|
|
114
|
+
|
|
115
|
+
`bytemass` is the last command: it reads what `file.list.list` yields — a
|
|
116
|
+
path or URI per file — and returns one `MassRow` per column chunk.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
rows = await pqbench.native.bytemass.bytemass(
|
|
120
|
+
lake, pqbench.native.BytemassRequest(inputs=["s3://…/part-00000.parquet"])
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
[`samples/walk.py`](samples/walk.py) is a runnable walk over the **whole
|
|
125
|
+
metastore**: it calls `walk`, spans every catalog and schema, descends the one
|
|
126
|
+
catalog the principal may read outside compute, and prints each table file.
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
PQB_ENDPOINT=https://<workspace>.cloud.databricks.com PQB_TOKEN=<bearer> \
|
|
130
|
+
AWS_REGION=us-east-2 python samples/walk.py
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
`Config(endpoint, token=None, dialect=Dialect.UNITY)` carries the endpoint, the
|
|
136
|
+
bearer, and the dialect (`UNITY` or `ICEBERG`); `Layer(endpoint=None,
|
|
137
|
+
token=None, dialect=None).resolve()` combines configuration layers into a
|
|
138
|
+
`Config`.
|
|
139
|
+
|
|
140
|
+
`credentials` is a `Credentials` record — what `credentials.get` returns — or
|
|
141
|
+
the mapping of the same fields; a read given none falls back to the process
|
|
142
|
+
environment. A request's `env` is the library's `BTreeMap<String, String>`,
|
|
143
|
+
passed straight through. A `BytemassRequest` names `inputs` (paths or glob
|
|
144
|
+
masks), `env` (the `AWS_*` options a remote read uses) and `indexes` (load the
|
|
145
|
+
parquet page indexes too, one extra range per file).
|
|
146
|
+
|
|
147
|
+
## Publish to PyPI
|
|
148
|
+
|
|
149
|
+
[`.github/workflows/publish-pypi.yml`](../.github/workflows/publish-pypi.yml)
|
|
150
|
+
builds manylinux, macOS, and Windows wheels with
|
|
151
|
+
[maturin](https://www.maturin.rs/) and uploads them with
|
|
152
|
+
[PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) — no token
|
|
153
|
+
is stored. It has its own dispatch, after the matching tag is released:
|
|
154
|
+
`gh workflow run publish-pypi.yml`, or **Publish Python package** from the
|
|
155
|
+
Actions tab. Publishing is separate because PyPI names the workflow file that
|
|
156
|
+
may upload, and its trusted publisher does not accept a job defined in a
|
|
157
|
+
workflow that another workflow calls.
|
|
158
|
+
|
|
159
|
+
Its version is the workspace's: `scripts/version.sh sync` writes
|
|
160
|
+
`[workspace.package] version` from `Cargo.toml` into `python/Cargo.toml` before
|
|
161
|
+
maturin reads it, and `make check-version` fails when the two disagree —
|
|
162
|
+
`python/` is its own crate and cannot inherit it.
|
|
163
|
+
|
|
164
|
+
Before the first upload:
|
|
165
|
+
|
|
166
|
+
1. On PyPI, add a pending publisher for this repository: owner `pqbench`,
|
|
167
|
+
project `pqbench`, workflow `publish-pypi.yml`, environment `pypi`.
|
|
168
|
+
2. In the GitHub repository settings, create an environment named `pypi`.
|
|
169
|
+
3. Run the **Publish Python package** workflow.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "pqbench"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
license.workspace = true
|
|
7
|
+
authors.workspace = true
|
|
8
|
+
repository.workspace = true
|
|
9
|
+
description = "A benchmarking library for Parquet files (in-memory read/write throughput, compression ratio)."
|
|
10
|
+
|
|
11
|
+
[features]
|
|
12
|
+
default = []
|
|
13
|
+
# S3 object reads. Without it, `s3://` fails at runtime, not at compile time.
|
|
14
|
+
# reqwest signs a HEAD to read `x-amz-storage-class`, which object_store drops.
|
|
15
|
+
aws = ["dep:object_store", "object_store/aws", "dep:async-trait"]
|
|
16
|
+
# Delta snapshot resolution requires Rust 1.94.1 or newer.
|
|
17
|
+
delta = ["dep:deltalake", "dep:futures"]
|
|
18
|
+
delta-s3 = ["delta", "deltalake/s3", "aws"]
|
|
19
|
+
# Iceberg snapshot planning reads metadata JSON and Avro manifests.
|
|
20
|
+
iceberg = ["dep:apache-avro"]
|
|
21
|
+
iceberg-s3 = ["iceberg", "aws"]
|
|
22
|
+
|
|
23
|
+
[dependencies]
|
|
24
|
+
snappy_src = "0.2.5"
|
|
25
|
+
zstd = "0.13"
|
|
26
|
+
lz4 = "1"
|
|
27
|
+
flate2 = { version = "1", default-features = false, features = ["zlib"] }
|
|
28
|
+
glob = "0.3"
|
|
29
|
+
# Codecs are enabled so `read_sample` can decode compressed files; without
|
|
30
|
+
# them parquet-rs fails at runtime (`Disabled feature at compile time`).
|
|
31
|
+
parquet = { version = "59", default-features = false, features = ["snap", "brotli", "lz4", "zstd", "flate2-rust_backend"] }
|
|
32
|
+
# Native roots only: the OS certificate store, where internal/private CAs live.
|
|
33
|
+
# `rustls-tls-native-roots` also brings the rustls backend and the ring
|
|
34
|
+
# provider, so the client still has TLS without `aws` (object_store is what
|
|
35
|
+
# otherwise enables them).
|
|
36
|
+
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "json"] }
|
|
37
|
+
async-trait = { version = "0.1", optional = true }
|
|
38
|
+
bytes = "1"
|
|
39
|
+
parquet-format-safe = "0.2.4"
|
|
40
|
+
serde = { version = "1.0.229", features = ["derive"] }
|
|
41
|
+
serde_json = "1.0.151"
|
|
42
|
+
deltalake = { version = "1.0", default-features = false, features = ["rustls"], optional = true }
|
|
43
|
+
futures = { version = "0.3", optional = true }
|
|
44
|
+
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
|
|
45
|
+
url = "2"
|
|
46
|
+
# Must match delta-rs's ObjectStore trait version: stores cross that internal
|
|
47
|
+
# boundary. Keep the pin until delta-rs upgrades its dependency.
|
|
48
|
+
object_store = { version = "=0.13.2", optional = true }
|
|
49
|
+
apache-avro = { version = "0.17", optional = true }
|
|
50
|
+
|
|
51
|
+
[dev-dependencies]
|
|
52
|
+
tempfile = "3"
|
|
53
|
+
tokio = { version = "1", features = ["rt", "macros"] }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//! Shared codec-sweep mechanism for the `lz` and `compression` commands.
|
|
2
|
+
//!
|
|
3
|
+
//! The two commands own their own request types ([`crate::lz::LzRequest`],
|
|
4
|
+
//! [`crate::compression::CompressionRequest`]); this module holds only the
|
|
5
|
+
//! resolution they both need: parsing `codec@level` specs, the analytics
|
|
6
|
+
//! config, and the raw pass count. It deliberately carries no request type of
|
|
7
|
+
//! its own, so neither command depends on the other. Its [`Error`] is the error
|
|
8
|
+
//! type both sweeps and their renderers return.
|
|
9
|
+
|
|
10
|
+
use crate::codecs::{Codec, CodecImpl, Error as CodecError};
|
|
11
|
+
use crate::stats;
|
|
12
|
+
|
|
13
|
+
/// The measurement decisions a request resolves to: the codec×level set, the
|
|
14
|
+
/// analytics config, and the raw pass count the upstream wants.
|
|
15
|
+
pub(crate) struct Sweep {
|
|
16
|
+
pub(crate) codec_configs: Vec<(Codec, u8)>,
|
|
17
|
+
pub(crate) stats_config: stats::Config,
|
|
18
|
+
pub(crate) passes: u32,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Resolve the shared sweep decisions from a command's arguments.
|
|
22
|
+
pub(crate) fn resolve(
|
|
23
|
+
codec_specs: &[String],
|
|
24
|
+
samples: u32,
|
|
25
|
+
warmup_iterations: u32,
|
|
26
|
+
mode: stats::Mode,
|
|
27
|
+
) -> Result<Sweep, Error> {
|
|
28
|
+
Ok(Sweep {
|
|
29
|
+
codec_configs: parse_codec_configs(codec_specs)?,
|
|
30
|
+
stats_config: stats::Config {
|
|
31
|
+
warmup_iterations: warmup_iterations as usize,
|
|
32
|
+
mode,
|
|
33
|
+
},
|
|
34
|
+
passes: samples + warmup_iterations,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Errors from a codec sweep: spec parsing, the codec/parquet layers, or
|
|
39
|
+
/// rendering.
|
|
40
|
+
#[derive(Debug)]
|
|
41
|
+
#[non_exhaustive]
|
|
42
|
+
pub enum Error {
|
|
43
|
+
/// A `codec@level` spec could not be parsed.
|
|
44
|
+
Spec(String),
|
|
45
|
+
/// The codec layer failed.
|
|
46
|
+
Codec(CodecError),
|
|
47
|
+
/// The parquet layer failed.
|
|
48
|
+
Parquet(crate::third_party::parquet::api::Error),
|
|
49
|
+
/// A renderer could not serialize its output.
|
|
50
|
+
Json(serde_json::Error),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl std::fmt::Display for Error {
|
|
54
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
55
|
+
match self {
|
|
56
|
+
Error::Spec(e) => write!(f, "{e}"),
|
|
57
|
+
Error::Codec(e) => write!(f, "{e}"),
|
|
58
|
+
Error::Parquet(e) => write!(f, "{e}"),
|
|
59
|
+
Error::Json(e) => write!(f, "json: {e}"),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl std::error::Error for Error {}
|
|
65
|
+
|
|
66
|
+
impl From<CodecError> for Error {
|
|
67
|
+
fn from(e: CodecError) -> Self {
|
|
68
|
+
Error::Codec(e)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl From<crate::third_party::parquet::api::Error> for Error {
|
|
73
|
+
fn from(e: crate::third_party::parquet::api::Error) -> Self {
|
|
74
|
+
Error::Parquet(e)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl From<serde_json::Error> for Error {
|
|
79
|
+
fn from(e: serde_json::Error) -> Self {
|
|
80
|
+
Error::Json(e)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Parse the `codec@level` specs; empty selects every wired codec at its first
|
|
85
|
+
/// level.
|
|
86
|
+
fn parse_codec_configs(specs: &[String]) -> Result<Vec<(Codec, u8)>, Error> {
|
|
87
|
+
if specs.is_empty() {
|
|
88
|
+
return Ok(Codec::all().map(|c| (c, default_level(c))).collect());
|
|
89
|
+
}
|
|
90
|
+
specs.iter().map(|spec| parse_spec(spec)).collect()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// One `codec@level` spec. The `@level` part is optional and defaults to the
|
|
94
|
+
/// codec's lowest level.
|
|
95
|
+
fn parse_spec(spec: &str) -> Result<(Codec, u8), Error> {
|
|
96
|
+
let Some((name, level)) = spec.split_once('@') else {
|
|
97
|
+
let codec = parse_codec(spec)?;
|
|
98
|
+
return Ok((codec, default_level(codec)));
|
|
99
|
+
};
|
|
100
|
+
let level = level
|
|
101
|
+
.parse::<u8>()
|
|
102
|
+
.map_err(|_| Error::Spec(format!("bad level in {spec}")))?;
|
|
103
|
+
Ok((parse_codec(name)?, level))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn parse_codec(name: &str) -> Result<Codec, Error> {
|
|
107
|
+
Codec::from_name(name).ok_or_else(|| Error::Spec(format!("unknown codec: {name}")))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn default_level(codec: Codec) -> u8 {
|
|
111
|
+
codec.level_range().first_level as u8
|
|
112
|
+
}
|