Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 81 additions & 90 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

161 changes: 80 additions & 81 deletions README.md

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@

## What is guaranteed

**A malformed file produces an exception, never worse.** A file that violates the format — bad
offsets, truncation, out-of-range structural fields, exceeded caps — raises a clean
`VortexFormatException` or `VortexUnsupportedException`. No out-of-bounds access, no unbounded
allocation, no hang. Parser fuzzing in CI is what enforces this, on every pull request.
A malformed file produces an exception, never anything worse. A file that violates the format (bad
offsets, truncation, out-of-range structural fields, exceeded caps) raises a clean
`VortexFormatException` or `VortexUnsupportedException`, with no out-of-bounds access, no unbounded
allocation and no hang. Parser fuzzing in CI enforces this on every pull request.

## What is not

**A well-formed file that lies is believed.** Statistics embedded in a file — zone bounds, null
counts, sortedness — are used for pruning, and a file whose structure is valid but whose statistics
are false may yield wrong results. This is inherent to every format with embedded statistics.
A well-formed file that lies is believed. Statistics embedded in a file (zone bounds, null counts,
sortedness) are used for pruning, so a file whose structure is valid but whose statistics are false
may yield wrong results. This is inherent to every format with embedded statistics.
`VerifyStatistics` trades throughput for validation when the producer is not trusted.

Two further things are explicitly not claimed: protection against a producer targeting resource
exhaustion *below* the configured caps, and any confidentiality property — the format's encryption
slot is an empty reserved table and this library writes it empty.
Two more things are explicitly not claimed: protection against a producer aiming at resource
exhaustion below the configured caps, and any confidentiality property. The format's encryption slot
is an empty reserved table, and this library writes it empty.

## Reporting

Report a vulnerability through the repository's **Security** tab, with *Report a vulnerability*.
That opens a private advisory visible only to you and the maintainers; a public issue is the wrong
place and cannot be taken back.
Report a vulnerability through the repository's Security tab, with "Report a vulnerability". That
opens a private advisory visible only to you and the maintainers. A public issue is the wrong place,
and cannot be taken back.

Include the file or the generator that reproduces it. A crash on a malformed input is a bug in
scope of the guarantee above and is treated as one.
Include the file, or the generator that reproduces it. A crash on a malformed input is a bug within
the scope of the guarantee above, and is treated as one.
430 changes: 235 additions & 195 deletions bench/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bench/Vorticity.Benchmarks.Queries/DatasetScenarios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ namespace Vorticity.Benchmarks.Queries;
internal sealed record DatasetScenario(string Name, Func<VortexDataset, Run, Task<long>> Query, int ProbeEvery = 1);

/// <summary>
/// The queries of the datasets' stage (PLAN-QUERIES-STREAMING.md, 6e): the readings as one object,
/// as sixteen, and as sixteen with an eighth of every object deleted. The first batch of a scan is
/// The queries a dataset is judged on: the readings as one object, as sixteen, and as sixteen with
/// an eighth of every object deleted. The first batch of a scan is
/// to stay flat from one object to sixteen; a group by on sixteen at a degree of many lanes within
/// 15 % of the file's.
/// </summary>
Expand Down
20 changes: 10 additions & 10 deletions bench/Vorticity.Benchmarks.Queries/EngineScenarios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,45 @@
namespace Vorticity.Benchmarks.Queries;

/// <summary>
/// The queries of the engine's stage (PLAN-QUERIES-STREAMING.md, 6j): each line of the stage is judged
/// on one of them, and a query that tracks its aggregation reports its lanes and its merge.
/// The queries the group-by engine is judged on, one per strategy, and a query that tracks its
/// aggregation reports its lanes and its merge.
/// </summary>
internal static class EngineScenarios
{
internal static IEnumerable<(string File, Scenario Scenario)> All(int large)
{
// 6k: the group by that streams, against the same query forced to block.
// The group by that streams, against the same query forced to block.
yield return ("readings", new Scenario("group by city day (composite), count avg, blocking", (file, run) => CityDayAsync(file, run, blocking: true), 1_000));
yield return ("readings", new Scenario("group by day (sorted), welford", (file, run) => DayWelfordAsync(file, run, blocking: false), 100));
yield return ("readings", new Scenario("group by day (sorted), welford, blocking", (file, run) => DayWelfordAsync(file, run, blocking: true), 100));

// 6l: a filter that keeps 7 % of the rows, contiguous, on a sorted key.
// A filter that keeps 7 % of the rows, contiguous, on a sorted key.
yield return ($"readings-{large}", new Scenario($"group by city under a 7 % day filter, {large / 1_000_000}M", CitiesOfSomeDaysAsync));

// 6m, 6n, 6o, 6p: what follows the pass.
// What follows the pass: a top-k, a chosen row, an order on the key, a window without order.
yield return ("names", new Scenario("order by count take 100, text key (1M groups)", TopNamesAsync));
yield return ("requests", new Scenario("order by max take 10 with its row, 1M groups", SlowestUsersAsync));
yield return ("requests", new Scenario("ohlc by day (first, max, min, last)", OhlcAsync));

// 6n: every group's chosen row, read after the pass, against the same query without it.
// Every group's chosen row, read after the pass, against the same query without it.
yield return ("requests", new Scenario("max by user with its row, every group (1M groups)", (file, run) => SlowestOfEveryUserAsync(file, run, row: true)));
yield return ("requests", new Scenario("max by user, every group (1M groups)", (file, run) => SlowestOfEveryUserAsync(file, run, row: false)));
yield return ("draws", new Scenario("order by key take 10, random int (1M groups)", FirstKeysAsync));
yield return ("readings", new Scenario("order by key descending take 7, 1000 days", LastDaysAsync));
yield return ("draws", new Scenario("take 10 without order, random int (1M groups)", AnyKeysAsync));

// 6c, 6q, 6d1: a table of keys and a merge, by cardinality.
// A table of keys and a merge, by cardinality.
yield return ("draws", new Scenario("group by random int (4 groups), count sum", (file, run) => DrawsAsync(file, run, d => d.K4)));
yield return ("draws", new Scenario("group by random int (100 groups), count sum", (file, run) => DrawsAsync(file, run, d => d.K100)));
yield return ("draws", new Scenario("group by random int (1000 groups), count sum", (file, run) => DrawsAsync(file, run, d => d.K1000)));
yield return ("draws", new Scenario("group by random int (100k groups), count sum", (file, run) => DrawsAsync(file, run, d => d.K100k), 10_000));
yield return ("draws", new Scenario("group by random int (1M groups), count sum", (file, run) => DrawsAsync(file, run, d => d.K1M), 100_000));

// 6b: a key of two fixed-width columns, at a few thousand groups and at nearly a group a row.
// A key of two fixed-width columns, at a few thousand groups and at nearly a group a row.
yield return ("draws", new Scenario("group by (random int, random int) (4 000 groups), count sum", (file, run) => PairsAsync(file, run, d => d.K4, d => d.K1000)));
yield return ("draws", new Scenario("group by (random int, random int) (1.8M groups), count sum", (file, run) => PairsAsync(file, run, d => d.K100k, d => d.K100), 100_000));

// 6d2: a hot key and a long tail, and keys each seen twenty times.
// A hot key and a long tail, and keys each seen twenty times.
yield return ("skewed", new Scenario("group by skewed key (one key 30 %, 1M rare), count sum", KeyedAsync, 100_000));

// The same key on a file of many chunks a lane: what the last ranges a queue hands out weigh.
Expand Down Expand Up @@ -273,7 +273,7 @@ private static async Task<long> PairsAsync(VortexFile file, Run run, Func<Probe<
return rows;
}

/// <summary>A key its zones prove final as the read goes (6g), or the same query forced to block.</summary>
/// <summary>A key its zones prove final as the read goes, or the same query forced to block.</summary>
private static async Task<long> LateKeysAsync(VortexFile file, Run run, bool blocking, bool zones = false)
{
long rows = 0;
Expand Down
2 changes: 1 addition & 1 deletion bench/gen-throughput.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Generate the 1M-row, single-encoding inputs `--throughput` measures.
#
# 468 MB for 57 encodings, so they are not committed. They go in a CACHE directory rather
# About half a gigabyte for 61 files, so they are not committed. They go in a CACHE directory rather
# than /tmp: a reboot wiping them is how a corpus gets regenerated by accident, and a
# regenerated corpus is exactly what the manifest exists to catch.
#
Expand Down
Loading
Loading