diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d8eaf2..89bbcec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ claim measured against the Rust (JNI) oracle; behavior divergences documented in ### Changed +- Column names are typed as `ColumnName` throughout vortex's own read and write APIs, not raw `String`. `DType.Struct.fieldNames()` returns `List` (the builder already validated then discarded the type — now it keeps it), `VortexReader.columnStats()` is keyed by `ColumnName`, `RowFilter.Column.column` is a `ColumnName`, and the writer's internal column maps and typed `Chunk.put(ColumnName, …)` builder follow. User-facing sugar is unchanged — `structBuilder().field(String)`, `RowFilter.eq(String, …)`, `ScanOptions.columns(String...)`, and `writeChunk(Map)` still take strings and validate at the edge. A footgun field name (blank/control) is now rejected structurally at `ColumnName` construction — a footgun-named schema can no longer be built. Framework/IO boundaries (Calcite, CSV, JDBC, Parquet, display) keep `String`. Wire output unchanged (integration oracle green). ([84769863](https://github.com/dfa1/vortex-java/commit/84769863)) + - `ScanOptions.columns()` returns `List` instead of `List`. The `columns(String...)` / `withColumns(String...)` factories still take strings at the API boundary but validate them into `ColumnName`, so a blank or control-character projection name fails fast rather than silently matching nothing. ([e478a3a7](https://github.com/dfa1/vortex-java/commit/e478a3a7)) - `Footer.arraySpecs()` / `Footer.layoutSpecs()` return typed `List` / `List` instead of `List`. The wire strings are parsed to their typed ids once at the footer boundary (with the blank-id guard), so array and layout nodes index directly into typed dictionaries — completing "strings at the boundary, types inside": the two scattered per-node `parse` calls in `SerializedArrayDecoder` and the layout converter are gone. ([0dd677ef](https://github.com/dfa1/vortex-java/commit/0dd677ef)) diff --git a/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexAggregates.java b/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexAggregates.java index 65930aa4..edf2aff3 100644 --- a/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexAggregates.java +++ b/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexAggregates.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.calcite; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.reader.ArrayStats; import io.github.dfa1.vortex.reader.Chunk; import io.github.dfa1.vortex.reader.ScanIterator; @@ -58,7 +59,7 @@ private VortexAggregates() { /// @param column the numeric column name /// @return the column's aggregate summary public static Summary of(VortexReader reader, String column) { - ArrayStats stats = reader.columnStats().getOrDefault(column, ArrayStats.empty()); + ArrayStats stats = reader.columnStats().getOrDefault(ColumnName.of(column), ArrayStats.empty()); long totalRows = totalRows(reader); long nullCount = stats.nullCount() == null ? 0L : stats.nullCount(); long count = totalRows - nullCount; diff --git a/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexTable.java b/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexTable.java index d8bc9f2f..bc02ce25 100644 --- a/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexTable.java +++ b/calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexTable.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.calcite; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ArrayStats; import io.github.dfa1.vortex.reader.Chunk; @@ -103,7 +104,7 @@ public record ColumnStats(ArrayStats stats, long totalRows) { /// @return the column's aggregated statistics paired with the file's total row count public ColumnStats statsAndRows(String column) { try (VortexReader reader = VortexReader.open(file)) { - ArrayStats stats = reader.columnStats().getOrDefault(column, ArrayStats.empty()); + ArrayStats stats = reader.columnStats().getOrDefault(ColumnName.of(column), ArrayStats.empty()); return new ColumnStats(stats, countRows(reader)); } catch (IOException e) { throw new UncheckedIOException("cannot read stats of " + file, e); @@ -137,7 +138,7 @@ public ZoneSum zoneSum(String column) { try (VortexReader reader = VortexReader.open(file)) { Number sum = new ZoneReducer(reader).sum(column); Long nullCount = reader.columnStats() - .getOrDefault(column, ArrayStats.empty()).nullCount(); + .getOrDefault(ColumnName.of(column), ArrayStats.empty()).nullCount(); return new ZoneSum(sum, nullCount, countRows(reader)); } catch (IOException e) { throw new UncheckedIOException("cannot read zone sum of " + file, e); @@ -420,7 +421,7 @@ private FilteredFold result() { /// @return the conjoined filter, or empty when any predicate is not fully translatable public Optional translatePushedFilters(List filters) { DType.Struct struct = struct(); - List names = struct.fieldNames(); + List names = struct.fieldNames().stream().map(ColumnName::value).toList(); List types = struct.fieldTypes(); List translated = new ArrayList<>(); for (RexNode node : filters) { @@ -467,7 +468,7 @@ private static Match classify(RowFilter filter, int zone, yield allIn ? Match.IN : Match.BOUNDARY; } case RowFilter.Column(var col, var predicate) -> - classifyColumn(predicate, zoneStats.get(col).get(zone), rowCount); + classifyColumn(predicate, zoneStats.get(col.value()).get(zone), rowCount); }; } @@ -567,7 +568,7 @@ private static Match compare(ArrayStats s, Object value, /// Whether `column` is an unsigned primitive, whose signed stat ordering [#compareStat] cannot /// safely classify (the fold abandons such columns to the scan). private static boolean isUnsigned(DType.Struct struct, String column) { - int idx = struct.fieldNames().indexOf(column); + int idx = struct.fieldNames().indexOf(ColumnName.of(column)); return idx >= 0 && struct.fieldTypes().get(idx) instanceof DType.Primitive p && p.ptype().isUnsigned(); } @@ -575,7 +576,7 @@ private static boolean isUnsigned(DType.Struct struct, String column) { /// ([Compare#values(Object, Object, DType)]) sorts NaN as the maximum and so cannot soundly fold a /// boundary partition (the fold abandons such a filter column to the NaN-correct scan). private static boolean isFloating(DType.Struct struct, String column) { - int idx = struct.fieldNames().indexOf(column); + int idx = struct.fieldNames().indexOf(ColumnName.of(column)); return idx >= 0 && struct.fieldTypes().get(idx) instanceof DType.Primitive p && p.ptype().isFloating(); } @@ -640,7 +641,7 @@ public RelDataType getRowType(RelDataTypeFactory typeFactory) { DType.Struct struct = struct(); RelDataTypeFactory.Builder builder = typeFactory.builder(); for (int i = 0; i < struct.fieldNames().size(); i++) { - builder.add(struct.fieldNames().get(i), toSqlType(typeFactory, struct.fieldTypes().get(i))); + builder.add(struct.fieldNames().get(i).value(), toSqlType(typeFactory, struct.fieldTypes().get(i))); } return builder.build(); } @@ -648,7 +649,7 @@ public RelDataType getRowType(RelDataTypeFactory typeFactory) { @Override public Enumerable scan(DataContext root, List filters, int[] projects) { DType.Struct struct = struct(); - List allNames = struct.fieldNames(); + List allNames = struct.fieldNames().stream().map(ColumnName::value).toList(); // Projection: the columns to decode and emit, in the order Calcite asked for. A null // projects array means "all columns". @@ -847,7 +848,7 @@ private static Optional toRowFilter(List filters, List out) { switch (filter) { case RowFilter.And(var parts) -> parts.forEach(f -> collectColumns(f, out)); - case RowFilter.Column(var col, var ignored) -> out.add(col); + case RowFilter.Column(var col, var ignored) -> out.add(col.value()); } } diff --git a/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateSumNullTest.java b/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateSumNullTest.java index e86fb0ed..59c937d2 100644 --- a/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateSumNullTest.java +++ b/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateSumNullTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.calcite; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.VortexWriter; @@ -45,7 +46,7 @@ class AggregateSumNullTest { /// one per-zone SUM row. `values` holds the raw longs; `valid[i] == false` marks row `i` null. private SchemaPlus tableOf(long[] values, boolean[] valid) throws IOException { DType.Struct schema = new DType.Struct( - List.of("v"), List.of(new DType.Primitive(PType.I64, true)), false); + List.of(ColumnName.of("v")), List.of(new DType.Primitive(PType.I64, true)), false); Path file = tmp.resolve("sum-nulls.vortex"); // Large chunk so the whole column is one chunk; zone maps on so the SUM stat is emitted. WriteOptions opts = new WriteOptions(1024, true, 0.90, 0, false, false); diff --git a/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereBoundaryTest.java b/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereBoundaryTest.java index ac1be05c..e3e0204a 100644 --- a/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereBoundaryTest.java +++ b/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereBoundaryTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.calcite; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.VortexWriter; @@ -201,7 +202,7 @@ void nullableAggColumnAcrossBoundaryChunk() throws Exception { // predicate `id > 0 AND id < 100` cuts chunk 0 (drops id 0, keeps ids 1,2,3) — a boundary // zone — and selects chunk 1 whole (IN). DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.I64, false), new DType.Primitive(PType.I64, true)), false); Path f = tmp.resolve("nullable-boundary.vortex"); @@ -238,7 +239,7 @@ void nonNumericMinAcrossBoundaryAbandonsButScanIsCorrect() throws Exception { // rewrite must abandon so the scan returns the value. chunk 0 id 0..3 / name {"a".."d"}, // chunk 1 id 10..13 / name {"e".."h"}; `id > 0 AND id < 100` cuts chunk 0 (keeps b,c,d). DType.Struct schema = new DType.Struct( - List.of("id", "name"), + List.of(ColumnName.of("id"), ColumnName.of("name")), List.of(new DType.Primitive(PType.I64, false), new DType.Utf8(false)), false); Path f = tmp.resolve("strmin-boundary.vortex"); @@ -267,7 +268,7 @@ void unsignedKeyAcrossBoundaryAbandonsButScanIsCorrect() throws Exception { // past the high bit, so the fold abandons any unsigned column outright — even on the boundary // path. chunk 0 id 0..3, chunk 1 id 10..13; `id > 1 AND id < 13` cuts both chunks. DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.U64, false), new DType.Primitive(PType.I64, false)), false); Path f = tmp.resolve("u64-boundary.vortex"); @@ -334,7 +335,7 @@ void neqWithNullInFilterColumnAcrossBoundaryExcludesNullAndValue() throws Except // null row AND the 30 row must both drop. This confirms the boundary mask's three-valued // logic for `<>` — a case the equality/range tests do not reach. DType.Struct schema = new DType.Struct( - List.of("id", "amt"), + List.of(ColumnName.of("id"), ColumnName.of("amt")), List.of(new DType.Primitive(PType.I64, false), new DType.Primitive(PType.I64, true)), false); Path f = tmp.resolve("neq-null-boundary.vortex"); @@ -385,7 +386,7 @@ void floatFilterColumnAcrossBoundaryAbandonsButScanIsNaNCorrect() throws Excepti // NaN-correct. chunk 0 f {1.0,NaN,2.0,3.0}, chunk 1 f {4.0,5.0,6.0,7.0}; `f >= 1.5` cuts // chunk 0 (a boundary) and keeps chunk 1 whole. DType.Struct schema = new DType.Struct( - List.of("f", "val"), + List.of(ColumnName.of("f"), ColumnName.of("val")), List.of(new DType.Primitive(PType.F64, false), new DType.Primitive(PType.I64, false)), false); Path f = tmp.resolve("float-nan-boundary.vortex"); diff --git a/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereCleanPartitionTest.java b/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereCleanPartitionTest.java index 07de8310..c8bcb30b 100644 --- a/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereCleanPartitionTest.java +++ b/calcite/src/test/java/io/github/dfa1/vortex/calcite/AggregateWhereCleanPartitionTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.calcite; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.VortexWriter; @@ -155,7 +156,7 @@ void narrowIntegerKeyFoldsFromStats() throws Exception { // a same-type comparison would never fire — this guards that the widening compare folds // narrow columns. Two chunks: ids 0..3, then 10..13. DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.I32, false), new DType.Primitive(PType.I32, false)), false); Path f = tmp.resolve("i32.vortex"); @@ -185,7 +186,7 @@ void narrowIntegerKeyFoldsFromStats() throws Exception { void nullAwareSumAndCountOverKeptZones() throws Exception { // Given a non-null clustered key and a nullable value with two nulls in the kept chunk DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.I64, false), new DType.Primitive(PType.I64, true)), false); Path f = tmp.resolve("nullable.vortex"); @@ -217,7 +218,7 @@ void unsignedKeyColumnAbandonsToScan() throws Exception { // Given a clustered U64 key: a signed stat compare could fold the wrong zones past the high // bit, so the fold must abandon unsigned columns to the (unsigned-aware) scan DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.U64, false), new DType.Primitive(PType.I64, false)), false); Path f = tmp.resolve("u64.vortex"); @@ -301,7 +302,7 @@ void isNotNullBoundaryChunkFoldsViaDecode() throws Exception { // builds an IS NOT NULL mask, and reduces only its selected rows, so the fold answers // without a full scan. DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.I64, false), new DType.Primitive(PType.I64, true)), false); Path f = tmp.resolve("isnotnull-boundary.vortex"); @@ -413,7 +414,7 @@ void isNullOverExpressionAbandonsToScan() throws Exception { /// `IS NULL` / `IS NOT NULL` clean-partition tests. private static Path nullPartitionedFile(String name) throws Exception { DType.Struct schema = new DType.Struct( - List.of("id", "val"), + List.of(ColumnName.of("id"), ColumnName.of("val")), List.of(new DType.Primitive(PType.I64, false), new DType.Primitive(PType.I64, true)), false); Path f = tmp.resolve(name); diff --git a/calcite/src/test/java/io/github/dfa1/vortex/calcite/OhlcSqlDemoTest.java b/calcite/src/test/java/io/github/dfa1/vortex/calcite/OhlcSqlDemoTest.java index 507fd504..f3982232 100644 --- a/calcite/src/test/java/io/github/dfa1/vortex/calcite/OhlcSqlDemoTest.java +++ b/calcite/src/test/java/io/github/dfa1/vortex/calcite/OhlcSqlDemoTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.calcite; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.reader.ScanIterator; import io.github.dfa1.vortex.reader.ScanOptions; import io.github.dfa1.vortex.reader.VortexReader; @@ -224,7 +225,8 @@ private static Pushdown runPushdown(Path file) throws Exception { total += c; } } - return new Pushdown(stats.get("low").min(), stats.get("high").max(), total); + return new Pushdown(stats.get(ColumnName.of("low")).min(), + stats.get(ColumnName.of("high")).max(), total); } } diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/FilterCommand.java b/cli/src/main/java/io/github/dfa1/vortex/cli/FilterCommand.java index eac00806..6bd49cf5 100644 --- a/cli/src/main/java/io/github/dfa1/vortex/cli/FilterCommand.java +++ b/cli/src/main/java/io/github/dfa1/vortex/cli/FilterCommand.java @@ -151,7 +151,7 @@ private static Comparable parseValue(String raw) { private static RowPredicate toRowPredicate(RowFilter filter) { return switch (filter) { - case RowFilter.Column(var col, var predicate) -> columnPredicate(col, predicate); + case RowFilter.Column(var col, var predicate) -> columnPredicate(col.value(), predicate); case RowFilter.And(var filters) -> { RowPredicate[] preds = filters.stream().map(FilterCommand::toRowPredicate).toArray(RowPredicate[]::new); yield (chunk, rowIdx) -> { diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/SchemaCommand.java b/cli/src/main/java/io/github/dfa1/vortex/cli/SchemaCommand.java index cfce43ef..fb4e7fa3 100644 --- a/cli/src/main/java/io/github/dfa1/vortex/cli/SchemaCommand.java +++ b/cli/src/main/java/io/github/dfa1/vortex/cli/SchemaCommand.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.VortexReader; @@ -41,8 +42,8 @@ private static void printSchema(PrintStream out, Path path, DType dtype, long ro out.printf("%s (%s rows, %d columns)%n%n", path.getFileName(), formatRows(rowCount), n); int idxWidth = Integer.toString(n).length(); - int nameWidth = s.fieldNames().stream().mapToInt(String::length).max().orElse(0); - List names = s.fieldNames(); + List names = s.fieldNames().stream().map(ColumnName::value).toList(); + int nameWidth = names.stream().mapToInt(String::length).max().orElse(0); List types = s.fieldTypes(); for (int i = 0; i < n; i++) { out.printf(" %" + idxWidth + "d %-" + nameWidth + "s %s%n", @@ -67,7 +68,7 @@ static String formatDType(DType dtype) { if (i > 0) { sb.append(", "); } - sb.append(s.fieldNames().get(i)).append(": ").append(formatDType(s.fieldTypes().get(i))); + sb.append(s.fieldNames().get(i).value()).append(": ").append(formatDType(s.fieldTypes().get(i))); } sb.append('>'); yield sb.toString(); diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/StatsCommand.java b/cli/src/main/java/io/github/dfa1/vortex/cli/StatsCommand.java index 45676362..292e466e 100644 --- a/cli/src/main/java/io/github/dfa1/vortex/cli/StatsCommand.java +++ b/cli/src/main/java/io/github/dfa1/vortex/cli/StatsCommand.java @@ -1,6 +1,7 @@ package io.github.dfa1.vortex.cli; import io.github.dfa1.vortex.reader.ArrayStats; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.VortexReader; @@ -32,21 +33,21 @@ static int run(String[] args) { return ExitStatus.ERROR; } long totalRows = reader.layout().rowCount(); - Map stats = reader.columnStats(); + Map stats = reader.columnStats(); System.out.printf("rows: %,d%n%n", totalRows); System.out.printf("%-20s %-12s %15s %15s%n", "column", "type", "min", "max"); System.out.println("-".repeat(67)); - List names = schema.fieldNames(); + List names = schema.fieldNames(); List types = schema.fieldTypes(); for (int i = 0; i < names.size(); i++) { - String name = names.get(i); + ColumnName name = names.get(i); String type = formatDType(types.get(i)); ArrayStats s = stats.getOrDefault(name, ArrayStats.empty()); String min = s.min() != null ? s.min().toString() : "n/a"; String max = s.max() != null ? s.max().toString() : "n/a"; - System.out.printf("%-20s %-12s %15s %15s%n", name, type, min, max); + System.out.printf("%-20s %-12s %15s %15s%n", name.value(), type, min, max); } return ExitStatus.OK; } catch (IOException e) { diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/InspectorRender.java b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/InspectorRender.java index 20df7dd4..e2c2b581 100644 --- a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/InspectorRender.java +++ b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/InspectorRender.java @@ -76,7 +76,7 @@ static List formatStatsArray(Array arr, DType.Struct statsDtype) { // A single-field stats table (e.g. NULL_COUNT only) is decoded to the bare field, not // a StructArray (the struct decoder collapses one-field structs). Render that one stat. if (statsDtype.fieldTypes().size() == 1) { - String name = statsDtype.fieldNames().getFirst(); + String name = statsDtype.fieldNames().getFirst().value(); DType fdtype = statsDtype.fieldTypes().getFirst(); int rowCount = (int) arr.length(); List rows = new ArrayList<>(rowCount); @@ -96,7 +96,7 @@ static List formatStatsArray(Array arr, DType.Struct statsDtype) { if (f > 0) { sb.append(", "); } - String name = statsDtype.fieldNames().get(f); + String name = statsDtype.fieldNames().get(f).value(); DType fdtype = statsDtype.fieldTypes().get(f); Array field = sa.field(f); sb.append(name).append('=').append(formatStatsCell(field, row, fdtype)); diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/LazyGridSource.java b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/LazyGridSource.java index b8c98f5c..8d2ebe67 100644 --- a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/LazyGridSource.java +++ b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/LazyGridSource.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli.tui; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.error.VortexException; import io.github.dfa1.vortex.reader.Chunk; @@ -59,7 +60,7 @@ private LazyGridSource(VortexHandle handle, IoWorker worker) throws InterruptedE if (!(handle.dtype() instanceof DType.Struct schema)) { throw new VortexException("view requires struct root dtype, got " + handle.dtype()); } - this.columns = schema.fieldNames(); + this.columns = schema.fieldNames().stream().map(ColumnName::value).toList(); this.columnDtypes = schema.fieldTypes(); AtomicReference rowCountsRef = new AtomicReference<>(); diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java index 88455491..da2fc566 100644 --- a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java +++ b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java @@ -738,7 +738,7 @@ private DType columnDtypeFor(InspectorTree.Node node) { private DType columnDtypeByName(String columnName) { DType root = tree.dtype(); if (root instanceof DType.Struct s) { - int idx = s.fieldNames().indexOf(columnName); + int idx = s.fieldNames().indexOf(ColumnName.of(columnName)); if (idx >= 0) { return s.fieldTypes().get(idx); } diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java b/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java index e80658af..2080d4f7 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.writer.VortexWriter; import io.github.dfa1.vortex.writer.WriteOptions; @@ -25,7 +26,7 @@ private CliTestSupport() { static Path writeSmallVortex(Path dir, String name) throws IOException { Path file = dir.resolve(name); DType.Struct schema = new DType.Struct( - List.of("id"), + List.of(ColumnName.of("id")), List.of(DType.I64), false); try (FileChannel ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -41,7 +42,7 @@ static Path writeSmallVortex(Path dir, String name) throws IOException { 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(ColumnName.of("id"), ColumnName.of("qty"), ColumnName.of("price"), ColumnName.of("name")), List.of( DType.I64, DType.I32, diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/SchemaCommandTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/SchemaCommandTest.java index 7d8742c6..25d156dc 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/SchemaCommandTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/SchemaCommandTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import org.junit.jupiter.api.Test; @@ -85,7 +86,7 @@ static Stream dtypeCases() { arguments(new DType.Extension("vortex.uuid", i64, null, true), "ext?"), arguments(DType.VARIANT, "variant"), arguments(new DType.Variant(true), "variant?"), - arguments(new DType.Struct(List.of("a", "b"), List.of(i64, DType.UTF8), false), + arguments(new DType.Struct(List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(i64, DType.UTF8), false), "struct")); } diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java index 23ac2070..6ecc9c98 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli.tui; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.TimeUnit; import io.github.dfa1.vortex.reader.array.Array; @@ -208,7 +209,7 @@ void unknownExtensionIdFallsThroughToStorageRendering() { void unrenderableTypeFallsBackToAngleBrackets() { try (Arena arena = Arena.ofConfined()) { // Given — StructArray has no scalar rendering - DType.Struct dtype = new DType.Struct(List.of("a"), List.of(I64), false); + DType.Struct dtype = new DType.Struct(List.of(ColumnName.of("a")), List.of(I64), false); StructArray sut = new StructArray(dtype, 1, List.of(ArrayFixtures.longs(arena, 1L))); // When / Then — falls into the default branch diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java index 3e545338..ee079ca3 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli.tui; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.reader.array.Array; @@ -139,7 +140,7 @@ void dateExtensionDecodeFailureFallsThroughToGeneric() { void unknownTypeFallsBackToAngleBrackets() { try (Arena arena = Arena.ofConfined()) { // Given — StructArray has no scalar rendering - DType.Struct dtype = new DType.Struct(List.of("a"), List.of(I64), false); + DType.Struct dtype = new DType.Struct(List.of(ColumnName.of("a")), List.of(I64), false); StructArray sut = new StructArray(dtype, 1, List.of(ArrayFixtures.longs(arena, 1L))); // When / Then — falls into the default branch @@ -155,7 +156,7 @@ class FormatStatsArray { void rendersOneRowPerStructEntry() { try (Arena arena = Arena.ofConfined()) { // Given - DType.Struct statsDtype = new DType.Struct(List.of("min"), List.of(I64), false); + DType.Struct statsDtype = new DType.Struct(List.of(ColumnName.of("min")), List.of(I64), false); StructArray stats = new StructArray(statsDtype, 2, List.of(ArrayFixtures.longs(arena, 5L, 6L))); // When @@ -170,7 +171,7 @@ void rendersOneRowPerStructEntry() { void maskedInvalidStatsCellRendersNull() { try (Arena arena = Arena.ofConfined()) { // Given — field masked: row 0 valid (5), row 1 null - DType.Struct statsDtype = new DType.Struct(List.of("min"), List.of(I64), false); + DType.Struct statsDtype = new DType.Struct(List.of(ColumnName.of("min")), List.of(I64), false); MaskedArray field = new MaskedArray(ArrayFixtures.longs(arena, 5L, 9L), ArrayFixtures.bools(arena, true, false)); StructArray stats = new StructArray(statsDtype, 2, List.of(field)); @@ -187,7 +188,7 @@ void maskedInvalidStatsCellRendersNull() { void multiFieldNonStructStatsArrayThrows() { try (Arena arena = Arena.ofConfined()) { // Given — a multi-field stats schema but a non-struct payload - DType.Struct statsDtype = new DType.Struct(List.of("min", "max"), List.of(I64, I64), false); + DType.Struct statsDtype = new DType.Struct(List.of(ColumnName.of("min"), ColumnName.of("max")), List.of(I64, I64), false); Array notAStruct = ArrayFixtures.longs(arena, 1L); // When / Then @@ -203,7 +204,7 @@ void singleFieldStatsArrayRendersBareField() { // Given — a single-field (NULL_COUNT-only) stats table decodes to the bare field, // not a StructArray; the renderer must still render it DType.Struct statsDtype = new DType.Struct( - List.of("null_count"), List.of(new DType.Primitive(PType.U64, true)), false); + List.of(ColumnName.of("null_count")), List.of(new DType.Primitive(PType.U64, true)), false); Array oneStat = ArrayFixtures.longs(arena, 0L, 2L); // When diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java index ee2e7107..e03b3e48 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.cli.tui; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.VortexHandle; import io.github.dfa1.vortex.reader.VortexReader; @@ -28,7 +29,7 @@ private TuiTestSupport() { static Path writeGridVortex(Path dir, String name, int rows) throws IOException { Path file = dir.resolve(name); DType.Struct schema = new DType.Struct( - List.of("a", "b", "c"), + List.of(ColumnName.of("a"), ColumnName.of("b"), ColumnName.of("c")), List.of( DType.I64, DType.I64, @@ -55,7 +56,7 @@ static Path writeGridVortex(Path dir, String name, int rows) throws IOException static Path writeMultiTypeVortex(Path dir, String name) throws IOException { Path file = dir.resolve(name); DType.Struct schema = new DType.Struct( - List.of("i", "d", "flag", "name"), + List.of(ColumnName.of("i"), ColumnName.of("d"), ColumnName.of("flag"), ColumnName.of("name")), List.of( DType.I64, DType.F64, @@ -88,7 +89,7 @@ static Path writeMultiTypeVortex(Path dir, String name) throws IOException { static Path writeRichVortex(Path dir, String name, int rows) throws IOException { Path file = dir.resolve(name); DType.Struct schema = new DType.Struct( - List.of("label", "n"), + List.of(ColumnName.of("label"), ColumnName.of("n")), List.of(DType.UTF8, DType.I64), false); String[] labels = {"red", "green", "blue"}; diff --git a/core/src/main/java/io/github/dfa1/vortex/core/model/DType.java b/core/src/main/java/io/github/dfa1/vortex/core/model/DType.java index 68cecc14..da871459 100644 --- a/core/src/main/java/io/github/dfa1/vortex/core/model/DType.java +++ b/core/src/main/java/io/github/dfa1/vortex/core/model/DType.java @@ -156,7 +156,7 @@ static StructBuilder structBuilder() { /// Fluent builder for [Struct] dtypes. Use [#structBuilder()] to obtain one. /// Preserves insertion order and rejects duplicate field names at [#build()]. final class StructBuilder { - private final LinkedHashMap fields = new LinkedHashMap<>(); + private final LinkedHashMap fields = new LinkedHashMap<>(); private boolean nullable; private StructBuilder() { @@ -186,7 +186,7 @@ public StructBuilder field(String name, DType type) { /// @return this builder /// @throws IllegalArgumentException if `name` duplicates a previously added field public StructBuilder field(ColumnName name, DType type) { - if (fields.putIfAbsent(name.value(), type) != null) { + if (fields.putIfAbsent(name, type) != null) { throw new IllegalArgumentException("duplicate field name: " + name); } return this; @@ -254,11 +254,11 @@ record Binary(boolean nullable) implements DType { /// Struct logical type with named, typed fields. /// - /// @param fieldNames ordered list of field names + /// @param fieldNames ordered list of validated [ColumnName] field names /// @param fieldTypes ordered list of field types, parallel to `fieldNames` /// @param nullable whether null values are permitted record Struct( - java.util.List fieldNames, + java.util.List fieldNames, java.util.List fieldTypes, boolean nullable ) implements DType { @@ -287,7 +287,7 @@ record Struct( /// @return the [DType] of the named field /// @throws IllegalArgumentException if no field with that name exists public DType field(String name) { - int i = fieldNames.indexOf(name); + int i = fieldNames.indexOf(ColumnName.of(name)); if (i < 0) { throw new IllegalArgumentException("unknown field: " + name); } diff --git a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeIsUnsignedTest.java b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeIsUnsignedTest.java index 5b2ed11d..50c2520c 100644 --- a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeIsUnsignedTest.java +++ b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeIsUnsignedTest.java @@ -30,7 +30,7 @@ void nonPrimitiveTypes_areNotUnsigned() { List types = List.of( DType.BOOL, DType.UTF8, DType.BINARY, DType.NULL, DType.VARIANT, new DType.Decimal((byte) 10, (byte) 2, false), - new DType.Struct(List.of("u"), List.of(DType.U64), false)); + new DType.Struct(List.of(ColumnName.of("u")), List.of(DType.U64), false)); // When / Then assertThat(types).hasSize(7).allSatisfy(t -> assertThat(t.isUnsigned()).isFalse()); diff --git a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructBuilderTest.java b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructBuilderTest.java index 93347c79..1efa6096 100644 --- a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructBuilderTest.java +++ b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructBuilderTest.java @@ -20,7 +20,8 @@ void build_preservesFieldInsertionOrder() { .build(); // Then - assertThat(sut.fieldNames()).containsExactly("timestamp", "symbol", "price"); + assertThat(sut.fieldNames()).containsExactly( + ColumnName.of("timestamp"), ColumnName.of("symbol"), ColumnName.of("price")); assertThat(sut.fieldTypes()).containsExactly( DType.I64, DType.UTF8, DType.F64); assertThat(sut.nullable()).isFalse(); @@ -70,7 +71,7 @@ void buildResult_equalsRecordConstructed_struct() { // When DType.Struct result = new DType.Struct( - List.of("a", "b"), + List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(DType.I32, DType.UTF8), false); @@ -85,8 +86,8 @@ void builder_isNotReusable_afterMutation_byField() { DType.Struct resultY = DType.structBuilder().field("y", DType.UTF8).build(); // Then - assertThat(resultX.fieldNames()).containsExactly("x"); - assertThat(resultY.fieldNames()).containsExactly("y"); + assertThat(resultX.fieldNames()).containsExactly(ColumnName.of("x")); + assertThat(resultY.fieldNames()).containsExactly(ColumnName.of("y")); } @org.junit.jupiter.params.ParameterizedTest diff --git a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructValidationTest.java b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructValidationTest.java index f5844f94..1353286e 100644 --- a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructValidationTest.java +++ b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeStructValidationTest.java @@ -18,7 +18,7 @@ class DTypeStructValidationTest { @Test void construct_arityMismatch_throwsIllegalArgumentException() { // Given two names, one type: the record constructor rejects the desync - var names = List.of("a", "b"); + var names = List.of(ColumnName.of("a"), ColumnName.of("b")); var types = List.of(I64); // When / Then assertThatThrownBy(() -> new DType.Struct(names, types, false)) @@ -38,7 +38,7 @@ void construct_nullNames_throwsNullPointerException() { @Test void construct_nullTypes_throwsNullPointerException() { // Given a null types list - var names = List.of("a"); + var names = List.of(ColumnName.of("a")); // When / Then assertThatThrownBy(() -> new DType.Struct(names, null, false)) .isInstanceOf(NullPointerException.class); @@ -49,9 +49,10 @@ void construct_duplicateNames_isLegalInMemory() { // Given — duplicate names, mirroring Rust StructFields (uniqueness is enforced at the // file boundary by the writer and parser, not by the in-memory type) // When - DType.Struct result = new DType.Struct(List.of("dup", "dup"), List.of(I64, I64), false); + DType.Struct result = new DType.Struct( + List.of(ColumnName.of("dup"), ColumnName.of("dup")), List.of(I64, I64), false); // Then - assertThat(result.fieldNames()).containsExactly("dup", "dup"); + assertThat(result.fieldNames()).containsExactly(ColumnName.of("dup"), ColumnName.of("dup")); } } diff --git a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeWithNullableTest.java b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeWithNullableTest.java index 48cd84a6..ef77573e 100644 --- a/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeWithNullableTest.java +++ b/core/src/test/java/io/github/dfa1/vortex/core/model/DTypeWithNullableTest.java @@ -24,7 +24,7 @@ static Stream nonNullableSamples() { Arguments.of("Decimal", new DType.Decimal((byte) 10, (byte) 2, false)), Arguments.of("Utf8", DType.UTF8), Arguments.of("Binary", DType.BINARY), - Arguments.of("Struct", new DType.Struct(List.of("a", "b"), List.of(i32, DType.UTF8), false)), + Arguments.of("Struct", new DType.Struct(List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(i32, DType.UTF8), false)), Arguments.of("List", new DType.List(i32, false)), Arguments.of("FixedSizeList", new DType.FixedSizeList(i32, 4, false)), Arguments.of("Extension", new DType.Extension("ip.address", i32, null, false)), @@ -82,14 +82,14 @@ void asNullable_isSugarForWithNullableTrue() { void withNullable_preservesCompoundPayload_struct() { // Given — a struct whose field names/types must ride through the flip verbatim DType i32 = DType.I32; - DType.Struct sut = new DType.Struct(List.of("id", "name"), List.of(i32, new DType.Utf8(true)), false); + DType.Struct sut = new DType.Struct(List.of(ColumnName.of("id"), ColumnName.of("name")), List.of(i32, new DType.Utf8(true)), false); // When DType.Struct result = (DType.Struct) sut.withNullable(true); // Then assertThat(result.nullable()).isTrue(); - assertThat(result.fieldNames()).containsExactly("id", "name"); + assertThat(result.fieldNames()).containsExactly(ColumnName.of("id"), ColumnName.of("name")); assertThat(result.fieldTypes()).containsExactly(i32, new DType.Utf8(true)); } diff --git a/core/src/test/java/io/github/dfa1/vortex/core/testing/OhlcData.java b/core/src/test/java/io/github/dfa1/vortex/core/testing/OhlcData.java index 893e9f23..6418f765 100644 --- a/core/src/test/java/io/github/dfa1/vortex/core/testing/OhlcData.java +++ b/core/src/test/java/io/github/dfa1/vortex/core/testing/OhlcData.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.core.testing; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; @@ -27,7 +28,9 @@ public final class OhlcData { /// The column schema: `date` (I32 epoch day), `symbol` (Utf8), `open`/`high`/`low`/`close` /// (F64), `volume` (I64); all non-nullable. public static final DType.Struct SCHEMA = new DType.Struct( - List.of("date", "symbol", "open", "high", "low", "close", "volume"), + List.of(ColumnName.of("date"), ColumnName.of("symbol"), ColumnName.of("open"), + ColumnName.of("high"), ColumnName.of("low"), ColumnName.of("close"), + ColumnName.of("volume")), List.of( new DType.Primitive(PType.I32, false), new DType.Utf8(false), diff --git a/csv/src/main/java/io/github/dfa1/vortex/csv/CsvExporter.java b/csv/src/main/java/io/github/dfa1/vortex/csv/CsvExporter.java index 12c2b351..0b3bf706 100644 --- a/csv/src/main/java/io/github/dfa1/vortex/csv/CsvExporter.java +++ b/csv/src/main/java/io/github/dfa1/vortex/csv/CsvExporter.java @@ -1,6 +1,7 @@ package io.github.dfa1.vortex.csv; import de.siegmar.fastcsv.writer.CsvWriter; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.error.VortexException; import io.github.dfa1.vortex.reader.array.Array; @@ -74,7 +75,9 @@ private static void export(VortexReader reader, CsvWriter csvWriter, ExportOptio if (!(reader.dtype() instanceof DType.Struct schema)) { throw new VortexException("only struct root dtype supported for CSV export"); } - List colNames = options.hasProjection() ? options.columns() : schema.fieldNames(); + List colNames = options.hasProjection() + ? options.columns() + : schema.fieldNames().stream().map(ColumnName::value).toList(); int colCount = colNames.size(); if (options.writeHeader()) { diff --git a/csv/src/main/java/io/github/dfa1/vortex/csv/CsvImporter.java b/csv/src/main/java/io/github/dfa1/vortex/csv/CsvImporter.java index c04a06cb..812df4c1 100644 --- a/csv/src/main/java/io/github/dfa1/vortex/csv/CsvImporter.java +++ b/csv/src/main/java/io/github/dfa1/vortex/csv/CsvImporter.java @@ -2,6 +2,7 @@ import de.siegmar.fastcsv.reader.CsvReader; import de.siegmar.fastcsv.reader.CsvRecord; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.VortexWriter; @@ -143,7 +144,7 @@ private static DType.Struct inferSchemaFromRows(String[] headers, List } } - List names = List.of(headers); + List names = Arrays.stream(headers).map(ColumnName::of).toList(); List types = new ArrayList<>(colCount); for (int c = 0; c < colCount; c++) { types.add(resolveType(canBeLong[c], canBeDouble[c], canBeBool[c])); @@ -212,7 +213,7 @@ static Map buildChunk(DType.Struct schema, List rows) int n = rows.size(); Map chunk = new LinkedHashMap<>(); for (int c = 0; c < schema.fieldNames().size(); c++) { - chunk.put(schema.fieldNames().get(c), buildColumn(schema.fieldTypes().get(c), rows, c, n)); + chunk.put(schema.fieldNames().get(c).value(), buildColumn(schema.fieldTypes().get(c), rows, c, n)); } return chunk; } diff --git a/csv/src/test/java/io/github/dfa1/vortex/csv/CsvExporterTest.java b/csv/src/test/java/io/github/dfa1/vortex/csv/CsvExporterTest.java index 59d7e6c6..0a302359 100644 --- a/csv/src/test/java/io/github/dfa1/vortex/csv/CsvExporterTest.java +++ b/csv/src/test/java/io/github/dfa1/vortex/csv/CsvExporterTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.csv; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.writer.VortexWriter; import io.github.dfa1.vortex.writer.WriteOptions; @@ -23,7 +24,7 @@ void exportsToCsvFile(@TempDir Path tmp) throws Exception { // Given Path vortex = tmp.resolve("data.vortex"); DType.Struct schema = new DType.Struct( - List.of("id", "name"), + List.of(ColumnName.of("id"), ColumnName.of("name")), List.of(DType.I64, DType.UTF8), false); try (FileChannel ch = FileChannel.open(vortex, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -48,7 +49,7 @@ void exportsToWriter(@TempDir Path tmp) throws Exception { // Given Path vortex = tmp.resolve("data.vortex"); DType.Struct schema = new DType.Struct( - List.of("x"), + List.of(ColumnName.of("x")), List.of(DType.F64), false); try (FileChannel ch = FileChannel.open(vortex, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -73,7 +74,7 @@ void suppressesHeaderWhenConfigured(@TempDir Path tmp) throws Exception { // Given Path vortex = tmp.resolve("data.vortex"); DType.Struct schema = new DType.Struct( - List.of("id"), + List.of(ColumnName.of("id")), List.of(DType.I64), false); try (FileChannel ch = FileChannel.open(vortex, StandardOpenOption.CREATE, StandardOpenOption.WRITE); diff --git a/csv/src/test/java/io/github/dfa1/vortex/csv/CsvImporterTest.java b/csv/src/test/java/io/github/dfa1/vortex/csv/CsvImporterTest.java index ef3cc563..76bd5055 100644 --- a/csv/src/test/java/io/github/dfa1/vortex/csv/CsvImporterTest.java +++ b/csv/src/test/java/io/github/dfa1/vortex/csv/CsvImporterTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.csv; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.array.LongArray; import io.github.dfa1.vortex.reader.array.VarBinArray; @@ -32,7 +33,7 @@ void infersTypedColumnsAndRoundTrips(@TempDir Path tmp) throws Exception { try (VortexReader reader = VortexReader.open(vortex)) { assertThat(reader.dtype()).isInstanceOf(DType.Struct.class); DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).containsExactly("id", "price", "active", "name"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("id", "price", "active", "name"); assertThat(schema.fieldTypes().get(0)).isEqualTo(DType.I64); assertThat(schema.fieldTypes().get(1)).isEqualTo(DType.F64); assertThat(schema.fieldTypes().get(2)).isEqualTo(DType.BOOL); @@ -66,7 +67,7 @@ void usesCustomDelimiter(@TempDir Path tmp) throws Exception { // Then try (VortexReader reader = VortexReader.open(vortex)) { DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).containsExactly("id", "name"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("id", "name"); } } @@ -83,7 +84,7 @@ void generatesHeadersWhenMissing(@TempDir Path tmp) throws Exception { // Then try (VortexReader reader = VortexReader.open(vortex)) { DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).containsExactly("col0", "col1"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("col0", "col1"); } } @@ -94,7 +95,7 @@ void respectsSchemaOverride(@TempDir Path tmp) throws Exception { Files.writeString(csv, "value\n42\n99\n"); Path vortex = tmp.resolve("data.vortex"); DType.Struct forcedSchema = new DType.Struct( - java.util.List.of("value"), + java.util.List.of(ColumnName.of("value")), java.util.List.of(DType.UTF8), false); diff --git a/docs/reference.md b/docs/reference.md index 1a991c96..06c732b0 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -81,7 +81,7 @@ all `Array` buffers obtained during scans become invalid. | `version()` | `int` | File format version | | `fileSize()` | `long` | File size in bytes | | `scan(ScanOptions)` | `ScanIterator` | Open a scan | -| `columnStats()` | `Map` | Aggregated min/max per column | +| `columnStats()` | `Map` | Aggregated min/max per column | | `slice(offset, length)` | `MemorySegment` | Zero-copy slice of mmap region | | `close()` | — | Releases mmap | @@ -138,7 +138,7 @@ Record: `(int chunkSize, boolean enableZoneMaps, double compressionRatioThreshol ### `ScanOptions` (`io.github.dfa1.vortex.reader.ScanOptions`) -Record: `(List columns, RowFilter rowFilter, long limit)`. Empty `columns` = read all. `NO_LIMIT` = +Record: `(List columns, RowFilter rowFilter, long limit)` (built via `columns(String...)`). Empty `columns` = read all. `NO_LIMIT` = `Long.MAX_VALUE`. | Factory / builder | Effect | diff --git a/inspector/src/main/java/io/github/dfa1/vortex/inspect/InspectorTree.java b/inspector/src/main/java/io/github/dfa1/vortex/inspect/InspectorTree.java index 70224a95..1f7dc22e 100644 --- a/inspector/src/main/java/io/github/dfa1/vortex/inspect/InspectorTree.java +++ b/inspector/src/main/java/io/github/dfa1/vortex/inspect/InspectorTree.java @@ -4,6 +4,7 @@ import io.github.dfa1.vortex.reader.ArrayStats; import io.github.dfa1.vortex.core.model.EncodingId; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.Footer; import io.github.dfa1.vortex.reader.layout.Layout; @@ -96,7 +97,8 @@ public static InspectorTree buildShallow(VortexHandle handle) { Footer footer = handle.footer(); Layout layout = handle.layout(); DType dtype = handle.dtype(); - List colNames = (dtype instanceof DType.Struct s) ? s.fieldNames() : List.of(); + List colNames = (dtype instanceof DType.Struct s) + ? s.fieldNames().stream().map(ColumnName::value).toList() : List.of(); Node root = shallowNode(layout, Optional.empty()); if (layout.isStruct()) { List named = new ArrayList<>(root.children().size()); @@ -167,7 +169,8 @@ public static InspectorTree build(VortexHandle handle, Progress progress) { int total = countPeekableSegments(layout, footer); int[] counter = {0}; - List colNames = (dtype instanceof DType.Struct s) ? s.fieldNames() : List.of(); + List colNames = (dtype instanceof DType.Struct s) + ? s.fieldNames().stream().map(ColumnName::value).toList() : List.of(); Set overallUsed = new LinkedHashSet<>(); Node root = buildNode(layout, Optional.empty(), handle, footer.arraySpecs(), overallUsed, progress, counter, total); diff --git a/inspector/src/main/java/io/github/dfa1/vortex/inspect/VortexInspector.java b/inspector/src/main/java/io/github/dfa1/vortex/inspect/VortexInspector.java index 2b1eadc1..1b77b736 100644 --- a/inspector/src/main/java/io/github/dfa1/vortex/inspect/VortexInspector.java +++ b/inspector/src/main/java/io/github/dfa1/vortex/inspect/VortexInspector.java @@ -161,9 +161,9 @@ private static void appendLayoutInline(StringBuilder sb, Layout layout) { private static void appendSchema(StringBuilder sb, DType dtype, String indent) { if (dtype instanceof DType.Struct s) { - int maxLen = s.fieldNames().stream().mapToInt(String::length).max().orElse(0); + int maxLen = s.fieldNames().stream().mapToInt(c -> c.value().length()).max().orElse(0); for (int i = 0; i < s.fieldNames().size(); i++) { - String name = s.fieldNames().get(i); + String name = s.fieldNames().get(i).value(); sb.append(indent).append(name) .append(" ".repeat(maxLen - name.length() + 1)) .append(formatDType(s.fieldTypes().get(i))).append('\n'); diff --git a/inspector/src/test/java/io/github/dfa1/vortex/inspect/InspectorTreeTest.java b/inspector/src/test/java/io/github/dfa1/vortex/inspect/InspectorTreeTest.java index 14cc8590..260f4e12 100644 --- a/inspector/src/test/java/io/github/dfa1/vortex/inspect/InspectorTreeTest.java +++ b/inspector/src/test/java/io/github/dfa1/vortex/inspect/InspectorTreeTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.inspect; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.reader.CompressionScheme; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.EncodingId; @@ -32,7 +33,7 @@ void build_withStructDType_assignsFieldNamesToColumns() { Layout valLeaf = leaf("vortex.constant", 10); Layout root = struct(10, List.of(idLeaf, valLeaf)); DType dtype = new DType.Struct( - List.of("id", "value"), + List.of(ColumnName.of("id"), ColumnName.of("value")), List.of(DType.I64, DType.F64), false); givenHandle(dtype, root, List.of("vortex.constant"), List.of()); @@ -52,7 +53,7 @@ void build_withFewerColNamesThanChildren_fillsWithSyntheticNames() { // Given — defensive path: malformed footer with a struct layout whose child count // exceeds the dtype's named fields. Should not throw; should fall back to col0/col1... Layout root = struct(0, List.of(leaf("vortex.constant", 0), leaf("vortex.constant", 0))); - DType dtype = new DType.Struct(List.of("only"), + DType dtype = new DType.Struct(List.of(ColumnName.of("only")), List.of(DType.I32), false); givenHandle(dtype, root, List.of("vortex.constant"), List.of()); @@ -103,7 +104,7 @@ void build_sumsSegmentBytesAndCountsSegments() { void build_setsTotalRowCountFromRootLayout() { // Given — total rows is the root layout's row count, regardless of struct/non-struct Layout root = struct(12_345L, List.of(leaf("vortex.constant", 12_345L))); - DType dtype = new DType.Struct(List.of("c"), + DType dtype = new DType.Struct(List.of(ColumnName.of("c")), List.of(DType.I32), false); givenHandle(dtype, root, List.of("vortex.constant"), List.of()); @@ -142,7 +143,7 @@ void build_reportsProgressOncePerPeekedSegment() { Layout c2 = new Layout(LayoutId.parse("vortex.flat"), 0, null, List.of(), List.of(1)); Layout c3 = new Layout(LayoutId.parse("vortex.flat"), 0, null, List.of(), List.of(2)); Layout root = struct(0, List.of(c1, c2, c3)); - DType dtype = new DType.Struct(List.of("a", "b", "c"), + DType dtype = new DType.Struct(List.of(ColumnName.of("a"), ColumnName.of("b"), ColumnName.of("c")), List.of(DType.I32, DType.I32, DType.I32), @@ -166,7 +167,7 @@ void build_reportsProgressOncePerPeekedSegment() { void build_progressNoop_isAcceptedAndProducesSameTree() { // Given Layout root = struct(0, List.of(leaf("vortex.constant", 0))); - DType dtype = new DType.Struct(List.of("c"), + DType dtype = new DType.Struct(List.of(ColumnName.of("c")), List.of(DType.I32), false); givenHandle(dtype, root, List.of("vortex.constant"), List.of()); @@ -183,7 +184,7 @@ void buildShallow_skipsAllSlicesAndStillNamesColumns() { Layout col0 = new Layout(LayoutId.parse("vortex.flat"), 10, null, List.of(), List.of(0)); Layout col1 = new Layout(LayoutId.parse("vortex.flat"), 10, null, List.of(), List.of(1)); Layout root = struct(10, List.of(col0, col1)); - DType dtype = new DType.Struct(List.of("id", "value"), + DType dtype = new DType.Struct(List.of(ColumnName.of("id"), ColumnName.of("value")), List.of(DType.I64, DType.F64), false); diff --git a/inspector/src/test/java/io/github/dfa1/vortex/inspect/VortexInspectorTest.java b/inspector/src/test/java/io/github/dfa1/vortex/inspect/VortexInspectorTest.java index 420499e0..5f3526a5 100644 --- a/inspector/src/test/java/io/github/dfa1/vortex/inspect/VortexInspectorTest.java +++ b/inspector/src/test/java/io/github/dfa1/vortex/inspect/VortexInspectorTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.inspect; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.reader.ArrayStats; import io.github.dfa1.vortex.reader.CompressionScheme; import io.github.dfa1.vortex.core.model.DType; @@ -114,7 +115,7 @@ void render_chainsChildrenWithArrow() { InspectorTree sut = new InspectorTree( 1, 1024L, - new DType.Struct(List.of("v"), List.of(DType.I32), false), + new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.I32), false), List.of("vortex.flat"), Set.of(), List.of(), 1000L, rootN); @@ -143,7 +144,7 @@ void render_aggregatesMinMaxAcrossChunks() { Set.of("vortex.flat"), ArrayStats.empty(), List.of(chunkedN)); InspectorTree sut = new InspectorTree(1, 1024L, - new DType.Struct(List.of("id"), List.of(DType.I64), false), + new DType.Struct(List.of(ColumnName.of("id")), List.of(DType.I64), false), List.of("vortex.flat"), Set.of(), List.of(), 1000L, rootN); // When @@ -192,7 +193,7 @@ private static InspectorTree struct2col(int version, long fileSize, List columnPath(DType root) { List names = new ArrayList<>(); if (root instanceof DType.Struct s) { - names.addAll(s.fieldNames()); + names.addAll(s.fieldNames().stream().map(io.github.dfa1.vortex.core.model.ColumnName::value).toList()); } else { names.add(""); } diff --git a/integration/src/test/java/io/github/dfa1/vortex/integration/RustJavaReaderComparisonIntegrationTest.java b/integration/src/test/java/io/github/dfa1/vortex/integration/RustJavaReaderComparisonIntegrationTest.java index 4ea8714f..2cd358ce 100644 --- a/integration/src/test/java/io/github/dfa1/vortex/integration/RustJavaReaderComparisonIntegrationTest.java +++ b/integration/src/test/java/io/github/dfa1/vortex/integration/RustJavaReaderComparisonIntegrationTest.java @@ -223,7 +223,7 @@ private static Stats javaStats(Path file) throws Exception { if (reader.dtype() instanceof DType.Struct schema) { for (int i = 0; i < schema.fieldNames().size(); i++) { if (schema.fieldTypes().get(i) instanceof DType.Extension) { - extensionCols.add(schema.fieldNames().get(i)); + extensionCols.add(schema.fieldNames().get(i).value()); } } } diff --git a/integration/src/test/java/io/github/dfa1/vortex/integration/RustWritesJavaReadsIntegrationTest.java b/integration/src/test/java/io/github/dfa1/vortex/integration/RustWritesJavaReadsIntegrationTest.java index a37d1c58..45570d33 100644 --- a/integration/src/test/java/io/github/dfa1/vortex/integration/RustWritesJavaReadsIntegrationTest.java +++ b/integration/src/test/java/io/github/dfa1/vortex/integration/RustWritesJavaReadsIntegrationTest.java @@ -166,7 +166,7 @@ private static String firstI64Column(Path file) throws IOException { if (vf.dtype() instanceof DType.Struct struct) { for (int i = 0; i < struct.fieldNames().size(); i++) { if (struct.fieldTypes().get(i) instanceof DType.Primitive(PType pt, boolean _) && pt == PType.I64) { - return struct.fieldNames().get(i); + return struct.fieldNames().get(i).value(); } } } diff --git a/integration/src/test/java/io/github/dfa1/vortex/integration/VariantJavaWritesRustReadsIntegrationTest.java b/integration/src/test/java/io/github/dfa1/vortex/integration/VariantJavaWritesRustReadsIntegrationTest.java index 0d76874b..580506bd 100644 --- a/integration/src/test/java/io/github/dfa1/vortex/integration/VariantJavaWritesRustReadsIntegrationTest.java +++ b/integration/src/test/java/io/github/dfa1/vortex/integration/VariantJavaWritesRustReadsIntegrationTest.java @@ -4,6 +4,7 @@ import dev.vortex.api.Session; import dev.vortex.arrow.ArrowAllocation; import dev.vortex.jni.NativeLoader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.proto.ProtoPrimitive; import io.github.dfa1.vortex.core.proto.ProtoScalar; @@ -37,7 +38,7 @@ class VariantJavaWritesRustReadsIntegrationTest { private static final BufferAllocator ALLOCATOR = ArrowAllocation.rootAllocator(); private static final DType.Struct VARIANT_SCHEMA = new DType.Struct( - List.of("v"), + List.of(ColumnName.of("v")), List.of(DType.VARIANT), false); diff --git a/integration/src/test/java/io/github/dfa1/vortex/integration/load/LargeCsvRoundTripLoadIntegrationTest.java b/integration/src/test/java/io/github/dfa1/vortex/integration/load/LargeCsvRoundTripLoadIntegrationTest.java index 4bfcb01f..5433c4f4 100644 --- a/integration/src/test/java/io/github/dfa1/vortex/integration/load/LargeCsvRoundTripLoadIntegrationTest.java +++ b/integration/src/test/java/io/github/dfa1/vortex/integration/load/LargeCsvRoundTripLoadIntegrationTest.java @@ -5,6 +5,7 @@ import de.siegmar.fastcsv.reader.CsvReader; import de.siegmar.fastcsv.reader.CsvRecord; import de.siegmar.fastcsv.writer.CsvWriter; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.csv.CsvExporter; import io.github.dfa1.vortex.csv.CsvImporter; @@ -44,7 +45,7 @@ class LargeCsvRoundTripLoadIntegrationTest { /// Schema is fixed (not inferred) so column types never drift with the /// sampled first chunk. Field names double as the CSV header. private static final DType.Struct SCHEMA = new DType.Struct( - List.of("id", "a", "b", "x", "y", "flag", "sym", "note"), + List.of(ColumnName.of("id"), ColumnName.of("a"), ColumnName.of("b"), ColumnName.of("x"), ColumnName.of("y"), ColumnName.of("flag"), ColumnName.of("sym"), ColumnName.of("note")), List.of( DType.I64, DType.I64, @@ -56,7 +57,7 @@ class LargeCsvRoundTripLoadIntegrationTest { DType.UTF8), false); - private static final String[] HEADER = SCHEMA.fieldNames().toArray(String[]::new); + private static final String[] HEADER = SCHEMA.fieldNames().stream().map(io.github.dfa1.vortex.core.model.ColumnName::value).toArray(String[]::new); /// Low-cardinality symbol set — plain ASCII, no delimiter/quote chars. private static final String[] SYMS = {"AAPL", "MSFT", "GOOG", "AMZN", "META", "NVDA"}; @@ -126,8 +127,8 @@ private static long assertEqualsOracle(Path exported, long rows) throws IOExcept throw new AssertionError("exported CSV is empty"); } List header = it.next().getFields(); - if (!header.equals(SCHEMA.fieldNames())) { - throw new AssertionError("header mismatch: " + header + " != " + SCHEMA.fieldNames()); + if (!header.equals(java.util.Arrays.asList(HEADER))) { + throw new AssertionError("header mismatch: " + header + " != " + java.util.Arrays.asList(HEADER)); } long i = 0; while (it.hasNext()) { diff --git a/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java b/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java index 4abc8a0f..d92e9511 100644 --- a/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java +++ b/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.jdbc; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.VortexWriter; @@ -83,11 +84,11 @@ public static void importQuery(Connection conn, String sql, Path vortexPath, Jdb } private static DType.Struct buildSchema(ResultSetMetaData meta, int colCount) throws SQLException { - List names = new ArrayList<>(colCount); + List names = new ArrayList<>(colCount); List types = new ArrayList<>(colCount); for (int c = 1; c <= colCount; c++) { boolean nullable = meta.isNullable(c) != ResultSetMetaData.columnNoNulls; - names.add(meta.getColumnLabel(c)); + names.add(ColumnName.of(meta.getColumnLabel(c))); types.add(SqlTypeToDType.map(meta.getColumnType(c), meta.getColumnTypeName(c), nullable)); } return new DType.Struct(names, types, false); @@ -270,7 +271,7 @@ private static UUID toUuid(Object raw) { private static Map toChunkMap(DType.Struct schema, Object[] buffers, boolean[][] validity, boolean[] anyNull, int rows) { - List names = schema.fieldNames(); + List names = schema.fieldNames().stream().map(ColumnName::value).toList(); Map chunk = new LinkedHashMap<>(); for (int c = 0; c < names.size(); c++) { Object trimmed = trimBuffer(buffers[c], rows); diff --git a/jdbc/src/test/java/io/github/dfa1/vortex/jdbc/JdbcImporterTest.java b/jdbc/src/test/java/io/github/dfa1/vortex/jdbc/JdbcImporterTest.java index b3f5882d..558ad340 100644 --- a/jdbc/src/test/java/io/github/dfa1/vortex/jdbc/JdbcImporterTest.java +++ b/jdbc/src/test/java/io/github/dfa1/vortex/jdbc/JdbcImporterTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.jdbc; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.array.BoolArray; import io.github.dfa1.vortex.reader.array.DoubleArray; @@ -63,7 +64,7 @@ void roundTripsAllSupportedTypes(@TempDir Path tmp) throws Exception { try (VortexReader reader = VortexReader.open(vortex)) { assertThat(reader.dtype()).isInstanceOf(DType.Struct.class); DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).containsExactly("ID", "NAME", "SCORE", "ACTIVE"); + assertThat(schema.fieldNames()).containsExactly(ColumnName.of("ID"), ColumnName.of("NAME"), ColumnName.of("SCORE"), ColumnName.of("ACTIVE")); assertThat(schema.fieldTypes().get(0)).isEqualTo(DType.I64); assertThat(schema.fieldTypes().get(1)).isEqualTo(DType.UTF8); assertThat(schema.fieldTypes().get(2)).isEqualTo(DType.F64); @@ -417,7 +418,7 @@ void selectsAllRowsFromTable(@TempDir Path tmp) throws Exception { // Then try (VortexReader reader = VortexReader.open(vortex)) { DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).containsExactly("ID", "LABEL"); + assertThat(schema.fieldNames()).containsExactly(ColumnName.of("ID"), ColumnName.of("LABEL")); try (ScanIterator iter = reader.scan(ScanOptions.all())) { assertThat(iter.hasNext()).isTrue(); try (Chunk chunk = iter.next()) { diff --git a/parquet/src/main/java/io/github/dfa1/vortex/parquet/ParquetImporter.java b/parquet/src/main/java/io/github/dfa1/vortex/parquet/ParquetImporter.java index 0637849f..89ca3f92 100644 --- a/parquet/src/main/java/io/github/dfa1/vortex/parquet/ParquetImporter.java +++ b/parquet/src/main/java/io/github/dfa1/vortex/parquet/ParquetImporter.java @@ -8,6 +8,7 @@ import dev.hardwood.schema.ColumnProjection; import dev.hardwood.schema.ColumnSchema; import dev.hardwood.schema.FileSchema; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.encode.DateTimePartsData; @@ -64,10 +65,10 @@ public static void importParquet(Path parquetPath, Path vortexPath, ImportOption : allColumns; int colCount = columns.size(); - List names = new ArrayList<>(colCount); + List names = new ArrayList<>(colCount); List types = new ArrayList<>(colCount); for (ColumnSchema col : columns) { - names.add(col.name()); + names.add(ColumnName.of(col.name())); types.add(mapDType(col)); } DType.Struct schema = new DType.Struct(names, types, false); diff --git a/parquet/src/test/java/io/github/dfa1/vortex/parquet/ParquetImporterTest.java b/parquet/src/test/java/io/github/dfa1/vortex/parquet/ParquetImporterTest.java index 142c5b73..2fdd2475 100644 --- a/parquet/src/test/java/io/github/dfa1/vortex/parquet/ParquetImporterTest.java +++ b/parquet/src/test/java/io/github/dfa1/vortex/parquet/ParquetImporterTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.parquet; +import io.github.dfa1.vortex.core.model.ColumnName; import dev.hardwood.metadata.FieldPath; import dev.hardwood.metadata.LogicalType; import dev.hardwood.metadata.PhysicalType; @@ -196,7 +197,7 @@ void importsFixture_schemaAndRowCount(@TempDir Path tmp) throws Exception { try (VortexReader reader = VortexReader.open(vortex)) { assertThat(reader.dtype()).isInstanceOf(DType.Struct.class); DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).contains("c_customer_sk", "c_first_name"); + assertThat(schema.fieldNames()).contains(ColumnName.of("c_customer_sk"), ColumnName.of("c_first_name")); assertThat(countRows(reader)).isEqualTo(100L); } } @@ -242,7 +243,7 @@ void projection_importsOnlyRequestedColumns(@TempDir Path tmp) throws Exception // Then — only the projected column survives try (VortexReader reader = VortexReader.open(vortex)) { DType.Struct schema = (DType.Struct) reader.dtype(); - assertThat(schema.fieldNames()).containsExactly("c_customer_sk"); + assertThat(schema.fieldNames()).containsExactly(ColumnName.of("c_customer_sk")); assertThat(countRows(reader)).isEqualTo(100L); } } diff --git a/performance/src/main/java/io/github/dfa1/vortex/performance/ComputeKernelBenchmark.java b/performance/src/main/java/io/github/dfa1/vortex/performance/ComputeKernelBenchmark.java index 44afa3ed..4d541a61 100644 --- a/performance/src/main/java/io/github/dfa1/vortex/performance/ComputeKernelBenchmark.java +++ b/performance/src/main/java/io/github/dfa1/vortex/performance/ComputeKernelBenchmark.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.performance; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.Chunk; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -130,7 +131,7 @@ public class ComputeKernelBenchmark { List.of("price", "measure", "category", "plain"); private static final DType.Struct SCHEMA = new DType.Struct( - COLUMNS, + COLUMNS.stream().map(ColumnName::of).toList(), List.of(DType.F64, DType.I64, DType.I64, DType.I64), false); diff --git a/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniReadBenchmark.java b/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniReadBenchmark.java index 4cd7694d..065acf6e 100644 --- a/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniReadBenchmark.java +++ b/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniReadBenchmark.java @@ -8,6 +8,7 @@ import dev.vortex.api.Session; import dev.vortex.arrow.ArrowAllocation; import dev.vortex.jni.NativeLoader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.array.DoubleArray; import io.github.dfa1.vortex.reader.array.LongArray; @@ -97,7 +98,7 @@ public class JavaVsJniReadBenchmark { )); private static final Session SESSION = Session.create(); private static final DType.Struct JAVA_SCHEMA = new DType.Struct( - List.of("date", "symbol", "open", "high", "low", "close", "volume"), + List.of(ColumnName.of("date"), ColumnName.of("symbol"), ColumnName.of("open"), ColumnName.of("high"), ColumnName.of("low"), ColumnName.of("close"), ColumnName.of("volume")), List.of( DType.I32, DType.UTF8, diff --git a/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniWriteBenchmark.java b/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniWriteBenchmark.java index e2c82b8c..caf8e5c0 100644 --- a/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniWriteBenchmark.java +++ b/performance/src/main/java/io/github/dfa1/vortex/performance/JavaVsJniWriteBenchmark.java @@ -3,6 +3,7 @@ import dev.vortex.api.Session; import dev.vortex.arrow.ArrowAllocation; import dev.vortex.jni.NativeLoader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.writer.VortexWriter; import io.github.dfa1.vortex.writer.WriteOptions; @@ -84,7 +85,7 @@ public class JavaVsJniWriteBenchmark { )); private static final DType.Struct JAVA_SCHEMA = new DType.Struct( - List.of("date", "symbol", "open", "high", "low", "close", "volume"), + List.of(ColumnName.of("date"), ColumnName.of("symbol"), ColumnName.of("open"), ColumnName.of("high"), ColumnName.of("low"), ColumnName.of("close"), ColumnName.of("volume")), List.of( DType.I32, DType.UTF8, diff --git a/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnByteDiff.java b/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnByteDiff.java index 0f6b36d7..ab7384b0 100644 --- a/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnByteDiff.java +++ b/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnByteDiff.java @@ -2,6 +2,7 @@ import dev.vortex.api.Session; import dev.vortex.jni.NativeLoader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.layout.Layout; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -99,7 +100,7 @@ private static Map perColumnBytes(Path file) throws IOException { throw new IllegalStateException("expected struct root, got: " + dtype); } Layout structLayout = unwrapToStruct(root); - List names = s.fieldNames(); + List names = s.fieldNames().stream().map(ColumnName::value).toList(); List children = structLayout.children(); Map out = new LinkedHashMap<>(); for (int i = 0; i < names.size(); i++) { diff --git a/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnTreeDiff.java b/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnTreeDiff.java index 1a26a943..dd0e5510 100644 --- a/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnTreeDiff.java +++ b/performance/src/main/java/io/github/dfa1/vortex/performance/TaxiColumnTreeDiff.java @@ -4,6 +4,7 @@ import dev.vortex.api.Session; import dev.vortex.jni.NativeLoader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.EncodingId; import io.github.dfa1.vortex.core.fbs.FbsArray; @@ -87,7 +88,7 @@ private static void dump(Path file, String column) throws IOException { if (!(dtype instanceof DType.Struct s)) { throw new IllegalStateException("expected struct root, got: " + dtype); } - int colIdx = s.fieldNames().indexOf(column); + int colIdx = s.fieldNames().indexOf(ColumnName.of(column)); if (colIdx < 0) { throw new IllegalArgumentException("missing column: " + column); } diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/PostscriptParser.java b/reader/src/main/java/io/github/dfa1/vortex/reader/PostscriptParser.java index ee961214..e8c4ece3 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/PostscriptParser.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/PostscriptParser.java @@ -267,7 +267,7 @@ private static DType convertDType(io.github.dfa1.vortex.core.fbs.FbsDType fbs, i throw new VortexException("struct names/dtypes length mismatch: " + s.namesLength() + " names, " + s.dtypesLength() + " dtypes"); } - var names = new ArrayList(s.namesLength()); + var names = new ArrayList(s.namesLength()); var types = new ArrayList(s.dtypesLength()); var seen = new HashSet(); for (int i = 0; i < s.namesLength(); i++) { @@ -289,7 +289,7 @@ private static DType convertDType(io.github.dfa1.vortex.core.fbs.FbsDType fbs, i + fieldIndex + "): " + reason + " — likely a bug in the pipeline that produced this file"); }); - names.add(name); + names.add(io.github.dfa1.vortex.core.model.ColumnName.of(name)); } for (int i = 0; i < s.dtypesLength(); i++) { types.add(convertDType(s.dtypes(i), depth + 1)); diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/RowFilter.java b/reader/src/main/java/io/github/dfa1/vortex/reader/RowFilter.java index fec38d7f..85bef779 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/RowFilter.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/RowFilter.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.reader.compute.Predicate; import java.util.List; @@ -30,7 +31,7 @@ static RowFilter and(RowFilter... filters) { /// @param val the value to compare against /// @return a `Column` bound to a [Predicate.Eq] static RowFilter eq(String col, Object val) { - return new Column(col, new Predicate.Eq(val)); + return new Column(ColumnName.of(col), new Predicate.Eq(val)); } /// Builds a filter selecting rows where `col` differs from `val`. @@ -39,7 +40,7 @@ static RowFilter eq(String col, Object val) { /// @param val the value to compare against /// @return a `Column` bound to a [Predicate.Neq] static RowFilter neq(String col, Object val) { - return new Column(col, new Predicate.Neq(val)); + return new Column(ColumnName.of(col), new Predicate.Neq(val)); } /// Builds a filter selecting rows where `col` is strictly greater than `val`. @@ -48,7 +49,7 @@ static RowFilter neq(String col, Object val) { /// @param val the exclusive lower bound /// @return a `Column` bound to a [Predicate.Gt] static RowFilter gt(String col, Comparable val) { - return new Column(col, new Predicate.Gt(val)); + return new Column(ColumnName.of(col), new Predicate.Gt(val)); } /// Builds a filter selecting rows where `col` is greater than or equal to `val`. @@ -57,7 +58,7 @@ static RowFilter gt(String col, Comparable val) { /// @param val the inclusive lower bound /// @return a `Column` bound to a [Predicate.Gte] static RowFilter gte(String col, Comparable val) { - return new Column(col, new Predicate.Gte(val)); + return new Column(ColumnName.of(col), new Predicate.Gte(val)); } /// Builds a filter selecting rows where `col` is strictly less than `val`. @@ -66,7 +67,7 @@ static RowFilter gte(String col, Comparable val) { /// @param val the exclusive upper bound /// @return a `Column` bound to a [Predicate.Lt] static RowFilter lt(String col, Comparable val) { - return new Column(col, new Predicate.Lt(val)); + return new Column(ColumnName.of(col), new Predicate.Lt(val)); } /// Builds a filter selecting rows where `col` is less than or equal to `val`. @@ -75,7 +76,7 @@ static RowFilter lt(String col, Comparable val) { /// @param val the inclusive upper bound /// @return a `Column` bound to a [Predicate.Lte] static RowFilter lte(String col, Comparable val) { - return new Column(col, new Predicate.Lte(val)); + return new Column(ColumnName.of(col), new Predicate.Lte(val)); } /// Builds a filter selecting rows where `col` is null. @@ -83,7 +84,7 @@ static RowFilter lte(String col, Comparable val) { /// @param col the column name /// @return a `Column` bound to a [Predicate.IsNull] static RowFilter isNull(String col) { - return new Column(col, new Predicate.IsNull()); + return new Column(ColumnName.of(col), new Predicate.IsNull()); } /// Builds a filter selecting rows where `col` is not null. @@ -91,7 +92,7 @@ static RowFilter isNull(String col) { /// @param col the column name /// @return a `Column` bound to a [Predicate.IsNotNull] static RowFilter isNotNull(String col) { - return new Column(col, new Predicate.IsNotNull()); + return new Column(ColumnName.of(col), new Predicate.IsNotNull()); } /// Conjoins this filter with `other` into a binary [RowFilter.And]. @@ -121,7 +122,7 @@ record And(List filters) implements RowFilter { /// /// @param column the column name, must be non-null /// @param predicate the value-test to apply to the column, must be non-null - record Column(String column, Predicate predicate) implements RowFilter { + record Column(ColumnName column, Predicate predicate) implements RowFilter { /// Validates both components. /// diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/ScanIterator.java b/reader/src/main/java/io/github/dfa1/vortex/reader/ScanIterator.java index fb7eb00a..95ed9e33 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/ScanIterator.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/ScanIterator.java @@ -184,7 +184,7 @@ private DType columnDType(ColumnName col) { columnDtypes = new HashMap<>(); if (file.dtype() instanceof DType.Struct struct) { for (int i = 0; i < struct.fieldNames().size(); i++) { - columnDtypes.put(ColumnName.of(struct.fieldNames().get(i)), struct.fieldTypes().get(i)); + columnDtypes.put(struct.fieldNames().get(i), struct.fieldTypes().get(i)); } } } @@ -193,12 +193,12 @@ private DType columnDType(ColumnName col) { private static SequencedMap expandStruct(StructArray sa) { DType.Struct sd = (DType.Struct) sa.dtype(); - List names = sd.fieldNames(); + List names = sd.fieldNames(); List types = sd.fieldTypes(); int n = names.size(); var map = new LinkedHashMap(n); for (int i = 0; i < n; i++) { - map.put(ColumnName.of(names.get(i)), new Chunk.Column(sa.field(i), types.get(i))); + map.put(names.get(i), new Chunk.Column(sa.field(i), types.get(i))); } return unmodifiable(map); } @@ -443,7 +443,7 @@ private Layout findZonedLayout(Layout root, ColumnName column) { if (!(file.dtype() instanceof DType.Struct struct) || !root.isStruct()) { return null; } - int idx = struct.fieldNames().indexOf(column.value()); + int idx = struct.fieldNames().indexOf(column); if (idx < 0 || idx >= root.children().size()) { return null; } @@ -464,7 +464,7 @@ private static Layout firstZoned(Layout layout) { } private static Array fieldOrNull(StructArray table, String field) { - if (((DType.Struct) table.dtype()).fieldNames().contains(field)) { + if (((DType.Struct) table.dtype()).fieldNames().contains(ColumnName.of(field))) { return table.field(field); } return null; @@ -544,11 +544,8 @@ private void initialize() { if (rootLayout.isStruct() && rootDtype instanceof DType.Struct structDtype) { List projection = options.columns(); for (int i = 0; i < rootLayout.children().size(); i++) { - String rawName = structDtype.fieldNames().get(i); + ColumnName colName = structDtype.fieldNames().get(i); DType colDtype = structDtype.fieldTypes().get(i); - // File-schema names are already policy-certified by PostscriptParser, so this - // ColumnName construction never throws — it just types the certified key. - ColumnName colName = ColumnName.of(rawName); if (!projection.isEmpty() && !projection.contains(colName)) { continue; } @@ -721,9 +718,9 @@ private boolean canPruneChunk(ChunkSpec chunk, RowFilter filter) { yield false; } case RowFilter.Column(var col, var predicate) -> { - // The filter's raw column string enters the typed internals here; a valid-but-absent - // name yields no layout (no pruning), matching the row-level scan it gates. - ColumnName name = ColumnName.of(col); + // A valid-but-absent name yields no layout (no pruning), matching the row-level + // scan it gates. + ColumnName name = col; Layout flat = chunk.layoutFor(name); if (flat == null) { yield false; diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/VortexReader.java b/reader/src/main/java/io/github/dfa1/vortex/reader/VortexReader.java index dba7c439..0ac98fd1 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/VortexReader.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/VortexReader.java @@ -1,6 +1,7 @@ package io.github.dfa1.vortex.reader; import static io.github.dfa1.vortex.core.io.VortexFormat.LE_INT; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.io.IoBounds; import io.github.dfa1.vortex.core.error.VortexException; @@ -230,9 +231,9 @@ private void validateColumns(List columns) { if (columns.isEmpty() || !(dtype instanceof DType.Struct struct)) { return; } - List known = struct.fieldNames(); + List known = struct.fieldNames(); for (String name : columns) { - if (!known.contains(name)) { + if (!known.contains(ColumnName.of(name))) { throw new VortexException("decodeChunk: unknown column: " + name); } } @@ -241,13 +242,13 @@ private void validateColumns(List columns) { /// Aggregated per-column statistics (global min/max across all chunks). /// Returns an empty map if the root layout is not a struct. /// Columns with no embedded stats return [ArrayStats#empty()]. - public Map columnStats() { + public Map columnStats() { if (!layout.isStruct() || !(dtype instanceof DType.Struct schema)) { return Map.of(); } - List names = schema.fieldNames(); + List names = schema.fieldNames(); List colLayouts = layout.children(); - Map result = new LinkedHashMap<>(); + Map result = new LinkedHashMap<>(); for (int i = 0; i < names.size() && i < colLayouts.size(); i++) { List flats = new ArrayList<>(); collectFlats(colLayouts.get(i), flats); diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/StructArray.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/StructArray.java index ba5ada12..0aead4aa 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/array/StructArray.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/StructArray.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader.array; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.error.VortexException; @@ -58,9 +59,9 @@ public Array field(int i) { /// @param name the field name to look up /// @return the field array with the given name public Array field(String name) { - List names = dtype.fieldNames(); + List names = dtype.fieldNames(); for (int i = 0; i < names.size(); i++) { - if (names.get(i).equals(name)) { + if (names.get(i).value().equals(name)) { return fields.get(i); } } diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoder.java b/reader/src/main/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoder.java index 8e1ef82c..3e9c1b81 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoder.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoder.java @@ -83,9 +83,11 @@ static DType dtypeFromProto(io.github.dfa1.vortex.core.proto.ProtoDType proto) { } if (proto.struct() != null) { var s = proto.struct(); - var names = new ArrayList(s.names().size()); + var names = new ArrayList(s.names().size()); var types = new ArrayList(s.dtypes().size()); - names.addAll(s.names()); + for (String name : s.names()) { + names.add(io.github.dfa1.vortex.core.model.ColumnName.of(name)); + } for (io.github.dfa1.vortex.core.proto.ProtoDType child : s.dtypes()) { types.add(dtypeFromProto(child)); } diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchema.java b/reader/src/main/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchema.java index 818dd0d4..cb8d18a9 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchema.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchema.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader.layout; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import static io.github.dfa1.vortex.core.io.VortexFormat.LE_INT; @@ -141,20 +142,20 @@ public static DType.Struct statsTableDtype(DType columnDtype, MemorySegment meta /// @param present stats present, in ordinal order /// @return reconstructed non-nullable struct dtype public static DType.Struct statsTableDtype(DType columnDtype, List present) { - List names = new ArrayList<>(present.size() * 2); + List names = new ArrayList<>(present.size() * 2); List types = new ArrayList<>(present.size() * 2); for (Stat stat : present) { DType stype = statDtype(stat, columnDtype); if (stype == null) { continue; } - names.add(stat.fieldName()); + names.add(ColumnName.of(stat.fieldName())); types.add(stype.withNullable(true)); if (stat == Stat.MAX) { - names.add(Stat.MAX_IS_TRUNCATED); + names.add(ColumnName.of(Stat.MAX_IS_TRUNCATED)); types.add(DType.BOOL); } else if (stat == Stat.MIN) { - names.add(Stat.MIN_IS_TRUNCATED); + names.add(ColumnName.of(Stat.MIN_IS_TRUNCATED)); types.add(DType.BOOL); } } diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/ComputeFilteredAggregateTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/ComputeFilteredAggregateTest.java index 458091a8..69368426 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/ComputeFilteredAggregateTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/ComputeFilteredAggregateTest.java @@ -89,7 +89,7 @@ void fusedMatchesBruteForce(int seed) { List leaves = new ArrayList<>(); int leafCount = 1 + random.nextInt(3); for (int k = 0; k < leafCount; k++) { - leaves.add(new RowFilter.Column("f" + random.nextInt(2), randomPredicate(random))); + leaves.add(new RowFilter.Column(ColumnName.of("f" + random.nextInt(2)), randomPredicate(random))); } RowFilter filter = leaves.size() == 1 ? leaves.getFirst() @@ -483,7 +483,7 @@ private static void assertMatchesBruteForce(FilteredAggregate result, int rows, private static boolean allLeavesAccept(List leaves, Map references, int i) { for (RowFilter leaf : leaves) { RowFilter.Column column = (RowFilter.Column) leaf; - if (!accepts(column.predicate(), references.get(column.column()), i)) { + if (!accepts(column.predicate(), references.get(column.column().value()), i)) { return false; } } diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/RowFilterTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/RowFilterTest.java index 943f9a9a..9849e408 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/RowFilterTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/RowFilterTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.reader.compute.Predicate; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ void gt_createsColumnBoundToGt() { RowFilter sut = RowFilter.gt("price", 100L); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("price", new Predicate.Gt(100L))); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("price"), new Predicate.Gt(100L))); } @Test @@ -27,7 +28,7 @@ void gte_createsColumnBoundToGte() { RowFilter sut = RowFilter.gte("price", 100L); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("price", new Predicate.Gte(100L))); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("price"), new Predicate.Gte(100L))); } @Test @@ -36,7 +37,7 @@ void lt_createsColumnBoundToLt() { RowFilter sut = RowFilter.lt("price", 500L); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("price", new Predicate.Lt(500L))); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("price"), new Predicate.Lt(500L))); } @Test @@ -45,7 +46,7 @@ void lte_createsColumnBoundToLte() { RowFilter sut = RowFilter.lte("price", 500L); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("price", new Predicate.Lte(500L))); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("price"), new Predicate.Lte(500L))); } @Test @@ -54,7 +55,7 @@ void eq_createsColumnBoundToEq() { RowFilter sut = RowFilter.eq("status", "open"); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("status", new Predicate.Eq("open"))); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("status"), new Predicate.Eq("open"))); } @Test @@ -63,7 +64,7 @@ void neq_createsColumnBoundToNeq() { RowFilter sut = RowFilter.neq("status", "closed"); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("status", new Predicate.Neq("closed"))); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("status"), new Predicate.Neq("closed"))); } @Test @@ -72,7 +73,7 @@ void isNull_createsColumnBoundToIsNull() { RowFilter sut = RowFilter.isNull("status"); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("status", new Predicate.IsNull())); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("status"), new Predicate.IsNull())); } @Test @@ -81,7 +82,7 @@ void isNotNull_createsColumnBoundToIsNotNull() { RowFilter sut = RowFilter.isNotNull("status"); // Then - assertThat(sut).isEqualTo(new RowFilter.Column("status", new Predicate.IsNotNull())); + assertThat(sut).isEqualTo(new RowFilter.Column(ColumnName.of("status"), new Predicate.IsNotNull())); } } @@ -114,7 +115,7 @@ void and_instanceMethod_chainsMultiple() { assertThat(outer.filters()).hasSize(2); assertThat(outer.filters().get(0)).isInstanceOf(RowFilter.And.class); assertThat(outer.filters().get(1)) - .isEqualTo(new RowFilter.Column("status", new Predicate.Neq("cancelled"))); + .isEqualTo(new RowFilter.Column(ColumnName.of("status"), new Predicate.Neq("cancelled"))); } @Test diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderIT.java b/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderIT.java index c2a10019..a92b3a8a 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderIT.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/VortexHttpReaderIT.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.io.VortexFormat; import io.github.dfa1.vortex.reader.array.ListArray; @@ -145,12 +146,12 @@ void scan_listVortex_decodesListArrayWithCorrectShape() throws Exception { // When try (var sut = VortexHttpReader.open(uri)) { DType.Struct schema = (DType.Struct) sut.dtype(); - List names = schema.fieldNames(); + List names = schema.fieldNames(); List types = schema.fieldTypes(); String listColName = null; for (int i = 0; i < types.size(); i++) { if (types.get(i) instanceof DType.List) { - listColName = names.get(i); + listColName = names.get(i).value(); break; } } @@ -183,12 +184,12 @@ void scan_listviewVortex_decodesListViewArrayWithCorrectShape() throws Exception // When try (var sut = VortexHttpReader.open(uri)) { DType.Struct schema = (DType.Struct) sut.dtype(); - List names = schema.fieldNames(); + List names = schema.fieldNames(); List types = schema.fieldTypes(); String listColName = null; for (int i = 0; i < types.size(); i++) { if (types.get(i) instanceof DType.List) { - listColName = names.get(i); + listColName = names.get(i).value(); break; } } diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayLimitedTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayLimitedTest.java index 401c6e02..c0fd35a6 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayLimitedTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayLimitedTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader.array; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.EncodingId; import io.github.dfa1.vortex.core.error.VortexException; @@ -110,7 +111,7 @@ class Composite { @Test void structLimitsEachField() { // Given - DType.Struct dtype = new DType.Struct(List.of("a", "b"), List.of(I64, I64), false); + DType.Struct dtype = new DType.Struct(List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(I64, I64), false); StructArray sut = new StructArray(dtype, 3, List.of(longs(1L, 2L, 3L), longs(10L, 20L, 30L))); diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayMaterializeTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayMaterializeTest.java index 41441c0f..48c46353 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayMaterializeTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/array/ArrayMaterializeTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.reader.array; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.EncodingId; import io.github.dfa1.vortex.core.error.VortexException; @@ -233,7 +234,7 @@ void nullArrayThrows() { @Test void structArrayThrows() { // Given a two-field struct - DType.Struct dtype = new DType.Struct(List.of("a", "b"), List.of(I64, I64), false); + DType.Struct dtype = new DType.Struct(List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(I64, I64), false); StructArray sut = new StructArray(dtype, 2, List.of(longs(1L, 2L), longs(3L, 4L))); // When / Then diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoderTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoderTest.java index 5a194d81..844525ee 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoderTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/decode/VariantEncodingDecoderTest.java @@ -3,6 +3,7 @@ import io.github.dfa1.vortex.encoding.TestSegments; import io.github.dfa1.vortex.reader.ReadRegistry; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.array.Array; import io.github.dfa1.vortex.reader.array.NullArray; @@ -228,7 +229,7 @@ void struct() { // Then assertThat(result).isEqualTo(new DType.Struct( - List.of("a", "b"), + List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(DType.I32, new DType.Utf8(true)), false)); } diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchemaTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchemaTest.java index 4584ea8b..0be91e98 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchemaTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/layout/ZonedStatsSchemaTest.java @@ -3,6 +3,7 @@ import java.lang.foreign.MemorySegment; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import org.junit.jupiter.api.Nested; @@ -131,7 +132,7 @@ void buildsStructWithMinMaxAndTruncationFlags() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(columnDtype, present); // Then — matches Rust's stats_table_dtype ordering exactly - assertThat(schema.fieldNames()).containsExactly( + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly( "max", "max_is_truncated", "min", "min_is_truncated", "sum"); @@ -157,7 +158,7 @@ void dropsMaxAndMinForDTypeNull() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(new DType.Null(true), present); // Then — only null_count survives; truncation flags drop with their parent stat - assertThat(schema.fieldNames()).containsExactly("null_count"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("null_count"); assertThat(schema.fieldTypes()).containsExactly(new DType.Primitive(PType.U64, true)); } @@ -174,7 +175,7 @@ void dropsSumForUnsupportedColumnDtype() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(columnDtype, present); // Then - assertThat(schema.fieldNames()).containsExactly("min", "min_is_truncated", "null_count"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("min", "min_is_truncated", "null_count"); } @Test @@ -190,7 +191,7 @@ void dropsNanCountForNonFloatColumn() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(columnDtype, present); // Then - assertThat(schema.fieldNames()).containsExactly("max", "max_is_truncated", "null_count"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("max", "max_is_truncated", "null_count"); } @Test @@ -204,7 +205,7 @@ void keepsNanCountForFloatColumn() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(columnDtype, present); // Then - assertThat(schema.fieldNames()).containsExactly("max", "max_is_truncated", "nan_count"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("max", "max_is_truncated", "nan_count"); assertThat(schema.fieldTypes()).element(2).isEqualTo(new DType.Primitive(PType.U64, true)); } @@ -220,7 +221,7 @@ void resolvesExtensionViaStorageDType() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(columnDtype, present); // Then — min keeps extension dtype, sum widens to i64 via storage - assertThat(schema.fieldNames()).containsExactly("min", "min_is_truncated", "sum"); + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly("min", "min_is_truncated", "sum"); assertThat(schema.fieldTypes().get(0)).isEqualTo(columnDtype.withNullable(true)); assertThat(schema.fieldTypes().get(2)).isEqualTo(new DType.Primitive(PType.I64, true)); } @@ -235,7 +236,7 @@ void allStatsTogetherForI32() { DType.Struct schema = ZonedStatsSchema.statsTableDtype(columnDtype, present); // Then — nan_count is dropped (i32 not float); everything else present. - assertThat(schema.fieldNames()).containsExactly( + assertThat(schema.fieldNames().stream().map(ColumnName::value).toList()).containsExactly( "is_constant", "is_sorted", "is_strict_sorted", "max", "max_is_truncated", "min", "min_is_truncated", diff --git a/writer/src/main/java/io/github/dfa1/vortex/writer/Chunk.java b/writer/src/main/java/io/github/dfa1/vortex/writer/Chunk.java index 439f5d72..e6888ec5 100644 --- a/writer/src/main/java/io/github/dfa1/vortex/writer/Chunk.java +++ b/writer/src/main/java/io/github/dfa1/vortex/writer/Chunk.java @@ -1,5 +1,7 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; + /// Typed chunk builder for [VortexWriter#writeChunk(java.util.function.Consumer)]. /// /// Validates each `put` against the writer's schema: @@ -24,12 +26,12 @@ /// | Bool | `boolean[]` | `Boolean[]` | public interface Chunk { - /// Adds a column's data to the chunk. + /// Adds a column's data to the chunk, addressing it by its validated [ColumnName]. /// /// @param column the column name; must exist in the writer's schema /// @param data the column data; type must match the schema (see class javadoc) /// @return this builder /// @throws IllegalArgumentException if `column` is not in the schema or /// `data` is of the wrong type for the column - Chunk put(String column, Object data); + Chunk put(ColumnName column, Object data); } diff --git a/writer/src/main/java/io/github/dfa1/vortex/writer/ChunkImpl.java b/writer/src/main/java/io/github/dfa1/vortex/writer/ChunkImpl.java index 8fab18de..f0738563 100644 --- a/writer/src/main/java/io/github/dfa1/vortex/writer/ChunkImpl.java +++ b/writer/src/main/java/io/github/dfa1/vortex/writer/ChunkImpl.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.encode.NullableData; @@ -13,14 +14,14 @@ final class ChunkImpl implements Chunk { private final DType.Struct schema; - private final Map data = new LinkedHashMap<>(); + private final Map data = new LinkedHashMap<>(); ChunkImpl(DType.Struct schema) { this.schema = schema; } @Override - public Chunk put(String column, Object value) { + public Chunk put(ColumnName column, Object value) { int idx = schema.fieldNames().indexOf(column); if (idx < 0) { throw new IllegalArgumentException("unknown column: " + column); @@ -29,17 +30,21 @@ public Chunk put(String column, Object value) { throw new IllegalArgumentException("duplicate put for column: " + column); } DType dtype = schema.fieldTypes().get(idx); - data.put(column, validateAndAdapt(column, dtype, value)); + data.put(column, validateAndAdapt(column.value(), dtype, value)); return this; } Map finish() { - for (String name : schema.fieldNames()) { + for (ColumnName name : schema.fieldNames()) { if (!data.containsKey(name)) { throw new IllegalStateException("missing column: " + name); } } - return data; + // The map entry point ([VortexWriter#writeChunk(Map)]) is keyed by raw name, so surface + // the validated keys as strings at this internal boundary. + Map result = new LinkedHashMap<>(data.size()); + data.forEach((name, value) -> result.put(name.value(), value)); + return result; } /// Validates a column's raw data against its schema dtype and adapts boxed nullable arrays diff --git a/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java b/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java index 9343af3e..33f7e794 100644 --- a/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java +++ b/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java @@ -5,6 +5,7 @@ import io.github.dfa1.vortex.writer.encode.FixedSizeListData; import io.github.dfa1.vortex.writer.encode.ListData; import io.github.dfa1.vortex.writer.encode.ListViewData; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.core.io.VortexFormat; @@ -65,9 +66,10 @@ /// /// Usage: /// ```java -/// var schema = new DType.Struct(List.of("id", "value"), -/// List.of(DType.I64, -/// DType.F64), false); +/// var schema = DType.structBuilder() +/// .field("id", DType.I64) +/// .field("value", DType.F64) +/// .build(); /// try (var channel = FileChannel.open(path, CREATE, WRITE); /// var writer = VortexWriter.create(channel, schema, WriteOptions.defaults())) { /// writer.writeChunk(Map.of("id", idArray, "value", valueArray)); @@ -108,19 +110,19 @@ public final class VortexWriter implements Closeable { private final List cascadeCodecs; private final WriteRegistry cascadeRegistry; private final List segs = new ArrayList<>(); - private final Map> colChunks = new LinkedHashMap<>(); + private final Map> colChunks = new LinkedHashMap<>(); private final Map encodingIdx = new LinkedHashMap<>(); private long bytesWritten = 0; // Global dict state: columns detected as low-cardinality on first chunk are buffered // here instead of encoded per-chunk. Flushed in close() as a single Dict layout. - private final Set dictCandidates = new LinkedHashSet<>(); - private final Map> dictBuffers = new LinkedHashMap<>(); - private final Map dictColRefs = new LinkedHashMap<>(); + private final Set dictCandidates = new LinkedHashSet<>(); + private final Map> dictBuffers = new LinkedHashMap<>(); + private final Map dictColRefs = new LinkedHashMap<>(); private boolean firstChunkSeen = false; // Per-column zone-maps, populated by flushZoneMaps() in close() when enableZoneMaps is set. - private final Map zoneMaps = new LinkedHashMap<>(); + private final Map zoneMaps = new LinkedHashMap<>(); // Stats (ProtoScalarValue bytes) of the most recently written segment, captured for ChunkRef. private byte[] lastStatsMin; private byte[] lastStatsMax; @@ -134,21 +136,14 @@ private VortexWriter( // Wire contract, enforced by the reference writer ("StructLayout must have unique field // names"): duplicate-name schemas are constructible via the DType.Struct record (only // StructBuilder validates), so guard here — colChunks below is name-keyed and would - // silently collapse the duplicates anyway. - // Write-side name policy — stricter than the wire format on purpose (a JSON parser - // accepts a "" key; a good JSON library still refuses to encourage writing one). The - // reader stays tolerant: foreign files with blank or control-character names are legal - // and must open. Mirrored in DType.StructBuilder.field for earlier, friendlier failure. - var uniqueNames = new java.util.HashSet(); - for (String name : schema.fieldNames()) { + // silently collapse the duplicates anyway. The name policy itself (non-blank, no control + // characters — NUL additionally aborts the reference toolchain's Arrow FFI export) is + // already certified by the ColumnName type carried in schema.fieldNames(). + var uniqueNames = new java.util.HashSet(); + for (ColumnName name : schema.fieldNames()) { if (!uniqueNames.add(name)) { throw new IllegalArgumentException("duplicate field name: " + name); } - // Policy chokepoint: ColumnName.violation — NUL additionally aborts the reference - // toolchain's Arrow FFI export (SIGABRT in arrow-rs, measured 2026-07-04). - io.github.dfa1.vortex.core.model.ColumnName.violation(name).ifPresent(reason -> { - throw new IllegalArgumentException(reason); - }); } this.channel = channel; this.schema = schema; @@ -157,7 +152,7 @@ private VortexWriter( this.defaultRegistry = buildRegistry(encodings); this.cascadeCodecs = buildCascadeCodecs(options); this.cascadeRegistry = buildRegistry(this.cascadeCodecs); - for (String name : schema.fieldNames()) { + for (ColumnName name : schema.fieldNames()) { colChunks.put(name, new ArrayList<>()); } } @@ -257,7 +252,7 @@ public static VortexWriter create( /// Counts rows for the length-consistency check in [#writeChunk]. Accepts the /// same shapes the writer takes plus pre-conversion [java.util.Collection]s /// from the extension-column auto-route path. - private static long rowCountForValidation(String colName, Object data) { + private static long rowCountForValidation(ColumnName colName, Object data) { if (data instanceof java.util.Collection coll) { return coll.size(); } @@ -322,7 +317,7 @@ private static int serializeDType(FbsBuilder fbb, DType dtype) { } int[] nameOffsets = new int[s.fieldNames().size()]; for (int i = 0; i < nameOffsets.length; i++) { - nameOffsets[i] = fbb.createString(s.fieldNames().get(i)); + nameOffsets[i] = fbb.createString(s.fieldNames().get(i).value()); } int namesVec = io.github.dfa1.vortex.core.fbs.FbsStruct_.createNamesVector(fbb, nameOffsets); int dtypesVec = io.github.dfa1.vortex.core.fbs.FbsStruct_.createDtypesVector(fbb, fieldOffsets); @@ -392,10 +387,10 @@ private static ByteBuffer buildPostscript( /// /// ```java /// writer.writeChunk(c -> c - /// .put("timestamp", new long[] {1_700_000_000_000L, 1_700_000_001_000L}) - /// .put("symbol", new String[] {"AAPL", "AAPL"}) - /// .put("price", new double[] {189.95, 190.10}) - /// .put("volume", new Long[] {100L, null})); // boxed → nullable + /// .put(ColumnName.of("timestamp"), new long[] {1_700_000_000_000L, 1_700_000_001_000L}) + /// .put(ColumnName.of("symbol"), new String[] {"AAPL", "AAPL"}) + /// .put(ColumnName.of("price"), new double[] {189.95, 190.10}) + /// .put(ColumnName.of("volume"), new Long[] {100L, null})); // boxed → nullable /// ``` /// /// @param builder consumer that populates a [Chunk] with all schema columns @@ -421,14 +416,14 @@ public void writeChunk(Map columns) throws IOException { // builder: boxed nullable arrays (Long[], Integer[], Boolean[], …) become NullableData, // raw primitive arrays pass through. Done before the row-count check so length validation // and encoding both see the normalized carrier. - Map adapted = new LinkedHashMap<>(); + Map adapted = new LinkedHashMap<>(); for (int i = 0; i < schema.fieldNames().size(); i++) { - String colName = schema.fieldNames().get(i); - Object data = columns.get(colName); + ColumnName colName = schema.fieldNames().get(i); + Object data = columns.get(colName.value()); if (data == null) { throw new IllegalArgumentException("missing column: " + colName); } - adapted.put(colName, ChunkImpl.validateAndAdapt(colName, schema.fieldTypes().get(i), data)); + adapted.put(colName, ChunkImpl.validateAndAdapt(colName.value(), schema.fieldTypes().get(i), data)); } // Pre-validate row counts so a length mismatch is rejected with a clear error @@ -436,9 +431,9 @@ public void writeChunk(Map columns) throws IOException { // file whose column chunks claim different row counts — readable but logically // inconsistent. long expectedLen = -1L; - String expectedFrom = null; + ColumnName expectedFrom = null; for (int i = 0; i < schema.fieldNames().size(); i++) { - String colName = schema.fieldNames().get(i); + ColumnName colName = schema.fieldNames().get(i); long len = rowCountForValidation(colName, adapted.get(colName)); if (expectedLen < 0) { expectedLen = len; @@ -451,7 +446,7 @@ public void writeChunk(Map columns) throws IOException { } for (int i = 0; i < schema.fieldNames().size(); i++) { - String colName = schema.fieldNames().get(i); + ColumnName colName = schema.fieldNames().get(i); DType colDtype = schema.fieldTypes().get(i); Object data = adapted.get(colName); @@ -737,8 +732,8 @@ private void flushZoneMaps() throws IOException { if (!options.enableZoneMaps()) { return; } - for (Map.Entry> e : colChunks.entrySet()) { - String colName = e.getKey(); + for (Map.Entry> e : colChunks.entrySet()) { + ColumnName colName = e.getKey(); List chunks = e.getValue(); if (chunks.isEmpty()) { continue; @@ -760,7 +755,7 @@ private void flushZoneMaps() throws IOException { // Dict-encoded columns (one zone per code chunk). MIN/MAX/SUM come from each chunk's logical // values (computed at dict-build time); NULL_COUNT always. Matches Rust, whose zone-map // stats are computed on the logical column dtype, independent of the dict encoding. - for (Map.Entry e : dictColRefs.entrySet()) { + for (Map.Entry e : dictColRefs.entrySet()) { DictColRef ref = e.getValue(); DType colDtype = columnDtype(e.getKey()); DType minMaxDtype = zoneMinMaxDtype(colDtype); @@ -774,7 +769,7 @@ private void flushZoneMaps() throws IOException { } } - private DType columnDtype(String colName) { + private DType columnDtype(ColumnName colName) { return schema.fieldTypes().get(schema.fieldNames().indexOf(colName)); } @@ -784,7 +779,7 @@ private DType columnDtype(String colName) { /// read only when the matching dtype is set; a `null` `sumBytes` entry marks an overflowed zone /// (recorded as a null sum). Field/bit order follows ZonedStatsSchema: MAX(3), MIN(4), SUM(5), /// NULL_COUNT(6). - private void emitZoneMap(String colName, DType minMaxDtype, List minBytes, List maxBytes, + private void emitZoneMap(ColumnName colName, DType minMaxDtype, List minBytes, List maxBytes, DType sumDtype, List sumBytes, long[] nullCounts) throws IOException { int nZones = nullCounts.length; boolean[] allValid = new boolean[nZones]; @@ -819,7 +814,8 @@ private void emitZoneMap(String colName, DType minMaxDtype, List minByte types.add(new DType.Primitive(PType.U64, true)); fields.add(new NullableData(nullCounts, allValid.clone())); - DType.Struct statsDtype = new DType.Struct(List.copyOf(names), List.copyOf(types), false); + DType.Struct statsDtype = new DType.Struct( + names.stream().map(ColumnName::of).toList(), List.copyOf(types), false); int zonesSegIdx = writeSegment(statsDtype, new StructData(fields), new StructEncodingEncoder()); zoneMaps.put(colName, new ZoneMapRef(zonesSegIdx, nZones, options.chunkSize(), minMaxDtype != null, sumDtype != null)); @@ -827,7 +823,7 @@ private void emitZoneMap(String colName, DType minMaxDtype, List minByte /// Wraps a column's data layout in a `vortex.stats` (zoned) layout when a zone-map was /// emitted for it; otherwise returns the data layout unchanged. - private int wrapZoneMap(FbsBuilder fbb, String colName, int dataLayout, long colRows) { + private int wrapZoneMap(FbsBuilder fbb, ColumnName colName, int dataLayout, long colRows) { ZoneMapRef zm = zoneMaps.get(colName); if (zm == null) { return dataLayout; @@ -1072,7 +1068,7 @@ private ByteBuffer buildLayout() { long totalRows = 0; for (int c = 0; c < colCount; c++) { - String colName = schema.fieldNames().get(c); + ColumnName colName = schema.fieldNames().get(c); DictColRef ref = dictColRefs.get(colName); if (ref != null) { int dictLayout = buildDictColLayout(fbb, ref); @@ -1147,7 +1143,7 @@ private static byte[] buildDictLayoutMetaBytes(PType codePType) { // ── Global dict helpers ─────────────────────────────────────────────────── private void flushDictColumns() throws IOException { - for (String colName : dictCandidates) { + for (ColumnName colName : dictCandidates) { List chunks = dictBuffers.getOrDefault(colName, List.of()); if (chunks.isEmpty()) { continue; @@ -1162,7 +1158,7 @@ private void flushDictColumns() throws IOException { } } - private void writeGlobalDictColumn(String colName, DType.Primitive dtype, List chunks) + private void writeGlobalDictColumn(ColumnName colName, DType.Primitive dtype, List chunks) throws IOException { PType ptype = dtype.ptype(); @@ -1234,7 +1230,7 @@ private void writeGlobalDictColumn(String colName, DType.Primitive dtype, List chunks) + private void writeGlobalDictUtf8Column(ColumnName colName, DType.Utf8 dtype, List chunks) throws IOException { // Build global string -> code map across all chunks (insertion order = code value). var valueMap = new LinkedHashMap(); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/BitpackedEncodingTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/BitpackedEncodingTest.java index d8846576..7a31b23c 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/BitpackedEncodingTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/BitpackedEncodingTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.writer.encode.BitpackedEncodingEncoder; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -23,7 +24,7 @@ class BitpackedEncodingTest { private static final DType.Struct I32_SCHEMA = new DType.Struct( - List.of("value"), + List.of(ColumnName.of("value")), List.of(DType.I32), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/ChunkImplTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/ChunkImplTest.java index 816af945..f45bdd07 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/ChunkImplTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/ChunkImplTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.writer.encode.NullableData; @@ -15,7 +16,7 @@ class ChunkImplTest { private static DType.Struct schema(DType dtype) { - return new DType.Struct(List.of("c"), List.of(dtype), false); + return new DType.Struct(List.of(ColumnName.of("c")), List.of(dtype), false); } private static DType prim(PType ptype, boolean nullable) { @@ -24,7 +25,7 @@ private static DType prim(PType ptype, boolean nullable) { private static Object putGet(DType dtype, Object value) { ChunkImpl sut = new ChunkImpl(schema(dtype)); - sut.put("c", value); + sut.put(ColumnName.of("c"), value); return sut.finish().get("c"); } @@ -37,7 +38,7 @@ void unknownColumnRejected() { ChunkImpl sut = new ChunkImpl(schema(prim(PType.I32, false))); // When / Then - assertThatThrownBy(() -> sut.put("nope", new int[]{1})) + assertThatThrownBy(() -> sut.put(ColumnName.of("nope"), new int[]{1})) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("unknown column"); } @@ -45,10 +46,10 @@ void unknownColumnRejected() { void duplicatePutRejected() { // Given ChunkImpl sut = new ChunkImpl(schema(prim(PType.I32, false))); - sut.put("c", new int[]{1}); + sut.put(ColumnName.of("c"), new int[]{1}); // When / Then - assertThatThrownBy(() -> sut.put("c", new int[]{2})) + assertThatThrownBy(() -> sut.put(ColumnName.of("c"), new int[]{2})) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("duplicate"); } @@ -58,7 +59,7 @@ void putReturnsSelfForChaining() { ChunkImpl sut = new ChunkImpl(schema(prim(PType.I32, false))); // When - Chunk result = sut.put("c", new int[]{1}); + Chunk result = sut.put(ColumnName.of("c"), new int[]{1}); // Then assertThat(result).isSameAs(sut); @@ -68,8 +69,8 @@ void putReturnsSelfForChaining() { void finishRejectsMissingColumn() { // Given — schema has two columns, only one put ChunkImpl sut = new ChunkImpl(new DType.Struct( - List.of("a", "b"), List.of(prim(PType.I32, false), prim(PType.I32, false)), false)); - sut.put("a", new int[]{1}); + List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(prim(PType.I32, false), prim(PType.I32, false)), false)); + sut.put(ColumnName.of("a"), new int[]{1}); // When / Then assertThatThrownBy(sut::finish) @@ -81,7 +82,7 @@ void finishReturnsAllColumns() { // Given ChunkImpl sut = new ChunkImpl(schema(prim(PType.I32, false))); int[] col = {1, 2, 3}; - sut.put("c", col); + sut.put(ColumnName.of("c"), col); // When Map result = sut.finish(); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/ColumnZoneStatsTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/ColumnZoneStatsTest.java index 120df7a5..1290a7ab 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/ColumnZoneStatsTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/ColumnZoneStatsTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ArrayStats; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -24,12 +25,12 @@ class ColumnZoneStatsTest { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("id"), + List.of(ColumnName.of("id")), List.of(DType.I64), false); private static final DType.Struct F64_SCHEMA = new DType.Struct( - List.of("v"), + List.of(ColumnName.of("v")), List.of(DType.F64), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/DeltaEncodingTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/DeltaEncodingTest.java index 6d0ca85b..00960cf0 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/DeltaEncodingTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/DeltaEncodingTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.writer.encode.DeltaEncodingEncoder; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -22,7 +23,7 @@ class DeltaEncodingTest { private static final DType.Struct I64_SCHEMA = new DType.Struct( - List.of("ts"), + List.of(ColumnName.of("ts")), List.of(DType.I64), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/DictEncodingTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/DictEncodingTest.java index 99f3182c..7e5b9d7e 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/DictEncodingTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/DictEncodingTest.java @@ -1,6 +1,7 @@ package io.github.dfa1.vortex.writer; import io.github.dfa1.vortex.core.io.VortexFormat; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.array.Array; import io.github.dfa1.vortex.writer.encode.DictEncodingEncoder; @@ -25,7 +26,7 @@ class DictEncodingTest { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("category"), + List.of(ColumnName.of("category")), List.of(DType.I32), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictF64Test.java b/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictF64Test.java index 68cc3c1e..5bee6b9d 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictF64Test.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictF64Test.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.VortexReader; @@ -24,7 +25,7 @@ class GlobalDictF64Test { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("rate"), + List.of(ColumnName.of("rate")), List.of(DType.F64), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictPrimitiveTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictPrimitiveTest.java index 5f03c688..c9b29100 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictPrimitiveTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictPrimitiveTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.VortexReader; @@ -25,7 +26,7 @@ class GlobalDictPrimitiveTest { private static DType.Struct i64Schema() { - return new DType.Struct(List.of("v"), List.of(DType.I64), false); + return new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.I64), false); } private static long[] writeI64(Path file, long[][] chunks, WriteOptions options) throws IOException { @@ -149,7 +150,7 @@ void lowCardinality_i32_usesGlobalDict(@TempDir Path tmp) throws IOException { // Given — a low-cardinality I32 column drives the global dict build through the int[] // unique-array and codes paths (the narrower I8/I16 carriers are NOT round-tripped here: // the reader's lazy dict rejects them — see lowCardinality_i16_globalDict_readerRejects). - var schema = new DType.Struct(List.of("v"), List.of(DType.I32), false); + var schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.I32), false); int[] data = {10, 20, 30, 10, 20, 30, 10, 20}; Path file = tmp.resolve("i32.vortex"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -174,7 +175,7 @@ void lowCardinality_i16_notDicted_roundTrips(@TempDir Path tmp) throws IOExcepti // excluded from dict candidacy — matching the Rust compressor, which does not dict narrow // ints (RustWritesJavaReadsIntegrationTest#jniWriter_javaReader_lowCardinalityI16) — so a // low-card I16 column encodes via the cascade and round-trips cleanly. - var schema = new DType.Struct(List.of("v"), List.of(DType.I16), false); + var schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.I16), false); short[] data = {1, 2, 3, 1, 2, 3, 1, 2}; Path file = tmp.resolve("i16.vortex"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -207,7 +208,7 @@ void lowCardinality_i16_notDicted_roundTrips(@TempDir Path tmp) throws IOExcepti void lowCardinality_f64_usesGlobalDict(@TempDir Path tmp) throws IOException { // Given — F64 is admitted to the dict path (unlike F16/F32); a low-card float column must // round-trip through the float dict build. - var schema = new DType.Struct(List.of("v"), List.of(DType.F64), false); + var schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.F64), false); double[] dictVals = {1.5, 2.5, 3.5}; int rows = 900; double[] data = new double[rows]; diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictUtf8Test.java b/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictUtf8Test.java index 1fb94d34..2004ed4e 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictUtf8Test.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/GlobalDictUtf8Test.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.VortexReader; @@ -22,7 +23,7 @@ class GlobalDictUtf8Test { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("status"), + List.of(ColumnName.of("status")), List.of(DType.UTF8), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/MultiChunkUtf8RoundTripTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/MultiChunkUtf8RoundTripTest.java index eff37234..401d3a1d 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/MultiChunkUtf8RoundTripTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/MultiChunkUtf8RoundTripTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.Chunk; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -28,7 +29,7 @@ class MultiChunkUtf8RoundTripTest { private static final DType.Struct STRING_SCHEMA = new DType.Struct( - List.of("s"), List.of(DType.UTF8), false); + List.of(ColumnName.of("s")), List.of(DType.UTF8), false); @Test void manyBatchesUtf8WithCascading_columnIsChunkedMode(@TempDir Path tmp) throws IOException { diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/NullCountPruningTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/NullCountPruningTest.java index 5d6707b9..994c8e46 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/NullCountPruningTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/NullCountPruningTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.reader.RowFilter; @@ -26,7 +27,7 @@ class NullCountPruningTest { Path tmp; private static final DType.Struct SCHEMA = new DType.Struct( - List.of("v"), List.of(new DType.Primitive(PType.I64, true)), false); + List.of(ColumnName.of("v")), List.of(new DType.Primitive(PType.I64, true)), false); // chunkSize large so each writeChunk is exactly one chunk (one zone). Three chunks of distinct // sizes and null patterns: 3 rows / 0 nulls, 2 rows / 1 null, 4 rows / all null. @@ -88,7 +89,7 @@ void columnStats_sumsNullCountAcrossChunks() throws IOException { // When Long result; try (VortexReader reader = VortexReader.open(file)) { - result = reader.columnStats().get("v").nullCount(); + result = reader.columnStats().get(ColumnName.of("v")).nullCount(); } // Then diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/TypedChunkBuilderTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/TypedChunkBuilderTest.java index bb01b886..f87a1165 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/TypedChunkBuilderTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/TypedChunkBuilderTest.java @@ -1,5 +1,7 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; + import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.VortexReader; @@ -36,9 +38,9 @@ void writeChunk_typed_roundTrips(@TempDir Path tmp) throws IOException { var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When sut.writeChunk(c -> c - .put("timestamp", new long[]{1_700_000_000_000L, 1_700_000_001_000L}) - .put("symbol", new String[]{"AAPL", "AAPL"}) - .put("price", new double[]{189.95, 190.10})); + .put(ColumnName.of("timestamp"), new long[]{1_700_000_000_000L, 1_700_000_001_000L}) + .put(ColumnName.of("symbol"), new String[]{"AAPL", "AAPL"}) + .put(ColumnName.of("price"), new double[]{189.95, 190.10})); } // Then — file is readable and values round-trip @@ -58,7 +60,7 @@ void put_unknownColumn_throws(@TempDir Path tmp) throws IOException { try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When / Then - assertThatThrownBy(() -> sut.writeChunk(c -> c.put("nope", new long[]{1}))) + assertThatThrownBy(() -> sut.writeChunk(c -> c.put(ColumnName.of("nope"), new long[]{1}))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("unknown column: nope"); } @@ -71,7 +73,7 @@ void put_wrongArrayType_throws(@TempDir Path tmp) throws IOException { try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When / Then — int[] for I64 column - assertThatThrownBy(() -> sut.writeChunk(c -> c.put("timestamp", new int[]{1, 2}))) + assertThatThrownBy(() -> sut.writeChunk(c -> c.put(ColumnName.of("timestamp"), new int[]{1, 2}))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("timestamp") .hasMessageContaining("I64"); @@ -86,8 +88,8 @@ void missingColumn_throws_atClose(@TempDir Path tmp) throws IOException { var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When / Then assertThatThrownBy(() -> sut.writeChunk(c -> c - .put("timestamp", new long[]{1L}) - .put("symbol", new String[]{"A"}))) + .put(ColumnName.of("timestamp"), new long[]{1L}) + .put(ColumnName.of("symbol"), new String[]{"A"}))) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("missing column: price"); } @@ -104,7 +106,7 @@ void nullable_i64Column_acceptsBoxedArrayWithNulls(@TempDir Path tmp) throws IOE try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); var sut = VortexWriter.create(ch, nullableSchema, WriteOptions.defaults())) { // When - sut.writeChunk(c -> c.put("v", new Long[]{1L, null, 3L})); + sut.writeChunk(c -> c.put(ColumnName.of("v"), new Long[]{1L, null, 3L})); } // Then — file is well-formed; the masked encoding output is verified by the @@ -119,7 +121,7 @@ void nonNullable_i64Column_rejectsBoxedArray(@TempDir Path tmp) throws IOExcepti try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When / Then - assertThatThrownBy(() -> sut.writeChunk(c -> c.put("timestamp", new Long[]{1L, 2L}))) + assertThatThrownBy(() -> sut.writeChunk(c -> c.put(ColumnName.of("timestamp"), new Long[]{1L, 2L}))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("non-nullable") .hasMessageContaining("timestamp"); @@ -134,8 +136,8 @@ void duplicatePut_throws(@TempDir Path tmp) throws IOException { var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When / Then assertThatThrownBy(() -> sut.writeChunk(c -> c - .put("timestamp", new long[]{1L}) - .put("timestamp", new long[]{2L}))) + .put(ColumnName.of("timestamp"), new long[]{1L}) + .put(ColumnName.of("timestamp"), new long[]{2L}))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("duplicate") .hasMessageContaining("timestamp"); @@ -150,9 +152,9 @@ void columnLengthMismatch_throws(@TempDir Path tmp) throws IOException { var sut = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) { // When / Then — timestamp has 2 rows, symbol has 3, price has 2 assertThatThrownBy(() -> sut.writeChunk(c -> c - .put("timestamp", new long[]{1L, 2L}) - .put("symbol", new String[]{"A", "B", "C"}) - .put("price", new double[]{1.0, 2.0}))) + .put(ColumnName.of("timestamp"), new long[]{1L, 2L}) + .put(ColumnName.of("symbol"), new String[]{"A", "B", "C"}) + .put(ColumnName.of("price"), new double[]{1.0, 2.0}))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("symbol") .hasMessageContaining("3 rows") diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/VortexWriterTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/VortexWriterTest.java index 852f6602..e02e809d 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/VortexWriterTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/VortexWriterTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.reader.array.LongArray; @@ -25,7 +26,7 @@ class VortexWriterTest { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("id", "value"), + List.of(ColumnName.of("id"), ColumnName.of("value")), List.of(DType.I64, DType.F64), false); @@ -86,7 +87,7 @@ void create_duplicateFieldNames_throwsIllegalArgumentException(@TempDir Path tmp // schemas ("StructLayout must have unique field names"), so ours must too — otherwise // we emit files the canonical implementation would never produce. var schema = new DType.Struct( - List.of("dup", "dup"), + List.of(ColumnName.of("dup"), ColumnName.of("dup")), List.of(new DType.Primitive(PType.I64, false), new DType.Primitive(PType.I64, false)), false); Path file = tmp.resolve("dup.vtx"); @@ -101,43 +102,31 @@ void create_duplicateFieldNames_throwsIllegalArgumentException(@TempDir Path tmp } @Test - void create_fieldNameWithNulByte_throwsIllegalArgumentException(@TempDir Path tmp) throws IOException { - // Given — a NUL byte inside a field name. Legal in our pure-Java stack, but it aborts - // the reference toolchain's Arrow FFI export (panic-cannot-unwind in arrow-rs, SIGABRT, - // measured 2026-07-04) — so the writer must never emit it. - var schema = new DType.Struct( - List.of("col\u0000hidden"), + void schema_fieldNameWithNulByte_isUnbuildable() { + // Given a NUL byte inside a field name: legal in our pure-Java stack, but it aborts the + // reference toolchain's Arrow FFI export (panic-cannot-unwind in arrow-rs, SIGABRT, + // measured 2026-07-04). With fieldNames typed as ColumnName the schema can no longer even + // be constructed, so the footgun is rejected before it can reach the writer. + // When / Then + assertThatThrownBy(() -> new DType.Struct( + List.of(ColumnName.of("col\u0000hidden")), List.of(new DType.Primitive(PType.I64, false)), - false); - Path file = tmp.resolve("nul.vtx"); - - // When / Then — options hoisted so only the subject call is in the lambda - WriteOptions opts = WriteOptions.defaults(); - try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { - assertThatThrownBy(() -> VortexWriter.create(ch, schema, opts)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("U+0000"); - } + false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("U+0000"); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(strings = {"", " ", " ", "a\nb", "tab\there"}) - void create_footgunFieldName_throwsIllegalArgumentException(String name, @TempDir Path tmp) throws IOException { - // Given — blank and control-character names are wire-legal but footguns: the write-side - // policy is stricter than the format on purpose (like a JSON library refusing to write - // a "" key it could parse). The reader mirrors this (PostscriptParserDTypeGuardsTest). - var schema = new DType.Struct( - List.of(name), + void schema_footgunFieldName_isUnbuildable(String name) { + // Given a blank or control-character field name — wire-legal footguns. With ColumnName- + // typed fieldNames the schema can't hold one; the write-side strictness is now structural. + // When / Then + assertThatThrownBy(() -> new DType.Struct( + List.of(ColumnName.of(name)), List.of(new DType.Primitive(PType.I64, false)), - false); - Path file = tmp.resolve("footgun.vtx"); - - // When / Then — options hoisted so only the subject call is in the lambda - WriteOptions opts = WriteOptions.defaults(); - try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { - assertThatThrownBy(() -> VortexWriter.create(ch, schema, opts)) - .isInstanceOf(IllegalArgumentException.class); - } + false)) + .isInstanceOf(IllegalArgumentException.class); } // ── writeChunk validation ───────────────────────────────────────────────── @@ -147,7 +136,7 @@ void writeChunk_autoroutesExtensionCollectionViaSpecExtension(@TempDir Path tmp) // Given — schema with a vortex.date column; user passes List directly, // expecting the writer to call DateExtension.encodeAll under the hood var dateSchema = new DType.Struct( - List.of("birthdays"), + List.of(ColumnName.of("birthdays")), List.of(io.github.dfa1.vortex.writer.encode.DateExtensionEncoder.INSTANCE.dtype(false)), false); List dates = List.of( @@ -183,7 +172,7 @@ void writeChunk_extensionCollectionColumn_rowCountValidatedAgainstSibling(@TempD // by its element count: if it reported anything else, the two columns would look mismatched // and writeChunk would reject a perfectly valid chunk. var schema = new DType.Struct( - List.of("birthdays", "id"), + List.of(ColumnName.of("birthdays"), ColumnName.of("id")), List.of(io.github.dfa1.vortex.writer.encode.DateExtensionEncoder.INSTANCE.dtype(false), DType.I64), false); @@ -216,7 +205,7 @@ void writeChunk_map_nullablePrimitive_acceptsBoxedArray(@TempDir Path tmp) throw // only the builder accepted them. Both now share ChunkImpl.validateAndAdapt, so the map // form routes the column through nullable → MaskedEncoding. The null round-trip itself is // asserted end-to-end (through the JNI reader) by the integration masked test. - var schema = new DType.Struct(List.of("v"), + var schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(new DType.Primitive(PType.I64, true)), false); Path file = tmp.resolve("nullable_map.vtx"); @@ -253,7 +242,7 @@ void writeChunk_roundTripsTimeExtension(@TempDir Path tmp) throws IOException { // to keep the test focused — TimeExtension tests cover both). DType.Extension timeDtype = io.github.dfa1.vortex.writer.encode.TimeExtensionEncoder.INSTANCE.dtype( io.github.dfa1.vortex.core.model.TimeUnit.Milliseconds, false); - var schema = new DType.Struct(List.of("clock"), List.of(timeDtype), false); + var schema = new DType.Struct(List.of(ColumnName.of("clock")), List.of(timeDtype), false); List times = List.of( java.time.LocalTime.of(0, 0, 0, 0), java.time.LocalTime.of(1, 1, 1, 500_000_000), @@ -282,7 +271,7 @@ void writeChunk_roundTripsTimestampExtension(@TempDir Path tmp) throws IOExcepti // Given — pre-epoch + epoch + future to exercise sign + boundary; ms resolution DType.Extension tsDtype = io.github.dfa1.vortex.writer.encode.TimestampExtensionEncoder.INSTANCE.dtype( io.github.dfa1.vortex.core.model.TimeUnit.Milliseconds, null, false); - var schema = new DType.Struct(List.of("events"), List.of(tsDtype), false); + var schema = new DType.Struct(List.of(ColumnName.of("events")), List.of(tsDtype), false); List instants = List.of( java.time.Instant.ofEpochMilli(-1_500L), java.time.Instant.EPOCH, @@ -310,7 +299,7 @@ void writeChunk_roundTripsTimestampExtension(@TempDir Path tmp) throws IOExcepti void chunkAs_mismatchedDomainType_throws(@TempDir Path tmp) throws IOException { // Given — a vortex.date column on disk, but caller asks for Instant var dateSchema = new DType.Struct( - List.of("birthdays"), + List.of(ColumnName.of("birthdays")), List.of(io.github.dfa1.vortex.writer.encode.DateExtensionEncoder.INSTANCE.dtype(false)), false); Path file = tmp.resolve("dates2.vtx"); @@ -335,7 +324,7 @@ void chunkAs_mismatchedDomainType_throws(@TempDir Path tmp) throws IOException { void writeChunk_roundTripsUuidExtension(@TempDir Path tmp) throws IOException { // Given — UUIDs cover both halves of the 16-byte buffer and a sign-extension edge DType.Extension uuidDtype = io.github.dfa1.vortex.writer.encode.UuidExtensionEncoder.INSTANCE.dtype(false); - var schema = new DType.Struct(List.of("ids"), List.of(uuidDtype), false); + var schema = new DType.Struct(List.of(ColumnName.of("ids")), List.of(uuidDtype), false); List ids = List.of( java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), new java.util.UUID(-1L, -1L), @@ -365,7 +354,7 @@ void writeChunk_cascadeCompressesTimestampExtensionStorage(@TempDir Path tmp) th // FrameOfReference + Bitpacked. Without cascade, storage stays as flat U64. DType.Extension tsDtype = io.github.dfa1.vortex.writer.encode.TimestampExtensionEncoder.INSTANCE.dtype( io.github.dfa1.vortex.core.model.TimeUnit.Milliseconds, null, false); - var schema = new DType.Struct(List.of("events"), List.of(tsDtype), false); + var schema = new DType.Struct(List.of(ColumnName.of("events")), List.of(tsDtype), false); long base = 1_733_000_000_000L; List instants = new ArrayList<>(4096); for (int i = 0; i < 4096; i++) { @@ -400,7 +389,7 @@ private static void writeOne(Path file, DType.Struct schema, Object data, WriteO throws IOException { try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); var sut = VortexWriter.create(ch, schema, options)) { - sut.writeChunk(Map.of(schema.fieldNames().get(0), data)); + sut.writeChunk(Map.of(schema.fieldNames().get(0).value(), data)); } } @@ -572,7 +561,7 @@ void create_withFullRegistry_roundTrips(@TempDir Path tmp) throws IOException { // a deterministic order (WriteRegistry's TreeMap) plus an honest accepts() (composite // encoders like ChunkedEncodingEncoder no longer claim raw primitive dtypes) keep selection // both stable across platforms and correct. - var schema = new DType.Struct(List.of("v"), + var schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.I64), false); Path file = tmp.resolve("registry.vortex"); long[] data = {1L, 2L, 3L, 4L}; diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/WriterZoneMapTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/WriterZoneMapTest.java index ee44d23d..86e77e1e 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/WriterZoneMapTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/WriterZoneMapTest.java @@ -6,6 +6,7 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.core.model.PType; import io.github.dfa1.vortex.reader.layout.Layout; @@ -35,7 +36,7 @@ class WriterZoneMapTest { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("v"), List.of(DType.I64), false); + List.of(ColumnName.of("v")), List.of(DType.I64), false); // Three zones of four rows: [0..3], [4..7], [8..11]. private static Path write(Path tmp, boolean zoneMaps) throws IOException { @@ -137,7 +138,7 @@ void zoneMaps_nullableColumn_recordsPerZoneNullCount(@TempDir Path tmp) throws I // Given a nullable I64 column across two zones of two rows: zone 0 = [10, null], // zone 1 = [null, null] DType.Struct schema = new DType.Struct( - List.of("v"), List.of(new DType.Primitive(PType.I64, true)), false); + List.of(ColumnName.of("v")), List.of(new DType.Primitive(PType.I64, true)), false); WriteOptions opts = new WriteOptions(2, true, 0.90, 0, true, false); Path file = tmp.resolve("nullable.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -174,7 +175,7 @@ void everyPrimitiveType_emitsZonedLayout(PType ptype, @TempDir Path tmp) throws // a plain primitive, not a dict). Every fixed-width primitive carries min/max stats, so // every one gets a vortex.stats layout — exercising each per-ptype stat-column arm. DType.Struct schema = new DType.Struct( - List.of("v"), List.of(new DType.Primitive(ptype, false)), false); + List.of(ColumnName.of("v")), List.of(new DType.Primitive(ptype, false)), false); WriteOptions opts = new WriteOptions(2, true, 0.90, 0, false, false); Path file = tmp.resolve("ptype-" + ptype + ".vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -215,7 +216,7 @@ void chunkWithoutStats_emitsNullCountOnlyZoneMap(@TempDir Path tmp) throws IOExc // dropped (it requires every chunk to carry stats), but NULL_COUNT and SUM are still emitted // — SUM is independent (the empty zone's sum is simply null). DType.Struct schema = new DType.Struct( - List.of("v"), List.of(DType.I64), false); + List.of(ColumnName.of("v")), List.of(DType.I64), false); WriteOptions opts = new WriteOptions(2, true, 0.90, 0, false, false); Path file = tmp.resolve("partial.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -239,7 +240,7 @@ void utf8Column_emitsStringMinMaxZoneMap(@TempDir Path tmp) throws IOException { // zone 1 = ["cherry", "date"]. vortex.varbin records full string min/max per chunk, so // the zone-map carries MAX+MIN+NULL_COUNT (string min/max), not null_count alone. DType.Struct schema = new DType.Struct( - List.of("s"), List.of(DType.UTF8), false); + List.of(ColumnName.of("s")), List.of(DType.UTF8), false); WriteOptions opts = new WriteOptions(2, true, 0.90, 0, false, false); Path file = tmp.resolve("utf8.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -277,7 +278,7 @@ void extensionColumn_emitsStorageMinMaxZoneMap(@TempDir Path tmp) throws IOExcep // storage primitive's min/max, so the zone-map carries MAX+MIN+NULL_COUNT — same as I64. DType ext = new DType.Extension( "test.ext", DType.I64, null, false); - DType.Struct schema = new DType.Struct(List.of("t"), List.of(ext), false); + DType.Struct schema = new DType.Struct(List.of(ColumnName.of("t")), List.of(ext), false); WriteOptions opts = new WriteOptions(2, true, 0.90, 0, false, false); Path file = tmp.resolve("ext.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -314,7 +315,7 @@ void dictColumn_emitsStringMinMaxZoneMapWrappingDict(@TempDir Path tmp) throws I // computed on the logical Utf8 values, independent of the dict encoding: zone 0 = a..b, // zone 1 = a..c → MAX+MIN+NULL_COUNT, with the column wrapped as vortex.stats over the dict. DType.Struct schema = new DType.Struct( - List.of("s"), List.of(DType.UTF8), false); + List.of(ColumnName.of("s")), List.of(DType.UTF8), false); WriteOptions opts = new WriteOptions(6, true, 0.90, 0, true, false); Path file = tmp.resolve("dict.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -351,7 +352,7 @@ void primitiveDictColumn_emitsNumericMinMaxZoneMapWrappingDict(@TempDir Path tmp // Given a low-cardinality I64 column across 2 chunks of 6 with heavy repeats → vortex.dict. // Zone-map min/max are computed on the logical I64 values: zone 0 = 1..2, zone 1 = 1..3. DType.Struct schema = new DType.Struct( - List.of("v"), List.of(DType.I64), false); + List.of(ColumnName.of("v")), List.of(DType.I64), false); WriteOptions opts = new WriteOptions(6, true, 0.90, 0, true, false); Path file = tmp.resolve("primdict.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -391,7 +392,7 @@ void zoneMaps_f64StatsPayloadDecodesPerZoneMinMaxSum(@TempDir Path tmp) throws I // the float stat path — scalarDouble plus the F64 arms of statColumn/sumColumn — is untested: // a decode that returned 0.0 or read the wrong scalar field would slip through. Fractional // .5 values also make a truncating (int) decode observable. - DType.Struct schema = new DType.Struct(List.of("v"), List.of(DType.F64), false); + DType.Struct schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(DType.F64), false); WriteOptions opts = new WriteOptions(4, true, 0.90, 0, true, false); Path file = tmp.resolve("zoned-f64.vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -432,7 +433,7 @@ void zoneMaps_perTypeStatsDecodePerZoneMinMax(PType ptype, @TempDir Path tmp) th // checks the layout is zoned; this pins the decoded per-zone MIN/MAX for the I8/I16/I32 and // F32 statColumn arms (and the f32 scalar field read in scalarDouble), which the I64/F64 // value tests never reach. - DType.Struct schema = new DType.Struct(List.of("v"), List.of(new DType.Primitive(ptype, false)), false); + DType.Struct schema = new DType.Struct(List.of(ColumnName.of("v")), List.of(new DType.Primitive(ptype, false)), false); WriteOptions opts = new WriteOptions(2, true, 0.90, 0, false, false); Path file = tmp.resolve("ptype-stats-" + ptype + ".vtx"); try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); @@ -479,7 +480,7 @@ private static DType.Struct perTypeStatsTableDtype(PType ptype) { default -> new DType.Primitive(PType.F64, true); }; return new DType.Struct( - List.of("max", "max_is_truncated", "min", "min_is_truncated", "sum", "null_count"), + List.of(ColumnName.of("max"), ColumnName.of("max_is_truncated"), ColumnName.of("min"), ColumnName.of("min_is_truncated"), ColumnName.of("sum"), ColumnName.of("null_count")), List.of(minMax, DType.BOOL, minMax, DType.BOOL, sum, new DType.Primitive(PType.U64, true)), false); } @@ -502,7 +503,7 @@ private static Object sample(PType ptype, int base) { private static DType.Struct statsTableDtype() { DType nullableI64 = new DType.Primitive(PType.I64, true); return new DType.Struct( - List.of("max", "max_is_truncated", "min", "min_is_truncated", "null_count"), + List.of(ColumnName.of("max"), ColumnName.of("max_is_truncated"), ColumnName.of("min"), ColumnName.of("min_is_truncated"), ColumnName.of("null_count")), List.of(nullableI64, DType.BOOL, nullableI64, DType.BOOL, new DType.Primitive(PType.U64, true)), false); @@ -512,7 +513,7 @@ private static DType.Struct statsTableDtype() { private static DType.Struct numericStatsTableDtype() { DType nullableI64 = new DType.Primitive(PType.I64, true); return new DType.Struct( - List.of("max", "max_is_truncated", "min", "min_is_truncated", "sum", "null_count"), + List.of(ColumnName.of("max"), ColumnName.of("max_is_truncated"), ColumnName.of("min"), ColumnName.of("min_is_truncated"), ColumnName.of("sum"), ColumnName.of("null_count")), List.of(nullableI64, DType.BOOL, nullableI64, DType.BOOL, nullableI64, new DType.Primitive(PType.U64, true)), false); @@ -522,7 +523,7 @@ private static DType.Struct numericStatsTableDtype() { private static DType.Struct f64StatsTableDtype() { DType nullableF64 = new DType.Primitive(PType.F64, true); return new DType.Struct( - List.of("max", "max_is_truncated", "min", "min_is_truncated", "sum", "null_count"), + List.of(ColumnName.of("max"), ColumnName.of("max_is_truncated"), ColumnName.of("min"), ColumnName.of("min_is_truncated"), ColumnName.of("sum"), ColumnName.of("null_count")), List.of(nullableF64, DType.BOOL, nullableF64, DType.BOOL, nullableF64, new DType.Primitive(PType.U64, true)), false); @@ -532,7 +533,7 @@ private static DType.Struct f64StatsTableDtype() { private static DType.Struct utf8StatsTableDtype() { DType nullableUtf8 = new DType.Utf8(true); return new DType.Struct( - List.of("max", "max_is_truncated", "min", "min_is_truncated", "null_count"), + List.of(ColumnName.of("max"), ColumnName.of("max_is_truncated"), ColumnName.of("min"), ColumnName.of("min_is_truncated"), ColumnName.of("null_count")), List.of(nullableUtf8, DType.BOOL, nullableUtf8, DType.BOOL, new DType.Primitive(PType.U64, true)), false); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneMapPruningTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneMapPruningTest.java index ab2276db..258c7cad 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneMapPruningTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneMapPruningTest.java @@ -1,6 +1,7 @@ package io.github.dfa1.vortex.writer; import io.github.dfa1.vortex.core.error.VortexException; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.Chunk; import io.github.dfa1.vortex.reader.ReadRegistry; @@ -37,12 +38,12 @@ class ZoneMapPruningTest { private static final DType.Struct SCHEMA = new DType.Struct( - List.of("id"), + List.of(ColumnName.of("id")), List.of(DType.I64), false); private static final DType.Struct U64_SCHEMA = new DType.Struct( - List.of("id"), + List.of(ColumnName.of("id")), List.of(DType.U64), false); @@ -94,7 +95,7 @@ private static ReadRegistry registry() { } private static final DType.Struct F32_SCHEMA = new DType.Struct( - List.of("v"), + List.of(ColumnName.of("v")), List.of(DType.F32), false); @@ -455,7 +456,7 @@ class PredicateLevelPruning { void between_prunesChunksDisjointFromRange(@TempDir Path tmp) throws IOException { // Given — [40,60] overlaps chunks 1 (40..50) and 2 (51..60); chunk 3 is disjoint Path file = writeThreeChunks(tmp); - RowFilter filter = new RowFilter.Column("id", new Predicate.Between(40L, 60L)); + RowFilter filter = new RowFilter.Column(ColumnName.of("id"), new Predicate.Between(40L, 60L)); // When / Then — survivors match a brute-force scan (chunk 3 pruned, never wrongly) assertPrunesLikeOracle(file, filter, v -> v >= 40 && v <= 60); @@ -465,7 +466,7 @@ void between_prunesChunksDisjointFromRange(@TempDir Path tmp) throws IOException void between_disjointFromEveryChunk_prunesAll(@TempDir Path tmp) throws IOException { // Given — [200,300] lies above every chunk's max Path file = writeThreeChunks(tmp); - RowFilter filter = new RowFilter.Column("id", new Predicate.Between(200L, 300L)); + RowFilter filter = new RowFilter.Column(ColumnName.of("id"), new Predicate.Between(200L, 300L)); // When / Then — no chunk can match, so all are pruned assertPrunesLikeOracle(file, filter, v -> v >= 200 && v <= 300); @@ -475,7 +476,7 @@ void between_disjointFromEveryChunk_prunesAll(@TempDir Path tmp) throws IOExcept void or_prunesOnlyChunksDisjointFromBothSides(@TempDir Path tmp) throws IOException { // Given — (id<40 OR id>120): chunk 1 matches the left, chunk 3 the right, chunk 2 neither Path file = writeThreeChunks(tmp); - RowFilter filter = new RowFilter.Column("id", + RowFilter filter = new RowFilter.Column(ColumnName.of("id"), new Predicate.Or(new Predicate.Lt(40L), new Predicate.Gt(120L))); // When / Then — only the middle chunk (no row < 40 or > 120) is pruned diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneReducerTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneReducerTest.java index dd30171b..bd44a816 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneReducerTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/ZoneReducerTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.VortexReader; @@ -22,10 +23,10 @@ class ZoneReducerTest { private static final DType.Struct I64_SCHEMA = new DType.Struct( - List.of("id"), List.of(DType.I64), false); + List.of(ColumnName.of("id")), List.of(DType.I64), false); private static final DType.Struct F64_SCHEMA = new DType.Struct( - List.of("v"), List.of(DType.F64), false); + List.of(ColumnName.of("v")), List.of(DType.F64), false); private static ReadRegistry registry() { return ReadRegistry.builder().registerServiceLoaded().build(); diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/encode/StructEncodingEncoderTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/encode/StructEncodingEncoderTest.java index 8140c9ed..081f73df 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/encode/StructEncodingEncoderTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/encode/StructEncodingEncoderTest.java @@ -1,5 +1,6 @@ package io.github.dfa1.vortex.writer.encode; +import io.github.dfa1.vortex.core.model.ColumnName; import io.github.dfa1.vortex.core.model.DType; import io.github.dfa1.vortex.reader.array.Array; import io.github.dfa1.vortex.reader.array.LongArray; @@ -46,7 +47,7 @@ class Encode { @Test void accepts_structDtype_trueForStruct_falseForPrimitive() { // Given - DType.Struct structDtype = new DType.Struct(List.of("x"), List.of(DTypes.I64), false); + DType.Struct structDtype = new DType.Struct(List.of(ColumnName.of("x")), List.of(DTypes.I64), false); // When // Then @@ -60,7 +61,7 @@ void roundTrip_twoI64Fields_preservesValues() { long[] ids = {1L, 2L, 3L}; long[] values = {10L, 20L, 30L}; DType.Struct dtype = new DType.Struct( - List.of("id", "value"), List.of(DTypes.I64, DTypes.I64), false); + List.of(ColumnName.of("id"), ColumnName.of("value")), List.of(DTypes.I64, DTypes.I64), false); StructData data = new StructData(List.of(ids, values)); // When @@ -85,7 +86,7 @@ void roundTrip_twoI64Fields_preservesValues() { void singleField_encodeResult_hasOneChildAndNoBuffers() { // Given long[] data = {7L, 14L, 21L}; - DType.Struct dtype = new DType.Struct(List.of("v"), List.of(DTypes.I64), false); + DType.Struct dtype = new DType.Struct(List.of(ColumnName.of("v")), List.of(DTypes.I64), false); // When EncodeResult result = ENCODER.encode(dtype, new StructData(List.of(data)), EncodeTestHelper.testCtx()); @@ -100,7 +101,7 @@ void singleField_encodeResult_hasOneChildAndNoBuffers() { @Test void fieldCountMismatch_throwsVortexException() { // Given - DType.Struct dtype = new DType.Struct(List.of("a", "b"), List.of(DTypes.I64, DTypes.I64), false); + DType.Struct dtype = new DType.Struct(List.of(ColumnName.of("a"), ColumnName.of("b")), List.of(DTypes.I64, DTypes.I64), false); StructData data = new StructData(List.of(new long[]{1L})); // When