Skip to content

Commit 5ea5817

Browse files
dfa1claude
andcommitted
feat(decimal): implement encode for vortex.decimal and vortex.decimal_byte_parts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 729c285 commit 5ea5817

5 files changed

Lines changed: 266 additions & 4 deletions

File tree

TODO.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@
9696
| `vortex.chunked` | — | ❌ | ❌ | medium | unblocks `chunked.vortex` (segment-level chunked array) |
9797
| `fastlanes.rle` | — | ❌ | ❌ | medium | unblocks `rle.vortex` |
9898
| `vortex.alprd` | — | ❌ | ❌ | medium | unblocks `alprd.vortex` |
99-
| `vortex.decimal` | `DecimalEncoding` | ✅ | ❌ stub | — | Decimal |
100-
| `vortex.decimal_byte_parts` | `DecimalBytePartsEncoding` | ✅ | ❌ stub | — | Decimal byte parts |
99+
| `vortex.decimal` | `DecimalEncoding` | ✅ | | — | Decimal |
100+
| `vortex.decimal_byte_parts` | `DecimalBytePartsEncoding` | ✅ | | — | Decimal byte parts |
101101
| `vortex.datetimeparts` | `DateTimePartsEncoding` | ✅ | ❌ stub | — | Timestamp parts |
102102
| `vortex.list` | — | ❌ | ❌ | hard | needs list array model; unblocks `list.vortex` |
103103
| `vortex.listview` | — | ❌ | ❌ | hard | unblocks `listview.vortex` |

core/src/main/java/io/github/dfa1/vortex/encoding/DecimalBytePartsEncoding.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import java.lang.foreign.MemorySegment;
1212
import java.nio.ByteBuffer;
13+
import java.util.List;
1314

1415
/// Decoder for {@code vortex.decimal_byte_parts} — decimal split into MSP + LSP children.
1516
///
@@ -35,14 +36,34 @@ public boolean accepts(DType dtype) {
3536

3637
@Override
3738
public EncodeResult encode(DType dtype, Object data) {
38-
throw new UnsupportedOperationException("encode not yet implemented for " + encodingId());
39+
return Encoder.encode((DType.Decimal) dtype, (long[]) data);
3940
}
4041

4142
@Override
4243
public Array decode(DecodeContext ctx) {
4344
return Decoder.decode(ctx);
4445
}
4546

47+
private static final class Encoder {
48+
49+
static EncodeResult encode(DType.Decimal dtype, long[] data) {
50+
DType mspDtype = new DType.Primitive(PType.I64, dtype.nullable());
51+
EncodeResult mspResult = new PrimitiveEncoding().encode(mspDtype, data);
52+
53+
EncodingProtos.DecimalBytePartsMetadata proto = EncodingProtos.DecimalBytePartsMetadata.newBuilder()
54+
.setZerothChildPtype(
55+
io.github.dfa1.vortex.proto.DTypeProtos.PType.forNumber(PType.I64.ordinal()))
56+
.setLowerPartCount(0)
57+
.build();
58+
ByteBuffer metaBuf = ByteBuffer.wrap(proto.toByteArray());
59+
60+
EncodeNode mspNode = EncodeNode.remapBufferIndices(mspResult.rootNode(), 0);
61+
EncodeNode root = new EncodeNode(
62+
EncodingId.VORTEX_DECIMAL_BYTE_PARTS, metaBuf, new EncodeNode[]{mspNode}, new int[]{});
63+
return new EncodeResult(root, List.copyOf(mspResult.buffers()), null, null);
64+
}
65+
}
66+
4667
private static final class Decoder {
4768

4869
private static Array decode(DecodeContext ctx) {

core/src/main/java/io/github/dfa1/vortex/encoding/DecimalEncoding.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.lang.foreign.MemorySegment;
1111
import java.nio.ByteBuffer;
12+
import java.util.List;
1213

1314
/// Decoder for {@code vortex.decimal} — canonical flat decimal storage.
1415
///
@@ -33,14 +34,62 @@ public boolean accepts(DType dtype) {
3334

3435
@Override
3536
public EncodeResult encode(DType dtype, Object data) {
36-
throw new UnsupportedOperationException("encode not yet implemented for " + encodingId());
37+
return Encoder.encode((DType.Decimal) dtype, (MemorySegment) data);
3738
}
3839

3940
@Override
4041
public Array decode(DecodeContext ctx) {
4142
return Decoder.decode(ctx);
4243
}
4344

45+
private static final class Encoder {
46+
47+
static EncodeResult encode(DType.Decimal dtype, MemorySegment data) {
48+
int valuesType = valuesType(dtype.precision());
49+
int bw = byteWidth(valuesType);
50+
if (data.byteSize() % bw != 0) {
51+
throw new VortexException(EncodingId.VORTEX_DECIMAL,
52+
"buffer size %d not multiple of byteWidth %d".formatted(data.byteSize(), bw));
53+
}
54+
ByteBuffer metaBuf = ByteBuffer.wrap(
55+
EncodingProtos.DecimalMetadata.newBuilder().setValuesType(valuesType).build().toByteArray());
56+
EncodeNode node = new EncodeNode(EncodingId.VORTEX_DECIMAL, metaBuf, new EncodeNode[0], new int[]{0});
57+
return new EncodeResult(node, List.of(data), null, null);
58+
}
59+
60+
private static int valuesType(byte precision) {
61+
if (precision <= 2) {
62+
return 0;
63+
}
64+
if (precision <= 4) {
65+
return 1;
66+
}
67+
if (precision <= 9) {
68+
return 2;
69+
}
70+
if (precision <= 18) {
71+
return 3;
72+
}
73+
if (precision <= 38) {
74+
return 4;
75+
}
76+
return 5;
77+
}
78+
79+
private static int byteWidth(int valuesType) {
80+
return switch (valuesType) {
81+
case 0 -> 1;
82+
case 1 -> 2;
83+
case 2 -> 4;
84+
case 3 -> 8;
85+
case 4 -> 16;
86+
case 5 -> 32;
87+
default -> throw new VortexException(EncodingId.VORTEX_DECIMAL,
88+
"unknown valuesType: " + valuesType);
89+
};
90+
}
91+
}
92+
4493
private static final class Decoder {
4594

4695
private static Array decode(DecodeContext ctx) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.github.dfa1.vortex.encoding;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
import io.github.dfa1.vortex.core.array.Array;
5+
import io.github.dfa1.vortex.proto.EncodingProtos;
6+
import org.junit.jupiter.api.Nested;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.lang.foreign.ValueLayout;
10+
import java.nio.ByteOrder;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class DecimalBytePartsEncodingTest {
15+
16+
@Nested
17+
class Encode {
18+
19+
@Test
20+
void roundTrip_longArray_preservesMspValues() throws Exception {
21+
// Given
22+
long[] values = {1L, -2L, 3L};
23+
DType dtype = new DType.Decimal((byte) 18, (byte) 0, false);
24+
var sut = new DecimalBytePartsEncoding();
25+
EncodingRegistry registry = EncodingRegistry.empty();
26+
registry.register(sut);
27+
registry.register(new PrimitiveEncoding());
28+
29+
// When
30+
EncodeResult encoded = sut.encode(dtype, values);
31+
DecodeContext ctx = EncodeTestHelper.toDecodeContext(encoded, values.length, dtype, registry);
32+
Array result = sut.decode(ctx);
33+
34+
// Then
35+
assertThat(result.length()).isEqualTo(values.length);
36+
Array msp = result.child(0);
37+
assertThat(msp.length()).isEqualTo(values.length);
38+
var le = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
39+
for (int i = 0; i < values.length; i++) {
40+
assertThat(msp.buffer(0).get(le, (long) i * 8)).isEqualTo(values[i]);
41+
}
42+
}
43+
44+
@Test
45+
void encodeNode_hasNoBuffers_andOneMspChild() {
46+
// Given
47+
long[] values = {10L, 20L};
48+
DType dtype = new DType.Decimal((byte) 18, (byte) 0, false);
49+
var sut = new DecimalBytePartsEncoding();
50+
51+
// When
52+
EncodeResult result = sut.encode(dtype, values);
53+
54+
// Then
55+
assertThat(result.rootNode().bufferIndices()).isEmpty();
56+
assertThat(result.rootNode().children()).hasSize(1);
57+
assertThat(result.buffers()).hasSize(1);
58+
}
59+
60+
@Test
61+
void metadata_zerothChildPtype_isI64_lowerPartCountIsZero() throws Exception {
62+
// Given
63+
long[] values = {42L};
64+
DType dtype = new DType.Decimal((byte) 18, (byte) 0, false);
65+
var sut = new DecimalBytePartsEncoding();
66+
67+
// When
68+
EncodeResult result = sut.encode(dtype, values);
69+
70+
// Then
71+
byte[] metaBytes = new byte[result.rootNode().metadata().remaining()];
72+
result.rootNode().metadata().duplicate().get(metaBytes);
73+
EncodingProtos.DecimalBytePartsMetadata meta =
74+
EncodingProtos.DecimalBytePartsMetadata.parseFrom(metaBytes);
75+
assertThat(meta.getZerothChildPtypeValue()).isEqualTo(7); // I64 ordinal
76+
assertThat(meta.getLowerPartCount()).isEqualTo(0);
77+
}
78+
}
79+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package io.github.dfa1.vortex.encoding;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
import io.github.dfa1.vortex.core.PType;
5+
import io.github.dfa1.vortex.core.VortexException;
6+
import io.github.dfa1.vortex.core.array.Array;
7+
import io.github.dfa1.vortex.proto.EncodingProtos;
8+
import org.junit.jupiter.api.Nested;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.CsvSource;
12+
13+
import java.lang.foreign.Arena;
14+
import java.lang.foreign.MemorySegment;
15+
import java.lang.foreign.ValueLayout;
16+
import java.nio.ByteOrder;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
class DecimalEncodingTest {
22+
23+
@Nested
24+
class Encode {
25+
26+
@Test
27+
void roundTrip_i64Precision_preservesBuffer() throws Exception {
28+
// Given
29+
long[] values = {100L, -200L, 300L};
30+
var le = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
31+
MemorySegment input = Arena.ofAuto().allocate((long) values.length * 8, 8);
32+
for (int i = 0; i < values.length; i++) {
33+
input.setAtIndex(le, i, values[i]);
34+
}
35+
DType dtype = new DType.Decimal((byte) 18, (byte) 2, false);
36+
var sut = new DecimalEncoding();
37+
EncodingRegistry registry = EncodingRegistry.empty();
38+
registry.register(sut);
39+
40+
// When
41+
EncodeResult encoded = sut.encode(dtype, input);
42+
DecodeContext ctx = EncodeTestHelper.toDecodeContext(encoded, values.length, dtype, registry);
43+
Array result = sut.decode(ctx);
44+
45+
// Then
46+
assertThat(result.length()).isEqualTo(values.length);
47+
for (int i = 0; i < values.length; i++) {
48+
assertThat(result.buffer(0).get(le, (long) i * 8)).isEqualTo(values[i]);
49+
}
50+
}
51+
52+
@Test
53+
void accepts_decimalDtype_true_primitiveReturnsFalse() {
54+
// Given
55+
var sut = new DecimalEncoding();
56+
57+
// When / Then
58+
assertThat(sut.accepts(new DType.Decimal((byte) 18, (byte) 2, false))).isTrue();
59+
assertThat(sut.accepts(new DType.Primitive(PType.I64, false))).isFalse();
60+
}
61+
62+
@ParameterizedTest(name = "precision={0} → valuesType={1}")
63+
@CsvSource({
64+
"1, 0",
65+
"2, 0",
66+
"3, 1",
67+
"4, 1",
68+
"5, 2",
69+
"9, 2",
70+
"10, 3",
71+
"18, 3",
72+
"19, 4",
73+
"38, 4",
74+
"39, 5",
75+
})
76+
void valuesType_matchesPrecisionBoundaries(int precision, int expectedValuesType) throws Exception {
77+
// Given
78+
int byteWidth = switch (expectedValuesType) {
79+
case 0 -> 1;
80+
case 1 -> 2;
81+
case 2 -> 4;
82+
case 3 -> 8;
83+
case 4 -> 16;
84+
default -> 32;
85+
};
86+
MemorySegment input = Arena.ofAuto().allocate(byteWidth);
87+
DType dtype = new DType.Decimal((byte) precision, (byte) 0, false);
88+
var sut = new DecimalEncoding();
89+
90+
// When
91+
EncodeResult encoded = sut.encode(dtype, input);
92+
93+
// Then
94+
byte[] metaBytes = new byte[encoded.rootNode().metadata().remaining()];
95+
encoded.rootNode().metadata().duplicate().get(metaBytes);
96+
EncodingProtos.DecimalMetadata meta = EncodingProtos.DecimalMetadata.parseFrom(metaBytes);
97+
assertThat(meta.getValuesType()).isEqualTo(expectedValuesType);
98+
}
99+
100+
@Test
101+
void invalidBufferSize_throws() {
102+
// Given
103+
MemorySegment input = Arena.ofAuto().allocate(7); // 7 not divisible by 8 (I64)
104+
DType dtype = new DType.Decimal((byte) 18, (byte) 0, false);
105+
var sut = new DecimalEncoding();
106+
107+
// When / Then
108+
assertThatThrownBy(() -> sut.encode(dtype, input))
109+
.isInstanceOf(VortexException.class)
110+
.hasMessageContaining("not multiple of byteWidth");
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)