Carquet logo

Apache Parquet · pure C11 · MIT

Parquet at the
speed of C.

A tiny, self-contained library to read and write Parquet files. No runtime, no garbage collector, no 50 MB framework: just tight C compiled down to the instructions your CPU is best at.

Benchmarks

Measured against Arrow C++, PyArrow and fastparquet.

Show as table

Why C

Close to the metal.
Far ahead of the pack.

C has no layer between your data and the silicon. Carquet uses that directly: columns are decoded with vector instructions picked at runtime, uncompressed pages are served straight from the memory map, and memory comes from arenas instead of millions of small allocations.

  • arenaNo per-value mallocMetadata and scratch space come from arena allocators. No garbage collector, no pauses.
  • mmapZero-copy readsUncompressed columns are handed out in place: 10M rows in well under a millisecond.
  • OpenMPParallel encode & async I/OPage-granular parallel compression with a background writer, byte-identical to the serial path.
  • 500K~500 KB libraryFour dependencies (zstd, zlib, lz4, brotli), fetched for you. Arrow C++ is 50 MB+.

SIMD inside

Augmented with SIMD.
Eight values per instruction.

The hot loops of Parquet decoding are written as vector kernels for every major instruction set. One portable binary detects the CPU at startup and dispatches to the widest kernel it supports, with a scalar fallback for everything else.

x86-64SSE4.2AVXAVX2AVX-512
ARM64NEONSVE
30+dispatched kernels
6instruction sets
up to 21×faster AVX2 bit-unpacking
1binary, runtime dispatch
  • Bit-unpacking & RLEDefinition levels and dictionary indices of any bit width, unpacked a register at a time.
  • Dictionary gathersIndex-to-value gathers with the bounds check fused into the vector loop.
  • Byte-stream splitFLOAT / DOUBLE transposes for BYTE_STREAM_SPLIT, both directions.
  • StatisticsMin / max for every numeric type, NaN-aware, computed while copying.
  • Nulls & booleansValidity bitmaps, non-null counts and boolean packing.
  • Codecs & DELTASnappy / LZ4 match copies and the prefix sums behind DELTA_BINARY_PACKED.

Install

Up and running in a minute.

Linux, macOS and Windows. GCC, Clang, MSVC and MinGW. Missing codecs are fetched and built for you.

The simplest path: clone, make, install. You get the static library, the headers, the carquet CLI, a pkg-config file and find_package(carquet) support.

$ git clone https://github.com/Vitruves/carquet.git && cd carquet
$ make                    # optimized build (make native for -march=native)
$ sudo make install       # /usr/local, or make install PREFIX=/opt/carquet

$ cc app.c $(pkg-config --cflags --libs carquet)

API

A small API that stays out of the way.

131 plain C functions, errors returned as values, no framework to adopt. Every snippet below compiles as-is with -Wall -Wextra -Werror.

Define a schema, then hand each column a plain C array. Nullable columns take a definition level per row and only the present values.

#include <carquet/carquet.h>
#include <stdio.h>

#define TRY(x) do { if ((x) != CARQUET_OK) return 1; } while (0)

int main(void) {
    carquet_error_t err = CARQUET_ERROR_INIT;
    carquet_logical_type_t utf8 = { .id = CARQUET_LOGICAL_STRING };

    carquet_schema_t* schema = carquet_schema_create(&err);
    TRY(carquet_schema_add_column(schema, "id",    CARQUET_PHYSICAL_INT64,      NULL,  CARQUET_REPETITION_REQUIRED, 0, 0));
    TRY(carquet_schema_add_column(schema, "name",  CARQUET_PHYSICAL_BYTE_ARRAY, &utf8, CARQUET_REPETITION_REQUIRED, 0, 0));
    TRY(carquet_schema_add_column(schema, "score", CARQUET_PHYSICAL_DOUBLE,     NULL,  CARQUET_REPETITION_OPTIONAL, 0, 0));

    carquet_writer_options_t opts;
    carquet_writer_options_init(&opts);
    opts.compression      = CARQUET_COMPRESSION_ZSTD;
    opts.write_page_index = true;               /* lets readers skip pages */

    carquet_writer_t* w = carquet_writer_create("people.parquet", schema, &opts, &err);
    if (!w) { fprintf(stderr, "%s\n", err.message); return 1; }

    int64_t ids[] = {1, 2, 3};
    carquet_byte_array_t names[] = {{(uint8_t*)"ada", 3}, {(uint8_t*)"linus", 5}, {(uint8_t*)"grace", 5}};
    double  scores[] = {9.5, 7.25};              /* present values only */
    int16_t defs[]   = {1, 0, 1};                /* row 2 is NULL */

    TRY(carquet_writer_write_batch(w, 0, ids,    3, NULL, NULL));
    TRY(carquet_writer_write_batch(w, 1, names,  3, NULL, NULL));
    TRY(carquet_writer_write_batch(w, 2, scores, 3, defs, NULL));
    TRY(carquet_writer_close(w));
    carquet_schema_free(schema);
    return 0;
}
  • All physical types & encodings
  • Snappy · GZIP · LZ4 · ZSTD · Brotli
  • Nested struct / list / map, any depth
  • Bloom filters & page indexes
  • Predicate pushdown
  • Arrow C Data Interface
  • Reads & writes with PyArrow, DuckDB, Spark

CLI

Look inside any Parquet file.

The carquet command ships with the library. Inspect schemas, metadata and statistics, print or export rows with page-level filter pushdown, validate files, and generate a ready-to-compile C reader from a file's schema.

  • schemaschema tree
  • infofull metadata
  • head tail sampleprint rows
  • cat exportslice, filter, CSV
  • statmin / max / nulls
  • count columnsquick answers
  • validateintegrity check
  • codegenC reader from a schema
zsh
$ carquet schema people.parquet
message schema {
  REQUIRED INT64 id;
  REQUIRED STRING name;
  OPTIONAL DOUBLE score;
}

$ carquet stat people.parquet
  Column  Type    Nulls  Min   Max
  ------  ------  -----  ----  -----
  id      INT64   0      1     3
  name    STRING  0      ada   linus
  score   DOUBLE  1      7.25  9.5

$ carquet head -n 3 people.parquet
  id  name   score
  --  -----  -----
  1   ada    9.5
  2   linus  null
  3   grace  7.25

$ carquet export -c id,name data.parquet > data.csv
$ carquet codegen -f data.parquet -o reader.c

Make Parquet the fast part.