Skip to content

Commit 3efa9ac

Browse files
authored
feat: add Proto Columns support in JDBC (#1252)
* feat: add code changes to support DML for Proto Columns * feat: support DML for Proto Columns * feat: add untyped null when inserting null value in array of proto columns * feat: add code changes to support DDL for Proto Columns * feat: code changes to support getArray and getResultSet in JdbcArray for Proto columns * feat: add unit tests for DML and DQL * feat: add integration tests for Proto Columns DDL * feat: add integration tests for Proto columns DML and DQL * feat: lint format * feat: code refactoring to throw exceptions and handle null values in JdbcArray * feat: update tests to validate null in JdbcArray * feat: Integration test refactoring * fix: add copyright header * feat: update junit assertions * feat: move array conversion logic for protos to seperate methods in JdbcTypeConverter * feat: add review suggestions to JdbcArray * feat: add review suggestion * feat: add review suggestions in JdbcParameterStore file * feat: add untyped null integration test * feat: add inter compatibilty and lint fix * feat: update schema and base64 protodescriptors files * feat: nit * chore: update java-spanner version * chore: lint fix * chore: skip tests on graalvm * chore: nit fixes
1 parent 255eeef commit 3efa9ac

20 files changed

+2590
-29
lines changed

clirr-ignored-differences.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,20 @@
5757
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
5858
<method>void setMaxPartitions(int)</method>
5959
</difference>
60+
<difference>
61+
<differenceType>7012</differenceType>
62+
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
63+
<method>byte[] getProtoDescriptors()</method>
64+
</difference>
65+
<difference>
66+
<differenceType>7012</differenceType>
67+
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
68+
<method>void setProtoDescriptors(byte[])</method>
69+
</difference>
70+
<difference>
71+
<differenceType>7012</differenceType>
72+
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
73+
<method>void setProtoDescriptors(java.io.InputStream)</method>
74+
</difference>
6075

6176
</differences>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ static int extractColumnType(Type type) {
4848
case BOOL:
4949
return Types.BOOLEAN;
5050
case BYTES:
51+
case PROTO:
5152
return Types.BINARY;
5253
case DATE:
5354
return Types.DATE;
@@ -56,6 +57,7 @@ static int extractColumnType(Type type) {
5657
case FLOAT64:
5758
return Types.DOUBLE;
5859
case INT64:
60+
case ENUM:
5961
return Types.BIGINT;
6062
case NUMERIC:
6163
case PG_NUMERIC:
@@ -145,6 +147,7 @@ static String getClassName(Type type) {
145147
case BOOL:
146148
return Boolean.class.getName();
147149
case BYTES:
150+
case PROTO:
148151
return byte[].class.getName();
149152
case DATE:
150153
return Date.class.getName();
@@ -153,6 +156,7 @@ static String getClassName(Type type) {
153156
case FLOAT64:
154157
return Double.class.getName();
155158
case INT64:
159+
case ENUM:
156160
return Long.class.getName();
157161
case NUMERIC:
158162
case PG_NUMERIC:
@@ -168,6 +172,7 @@ static String getClassName(Type type) {
168172
case BOOL:
169173
return Boolean[].class.getName();
170174
case BYTES:
175+
case PROTO:
171176
return byte[][].class.getName();
172177
case DATE:
173178
return Date[].class.getName();
@@ -176,6 +181,7 @@ static String getClassName(Type type) {
176181
case FLOAT64:
177182
return Double[].class.getName();
178183
case INT64:
184+
case ENUM:
179185
return Long[].class.getName();
180186
case NUMERIC:
181187
case PG_NUMERIC:

src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
import com.google.cloud.spanner.connection.AutocommitDmlMode;
2929
import com.google.cloud.spanner.connection.SavepointSupport;
3030
import com.google.cloud.spanner.connection.TransactionMode;
31+
import java.io.IOException;
32+
import java.io.InputStream;
3133
import java.sql.Connection;
3234
import java.sql.ResultSet;
3335
import java.sql.SQLException;
3436
import java.sql.Timestamp;
3537
import java.util.Iterator;
38+
import javax.annotation.Nonnull;
3639

3740
/**
3841
* JDBC connection with a number of additional Cloud Spanner specific methods. JDBC connections that
@@ -448,4 +451,35 @@ Iterator<com.google.cloud.spanner.jdbc.TransactionRetryListener> getTransactionR
448451
*/
449452
Iterator<com.google.cloud.spanner.connection.TransactionRetryListener>
450453
getTransactionRetryListenersFromConnection() throws SQLException;
454+
455+
/**
456+
* Sets the proto descriptors to use for the next DDL statement (single or batch) that will be
457+
* executed. The proto descriptor is automatically cleared after the statement is executed.
458+
*
459+
* @param protoDescriptors The proto descriptors to use with the next DDL statement (single or
460+
* batch) that will be executed on this connection.
461+
*/
462+
default void setProtoDescriptors(@Nonnull byte[] protoDescriptors) throws SQLException {
463+
throw new UnsupportedOperationException();
464+
}
465+
466+
/**
467+
* Sets the proto descriptors to use for the next DDL statement (single or batch) that will be
468+
* executed. The proto descriptor is automatically cleared after the statement is executed.
469+
*
470+
* @param protoDescriptors The proto descriptors to use with the next DDL statement (single or
471+
* batch) that will be executed on this connection.
472+
*/
473+
default void setProtoDescriptors(@Nonnull InputStream protoDescriptors)
474+
throws SQLException, IOException {
475+
throw new UnsupportedOperationException();
476+
}
477+
478+
/**
479+
* @return The proto descriptor that will be used with the next DDL statement (single or batch)
480+
* that is executed on this connection.
481+
*/
482+
default byte[] getProtoDescriptors() throws SQLException {
483+
throw new UnsupportedOperationException();
484+
}
451485
}

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

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
import com.google.cloud.spanner.Value;
2525
import com.google.cloud.spanner.ValueBinder;
2626
import com.google.common.collect.ImmutableList;
27+
import com.google.protobuf.AbstractMessage;
28+
import com.google.protobuf.Descriptors;
29+
import com.google.protobuf.Descriptors.Descriptor;
30+
import com.google.protobuf.Message;
31+
import com.google.protobuf.ProtocolMessageEnum;
2732
import com.google.rpc.Code;
2833
import java.math.BigDecimal;
2934
import java.sql.Array;
@@ -78,7 +83,16 @@ static JdbcArray createArray(JdbcDataType type, List<?> elements) {
7883
private JdbcArray(JdbcDataType type, Object[] elements) throws SQLException {
7984
this.type = type;
8085
if (elements != null) {
81-
this.data = java.lang.reflect.Array.newInstance(type.getJavaClass(), elements.length);
86+
if ((type.getCode() == Type.Code.PROTO
87+
&& AbstractMessage[].class.isAssignableFrom(elements.getClass()))
88+
|| (type.getCode() == Type.Code.ENUM
89+
&& ProtocolMessageEnum[].class.isAssignableFrom(elements.getClass()))) {
90+
this.data =
91+
java.lang.reflect.Array.newInstance(
92+
elements.getClass().getComponentType(), elements.length);
93+
} else {
94+
this.data = java.lang.reflect.Array.newInstance(type.getJavaClass(), elements.length);
95+
}
8296
try {
8397
System.arraycopy(elements, 0, this.data, 0, elements.length);
8498
} catch (Exception e) {
@@ -138,9 +152,17 @@ public Object getArray(long index, int count) throws SQLException {
138152
@Override
139153
public Object getArray(long index, int count, Map<String, Class<?>> map) throws SQLException {
140154
checkFree();
141-
if (data != null) {
142-
Object res = java.lang.reflect.Array.newInstance(type.getJavaClass(), count);
143-
System.arraycopy(data, (int) index - 1, res, 0, count);
155+
if (this.data != null) {
156+
Object res;
157+
if ((this.type.getCode() == Type.Code.PROTO
158+
&& AbstractMessage[].class.isAssignableFrom(this.data.getClass()))
159+
|| (this.type.getCode() == Type.Code.ENUM
160+
&& ProtocolMessageEnum[].class.isAssignableFrom(this.data.getClass()))) {
161+
res = java.lang.reflect.Array.newInstance(this.data.getClass().getComponentType(), count);
162+
} else {
163+
res = java.lang.reflect.Array.newInstance(this.type.getJavaClass(), count);
164+
}
165+
System.arraycopy(this.data, (int) index - 1, res, 0, count);
144166
return res;
145167
}
146168
return null;
@@ -167,24 +189,37 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException {
167189
JdbcPreconditions.checkArgument(startIndex >= 1L, "Start index must be >= 1");
168190
JdbcPreconditions.checkArgument(count >= 0, "Count must be >= 0");
169191
checkFree();
192+
Type spannerTypeForProto = getSpannerTypeForProto();
193+
Type spannerType =
194+
spannerTypeForProto == null ? this.type.getSpannerType() : spannerTypeForProto;
195+
170196
ImmutableList.Builder<Struct> rows = ImmutableList.builder();
171197
int added = 0;
172-
if (data != null) {
198+
if (this.data != null) {
173199
// Note that array index in JDBC is base-one.
174200
for (int index = (int) startIndex;
175-
added < count && index <= ((Object[]) data).length;
201+
added < count && index <= ((Object[]) this.data).length;
176202
index++) {
177-
Object value = ((Object[]) data)[index - 1];
203+
Object value = ((Object[]) this.data)[index - 1];
178204
ValueBinder<Struct.Builder> binder =
179205
Struct.newBuilder().set("INDEX").to(index).set("VALUE");
180206
Struct.Builder builder;
181-
switch (type.getCode()) {
207+
switch (this.type.getCode()) {
182208
case BOOL:
183209
builder = binder.to((Boolean) value);
184210
break;
185211
case BYTES:
186212
builder = binder.to(ByteArray.copyFrom((byte[]) value));
187213
break;
214+
case PROTO:
215+
if (value == null && AbstractMessage[].class.isAssignableFrom(this.data.getClass())) {
216+
builder = binder.to((ByteArray) null, spannerType.getProtoTypeFqn());
217+
} else if (value instanceof AbstractMessage) {
218+
builder = binder.to((AbstractMessage) value);
219+
} else {
220+
builder = binder.to(value != null ? ByteArray.copyFrom((byte[]) value) : null);
221+
}
222+
break;
188223
case DATE:
189224
builder = binder.to(JdbcTypeConverter.toGoogleDate((Date) value));
190225
break;
@@ -197,6 +232,16 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException {
197232
case INT64:
198233
builder = binder.to((Long) value);
199234
break;
235+
case ENUM:
236+
if (value == null
237+
&& ProtocolMessageEnum[].class.isAssignableFrom(this.data.getClass())) {
238+
builder = binder.to((Long) null, spannerType.getProtoTypeFqn());
239+
} else if (value instanceof ProtocolMessageEnum) {
240+
builder = binder.to((ProtocolMessageEnum) value);
241+
} else {
242+
builder = binder.to((Long) value);
243+
}
244+
break;
200245
case NUMERIC:
201246
builder = binder.to((BigDecimal) value);
202247
break;
@@ -217,7 +262,8 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException {
217262
default:
218263
throw new SQLFeatureNotSupportedException(
219264
String.format(
220-
"Array of type %s cannot be converted to a ResultSet", type.getCode().name()));
265+
"Array of type %s cannot be converted to a ResultSet",
266+
this.type.getCode().name()));
221267
}
222268
rows.add(builder.build());
223269
added++;
@@ -226,14 +272,54 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException {
226272
}
227273
}
228274
}
275+
229276
return JdbcResultSet.of(
230277
ResultSets.forRows(
231278
Type.struct(
232-
StructField.of("INDEX", Type.int64()),
233-
StructField.of("VALUE", type.getSpannerType())),
279+
StructField.of("INDEX", Type.int64()), StructField.of("VALUE", spannerType)),
234280
rows.build()));
235281
}
236282

283+
// Returns null if the type is not a PROTO or ENUM
284+
private Type getSpannerTypeForProto() throws SQLException {
285+
Type spannerType = null;
286+
if (this.data != null) {
287+
if (this.type.getCode() == Type.Code.PROTO
288+
&& AbstractMessage[].class.isAssignableFrom(this.data.getClass())) {
289+
spannerType = createSpannerProtoType();
290+
} else if (this.type.getCode() == Type.Code.ENUM
291+
&& ProtocolMessageEnum[].class.isAssignableFrom(this.data.getClass())) {
292+
spannerType = createSpannerProtoEnumType();
293+
}
294+
}
295+
return spannerType;
296+
}
297+
298+
private Type createSpannerProtoType() throws SQLException {
299+
Class<?> componentType = this.data.getClass().getComponentType();
300+
try {
301+
Message.Builder builder =
302+
(Message.Builder) componentType.getMethod("newBuilder").invoke(null);
303+
Descriptor msgDescriptor = builder.getDescriptorForType();
304+
return Type.proto(msgDescriptor.getFullName());
305+
} catch (Exception e) {
306+
throw JdbcSqlExceptionFactory.of(
307+
"Error occurred when getting proto message descriptor from data", Code.UNKNOWN, e);
308+
}
309+
}
310+
311+
private Type createSpannerProtoEnumType() throws SQLException {
312+
Class<?> componentType = this.data.getClass().getComponentType();
313+
try {
314+
Descriptors.EnumDescriptor enumDescriptor =
315+
(Descriptors.EnumDescriptor) componentType.getMethod("getDescriptor").invoke(null);
316+
return Type.protoEnum(enumDescriptor.getFullName());
317+
} catch (Exception e) {
318+
throw JdbcSqlExceptionFactory.of(
319+
"Error occurred when getting proto enum descriptor from data", Code.UNKNOWN, e);
320+
}
321+
}
322+
237323
@Override
238324
public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map)
239325
throws SQLException {

src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.cloud.spanner.jdbc.JdbcStatement.isNullOrEmpty;
2121

2222
import com.google.api.client.util.Preconditions;
23+
import com.google.cloud.ByteArray;
2324
import com.google.cloud.spanner.CommitResponse;
2425
import com.google.cloud.spanner.DatabaseId;
2526
import com.google.cloud.spanner.Mutation;
@@ -38,6 +39,8 @@
3839
import io.opentelemetry.api.OpenTelemetry;
3940
import io.opentelemetry.api.common.Attributes;
4041
import io.opentelemetry.api.common.AttributesBuilder;
42+
import java.io.IOException;
43+
import java.io.InputStream;
4144
import java.sql.Array;
4245
import java.sql.Blob;
4346
import java.sql.Clob;
@@ -867,4 +870,34 @@ public Iterator<TransactionRetryListener> getTransactionRetryListeners() throws
867870
checkClosed();
868871
return getSpannerConnection().getTransactionRetryListeners();
869872
}
873+
874+
@Override
875+
public void setProtoDescriptors(@Nonnull byte[] protoDescriptors) throws SQLException {
876+
Preconditions.checkNotNull(protoDescriptors);
877+
checkClosed();
878+
try {
879+
getSpannerConnection().setProtoDescriptors(protoDescriptors);
880+
} catch (SpannerException e) {
881+
throw JdbcSqlExceptionFactory.of(e);
882+
}
883+
}
884+
885+
@Override
886+
public void setProtoDescriptors(@Nonnull InputStream protoDescriptors)
887+
throws SQLException, IOException {
888+
Preconditions.checkNotNull(protoDescriptors);
889+
checkClosed();
890+
try {
891+
getSpannerConnection()
892+
.setProtoDescriptors(ByteArray.copyFrom(protoDescriptors).toByteArray());
893+
} catch (SpannerException e) {
894+
throw JdbcSqlExceptionFactory.of(e);
895+
}
896+
}
897+
898+
@Override
899+
public byte[] getProtoDescriptors() throws SQLException {
900+
checkClosed();
901+
return getSpannerConnection().getProtoDescriptors();
902+
}
870903
}

0 commit comments

Comments
 (0)