Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
da610c7
feat(inspector): extract inspector module with Lanterna TUI
dfa1 Jun 8, 2026
b8b8ee7
refactor(inspector): replace Lanterna with FFM-based ANSI terminal
dfa1 Jun 8, 2026
b9b7a5e
fix(cli): print exception class and cause chain on inspect errors
dfa1 Jun 8, 2026
988c3de
feat(cli): split inspect into 'inspect' (text) and 'tui' (interactive)
dfa1 Jun 8, 2026
e6a1678
feat(inspector): surface per-array min/max statistics
dfa1 Jun 8, 2026
65e13fd
ci: run inspector module tests on Windows
dfa1 Jun 8, 2026
aaabdd3
feat(inspector): progress callback during tree build; bar in tui CLI
dfa1 Jun 8, 2026
029762b
feat(inspector): xxd-style hex preview in TUI details pane
dfa1 Jun 8, 2026
f552a8c
feat(inspector): lazy TUI build + decoded data preview
dfa1 Jun 8, 2026
3174983
feat(inspector): non-blocking data fetch with spinner + status line
dfa1 Jun 8, 2026
db3dd61
fix(inspector): pin TUI I/O to handle's owning thread via IoWorker
dfa1 Jun 8, 2026
97ddfdf
fix(inspector): drop withLimit on TUI data scan; rejects GenericArray
dfa1 Jun 8, 2026
bd8b4e7
fix(scan): support GenericArray in ScanIterator.truncateArray; format…
dfa1 Jun 8, 2026
341aa9b
refactor(core): move decimal decode into GenericArray.getDecimal
dfa1 Jun 8, 2026
9087847
feat(core): decode decimal_byte_parts shape in GenericArray.getDecimal
dfa1 Jun 8, 2026
b2f4f53
feat(core): decode vortex.date cells via Extensions.localDate
dfa1 Jun 8, 2026
b424d9d
feat(inspector): show dictionary entries when a vortex.dict node is s…
dfa1 Jun 8, 2026
7905d5e
fix(inspector): format vortex.date columns using declared dtype
dfa1 Jun 8, 2026
c51f4b6
feat(inspector): spell out compression label, show bits/elem in TUI s…
dfa1 Jun 8, 2026
2e4c740
feat(inspector): tag per-chunk stats children in the TUI tree
dfa1 Jun 8, 2026
9422ffb
fix(core): derive decimal element width from buffer size, not precision
dfa1 Jun 8, 2026
97742ae
fix(core): localDateFromStorage replaced with extension-typed overload
dfa1 Jun 8, 2026
9f2b036
fix(inspector): read Layout.metadata bytes on IoWorker thread
dfa1 Jun 8, 2026
52e3078
debug(cli): unconditionally print stack trace from tui error path
dfa1 Jun 8, 2026
d2f75da
fix(inspector): InspectorTree.Node uses identity equality
dfa1 Jun 8, 2026
5990ec8
refactor(inspector): swap identity-Node override for IdentityHashMap …
dfa1 Jun 8, 2026
91b243f
refactor(inspector): rename RawTerminal to Terminal
dfa1 Jun 8, 2026
90dddfe
build(cli): set Enable-Native-Access manifest attribute on the uber-jar
dfa1 Jun 8, 2026
126733d
fix(core): reject null cells in GenericArray.getDecimal mantissa path
dfa1 Jun 9, 2026
c74c7ea
test(core): cover GenericArray.getDecimal i128 (precision > 18) path
dfa1 Jun 9, 2026
dffbb51
fix(core): bounds-check Extensions.localDate; trim doc to date-only
dfa1 Jun 9, 2026
d80fbc4
perf(core): GenericArray.getDecimal width 1/2/4/8 reads stay allocati…
dfa1 Jun 9, 2026
7fb14e6
docs(compatibility): list Vortex extension types + Java coverage status
dfa1 Jun 9, 2026
185cf99
feat(core): decode vortex.uuid cells via Extensions.uuid
dfa1 Jun 9, 2026
c18e6e2
feat(core): decode vortex.time cells via Extensions.localTime
dfa1 Jun 9, 2026
724d0de
feat(core): decode vortex.timestamp via Extensions.instant + zonedDat…
dfa1 Jun 9, 2026
05fd0a6
refactor(core): Extension sealed hierarchy replaces Extensions utilit…
dfa1 Jun 9, 2026
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,26 @@ jobs:

- name: Build and test
run: ./mvnw verify

inspector-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6

- name: Set up Azul Zulu JDK 25
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: '25'

- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Test inspector module
shell: bash
run: ./mvnw test -pl inspector -am
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
<artifactId>vortex-jdbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.github.dfa1.vortex</groupId>
<artifactId>vortex-inspector</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
21 changes: 21 additions & 0 deletions cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<groupId>io.github.dfa1.vortex</groupId>
<artifactId>vortex-reader</artifactId>
</dependency>
<dependency>
<groupId>io.github.dfa1.vortex</groupId>
<artifactId>vortex-inspector</artifactId>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down Expand Up @@ -71,6 +75,23 @@
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>io.github.dfa1.vortex.cli.VortexCli</mainClass>
<manifestEntries>
<!--
Enables the FFM downcalls in inspector/term/PosixTerminal
+ WindowsTerminal (tcgetattr, cfmakeraw, ioctl,
SetConsoleMode) without the user having to pass the
enable-native-access flag on the command line.
Per JEP 472 (finalized in JDK 25), the JVM otherwise
emits a "restricted method called" warning on each first
call and threatens to block restricted methods entirely
in a future release; this manifest attribute is the
standard way for an uber-jar to opt in once at build
time. Only the cli uber-jar gets this; vortex-core and
vortex-reader consumers still have to enable native
access themselves for their own integrations.
-->
<Enable-Native-Access>ALL-UNNAMED</Enable-Native-Access>
</manifestEntries>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
Expand Down
60 changes: 49 additions & 11 deletions cli/src/main/java/io/github/dfa1/vortex/cli/InspectCommand.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.github.dfa1.vortex.cli;

import io.github.dfa1.vortex.io.VortexInspector;
import io.github.dfa1.vortex.inspect.VortexInspector;
import io.github.dfa1.vortex.io.VortexHandle;
import io.github.dfa1.vortex.io.VortexHttpReader;
import io.github.dfa1.vortex.io.VortexReader;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;

Expand All @@ -14,20 +18,54 @@ private InspectCommand() {

static int run(String[] args) {
if (args.length != 2) {
System.err.println("usage: inspect <file.vortex>");
System.err.println("usage: inspect <file.vortex | http(s)://url>");
return ExitStatus.USAGE_ERROR;
}
Path path = Path.of(args[1]);
if (!Files.exists(path)) {
System.err.println("file not found: " + path);
return ExitStatus.FILE_NOT_FOUND;
}
try (VortexReader reader = VortexReader.open(path)) {
System.out.print(VortexInspector.inspect(reader));
try (VortexHandle handle = open(args[1])) {
if (handle == null) {
return ExitStatus.FILE_NOT_FOUND;
}
System.out.print(VortexInspector.inspect(handle));
return ExitStatus.OK;
} catch (IOException e) {
System.err.println("error: " + e.getMessage());
} catch (IOException | RuntimeException e) {
System.err.println("error: " + describe(e));
if (System.getenv("VORTEX_DEBUG") != null) {
e.printStackTrace(System.err);
}
return ExitStatus.ERROR;
}
}

private static String describe(Throwable t) {
StringBuilder sb = new StringBuilder();
Throwable cur = t;
while (cur != null) {
if (!sb.isEmpty()) {
sb.append(" -> ");
}
sb.append(cur.getClass().getSimpleName());
if (cur.getMessage() != null) {
sb.append(": ").append(cur.getMessage());
}
cur = cur.getCause();
}
return sb.toString();
}

private static VortexHandle open(String target) throws IOException {
if (target.startsWith("http://") || target.startsWith("https://")) {
try {
return VortexHttpReader.open(new URI(target));
} catch (URISyntaxException e) {
System.err.println("invalid URL: " + target);
return null;
}
}
Path path = Path.of(target);
if (!Files.exists(path)) {
System.err.println("file not found: " + path);
return null;
}
return VortexReader.open(path);
}
}
129 changes: 129 additions & 0 deletions cli/src/main/java/io/github/dfa1/vortex/cli/TuiCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package io.github.dfa1.vortex.cli;

import io.github.dfa1.vortex.inspect.InspectorTree;
import io.github.dfa1.vortex.inspect.IoWorker;
import io.github.dfa1.vortex.inspect.VortexInspectorTui;
import io.github.dfa1.vortex.io.VortexHandle;
import io.github.dfa1.vortex.io.VortexHttpReader;
import io.github.dfa1.vortex.io.VortexReader;

import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;

final class TuiCommand {

private TuiCommand() {
}

static int run(String[] args) {
if (args.length != 2) {
System.err.println("usage: tui <file.vortex | http(s)://url>");
return ExitStatus.USAGE_ERROR;
}
try (IoWorker worker = new IoWorker("vortex-tui-io")) {
VortexHandle handle = openOnWorker(worker, args[1]);
if (handle == null) {
return ExitStatus.FILE_NOT_FOUND;
}
try {
VortexInspectorTui.show(handle, worker, progressBar(System.err));
} finally {
closeOnWorker(worker, handle);
}
return ExitStatus.OK;
} catch (IOException | RuntimeException | InterruptedException e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
System.err.println("error: " + describe(e));
if (System.getenv("VORTEX_DEBUG") != null) {
e.printStackTrace(System.err);
}
return ExitStatus.ERROR;
}
}

private static VortexHandle openOnWorker(IoWorker worker, String target)
throws InterruptedException, IOException {
AtomicReference<VortexHandle> handle = new AtomicReference<>();
AtomicReference<IOException> failure = new AtomicReference<>();
worker.runAndAwait(() -> {
try {
handle.set(open(target));
} catch (IOException e) {
failure.set(e);
}
});
if (failure.get() != null) {
throw failure.get();
}
return handle.get();
}

private static void closeOnWorker(IoWorker worker, VortexHandle handle) {
try {
worker.runAndAwait(handle::close);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private static VortexHandle open(String target) throws IOException {
if (target.startsWith("http://") || target.startsWith("https://")) {
try {
return VortexHttpReader.open(new URI(target));
} catch (URISyntaxException e) {
System.err.println("invalid URL: " + target);
return null;
}
}
Path path = Path.of(target);
if (!Files.exists(path)) {
System.err.println("file not found: " + path);
return null;
}
return VortexReader.open(path);
}

private static InspectorTree.Progress progressBar(PrintStream out) {
int width = 30;
return (current, total) -> {
if (total <= 0) {
return;
}
int filled = (int) ((long) current * width / total);
StringBuilder bar = new StringBuilder(width + 32);
bar.append('\r').append("Loading metadata [");
for (int i = 0; i < width; i++) {
bar.append(i < filled ? '#' : '-');
}
bar.append("] ").append(current).append('/').append(total);
if (current == total) {
bar.append('\n');
}
out.print(bar);
out.flush();
};
}

private static String describe(Throwable t) {
StringBuilder sb = new StringBuilder();
Throwable cur = t;
while (cur != null) {
if (!sb.isEmpty()) {
sb.append(" -> ");
}
sb.append(cur.getClass().getSimpleName());
if (cur.getMessage() != null) {
sb.append(": ").append(cur.getMessage());
}
cur = cur.getCause();
}
return sb.toString();
}
}
4 changes: 3 additions & 1 deletion cli/src/main/java/io/github/dfa1/vortex/cli/VortexCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static void main(String[] args) {
}
int exit = switch (args[0]) {
case "inspect" -> InspectCommand.run(args);
case "tui" -> TuiCommand.run(args);
case "export" -> ExportCommand.run(args);
case "import" -> ImportCommand.run(args);
case "schema" -> SchemaCommand.run(args);
Expand All @@ -35,7 +36,8 @@ public static void main(String[] args) {

static void printUsage(PrintStream out) {
out.println("Usage: java -jar vortex.jar <subcommand> [args]");
out.println(" inspect <file.vortex> print file structure");
out.println(" inspect <file|url> print file structure; url is http(s)://");
out.println(" tui <file|url> open interactive inspector; url is http(s)://");
out.println(" export <file.vortex> write CSV to stdout");
out.println(" import <file.csv|file.parquet> [out.vortex] convert CSV or Parquet to Vortex");
out.println(" schema <file.vortex> print dtype (machine-readable)");
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/io/github/dfa1/vortex/core/DType.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ record Extension(
ByteBuffer metadata,
boolean nullable
) implements DType {

/// Returns the closed-world classification of this extension's id.
/// Pattern-match exhaustively: known ids resolve to the matching
/// record, anything else lands in {@link io.github.dfa1.vortex.core.Extension.Custom}.
///
/// @return the {@link io.github.dfa1.vortex.core.Extension} record
/// for this extension's id
public io.github.dfa1.vortex.core.Extension kind() {
return io.github.dfa1.vortex.core.Extension.of(extensionId);
}
}

/// Variant logical type for semi-structured data (analogous to Parquet variant / JSON).
Expand Down
Loading
Loading