Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `WriteOptions.withZstd(true)` now throws `IllegalArgumentException` when `allowedCascading()` is `0` — Zstd only ever competes inside the cascade, so at the default cascading depth the flag was a silent no-op producing byte-identical output to `defaults()`. ([#366](https://github.com/dfa1/vortex-java/pull/366))

## [0.14.0] — 2026-09-03

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Record: `(int chunkSize, boolean enableZoneMaps, double compressionRatioThreshol
|--------|-------|
| `withZoneMaps(boolean)` | Toggle per-chunk min/max/sum statistics |
| `withGlobalDict(boolean)` | Toggle the shared cross-chunk dictionary |
| `withZstd(boolean)` | Add Zstandard to the cascade codec competition |
| `withZstd(boolean)` | Add Zstandard to the cascade codec competition. Requires `allowedCascading > 0` — Zstd only competes inside the cascade, so `withZstd(true)` throws `IllegalArgumentException` at depth 0; combine with `cascading(depth)` |
| `withGlobalDictMaxRetainedBytes(long)` | Aggregate heap budget for buffered global-dict candidate columns |
| `withEdition(Edition)` | Enable an [edition](#editions) for its family, replacing any edition already enabled for that family. No method disables the guard entirely — an `unstable`-family encoding is reached by enabling its edition explicitly, e.g. `withEdition(Editions.UNSTABLE_2025_05_0)` |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
/// file size by 10–15% on real-world datasets compared to ALP+bitpack alone.
/// Trade-off: Zstd decompression is ~6× slower than ALP decode;
/// prefer the default (`false`) for read-heavy workloads.
/// Requires `allowedCascading > 0`: Zstd only ever competes inside the
/// cascade, so `true` at depth 0 would be a silent no-op — the compact
/// constructor rejects that combination instead.
/// @param globalDictMaxRetainedBytes aggregate byte budget for the buffered per-chunk code arrays all
/// global-dictionary candidate columns may retain in the heap while
/// waiting for `close()` (default 1 GB). A shared dictionary must see
Expand Down Expand Up @@ -55,8 +58,20 @@ public record WriteOptions(
MemorySize globalDictMaxRetainedBytes,
Map<EditionFamily, Edition> editions
) {
/// Defensively copies `editions` into an immutable map.
/// Defensively copies `editions` into an immutable map and rejects a Zstd flag that could
/// never take effect.
///
/// @throws IllegalArgumentException if `enableZstd` is `true` while `allowedCascading` is `0`:
/// Zstd only ever competes inside the cascade
/// (`VortexWriter` only builds a `CascadingCompressor`, which
/// is where the Zstd codec is added, when `allowedCascading >
/// 0`); at depth 0 the flag would be silently ignored.
public WriteOptions {
if (enableZstd && allowedCascading == 0) {
throw new IllegalArgumentException(
"enableZstd requires allowedCascading > 0 (Zstd only competes inside the cascade); "
+ "use WriteOptions.cascading(depth).withZstd(true)");
}
editions = Map.copyOf(editions);
}

Expand Down Expand Up @@ -120,8 +135,13 @@ public WriteOptions withGlobalDict(boolean enabled) {
/// Trade-off: Zstd decompression is ~6× slower than ALP reconstruction or bitpack unpack.
/// Use `false` (the default) for read-heavy or latency-sensitive workloads.
///
/// `enabled=true` requires `allowedCascading() > 0` — Zstd only ever competes inside the
/// cascade, so enabling it at depth 0 (e.g. straight off [#defaults()]) would be a silent
/// no-op. Combine with [#cascading(int)]: `WriteOptions.cascading(depth).withZstd(true)`.
///
/// @param enabled `true` to enable Zstd in the compression cascade
/// @return a new `WriteOptions` with the Zstd flag updated
/// @throws IllegalArgumentException if `enabled` is `true` and `allowedCascading()` is `0`
public WriteOptions withZstd(boolean enabled) {
return new WriteOptions(chunkSize, enableZoneMaps, compressionRatioThreshold, allowedCascading, globalDict, enabled,
globalDictMaxRetainedBytes, editions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

/// Unit tests for [WriteOptions] factories and copy-methods.
class WriteOptionsTest {
Expand Down Expand Up @@ -63,4 +64,25 @@ void withGlobalDictMaxRetainedBytes_returnsNewInstance() {
assertThat(result).isNotSameAs(base);
assertThat(base.globalDictMaxRetainedBytes()).isEqualTo(DEFAULT_BUDGET);
}

@Test
void withZstd_onDefaults_rejectedBecauseCascadingIsZero() {
// Given — defaults() has allowedCascading == 0, so Zstd could never be reached by
// VortexWriter (it is only added to the cascade codec list, which is only consulted when
// allowedCascading > 0); this must fail loudly rather than silently write plain files.
WriteOptions base = WriteOptions.defaults();

// When / Then
assertThatIllegalArgumentException().isThrownBy(() -> base.withZstd(true));
}

@Test
void cascading_withZstd_succeeds() {
// Given / When
WriteOptions result = WriteOptions.cascading(3).withZstd(true);

// Then
assertThat(result.enableZstd()).isTrue();
assertThat(result.allowedCascading()).isEqualTo(3);
}
}
Loading