Skip to content

Commit 3af669d

Browse files
committed
feat(spanner-jdbc): add support for INTERVAL data type
Adds support for the Spanner INTERVAL data type to the Spanner JDBC driver. - Introduce IntervalType implementing java.sql.SQLType with vendor type number 100016. - Map INTERVAL in JdbcDataType, AbstractJdbcWrapper, and metadata queries (both GoogleSQL and PostgreSQL dialects). - Add getInterval(int/String) to JdbcResultSet with support for Interval and ISO 8601 strings. - Support java.time.Duration and java.time.Period in JdbcTypeConverter, JdbcResultSet (getObject), JdbcParameterStore (setObject/setArray), and JdbcArray, consistent with pgjdbc behavior. - Support typed null and parameterized statement bindings for Interval, Duration, and Period.
1 parent 7d00726 commit 3af669d

16 files changed

Lines changed: 1190 additions & 3 deletions

java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.getMainTypeCode;
2020

2121
import com.google.cloud.spanner.Dialect;
22+
import com.google.cloud.spanner.Interval;
2223
import com.google.cloud.spanner.Type;
2324
import com.google.cloud.spanner.Type.Code;
2425
import com.google.common.base.Preconditions;
@@ -174,6 +175,8 @@ static String getClassName(Type type) {
174175
return String.class.getName();
175176
case TIMESTAMP:
176177
return Timestamp.class.getName();
178+
case INTERVAL:
179+
return Interval.class.getName();
177180
case ARRAY:
178181
switch (getMainTypeCode(type.getArrayElementType())) {
179182
case BOOL:
@@ -199,6 +202,8 @@ static String getClassName(Type type) {
199202
return String[].class.getName();
200203
case TIMESTAMP:
201204
return Timestamp[].class.getName();
205+
case INTERVAL:
206+
return Interval[].class.getName();
202207
}
203208
case STRUCT:
204209
default:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
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+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
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+
17+
package com.google.cloud.spanner.jdbc;
18+
19+
import com.google.spanner.v1.TypeCode;
20+
import java.sql.DatabaseMetaData;
21+
import java.sql.PreparedStatement;
22+
import java.sql.SQLType;
23+
24+
/**
25+
* Custom SQL type for Spanner INTERVAL data type. This type (or the vendor type number) must be
26+
* used when setting an INTERVAL parameter using {@link PreparedStatement#setObject(int, Object,
27+
* SQLType)}.
28+
*/
29+
public class IntervalType implements SQLType {
30+
public static final IntervalType INSTANCE = new IntervalType();
31+
32+
/**
33+
* Spanner does not have any type numbers, but the code values are unique. Add 100,000 to avoid
34+
* conflicts with the type numbers in java.sql.Types.
35+
*/
36+
public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.INTERVAL_VALUE;
37+
38+
/**
39+
* Define a short type number as well, as this is what is expected to be returned in {@link
40+
* DatabaseMetaData#getTypeInfo()}.
41+
*/
42+
public static final short SHORT_VENDOR_TYPE_NUMBER = (short) VENDOR_TYPE_NUMBER;
43+
44+
private IntervalType() {}
45+
46+
@Override
47+
public String getName() {
48+
return "INTERVAL";
49+
}
50+
51+
@Override
52+
public String getVendor() {
53+
return IntervalType.class.getPackage().getName();
54+
}
55+
56+
@Override
57+
public Integer getVendorTypeNumber() {
58+
return VENDOR_TYPE_NUMBER;
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return getName();
64+
}
65+
}

java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.cloud.ByteArray;
20+
import com.google.cloud.spanner.Interval;
2021
import com.google.cloud.spanner.ResultSets;
2122
import com.google.cloud.spanner.Struct;
2223
import com.google.cloud.spanner.Type;
@@ -37,6 +38,8 @@
3738
import java.sql.SQLException;
3839
import java.sql.SQLFeatureNotSupportedException;
3940
import java.sql.Timestamp;
41+
import java.time.Duration;
42+
import java.time.Period;
4043
import java.util.Arrays;
4144
import java.util.List;
4245
import java.util.Map;
@@ -97,6 +100,8 @@ private JdbcArray(JdbcDataType type, Object[] elements) throws SQLException {
97100
// Convert Byte[], Short[], and Integer[] to Long[] for INT64 type
98101
// since Spanner only supports ARRAY<INT64>
99102
this.data = convertToLongArray(elements);
103+
} else if (type == JdbcDataType.INTERVAL) {
104+
this.data = convertToIntervalArray(elements);
100105
} else {
101106
this.data = java.lang.reflect.Array.newInstance(type.getJavaClass(), elements.length);
102107
try {
@@ -129,11 +134,44 @@ private static Long[] convertToLongArray(Object[] elements) {
129134
return longElements;
130135
}
131136

137+
private static Interval[] convertToIntervalArray(Object[] elements) throws SQLException {
138+
Interval[] intervalElements = new Interval[elements.length];
139+
for (int i = 0; i < elements.length; i++) {
140+
if (elements[i] == null) {
141+
intervalElements[i] = null;
142+
} else if (elements[i] instanceof Duration) {
143+
intervalElements[i] = JdbcTypeConverter.toInterval((Duration) elements[i]);
144+
} else if (elements[i] instanceof Period) {
145+
intervalElements[i] = JdbcTypeConverter.toInterval((Period) elements[i]);
146+
} else if (elements[i] instanceof Interval) {
147+
intervalElements[i] = (Interval) elements[i];
148+
} else {
149+
throw JdbcSqlExceptionFactory.of(
150+
"Could not copy array elements. Make sure the supplied array only contains elements of class "
151+
+ Interval.class.getName()
152+
+ ", "
153+
+ Duration.class.getName()
154+
+ ", or "
155+
+ Period.class.getName(),
156+
Code.UNKNOWN);
157+
}
158+
}
159+
return intervalElements;
160+
}
161+
132162
private JdbcArray(JdbcDataType type, List<?> elements) {
133163
this.type = type;
134164
if (elements != null) {
135-
this.data = java.lang.reflect.Array.newInstance(type.getJavaClass(), elements.size());
136-
elements.toArray((Object[]) data);
165+
if (type == JdbcDataType.INTERVAL) {
166+
try {
167+
this.data = convertToIntervalArray(elements.toArray());
168+
} catch (SQLException e) {
169+
throw new IllegalArgumentException(e.getMessage(), e);
170+
}
171+
} else {
172+
this.data = java.lang.reflect.Array.newInstance(type.getJavaClass(), elements.size());
173+
elements.toArray((Object[]) data);
174+
}
137175
}
138176
}
139177

@@ -281,6 +319,9 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException {
281319
case TIMESTAMP:
282320
builder = binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
283321
break;
322+
case INTERVAL:
323+
builder = binder.to((Interval) value);
324+
break;
284325
case ARRAY:
285326
case STRUCT:
286327
default:

java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.cloud.spanner.Dialect;
20+
import com.google.cloud.spanner.Interval;
2021
import com.google.cloud.spanner.ResultSet;
2122
import com.google.cloud.spanner.Struct;
2223
import com.google.cloud.spanner.Type;
@@ -435,6 +436,39 @@ public Type getSpannerType() {
435436
return Type.uuid();
436437
}
437438
},
439+
INTERVAL {
440+
private final Set<String> aliases = new HashSet<>(Collections.singletonList("interval"));
441+
442+
@Override
443+
public int getSqlType() {
444+
return IntervalType.VENDOR_TYPE_NUMBER;
445+
}
446+
447+
@Override
448+
public Class<Interval> getJavaClass() {
449+
return Interval.class;
450+
}
451+
452+
@Override
453+
public Code getCode() {
454+
return Code.INTERVAL;
455+
}
456+
457+
@Override
458+
public List<Interval> getArrayElements(ResultSet resultSet, int columnIndex) {
459+
return resultSet.getIntervalList(columnIndex);
460+
}
461+
462+
@Override
463+
public Type getSpannerType() {
464+
return Type.interval();
465+
}
466+
467+
@Override
468+
public Set<String> getPostgreSQLAliases() {
469+
return aliases;
470+
}
471+
},
438472
STRUCT {
439473
@Override
440474
public int getSqlType() {

java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,44 @@ public ResultSet getTypeInfo() {
13291329
.set("NUM_PREC_RADIX")
13301330
.to((Long) null)
13311331
.build(),
1332+
Struct.newBuilder()
1333+
.set("TYPE_NAME")
1334+
.to("INTERVAL")
1335+
.set("DATA_TYPE")
1336+
.to((long) IntervalType.VENDOR_TYPE_NUMBER)
1337+
.set("PRECISION")
1338+
.to((Long) null)
1339+
.set("LITERAL_PREFIX")
1340+
.to("INTERVAL ")
1341+
.set("LITERAL_SUFFIX")
1342+
.to((String) null)
1343+
.set("CREATE_PARAMS")
1344+
.to((String) null)
1345+
.set("NULLABLE")
1346+
.to(DatabaseMetaData.typeNullable)
1347+
.set("CASE_SENSITIVE")
1348+
.to(false)
1349+
.set("SEARCHABLE")
1350+
.to(DatabaseMetaData.typePredBasic)
1351+
.set("UNSIGNED_ATTRIBUTE")
1352+
.to(false)
1353+
.set("FIXED_PREC_SCALE")
1354+
.to(false)
1355+
.set("AUTO_INCREMENT")
1356+
.to(false)
1357+
.set("LOCAL_TYPE_NAME")
1358+
.to("INTERVAL")
1359+
.set("MINIMUM_SCALE")
1360+
.to(0)
1361+
.set("MAXIMUM_SCALE")
1362+
.to(0)
1363+
.set("SQL_DATA_TYPE")
1364+
.to((Long) null)
1365+
.set("SQL_DATETIME_SUB")
1366+
.to((Long) null)
1367+
.set("NUM_PREC_RADIX")
1368+
.to((Long) null)
1369+
.build(),
13321370
getJsonType(connection.getDialect()))),
13331371
// Allow column 2 to be cast to short without any range checks.
13341372
ImmutableSet.of(2));

java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java

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

1717
package com.google.cloud.spanner.jdbc;
1818

19+
import com.google.cloud.spanner.Interval;
1920
import com.google.cloud.spanner.JdbcDataTypeConverter;
2021
import com.google.cloud.spanner.ResultSet;
2122
import com.google.cloud.spanner.Statement;
@@ -31,6 +32,9 @@
3132
import java.sql.Time;
3233
import java.sql.Timestamp;
3334
import java.sql.Types;
35+
import java.time.Duration;
36+
import java.time.Period;
37+
import java.util.UUID;
3438

3539
/** {@link ParameterMetaData} implementation for Cloud Spanner */
3640
class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMetaData {
@@ -156,6 +160,12 @@ private int getParameterTypeFromValue(int param) {
156160
return Types.NVARCHAR;
157161
} else if (byte[].class.isAssignableFrom(value.getClass())) {
158162
return Types.BINARY;
163+
} else if (Interval.class.isAssignableFrom(value.getClass())
164+
|| Duration.class.isAssignableFrom(value.getClass())
165+
|| Period.class.isAssignableFrom(value.getClass())) {
166+
return IntervalType.VENDOR_TYPE_NUMBER;
167+
} else if (UUID.class.isAssignableFrom(value.getClass())) {
168+
return UuidType.VENDOR_TYPE_NUMBER;
159169
} else {
160170
return Types.OTHER;
161171
}

0 commit comments

Comments
 (0)