Skip to content

Commit e82d1d3

Browse files
dfa1claude
andcommitted
fix(calcite): materialise narrow int columns by their real array width (+ coverage)
VortexTable.value() cast I8/U8/I16/U16 columns to IntArray, but the reader decodes them as ByteArray/ShortArray — any TINYINT/SMALLINT scan threw ClassCastException. The OHLC tests only used I64/F64/Utf8, so it slipped through. Dispatch each width to its own array type (all expose getInt with the correct sign/zero extension). Found by VortexAdapterCoverageTest, added here to raise the calcite module's new-code coverage above the Sonar gate: exercises the SQL type mapping and row materialisation across every column type (I8..U64, F32/F64, Utf8, Bool), the VortexSchema lookup (found + unknown), and VortexAggregates (exact Long sum for integer columns, Double for floats). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e41e28a commit e82d1d3

2 files changed

Lines changed: 198 additions & 1 deletion

File tree

calcite/src/main/java/io/github/dfa1/vortex/calcite/VortexTable.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import io.github.dfa1.vortex.reader.ScanOptions;
88
import io.github.dfa1.vortex.reader.VortexReader;
99
import io.github.dfa1.vortex.reader.array.BoolArray;
10+
import io.github.dfa1.vortex.reader.array.ByteArray;
1011
import io.github.dfa1.vortex.reader.array.DoubleArray;
1112
import io.github.dfa1.vortex.reader.array.FloatArray;
1213
import io.github.dfa1.vortex.reader.array.IntArray;
1314
import io.github.dfa1.vortex.reader.array.LongArray;
15+
import io.github.dfa1.vortex.reader.array.ShortArray;
1416
import io.github.dfa1.vortex.reader.array.VarBinArray;
1517

1618
import org.apache.calcite.DataContext;
@@ -269,7 +271,11 @@ private static Object value(Object array, DType type, long r) {
269271
case F64 -> ((DoubleArray) array).getDouble(r);
270272
case F32 -> (double) ((FloatArray) array).getFloat(r);
271273
case I64, U64 -> ((LongArray) array).getLong(r);
272-
case I32, U32, I16, U16, I8, U8 -> ((IntArray) array).getInt(r);
274+
// Narrow ints decode to their own array width (Byte/Short/Int), not IntArray —
275+
// each exposes getInt(r) with the correct sign/zero extension.
276+
case I32, U32 -> ((IntArray) array).getInt(r);
277+
case I16, U16 -> ((ShortArray) array).getInt(r);
278+
case I8, U8 -> ((ByteArray) array).getInt(r);
273279
default -> throw new IllegalStateException("unsupported ptype: " + p.ptype());
274280
};
275281
case DType.Utf8 _ -> ((VarBinArray) array).getString(r);
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package io.github.dfa1.vortex.calcite;
2+
3+
import io.github.dfa1.vortex.core.model.DType;
4+
import io.github.dfa1.vortex.reader.ReadRegistry;
5+
import io.github.dfa1.vortex.reader.VortexReader;
6+
import io.github.dfa1.vortex.writer.VortexWriter;
7+
import io.github.dfa1.vortex.writer.WriteOptions;
8+
9+
import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
10+
import org.apache.calcite.linq4j.Enumerator;
11+
import org.apache.calcite.rel.type.RelDataType;
12+
import org.apache.calcite.sql.type.SqlTypeName;
13+
import org.junit.jupiter.api.BeforeAll;
14+
import org.junit.jupiter.api.Nested;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.io.TempDir;
17+
18+
import java.nio.channels.FileChannel;
19+
import java.nio.file.Path;
20+
import java.nio.file.StandardOpenOption;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
27+
28+
/// Coverage for the adapter surface across every column type: SQL type mapping
29+
/// ([VortexTable#getRowType]), row materialisation ([VortexTable] scan + enumerator),
30+
/// [VortexSchema] lookup, and [VortexAggregates].
31+
class VortexAdapterCoverageTest {
32+
33+
// One column per logical type the adapter maps; three rows.
34+
private static final DType.Struct SCHEMA = DType.structBuilder()
35+
.field("i8", DType.I8)
36+
.field("i16", DType.I16)
37+
.field("i32", DType.I32)
38+
.field("i64", DType.I64)
39+
.field("u8", DType.U8)
40+
.field("u16", DType.U16)
41+
.field("u32", DType.U32)
42+
.field("u64", DType.U64)
43+
.field("f32", DType.F32)
44+
.field("f64", DType.F64)
45+
.field("s", DType.UTF8)
46+
.field("b", DType.BOOL)
47+
.build();
48+
49+
@TempDir
50+
static Path tmp;
51+
private static Path file;
52+
53+
@BeforeAll
54+
static void write() throws Exception {
55+
file = tmp.resolve("alltypes.vortex");
56+
try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
57+
var w = VortexWriter.create(ch, SCHEMA, WriteOptions.defaults())) {
58+
w.writeChunk(Map.ofEntries(
59+
Map.entry("i8", new byte[]{1, 2, 3}),
60+
Map.entry("i16", new short[]{10, 20, 30}),
61+
Map.entry("i32", new int[]{100, 200, 300}),
62+
Map.entry("i64", new long[]{1000L, 2000L, 3000L}),
63+
Map.entry("u8", new byte[]{4, 5, 6}),
64+
Map.entry("u16", new short[]{40, 50, 60}),
65+
Map.entry("u32", new int[]{400, 500, 600}),
66+
Map.entry("u64", new long[]{4000L, 5000L, 6000L}),
67+
Map.entry("f32", new float[]{1.5f, 2.5f, 3.5f}),
68+
Map.entry("f64", new double[]{1.25, 2.25, 3.25}),
69+
Map.entry("s", new String[]{"a", "b", "c"}),
70+
Map.entry("b", new boolean[]{true, false, true})));
71+
}
72+
}
73+
74+
private static ReadRegistry registry() {
75+
return ReadRegistry.builder().registerServiceLoaded().build();
76+
}
77+
78+
@Test
79+
void getRowType_mapsEveryColumnToItsSqlType() {
80+
// Given / When
81+
RelDataType rowType = new VortexTable(file).getRowType(new JavaTypeFactoryImpl());
82+
83+
// Then — one SqlTypeName per logical type
84+
Map<String, SqlTypeName> expected = Map.ofEntries(
85+
Map.entry("i8", SqlTypeName.TINYINT), Map.entry("u8", SqlTypeName.TINYINT),
86+
Map.entry("i16", SqlTypeName.SMALLINT), Map.entry("u16", SqlTypeName.SMALLINT),
87+
Map.entry("i32", SqlTypeName.INTEGER), Map.entry("u32", SqlTypeName.INTEGER),
88+
Map.entry("i64", SqlTypeName.BIGINT), Map.entry("u64", SqlTypeName.BIGINT),
89+
Map.entry("f32", SqlTypeName.REAL), Map.entry("f64", SqlTypeName.DOUBLE),
90+
Map.entry("s", SqlTypeName.VARCHAR), Map.entry("b", SqlTypeName.BOOLEAN));
91+
assertThat(rowType.getFieldList()).hasSize(12);
92+
expected.forEach((col, sql) ->
93+
assertThat(rowType.getField(col, false, false).getType().getSqlTypeName()).isEqualTo(sql));
94+
}
95+
96+
@Test
97+
void scan_materialisesEveryColumnToItsJavaType() {
98+
// Given
99+
VortexTable table = new VortexTable(file);
100+
101+
// When — full scan (no filter, all columns)
102+
List<Object[]> rows = new ArrayList<>();
103+
Enumerator<Object[]> en = table.scan(null, List.of(), null).enumerator();
104+
try {
105+
while (en.moveNext()) {
106+
rows.add(en.current());
107+
}
108+
} finally {
109+
en.close();
110+
}
111+
112+
// Then — first row carries the right boxed types and values
113+
assertThat(rows).hasSize(3);
114+
Object[] r0 = rows.getFirst();
115+
// column order matches the schema
116+
assertThat(r0[0]).isEqualTo(1); // i8 -> Integer
117+
assertThat(r0[2]).isEqualTo(100); // i32 -> Integer
118+
assertThat(r0[3]).isEqualTo(1000L); // i64 -> Long
119+
assertThat(r0[7]).isEqualTo(4000L); // u64 -> Long
120+
assertThat(r0[8]).isEqualTo(1.5); // f32 -> Double
121+
assertThat(r0[9]).isEqualTo(1.25); // f64 -> Double
122+
assertThat(r0[10]).isEqualTo("a"); // s -> String
123+
assertThat(r0[11]).isEqualTo(true); // b -> Boolean
124+
}
125+
126+
@Test
127+
void table_totalRowsAndStats() {
128+
// Given / When / Then
129+
VortexTable table = new VortexTable(file);
130+
assertThat(table.totalRows()).isEqualTo(3);
131+
assertThat(table.statsOf("i64")).isNotNull();
132+
}
133+
134+
@Nested
135+
class Schema {
136+
137+
@Test
138+
void table_returnsRegisteredTable() {
139+
// Given
140+
VortexSchema schema = new VortexSchema(Map.of("t", file));
141+
142+
// When / Then
143+
assertThat(schema.table("t")).isNotNull();
144+
}
145+
146+
@Test
147+
void table_unknownName_throws() {
148+
// Given
149+
VortexSchema schema = new VortexSchema(Map.of("t", file));
150+
151+
// When / Then
152+
assertThatThrownBy(() -> schema.table("nope"))
153+
.isInstanceOf(IllegalArgumentException.class)
154+
.hasMessageContaining("nope");
155+
}
156+
}
157+
158+
@Nested
159+
class Aggregates {
160+
161+
@Test
162+
void integerColumn_sumIsExactLong() throws Exception {
163+
// Given / When
164+
try (VortexReader reader = VortexReader.open(file, registry())) {
165+
VortexAggregates.Summary s = VortexAggregates.of(reader, "i64");
166+
167+
// Then — sum stays a Long (not promoted to Double), avg derived, min/max from stats
168+
assertThat(s.sum()).isInstanceOf(Long.class).isEqualTo(6000L);
169+
assertThat(s.count()).isEqualTo(3);
170+
assertThat(s.avg()).isEqualTo(2000.0);
171+
assertThat(((Number) s.min()).longValue()).isEqualTo(1000L);
172+
assertThat(((Number) s.max()).longValue()).isEqualTo(3000L);
173+
assertThat(s.minMaxSource()).isEqualTo(VortexAggregates.Source.ZONE_STATS_PUSHDOWN);
174+
assertThat(s.sumSource()).isEqualTo(VortexAggregates.Source.FULL_SCAN);
175+
}
176+
}
177+
178+
@Test
179+
void floatColumn_sumIsDouble() throws Exception {
180+
// Given / When
181+
try (VortexReader reader = VortexReader.open(file, registry())) {
182+
VortexAggregates.Summary s = VortexAggregates.of(reader, "f64");
183+
184+
// Then
185+
assertThat(s.sum()).isInstanceOf(Double.class);
186+
assertThat(s.sum().doubleValue()).isEqualTo(6.75);
187+
assertThat(s.avg()).isEqualTo(2.25);
188+
}
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)