Skip to content

Commit 2a0e475

Browse files
dfa1claude
andcommitted
feat(cli): add progress bar and compression ratio to import
ProgressListener callback in ImportOptions/CsvImporter fires per chunk. ImportCommand renders bar on stderr and prints CSV→vortex size savings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c822aab commit 2a0e475

5 files changed

Lines changed: 57 additions & 9 deletions

File tree

cli/src/main/java/io/github/dfa1/vortex/cli/ImportCommand.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.dfa1.vortex.cli;
22

33
import io.github.dfa1.vortex.csv.CsvImporter;
4+
import io.github.dfa1.vortex.csv.ImportOptions;
45

56
import java.io.IOException;
67
import java.nio.file.Files;
@@ -22,16 +23,47 @@ static int run(String[] args) {
2223
return 2;
2324
}
2425
Path vortexPath = args.length == 3 ? Path.of(args[2]) : deriveOutputPath(csvPath);
26+
ImportOptions options = ImportOptions.defaults()
27+
.withProgressListener(ImportCommand::renderProgress);
2528
try {
26-
CsvImporter.importCsv(csvPath, vortexPath);
27-
System.out.println("written: " + vortexPath);
29+
CsvImporter.importCsv(csvPath, vortexPath, options);
30+
clearProgress();
31+
long csvBytes = Files.size(csvPath);
32+
long vortexBytes = Files.size(vortexPath);
33+
double savings = 1.0 - (double) vortexBytes / csvBytes;
34+
System.out.printf("written: %s (%s → %s, %.1f%% smaller)%n",
35+
vortexPath, formatBytes(csvBytes), formatBytes(vortexBytes), savings * 100);
2836
return 0;
2937
} catch (IOException e) {
38+
clearProgress();
3039
System.err.println("error: " + e.getMessage());
3140
return 3;
3241
}
3342
}
3443

44+
private static void renderProgress(long done, long total) {
45+
int pct = total > 0 ? (int) (done * 100L / total) : 100;
46+
int filled = pct * 30 / 100;
47+
String bar = "=".repeat(filled) + (filled < 30 ? ">" : "") + " ".repeat(Math.max(0, 29 - filled));
48+
System.err.printf("\r [%s] %3d%% %,d / %,d rows", bar, pct, done, total);
49+
System.err.flush();
50+
}
51+
52+
private static void clearProgress() {
53+
System.err.printf("\r%60s\r", "");
54+
System.err.flush();
55+
}
56+
57+
private static String formatBytes(long bytes) {
58+
if (bytes < 1024L) {
59+
return bytes + " B";
60+
}
61+
if (bytes < 1024L * 1024) {
62+
return String.format("%.1f KB", bytes / 1024.0);
63+
}
64+
return String.format("%.1f MB", bytes / (1024.0 * 1024));
65+
}
66+
3567
private static Path deriveOutputPath(Path csvPath) {
3668
String name = csvPath.getFileName().toString();
3769
if (name.endsWith(".csv")) {

csv/src/main/java/io/github/dfa1/vortex/csv/CsvImporter.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,13 @@ public static void importCsv(Path csvPath, Path vortexPath, ImportOptions option
6161
StandardOpenOption.TRUNCATE_EXISTING);
6262
VortexWriter writer = VortexWriter.create(channel, schema, WriteOptions.defaults())) {
6363
int chunkSize = options.chunkSize();
64-
for (int start = 0; start < dataRows.size(); start += chunkSize) {
65-
int end = Math.min(start + chunkSize, dataRows.size());
64+
int total = dataRows.size();
65+
for (int start = 0; start < total; start += chunkSize) {
66+
int end = Math.min(start + chunkSize, total);
6667
writer.writeChunk(buildChunk(schema, dataRows.subList(start, end)));
68+
if (options.progressListener() != null) {
69+
options.progressListener().onProgress(end, total);
70+
}
6771
}
6872
}
6973
}

csv/src/main/java/io/github/dfa1/vortex/csv/ImportOptions.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@ public record ImportOptions(
77
char delimiter,
88
int chunkSize,
99
boolean hasHeader,
10-
DType.Struct schema
10+
DType.Struct schema,
11+
ProgressListener progressListener
1112
) {
1213
public static ImportOptions defaults() {
13-
return new ImportOptions(',', 65_536, true, null);
14+
return new ImportOptions(',', 65_536, true, null, null);
1415
}
1516

1617
/// Override the inferred schema. The struct's field names become column names;
1718
/// types control how each CSV column is parsed (positionally).
1819
public ImportOptions withSchema(DType.Struct overrideSchema) {
19-
return new ImportOptions(delimiter, chunkSize, hasHeader, overrideSchema);
20+
return new ImportOptions(delimiter, chunkSize, hasHeader, overrideSchema, progressListener);
21+
}
22+
23+
public ImportOptions withProgressListener(ProgressListener listener) {
24+
return new ImportOptions(delimiter, chunkSize, hasHeader, schema, listener);
2025
}
2126
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.github.dfa1.vortex.csv;
2+
3+
/// Callback invoked after each chunk is written during CSV import.
4+
@FunctionalInterface
5+
public interface ProgressListener {
6+
void onProgress(long rowsDone, long rowsTotal);
7+
}

csv/src/test/java/io/github/dfa1/vortex/csv/CsvImporterTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ void usesCustomDelimiter(@TempDir Path tmp) throws Exception {
6161
Path vortex = tmp.resolve("data.vortex");
6262

6363
// When
64-
CsvImporter.importCsv(csv, vortex, new ImportOptions(';', 65_536, true, null));
64+
CsvImporter.importCsv(csv, vortex, new ImportOptions(';', 65_536, true, null, null));
6565

6666
// Then
6767
try (VortexReader reader = VortexReader.open(vortex)) {
@@ -78,7 +78,7 @@ void generatesHeadersWhenMissing(@TempDir Path tmp) throws Exception {
7878
Path vortex = tmp.resolve("data.vortex");
7979

8080
// When
81-
CsvImporter.importCsv(csv, vortex, new ImportOptions(',', 65_536, false, null));
81+
CsvImporter.importCsv(csv, vortex, new ImportOptions(',', 65_536, false, null, null));
8282

8383
// Then
8484
try (VortexReader reader = VortexReader.open(vortex)) {

0 commit comments

Comments
 (0)