Skip to content

Commit c355a4b

Browse files
dfa1claude
andcommitted
refactor(writer): model Estimate as a three-state enum
Estimate's two variants were stateless empty records behind a sealed interface, with paired SKIP/skip() and ALWAYS_USE/alwaysUse() singletons (a Sonar S1845 case-only name clash) and a null sentinel for "no verdict". Collapse to a plain enum { SKIP, ALWAYS_USE, COMPLETE }: COMPLETE replaces null (defer to the full sample-encode evaluation), the clash disappears, and callers switch from instanceof to ==. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 912fcaf commit c355a4b

7 files changed

Lines changed: 33 additions & 53 deletions

File tree

writer/src/main/java/io/github/dfa1/vortex/writer/encode/CascadingCompressor.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ private EncodeResult encodeWithCtx(DType dtype, Object data, EncodeContext ctx)
163163

164164
// Stats-first selection (Rust vortex-compressor pattern): merge every eligible
165165
// encoder's StatsOptions, compute stats in one pass, query each encoder's
166-
// expectedRatio(). AlwaysUse short-circuits; Skip excludes; Ratio competes;
167-
// null defers to the sample-encoded path below.
166+
// expectedRatio(). ALWAYS_USE short-circuits; SKIP excludes; COMPLETE defers
167+
// to the sample-encoded path below.
168168
StatsOptions merged = StatsOptions.NONE;
169169
for (EncodingEncoder enc : encodings) {
170170
if (enc.accepts(dtype) && !ctx.excluded().contains(enc.encodingId())) {
@@ -175,9 +175,8 @@ private EncodeResult encodeWithCtx(DType dtype, Object data, EncodeContext ctx)
175175
? ArrayStats.EMPTY
176176
: ArrayStats.compute(p.ptype(), data, merged);
177177

178-
// First sweep: stats verdicts. AlwaysUse short-circuits; Skip excludes from
179-
// sample-encoded competition below; Ratio acts as a Skip hint (rough estimate,
180-
// not directly comparable to cascade-aware sample bytes); null defers.
178+
// First sweep: stats verdicts. ALWAYS_USE short-circuits; SKIP excludes from
179+
// the sample-encoded competition below; COMPLETE defers to it.
181180
boolean[] skipMask = new boolean[encodings.size()];
182181
for (int i = 0; i < encodings.size(); i++) {
183182
EncodingEncoder enc = encodings.get(i);
@@ -186,10 +185,10 @@ private EncodeResult encodeWithCtx(DType dtype, Object data, EncodeContext ctx)
186185
continue;
187186
}
188187
Estimate est = enc.expectedRatio(dtype, data, stats);
189-
if (est instanceof Estimate.AlwaysUse) {
188+
if (est == Estimate.ALWAYS_USE) {
190189
return spliceResult(enc, dtype, data, ctx);
191190
}
192-
if (est instanceof Estimate.Skip) {
191+
if (est == Estimate.SKIP) {
193192
skipMask[i] = true;
194193
}
195194
}

writer/src/main/java/io/github/dfa1/vortex/writer/encode/ConstantEncodingEncoder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ public StatsOptions statsOptions() {
3333
@Override
3434
public Estimate expectedRatio(DType dtype, Object data, ArrayStats stats) {
3535
if (stats.valueCount() == 0) {
36-
return Estimate.alwaysUse();
36+
return Estimate.ALWAYS_USE;
3737
}
3838
if (!stats.hasDistinctCount()) {
39-
return null;
39+
return Estimate.COMPLETE;
4040
}
4141
if (stats.distinctCount() == 1) {
42-
return Estimate.alwaysUse();
42+
return Estimate.ALWAYS_USE;
4343
}
44-
return Estimate.skip();
44+
return Estimate.SKIP;
4545
}
4646

4747
@Override

writer/src/main/java/io/github/dfa1/vortex/writer/encode/DictEncodingEncoder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ public StatsOptions statsOptions() {
4343
public Estimate expectedRatio(DType dtype, Object data, ArrayStats stats) {
4444
// Stats path only covers Primitive (Utf8 still uses sample-encoded selection).
4545
if (!(dtype instanceof DType.Primitive) || !stats.hasDistinctCount()) {
46-
return null;
46+
return Estimate.COMPLETE;
4747
}
4848
long n = stats.valueCount();
4949
long distinct = stats.distinctCount();
5050
if (n == 0) {
51-
return Estimate.skip();
51+
return Estimate.SKIP;
5252
}
5353
// Rust FloatDictScheme / IntDictScheme skip rule. Skip-only: the raw dict cost
5454
// ignores cascade bitpacking on the codes child, so a raw ratio over-estimates
5555
// dict's effectiveness vs encoders like ALP whose sample measure includes cascade.
5656
// Defer to the sample-encoded path for the actual win.
5757
if (distinct * 2 > n) {
58-
return Estimate.skip();
58+
return Estimate.SKIP;
5959
}
60-
return null;
60+
return Estimate.COMPLETE;
6161
}
6262

6363
@Override

writer/src/main/java/io/github/dfa1/vortex/writer/encode/EncodingEncoder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ default StatsOptions statsOptions() {
5353
/// @param dtype the logical type of the data
5454
/// @param data the input data
5555
/// @param stats pre-computed stats reflecting the merged [StatsOptions]
56-
/// @return [Estimate.Skip] / [Estimate.AlwaysUse], or `null` to defer to the
57-
/// sample-encoded selection path
56+
/// @return [Estimate#SKIP] / [Estimate#ALWAYS_USE], or [Estimate#COMPLETE] to
57+
/// defer to the sample-encoded selection path
5858
default Estimate expectedRatio(DType dtype, Object data, ArrayStats stats) {
59-
return null;
59+
return Estimate.COMPLETE;
6060
}
6161
}
Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
11
package io.github.dfa1.vortex.writer.encode;
22

3-
/// Compression estimate returned by an [EncodingEncoder] given pre-computed [ArrayStats].
4-
///
5-
/// Two terminal verdicts:
6-
/// - [Skip] — encoder is incompatible with this input; do not consider.
7-
/// - [AlwaysUse] — encoder is provably the best choice; pick immediately.
8-
///
9-
/// Encoders that are neither incompatible nor provably-best return `null` so the
10-
/// [CascadingCompressor] competes them by real sample-encode size rather than an
11-
/// up-front ratio estimate.
12-
public sealed interface Estimate {
3+
/// Fast-path verdict an [EncodingEncoder] returns from pre-computed [ArrayStats],
4+
/// letting the [CascadingCompressor] skip the expensive sample-encode probe.
5+
public enum Estimate {
136

14-
/// Skip this encoder for this input.
15-
record Skip() implements Estimate {
16-
}
7+
/// Encoder is incompatible with this input; exclude it from the competition.
8+
SKIP,
179

18-
/// Pick this encoder immediately; short-circuit the cascade.
19-
record AlwaysUse() implements Estimate {
20-
}
10+
/// Encoder is provably the best choice; pick it immediately and short-circuit.
11+
ALWAYS_USE,
2112

22-
Estimate SKIP = new Skip();
23-
Estimate ALWAYS_USE = new AlwaysUse();
24-
25-
/// @return the [Skip] verdict singleton
26-
static Estimate skip() {
27-
return SKIP;
28-
}
29-
30-
/// @return the [AlwaysUse] verdict singleton
31-
static Estimate alwaysUse() {
32-
return ALWAYS_USE;
33-
}
13+
/// No shortcut: run the complete sample-encode evaluation to size this encoder.
14+
COMPLETE
3415
}

writer/src/main/java/io/github/dfa1/vortex/writer/encode/RunEndEncodingEncoder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,20 @@ public StatsOptions statsOptions() {
3737
@Override
3838
public Estimate expectedRatio(DType dtype, Object data, ArrayStats stats) {
3939
if (!(dtype instanceof DType.Primitive) || !stats.hasDistinctCount()) {
40-
return null;
40+
return Estimate.COMPLETE;
4141
}
4242
long n = stats.valueCount();
4343
long distinct = stats.distinctCount();
4444
if (n == 0) {
45-
return Estimate.skip();
45+
return Estimate.SKIP;
4646
}
4747
// Skip rule: if every value is distinct, each row is its own run — pure overhead.
4848
// Defer to the sample-encoded path otherwise; RunEnd's actual compression depends
4949
// on run-length distribution which is not summarised by distinct count alone.
5050
if (distinct >= n) {
51-
return Estimate.skip();
51+
return Estimate.SKIP;
5252
}
53-
return null;
53+
return Estimate.COMPLETE;
5454
}
5555

5656
@Override

writer/src/main/java/io/github/dfa1/vortex/writer/encode/SparseEncodingEncoder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ public StatsOptions statsOptions() {
3939
@Override
4040
public Estimate expectedRatio(DType dtype, Object data, ArrayStats stats) {
4141
if (!(dtype instanceof DType.Primitive) || !stats.hasMostFrequent()) {
42-
return null;
42+
return Estimate.COMPLETE;
4343
}
4444
long n = stats.valueCount();
4545
// Sparse stores fill scalar (hardcoded 0) + n - topFreq patches. Skip unless the
4646
// dominant value's bit pattern is zero AND it covers more than half the array —
4747
// otherwise the patch buffer dwarfs raw storage.
4848
if (n == 0 || stats.mostFrequentBits() != 0L || stats.topFrequency() * 2 < n) {
49-
return Estimate.skip();
49+
return Estimate.SKIP;
5050
}
51-
return null;
51+
return Estimate.COMPLETE;
5252
}
5353

5454
/// Cascade gate: skip unless analytic sparse size beats raw-bitpacked size.

0 commit comments

Comments
 (0)