Skip to content

Commit e5cf8c6

Browse files
authored
fix: Expose Decimal Literal over Java (#3521)
1 parent 5759eac commit e5cf8c6

File tree

8 files changed

+194
-4
lines changed

8 files changed

+194
-4
lines changed

java/vortex-jni/src/main/java/dev/vortex/api/expressions/Literal.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package dev.vortex.api.expressions;
1717

18-
import com.google.common.base.Objects;
18+
import com.google.common.base.Preconditions;
1919
import dev.vortex.api.Expression;
20+
import java.math.BigDecimal;
21+
import java.util.Objects;
2022
import java.util.Optional;
2123

2224
public abstract class Literal<T> implements Expression {
@@ -44,7 +46,7 @@ public int hashCode() {
4446
public boolean equals(Object o) {
4547
if (!(o instanceof Literal)) return false;
4648
Literal<?> literal = (Literal<?>) o;
47-
return java.util.Objects.equals(value, literal.value);
49+
return Objects.equals(value, literal.value);
4850
}
4951

5052
public static Literal<Void> nullLit() {
@@ -79,6 +81,10 @@ public static Literal<Double> float64(Double value) {
7981
return new Float64Literal(value);
8082
}
8183

84+
public static Literal<BigDecimal> decimal(BigDecimal value, int precision, int scale) {
85+
return new DecimalLiteral(value, precision, scale);
86+
}
87+
8288
public static Literal<String> string(String value) {
8389
return new StringLiteral(value);
8490
}
@@ -165,6 +171,8 @@ public interface LiteralVisitor<U> {
165171

166172
U visitFloat64(Double literal);
167173

174+
U visitDecimal(BigDecimal decimal, int precision, int scale);
175+
168176
U visitString(String literal);
169177

170178
U visitBytes(byte[] literal);
@@ -260,6 +268,25 @@ public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
260268
}
261269
}
262270

271+
static final class DecimalLiteral extends Literal<BigDecimal> {
272+
private final int precision;
273+
private final int scale;
274+
275+
DecimalLiteral(BigDecimal value, int precision, int scale) {
276+
super(value);
277+
if (!Objects.isNull(value)) {
278+
Preconditions.checkArgument(scale == value.scale(), "scale %s ≠ value scale %s", scale, value.scale());
279+
}
280+
this.precision = precision;
281+
this.scale = scale;
282+
}
283+
284+
@Override
285+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
286+
return visitor.visitDecimal(getValue(), precision, scale);
287+
}
288+
}
289+
263290
static final class StringLiteral extends Literal<String> {
264291
StringLiteral(String value) {
265292
super(value);

java/vortex-jni/src/main/java/dev/vortex/api/expressions/proto/DTypes.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ static DTypeProtos.DType float64(boolean nullable) {
9191
.build();
9292
}
9393

94+
static DTypeProtos.DType decimal(boolean nullable, int precision, int scale) {
95+
return DTypeProtos.DType.newBuilder()
96+
.setDecimal(DTypeProtos.Decimal.newBuilder()
97+
.setNullable(nullable)
98+
.setPrecision(precision)
99+
.setScale(scale)
100+
.build())
101+
.build();
102+
}
103+
94104
static DTypeProtos.DType string(boolean nullable) {
95105
return DTypeProtos.DType.newBuilder()
96106
.setUtf8(DTypeProtos.Utf8.newBuilder().setNullable(nullable).build())
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* (c) Copyright 2025 SpiralDB Inc. All rights reserved.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.vortex.api.expressions.proto;
17+
18+
import com.google.protobuf.ByteString;
19+
import java.math.BigDecimal;
20+
import java.math.BigInteger;
21+
22+
final class EndianUtils {
23+
static byte[] reverse(ByteString src) {
24+
byte[] dst = new byte[src.size()];
25+
for (int i = 0; i < dst.length; i++) {
26+
dst[i] = src.byteAt(dst.length - 1 - i);
27+
}
28+
return dst;
29+
}
30+
31+
static byte[] littleEndianDecimal(BigDecimal decimal) {
32+
BigInteger unscaled = decimal.unscaledValue();
33+
byte[] bigEndianBytes = unscaled.toByteArray();
34+
35+
// Determine target size (1, 2, 4, 8, 16, or 32 bytes)
36+
int targetSize;
37+
if (bigEndianBytes.length <= 1) {
38+
targetSize = 1;
39+
} else if (bigEndianBytes.length <= 2) {
40+
targetSize = 2;
41+
} else if (bigEndianBytes.length <= 4) {
42+
targetSize = 4;
43+
} else if (bigEndianBytes.length <= 8) {
44+
targetSize = 8;
45+
} else if (bigEndianBytes.length <= 16) {
46+
targetSize = 16;
47+
} else if (bigEndianBytes.length <= 32) {
48+
targetSize = 32;
49+
} else {
50+
throw new IllegalArgumentException(
51+
"BigDecimal with " + bigEndianBytes.length + " bytes overflows maximum Vortex decimal size");
52+
}
53+
54+
byte[] result = new byte[targetSize];
55+
56+
// Copy bytes in reverse order (big endian to little endian)
57+
for (int i = 0; i < bigEndianBytes.length; i++) {
58+
result[i] = bigEndianBytes[bigEndianBytes.length - 1 - i];
59+
}
60+
61+
// Sign extend if negative
62+
if (unscaled.signum() < 0) {
63+
for (int i = bigEndianBytes.length; i < targetSize; i++) {
64+
result[i] = (byte) 0xFF;
65+
}
66+
}
67+
68+
return result;
69+
}
70+
}

java/vortex-jni/src/main/java/dev/vortex/api/expressions/proto/ExpressionProtoDeserializer.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717

1818
import com.google.common.base.Preconditions;
1919
import com.google.common.collect.Iterables;
20+
import com.google.protobuf.ByteString;
2021
import dev.vortex.api.Expression;
2122
import dev.vortex.api.expressions.*;
2223
import dev.vortex.proto.DTypeProtos;
2324
import dev.vortex.proto.ExprProtos;
2425
import dev.vortex.proto.ScalarProtos;
26+
import java.math.BigDecimal;
27+
import java.math.BigInteger;
2528
import java.util.List;
2629
import java.util.Optional;
2730

@@ -126,7 +129,18 @@ private static Expression deserializeLiteral(ExprProtos.Kind.Literal literal, Li
126129
case STRING_VALUE:
127130
return Literal.string(scalarValue.getStringValue());
128131
case BYTES_VALUE:
129-
return Literal.bytes(scalarValue.getBytesValue().toByteArray());
132+
if (dtype.hasDecimal()) {
133+
ByteString littleEndian = scalarValue.getBytesValue();
134+
byte[] bigEndian = EndianUtils.reverse(littleEndian);
135+
BigDecimal value = new BigDecimal(
136+
new BigInteger(bigEndian), dtype.getDecimal().getScale());
137+
return Literal.decimal(
138+
value,
139+
dtype.getDecimal().getPrecision(),
140+
dtype.getDecimal().getScale());
141+
} else {
142+
return Literal.bytes(scalarValue.getBytesValue().toByteArray());
143+
}
130144
default:
131145
throw new UnsupportedOperationException("Unsupported ScalarValue type encountered: " + scalarValue);
132146
}
@@ -229,6 +243,11 @@ private static Literal<?> nullLiteral(DTypeProtos.DType type) {
229243
default:
230244
throw new UnsupportedOperationException("Unsupported ScalarValue type encountered: " + type);
231245
}
246+
case DECIMAL:
247+
return Literal.decimal(
248+
null,
249+
type.getDecimal().getPrecision(),
250+
type.getDecimal().getScale());
232251
case UTF8:
233252
return Literal.string(null);
234253
case BINARY:

java/vortex-jni/src/main/java/dev/vortex/api/expressions/proto/LiteralToScalar.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import dev.vortex.api.expressions.Literal;
1919
import dev.vortex.proto.ScalarProtos;
20+
import java.math.BigDecimal;
2021
import java.util.Objects;
2122
import java.util.Optional;
2223

@@ -174,6 +175,15 @@ public ScalarProtos.Scalar visitFloat64(Double literal) {
174175
}
175176
}
176177

178+
@Override
179+
public ScalarProtos.Scalar visitDecimal(BigDecimal decimal, int precision, int scale) {
180+
if (Objects.isNull(decimal)) {
181+
return Scalars.nullDecimal(precision, scale);
182+
} else {
183+
return Scalars.decimal(decimal, precision, scale);
184+
}
185+
}
186+
177187
@Override
178188
public ScalarProtos.Scalar visitString(String literal) {
179189
if (Objects.isNull(literal)) {

java/vortex-jni/src/main/java/dev/vortex/api/expressions/proto/Scalars.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.protobuf.ByteString;
1919
import com.google.protobuf.NullValue;
2020
import dev.vortex.proto.ScalarProtos;
21+
import java.math.BigDecimal;
2122
import java.util.Optional;
2223

2324
final class Scalars {
@@ -174,6 +175,23 @@ static ScalarProtos.Scalar nullString() {
174175
.build();
175176
}
176177

178+
static ScalarProtos.Scalar decimal(BigDecimal decimal, int precision, int scale) {
179+
var littleEndian = EndianUtils.littleEndianDecimal(decimal);
180+
return ScalarProtos.Scalar.newBuilder()
181+
.setValue(ScalarProtos.ScalarValue.newBuilder()
182+
.setBytesValue(ByteString.copyFrom(littleEndian))
183+
.build())
184+
.setDtype(DTypes.decimal(false, precision, scale))
185+
.build();
186+
}
187+
188+
static ScalarProtos.Scalar nullDecimal(int precision, int scale) {
189+
return ScalarProtos.Scalar.newBuilder()
190+
.setValue(ScalarProtos.ScalarValue.newBuilder().setNullValue(NullValue.NULL_VALUE))
191+
.setDtype(DTypes.decimal(true, precision, scale))
192+
.build();
193+
}
194+
177195
static ScalarProtos.Scalar bytes(byte[] value) {
178196
return ScalarProtos.Scalar.newBuilder()
179197
.setValue(ScalarProtos.ScalarValue.newBuilder()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* (c) Copyright 2025 SpiralDB Inc. All rights reserved.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.vortex.api.expressions;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
20+
import dev.vortex.api.Expression;
21+
import dev.vortex.api.expressions.proto.ExpressionProtoDeserializer;
22+
import dev.vortex.api.expressions.proto.ExpressionProtoSerializer;
23+
import dev.vortex.proto.ExprProtos;
24+
import java.math.BigDecimal;
25+
import java.math.BigInteger;
26+
import org.junit.jupiter.api.Test;
27+
28+
public final class LiteralTest {
29+
@Test
30+
public void testLiteral_decimals() {
31+
Literal<BigDecimal> lit = Literal.decimal(new BigDecimal(BigInteger.valueOf(-1234L), 3), 38, 3);
32+
ExprProtos.Expr serialized = ExpressionProtoSerializer.serialize(lit);
33+
Expression out = ExpressionProtoDeserializer.deserialize(serialized);
34+
assertEquals(lit, out);
35+
}
36+
}

vortex-layout/src/segments/events.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ impl SegmentSource for EventsSegmentSource {
151151
}
152152

153153
/// A pending segment returned by the [`SegmentSource`].
154-
pub struct PendingSegment {
154+
struct PendingSegment {
155155
id: SegmentId,
156156
/// A weak shared future that we hand out to all requesters. Once all requesters have been
157157
/// dropped, typically because their row split has completed (or been pruned), then the weak

0 commit comments

Comments
 (0)