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
54 changes: 32 additions & 22 deletions cli/src/main/java/io/github/dfa1/vortex/cli/tui/GridRender.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,9 @@ static String formatCell(Array array, long i, DType declared) {
}
}
return switch (inner) {
// Long/IntArray have no dtype-aware getter, so gate the unsigned rendering on the
// ptype; a U64/U32 high-half value would otherwise show as a negative decimal
// (e.g. a U8 magnesium column widened to U64 with the sign bit set).
case LongArray a -> isUnsigned(a)
? Long.toUnsignedString(a.getLong(i))
: Long.toString(a.getLong(i));
case IntArray a -> isUnsigned(a)
? Integer.toUnsignedString(a.getInt(i))
: Integer.toString(a.getInt(i));
// Short/ByteArray.getInt already zero-extends U16/U8, so widening to int and
// printing signed decimal yields the correct unsigned value.
case ShortArray a -> Integer.toString(a.getInt(i));
case ByteArray a -> Integer.toString(a.getInt(i));
case DoubleArray a -> Double.toString(a.getDouble(i));
case FloatArray a -> Float.toString(a.getFloat(i));
case Float16Array a -> Float.toString(a.getFloat(i));
case BoolArray a -> Boolean.toString(a.getBoolean(i));
case VarBinArray a -> a.dtype() instanceof DType.Utf8
? a.getString(i)
: bytesToHex(a.getBytes(i));
case VarBinArray a -> a.dtype() instanceof DType.Utf8 ? a.getString(i) : bytesToHex(a.getBytes(i));
case DecimalArray a -> a.getDecimal(i).toPlainString();
default -> "<" + inner.getClass().getSimpleName() + ">";
default -> formatPrimitive(inner, i).orElse("<" + inner.getClass().getSimpleName() + ">");
};
} catch (RuntimeException e) {
String msg = e.getMessage();
Expand All @@ -89,6 +70,31 @@ static String formatCell(Array array, long i, DType declared) {
}
}

/// Formats the element at `i` of a numeric or boolean leaf array, shared by the grid and the
/// inspector, which differ only in how they render strings, bytes and decimals.
///
/// @param array the leaf value array (never a [MaskedArray]; the caller unwraps first)
/// @param i element index
/// @return the formatted value, or empty for any other array shape
static Optional<String> formatPrimitive(Array array, long i) {
return switch (array) {
// Long/IntArray have no dtype-aware getter, so gate the unsigned rendering on the
// ptype; a U64/U32 high-half value would otherwise show as a negative decimal
// (e.g. a U8 magnesium column widened to U64 with the sign bit set).
case LongArray a -> Optional.of(isUnsigned(a) ? Long.toUnsignedString(a.getLong(i)) : Long.toString(a.getLong(i)));
case IntArray a -> Optional.of(isUnsigned(a) ? Integer.toUnsignedString(a.getInt(i)) : Integer.toString(a.getInt(i)));
// Short/ByteArray.getInt already zero-extends U16/U8, so widening to int and
// printing signed decimal yields the correct unsigned value.
case ShortArray a -> Optional.of(Integer.toString(a.getInt(i)));
case ByteArray a -> Optional.of(Integer.toString(a.getInt(i)));
case DoubleArray a -> Optional.of(Double.toString(a.getDouble(i)));
case FloatArray a -> Optional.of(Float.toString(a.getFloat(i)));
case Float16Array a -> Optional.of(Float.toString(a.getFloat(i)));
case BoolArray a -> Optional.of(Boolean.toString(a.getBoolean(i)));
default -> Optional.empty();
};
}

/// Whether the array's dtype is an unsigned integer, so high-half values must render as
/// unsigned decimal rather than the two's-complement negative the raw signed getter returns.
///
Expand All @@ -112,7 +118,11 @@ private static Optional<String> formatExtension(DType.Extension ext, Array stora
};
}

private static String bytesToHex(byte[] bytes) {
/// Renders up to the first 16 bytes as `0x`-prefixed hex, with `...` when there are more.
///
/// @param bytes the bytes to render
/// @return the hex preview
static String bytesToHex(byte[] bytes) {
int n = Math.min(bytes.length, 16);
StringBuilder sb = new StringBuilder(n * 2 + 2);
sb.append("0x");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,9 @@
import io.github.dfa1.vortex.core.model.DType;
import io.github.dfa1.vortex.core.model.ExtensionId;
import io.github.dfa1.vortex.reader.array.Array;
import io.github.dfa1.vortex.reader.array.BoolArray;
import io.github.dfa1.vortex.reader.array.ByteArray;
import io.github.dfa1.vortex.reader.array.DecimalArray;
import io.github.dfa1.vortex.reader.array.DoubleArray;
import io.github.dfa1.vortex.reader.array.Float16Array;
import io.github.dfa1.vortex.reader.array.FloatArray;
import io.github.dfa1.vortex.reader.array.GenericArray;
import io.github.dfa1.vortex.reader.array.IntArray;
import io.github.dfa1.vortex.reader.array.LongArray;
import io.github.dfa1.vortex.reader.array.MaskedArray;
import io.github.dfa1.vortex.reader.array.ShortArray;
import io.github.dfa1.vortex.reader.array.StructArray;
import io.github.dfa1.vortex.reader.array.VarBinArray;
import io.github.dfa1.vortex.reader.extension.DateExtensionDecoder;
Expand Down Expand Up @@ -47,29 +39,12 @@ static String formatValue(Array array, int i, DType declared) {
}
}
return switch (array) {
// Long/IntArray have no dtype-aware getter, so gate the unsigned rendering on the
// ptype; a U64/U32 high-half value would otherwise show as a negative decimal.
case LongArray a -> isUnsigned(a)
? Long.toUnsignedString(a.getLong(i))
: Long.toString(a.getLong(i));
case IntArray a -> isUnsigned(a)
? Integer.toUnsignedString(a.getInt(i))
: Integer.toString(a.getInt(i));
// Short/ByteArray.getInt already zero-extends U16/U8, so a signed-decimal print of the
// widened int is the correct unsigned value.
case ShortArray a -> Integer.toString(a.getInt(i));
case ByteArray a -> Integer.toString(a.getInt(i));
case DoubleArray a -> Double.toString(a.getDouble(i));
case FloatArray a -> Float.toString(a.getFloat(i));
case Float16Array a -> Float.toString(a.getFloat(i));
case BoolArray a -> Boolean.toString(a.getBoolean(i));
case VarBinArray a -> a.dtype() instanceof DType.Utf8
? "\"" + a.getString(i) + "\""
: bytesToShortHex(a.getBytes(i));
case VarBinArray a -> a.dtype() instanceof DType.Utf8 ? "\"" + a.getString(i) + "\"" : GridRender.bytesToHex(a.getBytes(i));
case GenericArray a when a.dtype() instanceof DType.Decimal ->
tryDecimal(a::getDecimal, a, i);
case DecimalArray a -> tryDecimal(a::getDecimal, a, i);
default -> "<" + array.getClass().getSimpleName() + " " + array.dtype() + ">";
default -> GridRender.formatPrimitive(array, i)
.orElse("<" + array.getClass().getSimpleName() + " " + array.dtype() + ">");
};
}

Expand Down Expand Up @@ -124,15 +99,6 @@ private static String formatStatsCell(Array field, int row, DType declared) {
return formatValue(field, row, declared);
}

/// Whether the array's dtype is an unsigned integer, so high-half values must render as
/// unsigned decimal rather than the two's-complement negative the raw signed getter returns.
///
/// @param arr the value array under inspection
/// @return `true` when the dtype is a `U8`–`U64` primitive
private static boolean isUnsigned(Array arr) {
return arr.dtype() instanceof DType.Primitive p && p.ptype().isUnsigned();
}

private static String tryDecimal(LongFunction<BigDecimal> reader, Array a, int i) {
try {
return reader.apply(i).toPlainString();
Expand All @@ -145,19 +111,6 @@ private static String tryDecimal(LongFunction<BigDecimal> reader, Array a, int i
}
}

private static String bytesToShortHex(byte[] bytes) {
int n = Math.min(bytes.length, 16);
StringBuilder sb = new StringBuilder(n * 3 + 2);
sb.append("0x");
for (int i = 0; i < n; i++) {
sb.append(String.format("%02x", bytes[i] & 0xff));
}
if (bytes.length > n) {
sb.append("...");
}
return sb.toString();
}

/// Formats one 16-byte row of a hex dump: offset, hex columns, ASCII gutter.
///
/// @param data the bytes being dumped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ public final class Ansi {
/// Clear entire screen.
public static final String CLEAR_SCREEN = CSI + "2J";

/// Erase from the cursor to the end of the current line.
public static final String CLEAR_LINE_RIGHT = CSI + "K";

/// Move cursor to top-left.
public static final String CURSOR_HOME = CSI + "H";

Expand Down
15 changes: 2 additions & 13 deletions csv/src/main/java/io/github/dfa1/vortex/csv/CsvExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -295,26 +295,15 @@ private static String jsonValue(Array arr, long rowIdx) {
return switch (arr) {
case StructArray sa -> isNullStructRow(sa, rowIdx) ? "null" : jsonObject(sa, rowIdx);
case MaskedArray ma -> ma.isValid(rowIdx) ? jsonValue(ma.inner(), rowIdx) : "null";
case FixedSizeListArray fla -> jsonArray(fla.elements(), rowIdx * fla.fixedSize(), (rowIdx + 1L) * fla.fixedSize());
case ListArray la -> {
long start = offsetAt(la.offsets(), rowIdx);
long end = offsetAt(la.offsets(), rowIdx + 1);
yield jsonArray(la.elements(), start, end);
}
// A list view addresses each row by its own (offset, size) pair: rows may overlap or
// appear out of order in `elements`, so the next row's offset is not this row's end.
case ListViewArray lv -> {
long start = offsetAt(lv.offsets(), rowIdx);
yield jsonArray(lv.elements(), start, start + offsetAt(lv.sizes(), rowIdx));
}
case NullArray _ -> "null";
case UnionArray ua -> ua.isValid(rowIdx) ? jsonValue(ua.variant(ua.variantIndex(rowIdx)), rowIdx) : "null";
case VarBinArray va -> {
StringBuilder sb = new StringBuilder();
jsonString(sb, va.getString(rowIdx));
yield sb.toString();
}
// Numeric and boolean leaves render identically to a top-level cell, unquoted.
// Numeric and boolean leaves, and lists (already a JSON array), render identically to a
// top-level cell.
default -> cellValue(arr, rowIdx);
};
}
Expand Down
30 changes: 30 additions & 0 deletions csv/src/test/java/io/github/dfa1/vortex/csv/CsvExporterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,36 @@ void rendersListColumnAsJsonArray(@TempDir Path tmp) throws Exception {
"c,[]");
}

@Test
void rendersListOfListsAsNestedJsonArrays(@TempDir Path tmp) throws Exception {
// Given a List[List[I64]] column: the inner lists are rendered by jsonValue, not cellValue,
// so this pins that a list nested inside JSON renders the same as a top-level list cell.
Path vortex = tmp.resolve("listoflists.vortex");
DType.Struct schema = new DType.Struct(
List.of(ColumnName.of("ids")),
List.of(new DType.List(new DType.List(DType.I64, false), false)),
false);
try (FileChannel ch = FileChannel.open(vortex, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
VortexWriter writer = VortexWriter.create(ch, schema, WriteOptions.defaults())) {
writer.writeChunk(Map.of(
ColumnName.of("ids"), new ListData(
new ListData(new long[]{1L, 2L, 3L}, new long[]{0, 2, 3, 3}, 3), // [1,2] [3] []
new long[]{0, 2, 3}, // row 0 → [[1,2],[3]], row 1 → [[]]
2)));
}
Path csv = tmp.resolve("out.csv");

// When
CsvExporter.exportCsv(vortex, csv);
List<String> result = Files.readAllLines(csv);

// Then
assertThat(result).containsExactly(
"ids",
"\"[[1,2],[3]]\"",
"[[]]");
}

@Test
void rendersListOfStructsAsJsonArrayOfObjects(@TempDir Path tmp) throws Exception {
// Given a List[Struct] column — the osm-germany `members` shape (issue #257);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,12 @@ private static Array decodeString(ProtoScalarValue scalar, DType dtype, long n)
return new VarBinConstantArray(dtype, n, strBytes);
}

private static long scalarToRawBits(ProtoScalarValue scalar) {
/// Raw bits of a primitive scalar as a `long`: floats by their IEEE bit pattern, `0` when the
/// scalar carries no primitive value. Shared with [SparseEncodingDecoder]'s fill value.
///
/// @param scalar the scalar to read
/// @return the value's raw bits
static long scalarToRawBits(ProtoScalarValue scalar) {
if (scalar.int64_value() != null) {
return scalar.int64_value();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,4 @@ public void alignToByte() {
public long byteOffset() {
return bitPos >>> 3;
}

/// @return total bits consumed since construction
public long bitsConsumed() {
return bitPos;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public Array decode(DecodeContext ctx) {
throw new VortexException(EncodingId.VORTEX_SPARSE, "expected primitive dtype, got " + ctx.dtype());
}
PType valuePtype = ((DType.Primitive) ctx.dtype()).ptype();
long fillBits = scalarToLong(fillScalar);
long fillBits = ConstantEncodingDecoder.scalarToRawBits(fillScalar);

// Lazy path: keep fill bits + decoded patches; no n-sized buffer allocated.
// When numPatches == 0 we still decode zero-length children so the record's
Expand Down Expand Up @@ -330,24 +330,4 @@ private static byte[] fillBytes(ProtoScalarValue fill, boolean fillValid) {
return new byte[0];
}

private static long scalarToLong(ProtoScalarValue scalar) {
if (scalar.int64_value() != null) {
return scalar.int64_value();
}
if (scalar.uint64_value() != null) {
return scalar.uint64_value();
}
if (scalar.f16_value() != null) {
// Rust writes an f16 as its 16 bits in a u64; the legacy uint64_value form is read above
return scalar.f16_value();
}
if (scalar.f32_value() != null) {
return Float.floatToRawIntBits(scalar.f32_value());
}
if (scalar.f64_value() != null) {
return Double.doubleToRawLongBits(scalar.f64_value());
}
return 0L;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -76,17 +74,6 @@ public Instant instant(DType.Extension ext, Array storage, long i) {
return ExtensionStorage.instantFromRaw(ExtensionStorage.epochInteger(storage, i), unit);
}

/// Decodes the timestamp cell at row `i` to a [ZonedDateTime]
/// using the timezone from the metadata, defaulting to UTC when absent.
///
/// @param ext declared extension dtype
/// @param storage signed-integer storage array
/// @param i row index, `0 <= i < storage.length()`
/// @return decoded zoned date-time
public ZonedDateTime zonedDateTime(DType.Extension ext, Array storage, long i) {
return instant(ext, storage, i).atZone(timezone(ext).orElse(ZoneOffset.UTC));
}

/// Returns the IANA timezone recorded in the extension metadata.
///
/// @param ext declared extension dtype
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import io.github.dfa1.vortex.core.model.DType;
import io.github.dfa1.vortex.core.model.LayoutId;

import static io.github.dfa1.vortex.core.io.VortexFormat.LE_INT;

import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.util.ArrayList;
Expand Down Expand Up @@ -105,18 +103,6 @@ public ColumnName fieldName() {
private ZonedStatsSchema() {
}

/// Returns the zone length declared in the layout metadata (logical rows per zone),
/// or `0` if the metadata is too short to carry one.
///
/// @param metadata raw `vortex.stats` layout metadata, possibly `null`
/// @return decoded zone length, or `0` when absent
public static long zoneLength(MemorySegment metadata) {
if (metadata == null || metadata.byteSize() < 4) {
return 0L;
}
return Integer.toUnsignedLong(metadata.get(LE_INT, 0));
}

/// Returns the zone length declared in a newer `vortex.zoned` layout's aggregate-spec metadata
/// (field 1, `uint32 zone_len`, of the `ZonedMetadataProto` described in [#aggregateIds]), or
/// `0` when the blob is empty, carries an unsupported version, or is not well-formed protobuf.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ void zero_bits_returns_zero() {

// Then
assertThat(result).isZero();
assertThat(sut.bitsConsumed()).isZero();
// a zero-width read must not advance: the whole byte is still there
assertThat(sut.readBits(8)).isEqualTo(0xFFL);
}

@Test
Expand All @@ -54,7 +55,8 @@ void single_byte_low_nibble() {

// Then — LSB first: bits [3:0] = 0b0011 = 3
assertThat(result).isEqualTo(3L);
assertThat(sut.bitsConsumed()).isEqualTo(4);
// advanced exactly 4 bits: the high nibble 0b1011 comes next
assertThat(sut.readBits(4)).isEqualTo(0xBL);
}

@Test
Expand Down Expand Up @@ -148,7 +150,8 @@ void sequential_reads_advance_position() {
assertThat(b1).isEqualTo(1);
assertThat(b2).isZero();
assertThat(b3).isEqualTo(1);
assertThat(sut.bitsConsumed()).isEqualTo(4);
// advanced exactly 4 bits: the high nibble 0b1010 comes next
assertThat(sut.readBits(4)).isEqualTo(0xAL);
}

@Test
Expand Down Expand Up @@ -190,7 +193,6 @@ void already_aligned_is_noop() {

// Then — byte offset unchanged
assertThat(sut.byteOffset()).isEqualTo(1);
assertThat(sut.bitsConsumed()).isEqualTo(8);
}

@Test
Expand All @@ -204,7 +206,6 @@ void partial_byte_advances_to_next_byte() {

// Then — aligned to byte 1
assertThat(sut.byteOffset()).isEqualTo(1);
assertThat(sut.bitsConsumed()).isEqualTo(8);
}

@Test
Expand Down
Loading
Loading