Skip to content

Commit e8fea64

Browse files
authored
feat(java): support serializing time/date/timestamp types over FFI (#2766)
1 parent 889e4b7 commit e8fea64

File tree

10 files changed

+703
-4
lines changed

10 files changed

+703
-4
lines changed

java/vortex-jni/build.gradle.kts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
13
plugins {
24
`java-library`
35
`jvm-test-suite`
46
`maven-publish`
57
id("com.google.protobuf")
8+
id("com.gradleup.shadow") version "8.3.6"
69
}
710

811
dependencies {
@@ -34,6 +37,17 @@ protobuf {
3437
}
3538
}
3639

40+
// shade guava and protobuf dependencies
41+
tasks.withType<ShadowJar> {
42+
archiveClassifier.set("")
43+
relocate("com.google.protobuf", "dev.vortex.relocated.com.google.protobuf")
44+
relocate("com.google.common", "dev.vortex.relocated.com.google.common")
45+
}
46+
47+
tasks.build {
48+
dependsOn("shadowJar")
49+
}
50+
3751
publishing {
3852
publications {
3953
create<MavenPublication>("mavenJava") {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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;
17+
18+
import java.time.Instant;
19+
import java.time.LocalDateTime;
20+
import java.time.OffsetDateTime;
21+
import java.time.ZoneOffset;
22+
import java.time.temporal.ChronoUnit;
23+
24+
// Helpful functions borrowed from Iceberg class with same name.
25+
public final class DateTimeUtil {
26+
public static final OffsetDateTime EPOCH = Instant.ofEpochSecond(0).atOffset(ZoneOffset.UTC);
27+
28+
// Just assume timestamp is already at UTC for conversion purposes.
29+
// When decoded back to LocalDateTime
30+
public static long nanosFromTimestamp(LocalDateTime dateTime) {
31+
return ChronoUnit.NANOS.between(EPOCH, dateTime.atOffset(ZoneOffset.UTC));
32+
}
33+
34+
public static long nanosFromTimestamptz(OffsetDateTime dateTime) {
35+
return ChronoUnit.NANOS.between(EPOCH, dateTime);
36+
}
37+
}

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

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

1818
import com.google.common.base.Objects;
1919
import dev.vortex.api.Expression;
20+
import java.util.Optional;
2021

2122
public abstract class Literal<T> implements Expression {
2223
private final T value;
@@ -86,6 +87,42 @@ public static Literal<byte[]> bytes(byte[] value) {
8687
return new BytesLiteral(value);
8788
}
8889

90+
public static Literal<Integer> timeSeconds(Integer value) {
91+
return new TimeSeconds(value);
92+
}
93+
94+
public static Literal<Integer> timeMillis(Integer value) {
95+
return new TimeMillis(value);
96+
}
97+
98+
public static Literal<Long> timeMicros(Long value) {
99+
return new TimeMicros(value);
100+
}
101+
102+
public static Literal<Long> timeNanos(Long value) {
103+
return new TimeNanos(value);
104+
}
105+
106+
public static Literal<Integer> dateDays(Integer value) {
107+
return new DateDays(value);
108+
}
109+
110+
public static Literal<Long> dateMillis(Long value) {
111+
return new DateMillis(value);
112+
}
113+
114+
public static Literal<Long> timestampMillis(Long value, Optional<String> timeZone) {
115+
return new TimestampMillis(value, timeZone);
116+
}
117+
118+
public static Literal<Long> timestampMicros(Long value, Optional<String> timeZone) {
119+
return new TimestampMicros(value, timeZone);
120+
}
121+
122+
public static Literal<Long> timestampNanos(Long value, Optional<String> timeZone) {
123+
return new TimestampNanos(value, timeZone);
124+
}
125+
89126
@Override
90127
public <R> R accept(Expression.Visitor<R> visitor) {
91128
return visitor.visitLiteral(this);
@@ -106,6 +143,24 @@ public interface LiteralVisitor<U> {
106143

107144
U visitInt64(Long literal);
108145

146+
U visitDateDays(Integer days);
147+
148+
U visitDateMillis(Long millis);
149+
150+
U visitTimeSeconds(Integer seconds);
151+
152+
U visitTimeMillis(Integer seconds);
153+
154+
U visitTimeMicros(Long seconds);
155+
156+
U visitTimeNanos(Long seconds);
157+
158+
U visitTimestampMillis(Long epochMillis, Optional<String> timeZone);
159+
160+
U visitTimestampMicros(Long epochMicros, Optional<String> timeZone);
161+
162+
U visitTimestampNanos(Long epochNanos, Optional<String> timeZone);
163+
109164
U visitFloat32(Float literal);
110165

111166
U visitFloat64(Double literal);
@@ -226,4 +281,112 @@ public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
226281
return visitor.visitBytes(getValue());
227282
}
228283
}
284+
285+
static final class TimeSeconds extends Literal<Integer> {
286+
TimeSeconds(Integer value) {
287+
super(value);
288+
}
289+
290+
@Override
291+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
292+
return visitor.visitTimeSeconds(getValue());
293+
}
294+
}
295+
296+
static final class TimeMillis extends Literal<Integer> {
297+
TimeMillis(Integer value) {
298+
super(value);
299+
}
300+
301+
@Override
302+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
303+
return visitor.visitTimeMillis(getValue());
304+
}
305+
}
306+
307+
static final class TimeMicros extends Literal<Long> {
308+
TimeMicros(Long value) {
309+
super(value);
310+
}
311+
312+
@Override
313+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
314+
return visitor.visitTimeMicros(getValue());
315+
}
316+
}
317+
318+
static final class TimeNanos extends Literal<Long> {
319+
TimeNanos(Long value) {
320+
super(value);
321+
}
322+
323+
@Override
324+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
325+
return visitor.visitTimeNanos(getValue());
326+
}
327+
}
328+
329+
static final class DateDays extends Literal<Integer> {
330+
DateDays(Integer value) {
331+
super(value);
332+
}
333+
334+
@Override
335+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
336+
return visitor.visitDateDays(getValue());
337+
}
338+
}
339+
340+
static final class DateMillis extends Literal<Long> {
341+
DateMillis(Long value) {
342+
super(value);
343+
}
344+
345+
@Override
346+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
347+
return visitor.visitDateMillis(getValue());
348+
}
349+
}
350+
351+
static final class TimestampMillis extends Literal<Long> {
352+
private final Optional<String> timeZone;
353+
354+
TimestampMillis(Long value, Optional<String> timeZone) {
355+
super(value);
356+
this.timeZone = timeZone;
357+
}
358+
359+
@Override
360+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
361+
return visitor.visitTimestampMillis(getValue(), timeZone);
362+
}
363+
}
364+
365+
static final class TimestampMicros extends Literal<Long> {
366+
private final Optional<String> timeZone;
367+
368+
TimestampMicros(Long value, Optional<String> timeZone) {
369+
super(value);
370+
this.timeZone = timeZone;
371+
}
372+
373+
@Override
374+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
375+
return visitor.visitTimestampMicros(getValue(), timeZone);
376+
}
377+
}
378+
379+
static final class TimestampNanos extends Literal<Long> {
380+
private final Optional<String> timeZone;
381+
382+
TimestampNanos(Long value, Optional<String> timeZone) {
383+
super(value);
384+
this.timeZone = timeZone;
385+
}
386+
387+
@Override
388+
public <U> U acceptLiteralVisitor(LiteralVisitor<U> visitor) {
389+
return visitor.visitTimestampNanos(getValue(), timeZone);
390+
}
391+
}
229392
}

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
*/
1616
package dev.vortex.api.expressions.proto;
1717

18+
import static dev.vortex.api.expressions.proto.TemporalMetadatas.TIME_UNIT_MICROS;
19+
import static dev.vortex.api.expressions.proto.TemporalMetadatas.TIME_UNIT_NANOS;
20+
21+
import com.google.protobuf.ByteString;
1822
import dev.vortex.proto.DTypeProtos;
23+
import java.util.Optional;
1924

2025
final class DTypes {
2126
private DTypes() {}
@@ -97,4 +102,86 @@ static DTypeProtos.DType binary(boolean nullable) {
97102
.setBinary(DTypeProtos.Binary.newBuilder().setNullable(nullable).build())
98103
.build();
99104
}
105+
106+
static DTypeProtos.DType dateDays(boolean nullable) {
107+
return DTypeProtos.DType.newBuilder()
108+
.setExtension(DTypeProtos.Extension.newBuilder()
109+
.setId("vortex.date")
110+
.setStorageDtype(DTypes.int32(nullable))
111+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.DATE_DAYS.get()))
112+
.build())
113+
.build();
114+
}
115+
116+
static DTypeProtos.DType dateMillis(boolean nullable) {
117+
return DTypeProtos.DType.newBuilder()
118+
.setExtension(DTypeProtos.Extension.newBuilder()
119+
.setId("vortex.date")
120+
.setStorageDtype(DTypes.int64(nullable))
121+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.DATE_MILLIS.get()))
122+
.build())
123+
.build();
124+
}
125+
126+
static DTypeProtos.DType timeSeconds(boolean nullable) {
127+
return DTypeProtos.DType.newBuilder()
128+
.setExtension(DTypeProtos.Extension.newBuilder()
129+
.setId("vortex.time")
130+
.setStorageDtype(DTypes.int32(nullable))
131+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.TIME_SECONDS.get()))
132+
.build())
133+
.build();
134+
}
135+
136+
static DTypeProtos.DType timeMillis(boolean nullable) {
137+
return DTypeProtos.DType.newBuilder()
138+
.setExtension(DTypeProtos.Extension.newBuilder()
139+
.setId("vortex.time")
140+
.setStorageDtype(DTypes.int32(nullable))
141+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.TIME_MILLIS.get()))
142+
.build())
143+
.build();
144+
}
145+
146+
static DTypeProtos.DType timeMicros(boolean nullable) {
147+
return DTypeProtos.DType.newBuilder()
148+
.setExtension(DTypeProtos.Extension.newBuilder()
149+
.setId("vortex.time")
150+
.setStorageDtype(DTypes.int64(nullable))
151+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.TIME_MICROS.get()))
152+
.build())
153+
.build();
154+
}
155+
156+
static DTypeProtos.DType timeNanos(boolean nullable) {
157+
return DTypeProtos.DType.newBuilder()
158+
.setExtension(DTypeProtos.Extension.newBuilder()
159+
.setId("vortex.time")
160+
.setStorageDtype(DTypes.int64(nullable))
161+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.TIME_NANOS.get()))
162+
.build())
163+
.build();
164+
}
165+
166+
static DTypeProtos.DType timestampMillis(Optional<String> timeZone, boolean nullable) {
167+
return timestamp(TemporalMetadatas.TIME_UNIT_MILLIS, timeZone, nullable);
168+
}
169+
170+
static DTypeProtos.DType timestampMicros(Optional<String> timeZone, boolean nullable) {
171+
return timestamp(TIME_UNIT_MICROS, timeZone, nullable);
172+
}
173+
174+
static DTypeProtos.DType timestampNanos(Optional<String> timeZone, boolean nullable) {
175+
return timestamp(TIME_UNIT_NANOS, timeZone, nullable);
176+
}
177+
178+
private static DTypeProtos.DType timestamp(byte timeUnit, Optional<String> timeZone, boolean nullable) {
179+
return DTypeProtos.DType.newBuilder()
180+
.setExtension(DTypeProtos.Extension.newBuilder()
181+
.setId("vortex.timestamp")
182+
.setStorageDtype(DTypes.int64(nullable))
183+
.setMetadata(ByteString.copyFrom(TemporalMetadatas.timestamp(timeUnit, timeZone)))
184+
.build())
185+
.build();
186+
}
100187
}

0 commit comments

Comments
 (0)