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
24 changes: 24 additions & 0 deletions cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ static Path writeSmallVortex(Path dir, String name) throws IOException {
return file;
}

/// Writes a 3-row file with mixed column types: `id` (I64), `qty` (I32),
/// `price` (F64), and `name` (Utf8). Used to exercise per-type formatting and
/// comparison branches that the single-column [#writeSmallVortex] cannot reach.
static Path writeTypedVortex(Path dir, String name) throws IOException {
Path file = dir.resolve(name);
DType.Struct schema = new DType.Struct(
List.of("id", "qty", "price", "name"),
List.of(
new DType.Primitive(PType.I64, false),
new DType.Primitive(PType.I32, false),
new DType.Primitive(PType.F64, false),
new DType.Utf8(false)),
false);
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
VortexWriter writer = VortexWriter.create(ch, schema, WriteOptions.defaults())) {
writer.writeChunk(Map.of(
"id", new long[]{1L, 2L, 3L},
"qty", new int[]{10, 20, 30},
"price", new double[]{100.0, 200.0, 300.0},
"name", new String[]{"alice", "bob", "carol"}));
}
return file;
}

/// Captured streams + exit status from one CLI invocation.
record Captured(int status, String stdout, String stderr) {
}
Expand Down
104 changes: 104 additions & 0 deletions cli/src/test/java/io/github/dfa1/vortex/cli/FilterCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
import static io.github.dfa1.vortex.cli.CliTestSupport.writeSmallVortex;
import static io.github.dfa1.vortex.cli.CliTestSupport.writeTypedVortex;
import static org.assertj.core.api.Assertions.assertThat;

class FilterCommandTest {
Expand Down Expand Up @@ -90,6 +91,109 @@ void unknownColumn_returnsErrorNotCrash() {
assertThat(result.stderr()).contains("error:");
}

@Test
void doubleValueAgainstLongColumn_returnsOk() {
// Given — a fractional literal parses as Double, compared against an I64 column
// (exercises parseValue's Double branch and compareNumeric's Double path)
// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", file.toString(), "id", ">", "1.5"}));

// Then — id in {2, 3} match
assertThat(result.status()).isEqualTo(ExitStatus.OK);
assertThat(result.stdout()).startsWith("id");
}

@Test
void unknownOperator_returnsUsageError() {
// Given — a lone '!' is a recognised operator char but not a valid operator
// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", file.toString(), "id", "!", "1"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR);
assertThat(result.stderr()).contains("unknown operator");
}

@Test
void emptyValue_returnsUsageError() {
// Given — operator present but no value after it
// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", file.toString(), "id", ">"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR);
assertThat(result.stderr()).contains("invalid filter expression");
}

@Test
void operatorAtStart_returnsUsageError() {
// Given — operator at index 0 means an empty column name
// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", file.toString(), "=", "1"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR);
assertThat(result.stderr()).contains("invalid filter expression");
}

@Test
void invalidColumnName_returnsUsageError() {
// Given — '-' is not a legal column-name character
// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", file.toString(), "a-b", "=", "1"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR);
assertThat(result.stderr()).contains("invalid filter expression");
}

@Test
void filtersDoubleColumn_returnsOk() throws IOException {
// Given — F64 column with a fractional threshold (compareDouble path)
Path typed = writeTypedVortex(tmp, "typed.vortex");

// When — price in {200.0, 300.0} match
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", typed.toString(), "price", ">", "150.0"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.OK);
assertThat(result.stdout()).startsWith("id");
}

@Test
void filtersIntColumn_returnsOk() throws IOException {
// Given — I32 column (compareValue's IntArray branch)
Path typed = writeTypedVortex(tmp, "typed.vortex");

// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", typed.toString(), "qty", ">=", "20"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.OK);
assertThat(result.stdout()).startsWith("id");
}

@Test
void filtersStringColumn_returnsOk() throws IOException {
// Given — Utf8 column (compareValue's VarBinArray branch, lexicographic compare)
Path typed = writeTypedVortex(tmp, "typed.vortex");

// When
CliTestSupport.Captured result = capture(() ->
FilterCommand.run(new String[]{"filter", typed.toString(), "name", "==", "bob"}));

// Then
assertThat(result.status()).isEqualTo(ExitStatus.OK);
assertThat(result.stdout()).startsWith("id");
}

@Test
void operatorPrefixAttack_doesNotCauseBacktracking() {
// Given — input designed to trigger polynomial backtracking under the previous regex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

import io.github.dfa1.vortex.core.DType;
import io.github.dfa1.vortex.core.PType;
import io.github.dfa1.vortex.encoding.TimeUnit;
import io.github.dfa1.vortex.reader.array.Array;
import io.github.dfa1.vortex.reader.array.ByteArray;
import io.github.dfa1.vortex.reader.array.FixedSizeListArray;
import io.github.dfa1.vortex.reader.array.IntArray;
import io.github.dfa1.vortex.reader.array.LazyConstantDecimalArray;
import io.github.dfa1.vortex.reader.array.LongArray;
import io.github.dfa1.vortex.reader.array.MaskedArray;
import io.github.dfa1.vortex.reader.array.StructArray;
import io.github.dfa1.vortex.reader.extension.TimeExtensionDecoder;
import io.github.dfa1.vortex.reader.extension.TimestampExtensionDecoder;
import io.github.dfa1.vortex.reader.extension.UuidExtensionDecoder;
import org.junit.jupiter.api.Test;

import java.lang.foreign.Arena;
Expand Down Expand Up @@ -80,6 +86,45 @@ void rendersDateExtensionFromIntStorage() {
}
}

@Test
void rendersTimeExtensionFromIntStorage() {
try (Arena arena = Arena.ofConfined()) {
// Given — vortex.time (ms unit) over I32 storage; 0 ms-of-day = midnight
DType timeExt = TimeExtensionDecoder.INSTANCE.dtype(TimeUnit.Milliseconds, false);
IntArray storage = ArrayFixtures.ints(arena, 0);

// When / Then
assertThat(GridRender.formatCell(storage, 0, timeExt)).isEqualTo("00:00");
}
}

@Test
void rendersTimestampExtensionFromLongStorage() {
try (Arena arena = Arena.ofConfined()) {
// Given — vortex.timestamp (ms unit, no tz) over I64 storage; 0 ms = the epoch
DType tsExt = TimestampExtensionDecoder.INSTANCE.dtype(TimeUnit.Milliseconds, null, false);
LongArray storage = ArrayFixtures.longs(arena, 0L);

// When / Then
assertThat(GridRender.formatCell(storage, 0, tsExt)).isEqualTo("1970-01-01T00:00:00Z");
}
}

@Test
void rendersUuidExtensionFromFixedSizeListStorage() {
try (Arena arena = Arena.ofConfined()) {
// Given — vortex.uuid over FixedSizeList(U8,16); all-zero bytes = the nil UUID
DType uuidExt = UuidExtensionDecoder.INSTANCE.dtype(false);
ByteArray elems = ArrayFixtures.bytes(arena, new byte[16]);
FixedSizeListArray storage = new FixedSizeListArray(
new DType.FixedSizeList(new DType.Primitive(PType.U8, false), 16, false), 1, elems);

// When / Then
assertThat(GridRender.formatCell(storage, 0, uuidExt))
.isEqualTo("00000000-0000-0000-0000-000000000000");
}
}

@Test
void maskedNullCellRendersEmpty() {
try (Arena arena = Arena.ofConfined()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.dfa1.vortex.core.DType;
import io.github.dfa1.vortex.core.PType;
import io.github.dfa1.vortex.reader.array.Array;
import io.github.dfa1.vortex.reader.array.GenericArray;
import io.github.dfa1.vortex.reader.array.IntArray;
import io.github.dfa1.vortex.reader.array.LazyConstantDecimalArray;
import io.github.dfa1.vortex.reader.array.MaskedArray;
Expand All @@ -11,6 +12,8 @@
import org.junit.jupiter.api.Test;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.math.BigDecimal;
import java.util.List;

Expand Down Expand Up @@ -57,6 +60,33 @@ void rendersDecimal() {
assertThat(InspectorRender.formatValue(sut, 0, decimal)).isEqualTo("3.14");
}

@Test
void rendersGenericArrayDecimal() {
try (Arena arena = Arena.ofConfined()) {
// Given — GenericArray over an 8-byte LE mantissa 123 at scale 2 → 1.23
DType decimal = new DType.Decimal((byte) 10, (byte) 2, false);
MemorySegment buf = arena.allocate(8, 8);
buf.setAtIndex(ValueLayout.JAVA_LONG, 0, 123L);
Array sut = new GenericArray(decimal, 1, buf.asReadOnly());

// When / Then
assertThat(InspectorRender.formatValue(sut, 0, decimal)).isEqualTo("1.23");
}
}

@Test
void genericArrayDecimalBadShapeRendersFallback() {
try (Arena arena = Arena.ofConfined()) {
// Given — two buffers: getDecimal rejects the shape with a non-"null cell" error
DType decimal = new DType.Decimal((byte) 10, (byte) 2, false);
MemorySegment buf = arena.allocate(8, 8);
Array sut = new GenericArray(decimal, 1, new MemorySegment[]{buf, buf}, new Array[0]);

// When / Then — tryDecimal swallows it into the <ClassName dtype> fallback
assertThat(InspectorRender.formatValue(sut, 0, decimal)).startsWith("<GenericArray");
}
}

@Test
void rendersDateExtension() {
try (Arena arena = Arena.ofConfined()) {
Expand Down