Packages

Fast, bounded, and ergonomic Parquet streaming for Elixir on local and S3-compatible object storage, powered by Rust

Current section

Files

Jump to
parquex README.md
Raw

README.md

# Parquex
[![Hex.pm](https://img.shields.io/hexpm/v/parquex.svg)](https://hex.pm/packages/parquex)
[![HexDocs](https://img.shields.io/badge/HexDocs-API%20reference-6e4a7e.svg)](https://hexdocs.pm/parquex)
[![CI](https://github.com/mindreframer/parquex/actions/workflows/ci.yml/badge.svg)](https://github.com/mindreframer/parquex/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mindreframer/parquex/blob/main/LICENSE)
Parquex writes and reads Parquet files on local and S3-compatible object storage from Elixir.
The Parquet encoder, decoder, compression, range reads, and multipart uploads run in Rust. Elixir receives clear store, schema, batch, writer, and stream APIs.
## Installation
Add Parquex to `mix.exs`:
```elixir
def deps do
[
{:parquex, "~> 0.3"}
]
end
```
Supported systems download a precompiled native library during dependency compilation.
## Open a store
A store describes one local directory or one S3-compatible namespace. Every operation uses a relative key inside that store.
```elixir
{:ok, store} = Parquex.Store.open(:local, root: "/srv/my_app/data")
```
For AWS S3 and providers that use the standard AWS credential chain:
```elixir
{:ok, store} =
Parquex.Store.open(:s3,
bucket: "my-events",
prefix: "production",
region: "eu-central-1"
)
```
Custom S3-compatible services can set an endpoint and use path-style requests:
```elixir
{:ok, store} =
Parquex.Store.open(:s3,
bucket: "my-events",
endpoint: "https://s3.eu-central-1.wasabisys.com",
region: "eu-central-1",
path_style: true,
credential_provider: :explicit,
access_key_id: System.fetch_env!("S3_ACCESS_KEY_ID"),
secret_access_key: System.fetch_env!("S3_SECRET_ACCESS_KEY")
)
```
See [Stores](docs/stores.md) for object operations and S3 settings.
## Write and read a Parquet file
`Parquex.write/4` infers a schema from finite rows or columns:
```elixir
rows = [
%{"id" => 1, "name" => "one", "occurred_at" => ~U[2026-08-03 10:00:00Z]},
%{"id" => 2, "name" => "two", "occurred_at" => ~U[2026-08-03 10:00:01Z]}
]
{:ok, metadata} =
Parquex.write(store, "events/part-1.parquet", rows,
compression: :zstd,
batch_rows: 1_024
)
{:ok, rows} = Parquex.read(store, metadata.key)
```
`Parquex.read/3` materializes all selected rows. Use `Parquex.stream/3` when the file can be large:
```elixir
{:ok, stream} =
Parquex.stream(store, "events/part-1.parquet",
columns: [:id, :name],
where: {:gte, :id, 1},
batch_size: 1_024
)
Enum.each(stream, &IO.inspect/1)
```
Each stream item is a bounded `Parquex.Batch`. Reading stops cleanly when enumeration halts or the owning process exits.
## Write continuous input
Continuous input uses an explicit schema and bounded batches:
```elixir
schema =
Parquex.Schema.new!([
{:id, :int64, false},
{:payload, :binary, true}
])
{:ok, writer} =
Parquex.open_writer(store, "events/stream.parquet", schema,
compression: :zstd,
max_batch_rows: 65_536
)
{:ok, batch} =
Parquex.Batch.new(schema, %{
"id" => [1, 2],
"payload" => [<<1>>, nil]
})
:ok = Parquex.Writer.write_batch(writer, batch)
{:ok, metadata} = Parquex.Writer.close(writer)
```
The writer feeds Parquet output directly into a local temporary file or an S3 multipart upload. A valid file becomes visible when `close/1` succeeds. `Parquex.Writer.cancel/1` discards incomplete output.
See [Parquet files](docs/parquet-files.md) for schemas, compression, projection, filtering, and memory bounds.
## Group files by time
`Parquex.Dataset` routes rows into UTC minute, hour, day, ISO week, or month folders and creates uniquely named Parquet parts:
```elixir
schema =
Parquex.Schema.new!([
{:occurred_at, {:timestamp, :microsecond}, false},
{:space_id, :string, false},
{:sequence, :int64, false},
{:payload, :string, false}
])
dataset =
Parquex.Dataset.new!(store, "event_log",
schema: schema,
partition_by: {:time, :occurred_at, :hour},
timestamp_unit: :microsecond,
compression: :zstd
)
{:ok, report} = Parquex.Dataset.write(dataset, events, batch_rows: 1_024)
```
An event at 10:15 UTC is written beneath a key such as:
```text
event_log/year=2026/month=8/day=3/hour=10/part-….parquet
```
Read a half-open time range with `Parquex.Dataset.stream/2` or materialize it with `Parquex.Dataset.read/2`:
```elixir
{:ok, stream} =
Parquex.Dataset.stream(dataset,
from: ~U[2026-08-03 10:00:00Z],
until: ~U[2026-08-03 12:00:00Z],
batch_size: 1_024
)
```
See [Time datasets](docs/datasets.md) for partition paths, rotation, and range reads.
## Replacement and memory behavior
Writing a key that already exists replaces its value. Two writers may target the same key; the writer that completes last determines the stored value. Generated dataset part names make same-key writes unusual for dataset output.
S3 writes use multipart upload directly at the requested key and work with ordinary S3-compatible providers. A transport failure during multipart completion can leave the outcome uncertain, so applications can inspect the key before deciding whether to retry.
Streaming memory is controlled by batch, row-group, page, range, prefetch, request-concurrency, multipart-part, and active-partition limits. Finite `read` and inferred `write` helpers materialize their result or input by design.
## Native runtime
Precompiled NIF 2.16 archives are published for macOS on ARM64 and x86-64, Linux on ARM64 and x86-64 with glibc or musl, and Windows on x86-64. Set `PARQUEX_BUILD=1` to compile the Rust crate locally with the pinned Rust toolchain.
See [Native runtime](docs/runtime.md) for ownership, cancellation, source builds, and supported targets. Telemetry events are described in [Telemetry](docs/telemetry.md).
## Development
Run the full local gate with:
```sh
bin/qa_check.sh
```
The command starts or reuses the project RustFS container and leaves it running for fast repeated checks.