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
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ procedure):
`forEachChunk`; use `Optional`, records, sealed types, pattern switches, virtual threads, FFM).
New APIs should feel like JDK APIs.
- Always braces for `if`/`else`/`for`/`while`, even one-liners (`if (c) { return a; }`).
- **Enums: never call `values()` on a call path.** `values()` allocates a fresh array on every call (64 B for
`PType`), which the JIT does not remove. Look a constant up by wire tag, ordinal or number with a `switch`
expression: no allocation, and about 4x faster than `values()[i]` when the ordinals repeat, as in real files
(it is a jump table, so uniformly random ordinals cost a mispredict). Pin it to the declaration order with an
`@EnumSource` round-trip test. The generated proto enums already do this (`proto-gen`'s `CodeGen`). To
iterate all constants, take the array once: `private static final List<X> ALL = List.of(values());`.
`values()` inside a static initializer is fine; tests are exempt.
- **Time quantities use `java.time.Duration`, never `long`** (no `long timeoutMs`/`delayNanos`).
Exception: low-level JDK interop taking `long ns` (`Thread.sleep`, `LockSupport.parkNanos`,
`System.nanoTime` math) — convert at the call site via `duration.toNanos()`/`toMillis()`.
Expand Down
6 changes: 5 additions & 1 deletion cli/src/main/java/io/github/dfa1/vortex/cli/FileFormat.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.dfa1.vortex.cli;

import java.util.List;
import java.util.Optional;

/// The file formats `import`/`export` recognize by extension. The single source of truth for
Expand All @@ -11,6 +12,9 @@ enum FileFormat {
PARQUET(".parquet"),
VORTEX(".vortex");

// values() clones its array on every call; take it once.
private static final List<FileFormat> ALL = List.of(values());

private final String extension;

FileFormat(String extension) {
Expand All @@ -30,7 +34,7 @@ boolean matches(String fileName) {
/// @param fileName a file name or URL path, e.g. `"data.parquet"`
/// @return the matching format, or empty if none of `.csv`/`.parquet`/`.vortex` matches
static Optional<FileFormat> of(String fileName) {
for (FileFormat format : values()) {
for (FileFormat format : ALL) {
if (format.matches(fileName)) {
return Optional.of(format);
}
Expand Down
24 changes: 17 additions & 7 deletions core/src/main/java/io/github/dfa1/vortex/core/model/PType.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,23 @@ public boolean isUnsigned() {
/// @throws VortexException if `ordinal` is negative or greater than the largest defined
/// [PType] ordinal
public static PType fromOrdinal(int ordinal) {
PType[] all = values();
if (ordinal < 0 || ordinal >= all.length) {
throw new VortexException(
"invalid PType ordinal=" + ordinal
+ " (expected 0.." + (all.length - 1) + ")");
}
return all[ordinal];
// A switch, not values()[ordinal]: values() clones the array on every call, and this runs
// for every dtype decoded. A test pins the cases to the declaration order.
return switch (ordinal) {
case 0 -> U8;
case 1 -> U16;
case 2 -> U32;
case 3 -> U64;
case 4 -> I8;
case 5 -> I16;
case 6 -> I32;
case 7 -> I64;
case 8 -> F16;
case 9 -> F32;
case 10 -> F64;
default -> throw new VortexException(
"invalid PType ordinal=" + ordinal + " (expected 0.." + F64.ordinal() + ")");
};
}

/// Returns the narrowest unsigned integer ptype that can hold every value in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ public enum TimeUnit {
/// @return the corresponding `TimeUnit`
public static TimeUnit fromTag(byte tag) {
int i = Byte.toUnsignedInt(tag);
TimeUnit[] values = values();
if (i >= values.length) {
throw new IllegalArgumentException("unknown TimeUnit tag: " + i);
}
return values[i];
return switch (i) {
case 0 -> Nanoseconds;
case 1 -> Microseconds;
case 2 -> Milliseconds;
case 3 -> Seconds;
case 4 -> Days;
default -> throw new IllegalArgumentException("unknown TimeUnit tag: " + i);
};
}

/// Returns the number of this time unit's divisions in one second (e.g. 1_000_000_000 for nanoseconds).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ public int value() {
/// @return matching enum constant
/// @throws IllegalArgumentException if no constant matches
public static ProtoNullValue fromValue(int value) {
for (ProtoNullValue v : values()) {
if (v.value == value) {
return v;
}
}
throw new IllegalArgumentException("unknown ProtoNullValue value: " + value);
// A switch, not a loop over values(): values() clones its array on every call.
return switch (value) {
case 0 -> NULL_VALUE;
default -> throw new IllegalArgumentException("unknown ProtoNullValue value: " + value);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,20 @@ public int value() {
/// @return matching enum constant
/// @throws IllegalArgumentException if no constant matches
public static ProtoPType fromValue(int value) {
for (ProtoPType v : values()) {
if (v.value == value) {
return v;
}
}
throw new IllegalArgumentException("unknown ProtoPType value: " + value);
// A switch, not a loop over values(): values() clones its array on every call.
return switch (value) {
case 0 -> U8;
case 1 -> U16;
case 2 -> U32;
case 3 -> U64;
case 4 -> I8;
case 5 -> I16;
case 6 -> I32;
case 7 -> I64;
case 8 -> F16;
case 9 -> F32;
case 10 -> F64;
default -> throw new IllegalArgumentException("unknown ProtoPType value: " + value);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.IOException;
Expand Down Expand Up @@ -447,4 +448,41 @@ private static ProtoReader readerOver(byte[] bytes) {
MemorySegment seg = MemorySegment.ofArray(bytes);
return new ProtoReader(seg, 0, seg.byteSize());
}

@Nested
class GeneratedEnums {
@ParameterizedTest
@EnumSource(ProtoPType.class)
void protoPTypeFromValue_mapsEachConstantBack(ProtoPType type) {
// Given — fromValue is a generated switch over the proto numbers

// When
ProtoPType result = ProtoPType.fromValue(type.value());

// Then
assertThat(result).isSameAs(type);
}

@ParameterizedTest
@EnumSource(ProtoNullValue.class)
void protoNullValueFromValue_mapsEachConstantBack(ProtoNullValue value) {
// Given — as above

// When
ProtoNullValue result = ProtoNullValue.fromValue(value.value());

// Then
assertThat(result).isSameAs(value);
}

@Test
void fromValue_unknownNumber_throws() {
// Given — a number no constant declares

// When / Then
assertThatThrownBy(() -> ProtoPType.fromValue(99))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("unknown ProtoPType value: 99");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/// Walks a [TypeRegistry] and emits one `.java` file per message or enum.
///
Expand Down Expand Up @@ -86,12 +88,19 @@ public int value() {
/// @return matching enum constant
/// @throws IllegalArgumentException if no constant matches
public static %1$s fromValue(int value) {
for (%1$s v : values()) {
if (v.value == value) {
return v;
}
}
throw new IllegalArgumentException("unknown %1$s value: " + value);
// A switch, not a loop over values(): values() clones its array on every call.
return switch (value) {
""".formatted(e.javaName()));
Set<Integer> seen = new HashSet<>();
for (Ast.EnumValue v : values) {
// An aliased number keeps the first constant, as a linear scan would.
if (seen.add(v.number())) {
sb.append(" case ").append(v.number()).append(" -> ").append(v.name()).append(";\n");
}
}
sb.append("""
default -> throw new IllegalArgumentException("unknown %1$s value: " + value);
};
}
""".formatted(e.javaName()));
sb.append("}\n");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ static DType dtypeFromProto(io.github.dfa1.vortex.core.proto.ProtoDType proto) {
}
if (proto.primitive() != null) {
return new DType.Primitive(
PType.values()[proto.primitive().type().value()],
PType.fromOrdinal(proto.primitive().type().value()),
proto.primitive().nullable());
}
if (proto.decimal() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,8 @@ private static PType readDictLayoutCodesPType(MemorySegment rawMeta) {
byte tag = rawMeta.get(ValueLayout.JAVA_BYTE, 0);
if (tag == 0x08 && rawMeta.byteSize() > 1) {
int ordinal = rawMeta.get(ValueLayout.JAVA_BYTE, 1) & 0xFF;
PType[] values = PType.values();
if (ordinal < values.length) {
return values[ordinal];
if (ordinal <= PType.F64.ordinal()) {
return PType.fromOrdinal(ordinal);
}
}
return PType.U8;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ public enum Stat {
/// Stat ordinal 8.
NAN_COUNT("nan_count");

/// The stat at `ordinal`, or `null` past the last one (a bit set by a newer Vortex writer).
///
/// A switch, not values()[ordinal]: values() clones the array on every call, and this runs
/// per bit of every layout's stats bitset.
///
/// @param ordinal the bit index in the layout's stats bitset
/// @return the stat, or `null` when `ordinal` is not a known stat
static Stat fromOrdinal(int ordinal) {
return switch (ordinal) {
case 0 -> IS_CONSTANT;
case 1 -> IS_SORTED;
case 2 -> IS_STRICT_SORTED;
case 3 -> MAX;
case 4 -> MIN;
case 5 -> SUM;
case 6 -> NULL_COUNT;
case 7 -> UNCOMPRESSED_SIZE_IN_BYTES;
case 8 -> NAN_COUNT;
default -> null;
};
}

/// Trailing struct-field name added next to [#MAX] for truncated max indicators.
public static final ColumnName MAX_IS_TRUNCATED = ColumnName.of("max_is_truncated");
/// Trailing struct-field name added next to [#MIN] for truncated min indicators.
Expand Down Expand Up @@ -129,7 +151,7 @@ public static long aggregateZoneLength(MemorySegment metadata) {

/// Returns the stats present in the layout metadata bitset, in ordinal order.
///
/// Unknown bits (set at an index past [Stat#values()]'s length, which
/// Unknown bits (set at an index past the last [Stat], which
/// would mean a newer Vortex writer) are silently skipped — matching the Rust
/// reader's forward-compatibility behavior.
///
Expand All @@ -139,14 +161,16 @@ public static List<Stat> presentStats(MemorySegment metadata) {
if (metadata == null || metadata.byteSize() <= 4) {
return List.of();
}
Stat[] all = Stat.values();
List<Stat> out = new ArrayList<>(all.length);
List<Stat> out = new ArrayList<>();
int bitIndex = 0;
for (long pos = 4; pos < metadata.byteSize(); pos++) {
int b = metadata.get(ValueLayout.JAVA_BYTE, pos) & 0xff;
for (int bit = 0; bit < 8; bit++) {
if ((b & (1 << bit)) != 0 && bitIndex < all.length) {
out.add(all[bitIndex]);
if ((b & (1 << bit)) != 0) {
Stat stat = Stat.fromOrdinal(bitIndex);
if (stat != null) {
out.add(stat);
}
}
bitIndex++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import io.github.dfa1.vortex.core.model.PType;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.ParameterizedTest;

import java.util.EnumSet;
import java.util.List;
Expand Down Expand Up @@ -53,6 +56,33 @@ void returnsZeroForShortMetadata() {
}
}

@Nested
class StatLookup {
@ParameterizedTest
@EnumSource(ZonedStatsSchema.Stat.class)
void fromOrdinal_mapsEachStatBack(ZonedStatsSchema.Stat stat) {
// Given — fromOrdinal is a switch, so it can drift from the declaration order

// When
ZonedStatsSchema.Stat result = ZonedStatsSchema.Stat.fromOrdinal(stat.ordinal());

// Then
assertThat(result).isSameAs(stat);
}

@ParameterizedTest
@ValueSource(ints = {-1, 9, 63})
void fromOrdinal_pastTheLastStat_isNull(int ordinal) {
// Given — a bit set by a newer Vortex writer, which readers skip

// When
ZonedStatsSchema.Stat result = ZonedStatsSchema.Stat.fromOrdinal(ordinal);

// Then
assertThat(result).isNull();
}
}

@Nested
class PresentStats {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@
public final class VortexWriter implements Closeable {

// Indices into layout_specs list in the FbsFooter
// values() clones its array on every call; the edition filter reads it once per writer.
private static final List<EncodingId.WellKnown> WELL_KNOWN_IDS = List.of(EncodingId.WellKnown.values());

private static final int LAYOUT_FLAT = 0;
private static final int LAYOUT_CHUNKED = 1;
private static final int LAYOUT_STRUCT = 2;
Expand Down Expand Up @@ -265,7 +268,7 @@ private static Set<EncodingId> editionExcluded(
// Every well-known id, not only the encoders listed here: an encoder can carry its own private
// candidate list (Sparse's bool patch-index cascade offers fastlanes.delta), and those
// candidates must be filtered by the same edition policy.
for (EncodingId.WellKnown id : EncodingId.WellKnown.values()) {
for (EncodingId.WellKnown id : WELL_KNOWN_IDS) {
if (!allowed.contains(id)) {
excluded.add(id);
}
Expand Down
Loading