Skip to content
407 changes: 42 additions & 365 deletions README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ not verify for you afterwards.
- [ ] If the release changed the CLI output or the report shape,
regenerate the committed artifacts that carry captured output and
the tool version: re-execute `examples/taxi-optimization` (its
README run instructions) and refresh `viewer/screenshot.png`, both
against the bumped build, so the banners match the released
version.
README run instructions) and refresh `viewer/screenshot.png` and
the README terminal shots (`assets/readme/cli-*.png`, real output
rendered in a terminal frame), all against the bumped build, so
the banners match the released version.
- [ ] `cargo publish --dry-run`
- [ ] `maturin build --release` locally: the wheel builds and installs in a
clean venv, `import delta_explain` and the bundled binary both work
Expand Down
9 changes: 8 additions & 1 deletion VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ Goal, delivered: shift from "file counter" to "pruning advisor", and make a repo

Still planned in this track:

- **`--engine-profile`**: emulate a specific engine's pruning strength (`max` | `datafusion` | `spark` | `kernel`) so a CI gate asserts what *your* engine will do, not the metadata's theoretical best. The known divergences (IN-list strategies) are documented in the README today; this makes them selectable.
- **`--engine-profile`**: emulate a specific engine's pruning strength (`max` | `datafusion` | `spark` | `kernel`) so a CI gate asserts what *your* engine will do, not the metadata's theoretical best. The known divergences (IN-list strategies) are documented in the semantics contract today; this makes them selectable.

## v0.7: Per-column stats coverage (shipped September 2026)

Goal, delivered: answer "can data skipping even work here?" per column, instead of assuming it.

- **Per-column stats coverage**: for every column a stats-safe fragment references, the analysis reports how many of the files entering the data-skipping phase actually carry the statistics that fragment needs (`min_max` for range operators, `null_count` / `null_count_and_num_records` for the null tests), so "stats-safe but cannot prune" becomes visible per predicate column instead of hidden behind the table-wide stats mode. Columns are reported under their logical names, with coverage resolved by physical name under column mapping; the JSON contract grows additively (`analysis.stats_coverage`, `schema_version` 0.5.0).
- Deliberately **not** shipped with it: a confidence downgrade on zero coverage. `incomplete` marks an attribution failure of the analysis; missing statistics are a property of the table, fully explained by the coverage numbers themselves. The CI channel for the structural no-op is a coverage gate, planned as part of the finer-gates milestone.

## Later: Fidelity and coverage (planned)

Expand Down
Binary file added assets/readme/cli-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/readme/cli-verbose.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# Reference

- [What delta-explain guarantees](reference/semantics.md)
- [Predicate syntax](reference/predicate-syntax.md)
- [CLI reference](reference/cli.md)
- [The JSON report, field by field](reference/json-schema.md)
- [What it is validated against](reference/validation.md)

Expand Down
6 changes: 6 additions & 0 deletions book/src/getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ same environment) plus a thin Python API; see [From Python](../guides/python.md)
cargo install delta-explain
```

For the latest development version, install from Git instead:

```bash
cargo install --git https://github.com/cdelmonte-zg/delta-explain
```

## Docker (amd64 + arm64)

```bash
Expand Down
79 changes: 76 additions & 3 deletions book/src/guides/ci-gating.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,65 @@ delta-explain s3://lake/events -w "region = 'eu' AND ts > '2026-06-01'" \
```

On failure the report still prints (with `result: "fail"`) and stderr carries
`ASSERTION FAILED: ...`. The exit-code contract is precise and stable; see the
table in [What delta-explain guarantees](../reference/semantics.md).
`ASSERTION FAILED: ...` - the terminal shows both, and the exit code flips
to 1:

```
$ delta-explain ./table -w "country = 'DE' AND age > 40" --min-pruning 90
ASSERTION FAILED: total pruning 83.3% is below threshold 90.0%
Delta table: ./table
Version: 5
Predicate: country = 'DE' AND age > 40

Predicate Analysis:
partition-safe: country = 'DE'
stats-safe: age > 40
stats coverage:
age [min_max]: 2/2 candidate files (100%)
unsplittable: -
confidence: conservative

Files in snapshot: 6

Phase 1: Partition pruning [exact]
predicate: country = 'DE'
files remaining: 2 (-4, 67% pruned)

Phase 2: Data skipping (min/max statistics) [conservative]
predicate: age > 40
files remaining: 1 (-1, 50% pruned)

Total reduction: 6 -> 1 files (83% pruned)

$ echo $?
1
```

The exit-code contract is precise and stable; see the table in
[What delta-explain guarantees](../reference/semantics.md).

Statistics are resolved through the kernel's log replay, checkpoint Parquet
included, so `--assert-stats` flags a file only when its `add` action genuinely
carries no statistics: long-lived tables whose older commits have been
consolidated into a checkpoint do not produce false positives.

## Calibrating the threshold

The `--min-pruning` threshold is per-invocation, applied to the current
predicate against the current snapshot. Calibrate it against a baseline pruning
percentage in dev (set the gate a few points below it); a flat threshold across
heterogeneous partitions will misfire. Note also that 100% pruning can signal a
broken or unexpectedly empty predicate, so pair `--min-pruning` with a sanity
check on `final_files > 0` when the workload is expected to read data.

## Predicate parity

The pruning percentage `delta-explain` reports reflects the predicate you pass
to `-w`. If the runtime query wraps a column in `LOWER`, `CAST`, or a UDF, the
engine may prune less than the gate suggests. Use a CI predicate that is
semantically equivalent to the runtime predicate and explicitly track that
equivalence: a gate on `country = 'DE'` does not automatically validate a
production query using `LOWER(country) = 'de'`.

## In a pipeline (JSON)

Expand All @@ -34,13 +91,29 @@ a partial document.
A composite action wraps the CLI with matching inputs. Pin the release tag:

```yaml
- uses: cdelmonte-zg/delta-explain@v0.6.0
- uses: cdelmonte-zg/delta-explain@v0.7.0
with:
table: s3://lake/events
where: "region = 'eu'"
min-pruning: "80"
```

## Docker in a pipeline

The same gate without the composite action, from any CI system that can run a
container:

```yaml
- name: Verify pruning after ETL
run: |
docker run --rm \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION \
ghcr.io/cdelmonte-zg/delta-explain:0.7.0 \
--env-creds s3://warehouse/events \
-w "date = '2024-01-15'" \
--min-pruning 90 --assert-stats --format json
```

## Attach a report artifact

Generate a verbose JSON report and render it with the
Expand Down
32 changes: 32 additions & 0 deletions book/src/reference/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# CLI reference

The complete flag list, as printed by `delta-explain --help`:

```
delta-explain <PATH> [OPTIONS]

Arguments:
<PATH> Path to the Delta table (local path, s3://, az://, gs://)

Options:
-w, --where <PREDICATE> Predicate (e.g. "age > 30 AND country = 'DE'")
-v, --verbose Show per-file details (kept/dropped with reason);
in JSON, adds the "files" array
--limit <N> Cap per-file listings at N entries
--explain-why Diagnose why the predicate pruned as it did, with
suggestions; in JSON, adds the "explain" array
--format <FORMAT> Output format: text (default) or json
--min-pruning <PCT> Fail if total pruning is below this percentage
--assert-stats Fail if any file is missing statistics
--at-version <N> Analyze the table at this version (time travel)
--profile <NAME> Static AWS credentials from ~/.aws/credentials (S3)
--region <REGION> AWS region (S3 / S3-compatible)
--option <KEY=VALUE> Object store config (repeatable)
--env-creds Read cloud credentials from environment variables
--public Access a public bucket (skip auth)
```

Gate flags (`--min-pruning`, `--assert-stats`) are covered in
[Gating pruning in CI](../guides/ci-gating.md); credential flags
(`--env-creds`, `--profile`, `--option`, `--region`, `--public`) in
[Cloud storage](../guides/cloud.md).
101 changes: 101 additions & 0 deletions book/src/reference/predicate-syntax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Predicate syntax

`delta-explain` accepts standard SQL WHERE-clause syntax, parsed via
[sqlparser-rs](https://github.com/sqlparser-rs/sqlparser-rs).

```sql
-- Comparisons
age > 30
country = 'DE'
score >= 90.5

-- Logical operators
age > 30 AND country = 'DE'
country = 'DE' OR country = 'IT'
NOT country = 'US'

-- IN lists
country IN ('DE', 'IT', 'US')
country NOT IN ('US')

-- BETWEEN
age BETWEEN 20 AND 40

-- NULL checks
name IS NOT NULL
age IS NULL

-- Parentheses
(country = 'DE' OR country = 'IT') AND age > 30

-- Nested columns
payload.age > 30
```

Also supported: `IS [NOT] DISTINCT FROM`, `DATE '...'` / `TIMESTAMP '...'`
literal forms, schema-driven coercion (a quoted `'2026-07-01'` against a
`DATE` column just works, including `DECIMAL` and narrow integers), and
`LIKE`: prefix patterns (`country LIKE 'D%'`) prune on partition values and
on string min/max statistics, and on partition columns every other shape
(`'%son'`, `_`, `NOT LIKE`) prunes exactly too.

Subqueries, functions, and non-prefix `LIKE` on data columns are outside the
pruning language: they warn and keep files instead of failing. The exact
rules are the [degradation rules](semantics.md#degradation-rules) in the
semantics contract.

## What the analysis shows

The analysis block always displays the predicate *as analyzed*, so
normalization is visible rather than silent. A prefix `LIKE` appears as the
lexicographic range it was rewritten to, and prunes through the ordinary
partition rules:

```
$ delta-explain ./table -w "country LIKE 'D%' AND age > 40"

Predicate Analysis:
partition-safe: country >= 'D' AND country < 'E'
stats-safe: age > 40
stats coverage:
age [min_max]: 2/2 candidate files (100%)
unsplittable: -
confidence: conservative

Files in snapshot: 6

Phase 1: Partition pruning [exact]
predicate: country >= 'D' AND country < 'E'
files remaining: 2 (-4, 67% pruned)

Phase 2: Data skipping (min/max statistics) [conservative]
predicate: age > 40
files remaining: 1 (-1, 50% pruned)

Total reduction: 6 -> 1 files (83% pruned)
```

A construct outside the pruning language degrades loudly instead of erroring:
the fragment routes to `unsplittable`, confidence drops to `incomplete`, a
warning names the offending expression, and the sibling conjunct still prunes:

```
$ delta-explain ./table -w "UPPER(country) = 'DE' AND age > 40"

Predicate Analysis:
partition-safe: -
stats-safe: age > 40
stats coverage:
age [min_max]: 6/6 candidate files (100%)
unsplittable: UPPER(country) = 'DE'
confidence: incomplete

Files in snapshot: 6

Phase 1: Data skipping (min/max statistics) [incomplete]
predicate: age > 40 (+1 unsupported fragment, keeps all files)
files remaining: 3 (-3, 50% pruned)

Warnings!
[UNSUPPORTED_EXPRESSION]: Unsupported expression: UPPER(country); the fragment 'UPPER(country) = 'DE'' cannot contribute to pruning and is applied conservatively (keeps all files)
```
13 changes: 9 additions & 4 deletions docs/semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,15 @@ adjusted:

The report reflects what the metadata makes possible, computed with the
strongest sound techniques in production use. Engines make different
choices; the known divergences are documented in the README's *Current
limitations* (notably `IN`-list strategies). The report never overstates
correctness: only, potentially, the pruning a specific engine will
realize.
choices; the known divergence is the `IN`-list strategy. delta-explain
expands `IN` lists into OR-of-equalities, the strongest sound form, with
no size cap; DataFusion-based engines (delta-rs) do the same expansion
but stop skipping past 20 list items, and delta-spark evaluates an
imprecise range test over the whole list
(`min(values) <= col <= max(values)`), which keeps more files on sparse
lists. On `IN`-heavy predicates a specific engine may therefore prune
less than this report shows. The report never overstates correctness:
only, potentially, the pruning a specific engine will realize.

## Exit codes and the error contract

Expand Down