Vorticity.RowEncoding 0.7.1

dotnet add package Vorticity.RowEncoding --version 0.7.1
                    
NuGet\Install-Package Vorticity.RowEncoding -Version 0.7.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Vorticity.RowEncoding" Version="0.7.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Vorticity.RowEncoding" Version="0.7.1" />
                    
Directory.Packages.props
<PackageReference Include="Vorticity.RowEncoding" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Vorticity.RowEncoding --version 0.7.1
                    
#r "nuget: Vorticity.RowEncoding, 0.7.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Vorticity.RowEncoding@0.7.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Vorticity.RowEncoding&version=0.7.1
                    
Install as a Cake Addin
#tool nuget:?package=Vorticity.RowEncoding&version=0.7.1
                    
Install as a Cake Tool

Vorticity

NuGet CI License

Vorticity reads, writes and queries columnar files in .NET. It speaks Vortex and Apache Parquet, works on a local disk or straight against S3, Azure Blob Storage and Google Cloud Storage, and is written in C# from end to end, with no native library and no third-party dependency.

It is also built for speed: it reads and writes Vortex faster than Vortex's own Rust implementation, its group bys beat DuckDB's on every query we measured, and the managed Zstandard it relies on decompresses faster than the native libzstd. The figures are at the end of this page.

Quick start

The packages target .NET 11. The generator runs at build time only, and PrivateAssets keeps it out of whatever your project packs:

<PackageReference Include="Vorticity" Version="0.6.0" />
<PackageReference Include="Vorticity.Generators" Version="0.6.0" PrivateAssets="all" />

A record declares the columns of a file. It is a schema rather than a row: each member is a column, in declaration order and under its own name.

using Vorticity;

[VortexRecord]
public partial record struct Reading(int Day, double? Celsius, string City);

Write the records, then ask the file a question:

await using (VortexFileWriter writer = VortexSession.Default.CreateWriter<Reading>("readings.vortex"))
{
    await writer.WriteAsync<Reading>(readings);   // a Reading[], copied in column by column
    await writer.CompleteAsync();                 // disposed without it, the file is deleted
}

await using VortexFile file = await VortexFile.OpenAsync("readings.vortex");

long hotDays = await file.Scan<Reading>()
    .Where(r => r.Day >= 900 && r.City == "Paris" && r.Celsius > 30.0)
    .CountAsync();

The lambda given to Where runs once, when the scan is built, and what it records becomes the plan. The file's statistics, zone maps and indexes then skip whatever they can before a block is decoded, and the count comes from the statistics when they settle it, from the encoded blocks otherwise, never from materialised rows. A filter the scan cannot push down, such as r.Day % 2 == 0, does not compile.

A group by is a query too, written as a method chain or in LINQ's query syntax:

var hottest =
    from r in file.Scan<Reading>()
    where r.Day >= 900
    group r by r.City into g
    orderby g.Average(x => x.Celsius) descending
    select (g.Key, g.Count(), g.Average(x => x.Celsius));

await foreach (CityHeat city in hottest.Take(3).As<CityHeat>().ToRecordsAsync())
{
    Console.WriteLine($"{city.City}: {city.Mean:F1} °C over {city.Readings} readings");
}

[VortexRecord]
public partial record struct CityHeat(string City, long Readings, double? Mean);

When the values themselves are what you need, a scan lends each batch's columns as spans over the decoded buffers, with no copy and next to no allocation per batch:

long hot = 0;
await foreach (var (_, celsius, _) in file.Scan<Reading>().Where(r => r.City == "Paris"))
{
    ReadOnlySpan<double> values = celsius.Values;   // the decoded buffer, valid until the next batch
    for (int i = 0; i < values.Length; i++)
    {
        if (celsius.IsValid(i) && values[i] > 30.0) hot++;
    }
}

ToRecordsAsync() yields one record per row when rows are what you need, and a file whose schema is only known at run time is read by column name, as in file.Scan("Day", "City").Where($"Day >= {min}").

Parquet

A Parquet file goes through the same engine, so the same records, filters, aggregates and group bys work on it, and one scan converts it to Vortex:

using Vorticity.Parquet;

#pragma warning disable VX0003   // the Parquet API is experimental until its surface settles

await using ParquetFile parquet = await ParquetFile.OpenAsync("readings.parquet");
double? lyon = await parquet.Scan<Reading>().Where(r => r.City == "Lyon").AverageAsync(r => r.Celsius);

await using VortexFileWriter writer = VortexSession.Default.CreateWriter<Reading>("readings.vortex");
await writer.WriteAsync(parquet.Scan<Reading>());
await writer.CompleteAsync();

VortexSession.Default.CreateParquetWriter<Reading>(path) writes Parquet the same way. Row group statistics, the page index, Bloom filters and dictionaries prune a scan before a page is read.

Object storage

With Vorticity.ObjectStorage, a location opens and writes like a path:

using Vorticity.ObjectStorage;

await using VortexSession session = VortexSession.Create(o => o.ObjectStores.UseEnvironment());

await using VortexFile file = await session.OpenAsync("s3://lake/readings/2026-10.vortex");
double? mean = await file.Scan<Reading>().Where(r => r.Day >= 900).AverageAsync(r => r.Celsius);

UseEnvironment serves every s3://, az:// and gs:// location from the environment, which it reads the way each cloud's own SDKs do, Kubernetes workload identities included. AWS_ENDPOINT_URL_S3 points s3:// at R2, OVHcloud, Scaleway or any other service that speaks the S3 API. Opening a file costs a single request on Amazon S3, a scan coalesces each batch's reads into ranges of up to 16 MiB issued together, and a large write uploads its parts in parallel as they fill. Parquet files open from a local path for now. object-storage.md covers stores, credentials, retries and what each operation costs.

Building from source

global.json pins a .NET 11 release candidate SDK, so until .NET 11 ships, a machine with only .NET 10 is refused before anything builds. That SDK is all dotnet build and dotnet test need. A Rust toolchain is needed only to compare against the Rust reference: the cross-check, the bench's ratio gates and the corpus generator. CONTRIBUTING.md says what to run before a change is done.

Documentation

where what you find
docs/guide/ how to use the library, one page per task, each backed by a program the samples project compiles and runs
docs/design/ why the library is shaped this way: scope, byte layout, engine, Parquet, object storage, encryption, public API
bench/README.md how to run the bench, and the gates a change has to pass

Performance

Where Vorticity is compared with another implementation, the figure is a speedup: the other side's time divided by Vorticity's, so anything above 1.00x means Vorticity took less time. The machines are an Apple M4 Pro (arm64, 14 cores, macOS) and an AMD Ryzen 9 7950X (x64, 16 cores and 32 threads, Windows 11). The figures against Rust were measured on 2026-10-14 on the M4 Pro and on 2026-10-12 on the Ryzen, those for Parquet on 2026-10-12 and those against DuckDB on 2026-10-14, and how it is measured says what both sides do.

Against Vortex's Rust implementation

Vortex Rust 0.86.1 and Vorticity, each in its own process, read and write a table of four columns and a million rows. On the file Vorticity writes:

scenario M4 Pro, 1 core M4 Pro, 14 cores 7950X, 1 core 7950X, 32 threads
read every column 2.22x 2.35x 2.91x 1.63x
read one column of four 2.89x 2.59x 2.74x 2.08x
filter, 1 % of the rows 1.38x 2.72x 1.27x 2.40x
filter, half the rows 2.14x 2.05x 2.69x 1.77x
take 1,000 scattered rows 1.73x 1.06x 1.47x 1.16x
write the table back out 2.32x 1.94x 2.40x 1.71x

Rust keeps the lead in a few places on the M4 Pro: a prefix filter over FSST strings (0.94x) and 10 of the 61 encodings decoded alone, from 0.86x to 1.00x, mostly columns stored plain or masked. The Pcodec files decode in less time there, from 1.07x to 3.59x. On the Ryzen, Vorticity decodes all 61 encodings in less time, Pcodec files included.

Against DuckDB

DuckDB 1.5.2 answers the same group bys on the same files with the same number of threads, from a table of its own loaded beforehand and through its Vortex extension. Vorticity leads all 13 queries on both, from 40 million rows grouped by 10³ to 10⁷ keys to db-benchmark's group by on 10 million rows:

DuckDB reading one thread 14 threads
its own table, loaded beforehand 1.43x to 6.14x 1.18x to 3.61x
the file, through its Vortex extension 1.57x to 5.81x 1.38x to 4.44x

The narrowest lead is db-benchmark's q10 on 14 threads, six keys and nearly a group per row, at 1.18x. These figures come from the M4 Pro.

Parquet

Parquet reads through the same engine as Vortex: a page decompresses straight into the buffer that becomes its column, plain values and dictionaries are read as views rather than copied, and a scan of the taxi trips below, 3 million rows, allocates 82 KiB. The table times a scan of every column of three real files on the Ryzen 9 7950X, against the same rows rewritten as Vortex:

scan of every column, in ms Parquet, 1 thread Parquet, 32 threads Vortex, 32 threads
New York's taxi trips, 3.1M rows of 19 columns, GZIP 158 37.7 22.2
ClickBench's hits, 1M rows of 105 columns, Snappy 174 74.3 67.5
a nested file, 2M rows of 22 columns, Snappy 119 76.3 62.0

On one thread the codec weighs most: inflating the taxi trips' GZIP pages takes 121 ms of the 158, even though Vorticity decodes GZIP with its own DEFLATE decoder, 19 % faster than .NET's GZipStream.

Zstandard and LZ4

The codecs come from ManagedCompressions, which writes the exact bytes of libzstd 1.5.7 and lz4 1.10.0. Against the native libraries, on silesia.tar in one call, at the levels in use:

managed against native Apple M4 Pro Ryzen 9 7950X
Zstandard, decompression 1.30x to 1.49x 1.01x to 1.11x
Zstandard, compression 0.94x to 1.03x 0.92x to 1.09x
LZ4, decompression 1.03x to 1.04x 0.99x to 1.08x
LZ4, compression 0.85x to 0.96x 0.88x to 0.95x

On x64, native libzstd decodes with hand-written assembly that arm64 does not have, which is why the managed decoder's lead is smaller there.

How it is measured

Against Rust, both sides are compiled ahead of time, run on the same machine and do the same work. Each opens the file anew at every call, decodes every value it returns to its plain form and must return the same rows, and a write hands its bytes to a sink that keeps none. On the M4 Pro both read the file mapped, where it lies. Under Windows, Rust maps the file while Vorticity copies the segments it reads, as a session that keeps no mapping does there. On both machines both sides also read every value they return, the same bytes, as the Parquet figures read theirs on the caller's thread. Rust is built the way upstream builds its own benchmarks, and on one core its figure is the faster of its two ways of splitting a scan. Against DuckDB, the two sides take turns in pairs and must give the same answer to every query. The page cache is warm, cores are not pinned, and your data and your machine will differ.

Every figure is on the arm64 page, the x64 page, the DuckDB page and the Parquet page, each with its machine, commit and date. 05-benchmarks.md explains what is compared and how, and bench/gate.sh runs everything that gates a change before it is pushed.

Status and license

Vorticity is developed with the help of LLMs, and its API may still change in breaking ways before v1.0. It is licensed under Apache-2.0, as LICENSE and NOTICE say.

Vortex™ is a trademark of LF Projects, LLC, DuckDB is a trademark of the DuckDB Foundation, and Apache Parquet is a trademark of The Apache Software Foundation. Vorticity is an independent implementation, not affiliated with or endorsed by any of them.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Vorticity.RowEncoding:

Package Downloads
Vorticity.Dataset

A versioned dataset over an object store: immutable data and commit objects, a prolly tree of leaves, and the store seam an S3 library implements. The format is experimental and this repository is the first to write it.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.1 39 10/17/2026
0.7.0 55 10/16/2026
0.6.0 61 10/11/2026
0.5.1 89 10/9/2026
0.5.0 86 10/9/2026
0.4.1 94 10/4/2026
0.4.0 95 10/4/2026
0.3.0 94 10/2/2026
0.2.0 97 9/30/2026
0.1.0 95 9/29/2026