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 fb913fe7..acede27e 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 @@ -10,6 +10,7 @@ import io.github.dfa1.vortex.reader.array.FloatArray; import io.github.dfa1.vortex.reader.array.GenericArray; import io.github.dfa1.vortex.reader.array.IntArray; +import io.github.dfa1.vortex.reader.array.LazyDecimalBytePartsArray; import io.github.dfa1.vortex.reader.array.LongArray; import io.github.dfa1.vortex.reader.array.ShortArray; import io.github.dfa1.vortex.reader.array.VarBinArray; @@ -867,14 +868,16 @@ private static String formatValue(Array array, int i, DType declared) { ? "\"" + a.getString(i) + "\"" : bytesToShortHex(a.getBytes(i)); case GenericArray a when a.dtype() instanceof DType.Decimal -> - tryDecimal(a, i); + tryDecimal(a::getDecimal, a, i); + case LazyDecimalBytePartsArray a -> tryDecimal(a::getDecimal, a, i); default -> "<" + array.getClass().getSimpleName() + " " + array.dtype() + ">"; }; } - private static String tryDecimal(GenericArray a, int i) { + private static String tryDecimal(java.util.function.LongFunction reader, + Array a, int i) { try { - return a.getDecimal(i).toPlainString(); + return reader.apply(i).toPlainString(); } catch (RuntimeException e) { String msg = e.getMessage(); if (msg != null && msg.contains("null cell")) { 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 8cc7c974..6ee50024 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 @@ -24,6 +24,7 @@ import io.github.dfa1.vortex.reader.array.FloatArray; import io.github.dfa1.vortex.reader.array.GenericArray; import io.github.dfa1.vortex.reader.array.IntArray; +import io.github.dfa1.vortex.reader.array.LazyDecimalBytePartsArray; import io.github.dfa1.vortex.reader.array.LongArray; import io.github.dfa1.vortex.reader.array.MaskedArray; import io.github.dfa1.vortex.reader.array.MaterializedBoolArray; @@ -279,6 +280,8 @@ private static Array truncateArray(Array arr, long rows, SegmentAllocator arena) } case EmptyArray a -> a; case GenericArray a -> a.withLength(rows); + case LazyDecimalBytePartsArray a -> + new LazyDecimalBytePartsArray(a.dtype(), rows, truncateArray(a.msp(), rows, arena)); default -> throw new VortexException("limit: truncation not supported for " + arr.getClass().getSimpleName()); }; diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/Array.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/Array.java index 7da200b0..83d11d14 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/array/Array.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/Array.java @@ -9,9 +9,9 @@ /// is tied to the `VortexFile`'s Arena. public sealed interface Array permits BoolArray, ByteArray, DoubleArray, EmptyArray, FixedSizeListArray, Float16Array, - FloatArray, GenericArray, IntArray, ListArray, ListViewArray, LongArray, - MaskedArray, NullArray, ShortArray, StructArray, UnknownArray, VarBinArray, - VariantArray { + FloatArray, GenericArray, IntArray, LazyDecimalBytePartsArray, ListArray, + ListViewArray, LongArray, MaskedArray, NullArray, ShortArray, StructArray, + UnknownArray, VarBinArray, VariantArray { /// Returns the number of elements in this array. /// diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/DecimalBytePartsArrays.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/DecimalBytePartsArrays.java new file mode 100644 index 00000000..7229ed84 --- /dev/null +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/DecimalBytePartsArrays.java @@ -0,0 +1,43 @@ +package io.github.dfa1.vortex.reader.array; + +import io.github.dfa1.vortex.core.VortexException; + +import java.math.BigInteger; + +/// Package-private helper for the {@link LazyDecimalBytePartsArray} record. +/// +/// `vortex.decimal_byte_parts` with `lower_part_count == 0` stores the +/// decimal mantissa as a single signed-integer child column whose ptype the +/// encoder picks (one of `i8 / i16 / i32 / i64`). The child may be wrapped +/// in {@link MaskedArray} for nullable columns. {@link #readMantissa(Array, long)} +/// centralises the per-row dispatch so the record itself stays compact. +final class DecimalBytePartsArrays { + + private DecimalBytePartsArrays() { + } + + /// Reads `arr[i]` as a signed-magnitude {@link BigInteger} mantissa. + /// Recurses through {@link MaskedArray}; throws on null cells so callers + /// don't silently get a zero mantissa for invalid rows. + /// + /// @param arr source typed Array (must be one of Byte/Short/Int/Long, optionally MaskedArray-wrapped) + /// @param i row index + /// @return cell value as a {@link BigInteger} + /// @throws VortexException for null cells or unsupported array types + static BigInteger readMantissa(Array arr, long i) { + return switch (arr) { + case ByteArray a -> BigInteger.valueOf(a.getByte(i)); + case ShortArray a -> BigInteger.valueOf(a.getShort(i)); + case IntArray a -> BigInteger.valueOf(a.getInt(i)); + case LongArray a -> BigInteger.valueOf(a.getLong(i)); + case MaskedArray a -> { + if (!a.isValid(i)) { + throw new VortexException("DecimalByteParts: null cell at index " + i); + } + yield readMantissa(a.inner(), i); + } + default -> throw new VortexException( + "DecimalByteParts: unsupported mantissa child type: " + arr.getClass().getSimpleName()); + }; + } +} diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/GenericArray.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/GenericArray.java index 26350beb..2897db7e 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/array/GenericArray.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/GenericArray.java @@ -76,25 +76,21 @@ MemorySegment buffer(int i) { return buffers[i]; } - /// Decodes the decimal value at row `i`. + /// Decodes the decimal value at row `i` from a single-buffer layout. /// - /// Handles the two shapes produced by Vortex decimal decoders: + /// The buffer holds one little-endian two's-complement integer per row. Element + /// width is derived from the buffer's byte size divided by {@link #length()}, + /// not from the dtype's precision — `vortex.decimal` writes whatever width + /// the encoder chose in its `valuesType` metadata, which can be narrower + /// than the precision alone would allow. /// - /// - **single-buffer**: one raw buffer of little-endian two's-complement - /// integers (one element per row). Element width is derived from the - /// buffer's byte size divided by {@link #length()}, not from the - /// dtype's precision — `vortex.decimal` writes whatever width - /// the encoder chose in its `valuesType` metadata, which can be - /// narrower than the precision alone would allow. - /// - **child-array**: zero buffers, one child holding the most-significant - /// integer part as a {@link LongArray}, {@link IntArray}, {@link ShortArray}, - /// or {@link ByteArray}. Produced by `vortex.decimal_byte_parts` - /// when `lower_part_count == 0`. + /// The child-array shape produced by `vortex.decimal_byte_parts` is now + /// handled by {@link LazyDecimalBytePartsArray} directly. /// /// @param i row index, `0 <= i < length()` /// @return decoded value as a {@link BigDecimal} with the dtype's scale - /// @throws VortexException if the dtype isn't decimal or the array - /// shape doesn't match either supported layout + /// @throws VortexException if the dtype isn't decimal or the array + /// shape isn't the single-buffer layout /// @throws IndexOutOfBoundsException if `i` is outside `[0, length())` public BigDecimal getDecimal(long i) { if (i < 0 || i >= length) { @@ -103,15 +99,11 @@ public BigDecimal getDecimal(long i) { if (!(dtype instanceof DType.Decimal d)) { throw new VortexException("getDecimal called on non-decimal dtype: " + dtype); } - BigInteger mantissa; - if (buffers.length == 1 && children.length == 0) { - mantissa = readSingleBufferMantissa(buffers[0], length, i); - } else if (buffers.length == 0 && children.length == 1) { - mantissa = mantissaFromChild(children[0], i); - } else { + if (buffers.length != 1 || children.length != 0) { throw new VortexException("getDecimal: unsupported decimal shape buffers=" + buffers.length + " children=" + children.length); } + BigInteger mantissa = readSingleBufferMantissa(buffers[0], length, i); return new BigDecimal(mantissa, d.scale()); } @@ -128,24 +120,6 @@ private static BigInteger readSingleBufferMantissa(MemorySegment buf, long lengt return readSignedLe(buf, i * width, width); } - private static BigInteger mantissaFromChild(Array child, long i) { - return switch (child) { - case LongArray a -> BigInteger.valueOf(a.getLong(i)); - case IntArray a -> BigInteger.valueOf(a.getInt(i)); - case ShortArray a -> BigInteger.valueOf(a.getShort(i)); - case ByteArray a -> BigInteger.valueOf(a.getByte(i)); - case MaskedArray a -> { - if (!a.isValid(i)) { - throw new VortexException("getDecimal: null cell at index " + i); - } - yield mantissaFromChild(a.inner(), i); - } - default -> - throw new VortexException("getDecimal: unsupported mantissa child type " - + child.getClass().getSimpleName()); - }; - } - private static final ValueLayout.OfShort SHORT_LE = ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN); private static final ValueLayout.OfInt INT_LE = diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyDecimalBytePartsArray.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyDecimalBytePartsArray.java new file mode 100644 index 00000000..7ba044bb --- /dev/null +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyDecimalBytePartsArray.java @@ -0,0 +1,39 @@ +package io.github.dfa1.vortex.reader.array; + +import io.github.dfa1.vortex.core.DType; +import io.github.dfa1.vortex.core.VortexException; + +import java.math.BigDecimal; + +/// Lazy `vortex.decimal_byte_parts` reassembly. +/// +/// With `lower_part_count == 0` (the only shape this codebase currently +/// emits or accepts) the encoding stores its mantissa as a single signed-integer +/// child column, paired with the parent's {@link DType.Decimal} precision and +/// scale. {@link #getDecimal(long)} reads one cell from the child via +/// {@link DecimalBytePartsArrays#readMantissa(Array, long)} and combines it with +/// the dtype scale to produce a {@link BigDecimal} on demand — no buffer +/// materialisation occurs at construction time. +/// +/// @param dtype the parent {@link DType.Decimal} dtype (precision + scale + nullable) +/// @param length total logical row count +/// @param msp child array holding the most-significant integer part of the mantissa +public record LazyDecimalBytePartsArray(DType dtype, long length, Array msp) implements Array { + + /// Reads cell `i` as a {@link BigDecimal} with the parent dtype's scale. + /// + /// @param i row index, `0 <= i < length()` + /// @return decoded `BigDecimal` + /// @throws VortexException if the dtype isn't a {@link DType.Decimal} or the + /// mantissa cell is null + /// @throws IndexOutOfBoundsException if `i` is outside `[0, length())` + public BigDecimal getDecimal(long i) { + if (i < 0 || i >= length) { + throw new IndexOutOfBoundsException("index " + i + " out of bounds for length " + length); + } + if (!(dtype instanceof DType.Decimal d)) { + throw new VortexException("LazyDecimalBytePartsArray: non-decimal dtype " + dtype); + } + return new BigDecimal(DecimalBytePartsArrays.readMantissa(msp, i), d.scale()); + } +} diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/decode/DecimalBytePartsEncodingDecoder.java b/reader/src/main/java/io/github/dfa1/vortex/reader/decode/DecimalBytePartsEncodingDecoder.java index 977c5c17..187ec1f2 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/decode/DecimalBytePartsEncodingDecoder.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/decode/DecimalBytePartsEncodingDecoder.java @@ -4,7 +4,7 @@ import io.github.dfa1.vortex.core.PType; import io.github.dfa1.vortex.core.VortexException; import io.github.dfa1.vortex.reader.array.Array; -import io.github.dfa1.vortex.reader.array.GenericArray; +import io.github.dfa1.vortex.reader.array.LazyDecimalBytePartsArray; import io.github.dfa1.vortex.encoding.EncodingId; import io.github.dfa1.vortex.proto.DecimalBytePartsMetadata; @@ -57,7 +57,6 @@ public Array decode(DecodeContext ctx) { mspNode, mspDtype, ctx.rowCount(), ctx.segmentBuffers(), ctx.registry(), ctx.arena()); Array mspArray = ctx.registry().decode(mspCtx); - return new GenericArray(ctx.dtype(), ctx.rowCount(), new MemorySegment[0], - new Array[]{mspArray}); + return new LazyDecimalBytePartsArray(ctx.dtype(), ctx.rowCount(), mspArray); } } diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/array/GenericArrayTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/array/GenericArrayTest.java index 85ac16c0..1602fd9b 100644 --- a/reader/src/test/java/io/github/dfa1/vortex/reader/array/GenericArrayTest.java +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/array/GenericArrayTest.java @@ -122,27 +122,6 @@ void getDecimal_smallPrecisionUsesNarrowerBuffer() { } } - @Test - void getDecimal_childArrayShape_decodesViaMostSignificantPart() { - // Given — the shape vortex.decimal_byte_parts decoders produce when - // lower_part_count == 0: zero buffers, one LongArray child carrying - // the i64 mantissa. - try (Arena arena = Arena.ofConfined()) { - MemorySegment mspBuf = arena.allocate(24); - mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 0, 4321L); - mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 8, -100L); - mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 16, 0L); - LongArray msp = new MaterializedLongArray(new DType.Primitive(PType.I64, false), 3, mspBuf); - DType.Decimal dec = new DType.Decimal((byte) 15, (byte) 2, false); - GenericArray sut = new GenericArray(dec, 3, new MemorySegment[0], new Array[]{msp}); - - // When / Then - assertThat(sut.getDecimal(0)).isEqualByComparingTo(new BigDecimal("43.21")); - assertThat(sut.getDecimal(1)).isEqualByComparingTo(new BigDecimal("-1.00")); - assertThat(sut.getDecimal(2)).isEqualByComparingTo(BigDecimal.ZERO); - } - } - @Test void getDecimal_i128Buffer_decodesWideMantissa() { // Given — decimal(38,4) stores mantissas wider than i64; vortex.decimal @@ -242,35 +221,6 @@ void getDecimal_indexOutOfBounds_throws() { } } - @Test - void getDecimal_nullCellInMaskedChild_throws() { - // Given — mantissa-child shape with a MaskedArray wrapping a LongArray; - // the validity bitmap says index 1 is null. Without the validity check - // the previous code would happily decode whatever bytes sat at that - // slot and return a garbage BigDecimal. - try (Arena arena = Arena.ofConfined()) { - MemorySegment mspBuf = arena.allocate(16); - mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 0, 1234L); - mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 8, 9999L); - LongArray msp = new MaterializedLongArray(new DType.Primitive(PType.I64, false), 2, mspBuf); - - MemorySegment validityBuf = arena.allocate(1); - // bit 0 set = index 0 valid; bit 1 clear = index 1 null - validityBuf.set(ValueLayout.JAVA_BYTE, 0, (byte) 0b0000_0001); - BoolArray validity = new MaterializedBoolArray(new DType.Bool(false), 2, validityBuf); - - MaskedArray masked = new MaskedArray(msp, validity); - DType.Decimal dec = new DType.Decimal((byte) 15, (byte) 2, true); - GenericArray sut = new GenericArray(dec, 2, new MemorySegment[0], new Array[]{masked}); - - // When / Then - assertThat(sut.getDecimal(0)).isEqualByComparingTo(new BigDecimal("12.34")); - assertThatThrownBy(() -> sut.getDecimal(1)) - .isInstanceOf(io.github.dfa1.vortex.core.VortexException.class) - .hasMessageContaining("null cell at index 1"); - } - } - @Test void getDecimal_nonDecimalDtype_throws() { // Given — guards against silently returning garbage on misuse diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyDecimalBytePartsArrayTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyDecimalBytePartsArrayTest.java new file mode 100644 index 00000000..daa4ef72 --- /dev/null +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyDecimalBytePartsArrayTest.java @@ -0,0 +1,124 @@ +package io.github.dfa1.vortex.reader.array; + +import io.github.dfa1.vortex.core.DType; +import io.github.dfa1.vortex.core.PType; +import io.github.dfa1.vortex.core.VortexException; +import org.junit.jupiter.api.Test; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LazyDecimalBytePartsArrayTest { + + @Test + void getDecimal_i64Mantissa_reassemblesAtDtypeScale() { + // Given — vortex.decimal_byte_parts with lower_part_count == 0 stores the + // mantissa as a single signed-integer child. decimal(15, 2) means the i64 + // mantissa 4321 represents 43.21. + try (Arena arena = Arena.ofConfined()) { + MemorySegment mspBuf = arena.allocate(24); + mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 0, 4321L); + mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 8, -100L); + mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 16, 0L); + LongArray msp = new MaterializedLongArray(new DType.Primitive(PType.I64, false), 3, mspBuf); + DType.Decimal dec = new DType.Decimal((byte) 15, (byte) 2, false); + LazyDecimalBytePartsArray sut = new LazyDecimalBytePartsArray(dec, 3, msp); + + // When / Then + assertThat(sut.getDecimal(0)).isEqualByComparingTo(new BigDecimal("43.21")); + assertThat(sut.getDecimal(1)).isEqualByComparingTo(new BigDecimal("-1.00")); + assertThat(sut.getDecimal(2)).isEqualByComparingTo(BigDecimal.ZERO); + } + } + + @Test + void getDecimal_widensFromNarrowerChildPtype() { + // Given — the writer picks the narrowest ptype that fits, so a small-range + // mantissa column may land as i32 (or narrower) even at high precision. + // Without per-row ptype dispatch the reader would mis-decode anything + // narrower than i64. + try (Arena arena = Arena.ofConfined()) { + MemorySegment mspBuf = arena.allocate(12); + mspBuf.set(ValueLayout.JAVA_INT_UNALIGNED, 0, 1234); + mspBuf.set(ValueLayout.JAVA_INT_UNALIGNED, 4, -50); + mspBuf.set(ValueLayout.JAVA_INT_UNALIGNED, 8, 0); + IntArray msp = new MaterializedIntArray(new DType.Primitive(PType.I32, false), 3, mspBuf); + DType.Decimal dec = new DType.Decimal((byte) 15, (byte) 2, false); + LazyDecimalBytePartsArray sut = new LazyDecimalBytePartsArray(dec, 3, msp); + + // When / Then + assertThat(sut.getDecimal(0)).isEqualByComparingTo(new BigDecimal("12.34")); + assertThat(sut.getDecimal(1)).isEqualByComparingTo(new BigDecimal("-0.50")); + assertThat(sut.getDecimal(2)).isEqualByComparingTo(BigDecimal.ZERO); + } + } + + @Test + void getDecimal_nullCellInMaskedChild_throws() { + // Given — nullable decimal columns wrap the mantissa child in MaskedArray. + // Without honouring the validity bitmap, getDecimal would happily return + // a garbage BigDecimal for a row whose mantissa bytes are undefined. + try (Arena arena = Arena.ofConfined()) { + MemorySegment mspBuf = arena.allocate(16); + mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 0, 1234L); + mspBuf.set(ValueLayout.JAVA_LONG_UNALIGNED, 8, 9999L); + LongArray msp = new MaterializedLongArray(new DType.Primitive(PType.I64, false), 2, mspBuf); + + MemorySegment validityBuf = arena.allocate(1); + // bit 0 set = index 0 valid; bit 1 clear = index 1 null + validityBuf.set(ValueLayout.JAVA_BYTE, 0, (byte) 0b0000_0001); + BoolArray validity = new MaterializedBoolArray(new DType.Bool(false), 2, validityBuf); + + MaskedArray masked = new MaskedArray(msp, validity); + DType.Decimal dec = new DType.Decimal((byte) 15, (byte) 2, true); + LazyDecimalBytePartsArray sut = new LazyDecimalBytePartsArray(dec, 2, masked); + + // When / Then + assertThat(sut.getDecimal(0)).isEqualByComparingTo(new BigDecimal("12.34")); + assertThatThrownBy(() -> sut.getDecimal(1)) + .isInstanceOf(VortexException.class) + .hasMessageContaining("null cell at index 1"); + } + } + + @Test + void getDecimal_indexOutOfBounds_throws() { + // Given — explicit bounds check guards against silent garbage reads + try (Arena arena = Arena.ofConfined()) { + MemorySegment mspBuf = arena.allocate(8); + LongArray msp = new MaterializedLongArray(new DType.Primitive(PType.I64, false), 1, mspBuf); + DType.Decimal dec = new DType.Decimal((byte) 4, (byte) 0, false); + LazyDecimalBytePartsArray sut = new LazyDecimalBytePartsArray(dec, 1, msp); + + // When / Then + assertThatThrownBy(() -> sut.getDecimal(-1)) + .isInstanceOf(IndexOutOfBoundsException.class); + assertThatThrownBy(() -> sut.getDecimal(1)) + .isInstanceOf(IndexOutOfBoundsException.class) + .hasMessageContaining("out of bounds"); + assertThatThrownBy(() -> sut.getDecimal(Long.MAX_VALUE)) + .isInstanceOf(IndexOutOfBoundsException.class); + } + } + + @Test + void getDecimal_nonDecimalDtype_throws() { + // Given — guards against constructing the record with the wrong parent dtype + try (Arena arena = Arena.ofConfined()) { + MemorySegment mspBuf = arena.allocate(8); + LongArray msp = new MaterializedLongArray(new DType.Primitive(PType.I64, false), 1, mspBuf); + LazyDecimalBytePartsArray sut = new LazyDecimalBytePartsArray( + new DType.Primitive(PType.I64, false), 1, msp); + + // When / Then + assertThatThrownBy(() -> sut.getDecimal(0)) + .isInstanceOf(VortexException.class) + .hasMessageContaining("non-decimal"); + } + } +} diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/encode/DecimalBytePartsEncodingEncoderTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/encode/DecimalBytePartsEncodingEncoderTest.java index a38cb6bd..c7a66a2d 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/encode/DecimalBytePartsEncodingEncoderTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/encode/DecimalBytePartsEncodingEncoderTest.java @@ -1,12 +1,9 @@ package io.github.dfa1.vortex.writer.encode; import io.github.dfa1.vortex.core.DType; -import io.github.dfa1.vortex.reader.array.Array; -import io.github.dfa1.vortex.reader.array.ArraySegments; -import io.github.dfa1.vortex.reader.array.GenericArray; +import io.github.dfa1.vortex.reader.array.LazyDecimalBytePartsArray; import io.github.dfa1.vortex.reader.decode.DecodeContext; -import io.github.dfa1.vortex.encoding.PTypeIO; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.decode.TestRegistry; import io.github.dfa1.vortex.proto.DecimalBytePartsMetadata; @@ -14,13 +11,17 @@ import io.github.dfa1.vortex.reader.decode.PrimitiveEncodingDecoder; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; + import static org.assertj.core.api.Assertions.assertThat; class DecimalBytePartsEncodingEncoderTest { @Test void roundTrip_longArray_preservesMspValues() { - // Given + // Given — scale=0 means the reassembled BigDecimal equals the raw mantissa, + // so round-tripping the input values verifies both the writer's child + // payload and the lazy reader's reassembly without needing a scale factor. long[] values = {1L, -2L, 3L}; DType dtype = new DType.Decimal((byte) 18, (byte) 0, false); var encoder = new DecimalBytePartsEncodingEncoder(); @@ -30,14 +31,12 @@ void roundTrip_longArray_preservesMspValues() { // When EncodeResult encoded = encoder.encode(dtype, values, EncodeTestHelper.testCtx()); DecodeContext ctx = DecodeTestHelper.toDecodeContext(encoded, values.length, dtype, registry); - GenericArray result = (GenericArray) decoder.decode(ctx); + LazyDecimalBytePartsArray result = (LazyDecimalBytePartsArray) decoder.decode(ctx); // Then assertThat(result.length()).isEqualTo(values.length); - Array msp = result.child(0); - assertThat(msp.length()).isEqualTo(values.length); for (int i = 0; i < values.length; i++) { - assertThat(ArraySegments.of(msp).get(PTypeIO.LE_LONG, (long) i * 8)).isEqualTo(values[i]); + assertThat(result.getDecimal(i)).isEqualByComparingTo(BigDecimal.valueOf(values[i])); } }