Skip to content

Commit 42ac3b7

Browse files
committed
Support Oracle JSON data type for reading and writing
Closes #1469 Oracle 21c introduced a native JSON column type. Signed-off-by: Thomas Segismont <tsegismont@gmail.com>
1 parent 775a207 commit 42ac3b7

10 files changed

Lines changed: 459 additions & 40 deletions

File tree

vertx-oracle-client/src/main/asciidoc/index.adoc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ Currently the client supports the following Oracle data types:
200200
| `BLOB`
201201
| `io.vertx.core.buffer.Buffer`
202202
| See <<handling-blob>>
203+
204+
| `JSON`
205+
| `java.lang.Object`
206+
| Native JSON type (Oracle 21c+). See <<handling-json>>
203207
|===
204208

205209
Tuple decoding uses the above types when storing values.
@@ -215,6 +219,35 @@ However, when reading `BLOB` data, the client returns an {@link io.vertx.core.bu
215219
{@link examples.OracleClientExamples#blobUsage}
216220
----
217221

222+
[[handling-json]]
223+
=== Handling JSON
224+
225+
Oracle Database 21c and later provide native JSON type support.
226+
227+
IMPORTANT: Requires Oracle Database 21c or later and Oracle JDBC driver 21.1 or later.
228+
229+
The `JSON` data type is represented by the following Java types:
230+
231+
- `io.vertx.core.json.JsonObject`
232+
- `io.vertx.core.json.JsonArray`
233+
- `java.lang.String` (for JSON string scalars)
234+
- `java.math.BigDecimal` (for JSON decimal numbers)
235+
- `java.lang.Double` (for JSON double-precision numbers)
236+
- `java.lang.Boolean`
237+
- `io.vertx.sqlclient.Tuple#JSON_NULL` for the JSON null literal
238+
239+
[source,$lang]
240+
----
241+
{@link examples.OracleClientExamples#jsonExample(io.vertx.sqlclient.Pool)}
242+
----
243+
244+
Oracle distinguishes between SQL NULL and the JSON null literal:
245+
246+
[source,$lang]
247+
----
248+
{@link examples.OracleClientExamples#jsonNullExample(io.vertx.sqlclient.Pool)}
249+
----
250+
218251
== Tracing queries
219252

220253
include::tracing.adoc[]

vertx-oracle-client/src/main/java/examples/OracleClientExamples.java

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -287,23 +287,40 @@ public void booleanExample02(SqlClient client) {
287287
});
288288
}
289289

290-
public void jsonExample() {
290+
public void jsonExample(Pool pool) {
291+
JsonObject json = new JsonObject()
292+
.put("name", "Alice")
293+
.put("age", 30);
291294

292-
// Create a tuple
293-
Tuple tuple = Tuple.of(
294-
Tuple.JSON_NULL,
295-
new JsonObject().put("foo", "bar"),
296-
3);
297-
298-
// Retrieving json
299-
Object value = tuple.getValue(0); // Expect JSON_NULL
300-
301-
//
302-
value = tuple.get(JsonObject.class, 1); // Expect JSON object
295+
pool
296+
.preparedQuery("INSERT INTO users (id, data) VALUES (?, ?)")
297+
.execute(Tuple.of(1, json))
298+
.compose(v -> pool
299+
.preparedQuery("SELECT data FROM users WHERE id = ?")
300+
.execute(Tuple.of(1)))
301+
.onComplete(ar -> {
302+
if (ar.succeeded()) {
303+
Row row = ar.result().iterator().next();
304+
JsonObject data = row.getJsonObject(0);
305+
System.out.println("Received: " + data.getString("name"));
306+
}
307+
});
308+
}
303309

304-
//
305-
value = tuple.get(Integer.class, 2); // Expect 3
306-
value = tuple.getInteger(2); // Expect 3
310+
public void jsonNullExample(Pool pool) {
311+
pool
312+
.preparedQuery("INSERT INTO users (id, data) VALUES (?, ?)")
313+
.execute(Tuple.of(2, Tuple.JSON_NULL))
314+
.compose(v -> pool
315+
.preparedQuery("SELECT data FROM users WHERE id = ?")
316+
.execute(Tuple.of(2)))
317+
.onComplete(ar -> {
318+
if (ar.succeeded()) {
319+
Row row = ar.result().iterator().next();
320+
Object value = row.getJson(0);
321+
System.out.println("Is JSON null: " + (value == Tuple.JSON_NULL));
322+
}
323+
});
307324
}
308325

309326
public void numericExample(Row row) {

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/Helper.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2011-2023 Contributors to the Eclipse Foundation
2+
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License 2.0 which is available at
@@ -10,13 +10,22 @@
1010
*/
1111
package io.vertx.oracleclient.impl;
1212

13-
import io.vertx.core.*;
13+
import io.vertx.core.Context;
14+
import io.vertx.core.Future;
15+
import io.vertx.core.VertxException;
1416
import io.vertx.core.buffer.Buffer;
17+
import io.vertx.core.json.JsonArray;
18+
import io.vertx.core.json.JsonObject;
1519
import io.vertx.oracleclient.OracleException;
1620
import io.vertx.sqlclient.Tuple;
1721
import oracle.sql.TIMESTAMPTZ;
22+
import oracle.sql.json.*;
1823

1924
import java.sql.*;
25+
import java.util.LinkedHashMap;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.Map;
2029
import java.util.function.Function;
2130
import java.util.function.Supplier;
2231

@@ -158,10 +167,51 @@ public static Object convertSqlValue(Object value) throws SQLException {
158167
return Tuple.of(((Struct) value).getAttributes());
159168
}
160169

170+
if (value instanceof OracleJsonValue) {
171+
return convertOracleJsonValue((OracleJsonValue) value);
172+
}
173+
161174
// fallback to String
162175
return value.toString();
163176
}
164177

178+
private static Object convertOracleJsonValue(OracleJsonValue oracleJson) {
179+
if (oracleJson instanceof OracleJsonObject) {
180+
OracleJsonObject obj = (OracleJsonObject) oracleJson;
181+
Map<String, Object> map = new LinkedHashMap<>(obj.size());
182+
for (Map.Entry<String, OracleJsonValue> entry : obj.entrySet()) {
183+
map.put(entry.getKey(), convertOracleJsonValue(entry.getValue()));
184+
}
185+
return new JsonObject(map);
186+
} else if (oracleJson instanceof OracleJsonArray) {
187+
OracleJsonArray arr = (OracleJsonArray) oracleJson;
188+
List<Object> list = new ArrayList<>(arr.size());
189+
for (OracleJsonValue element : arr) {
190+
list.add(convertOracleJsonValue(element));
191+
}
192+
return new JsonArray(list);
193+
} else if (oracleJson instanceof OracleJsonString) {
194+
return ((OracleJsonString) oracleJson).getString();
195+
} else if (oracleJson instanceof OracleJsonDecimal) {
196+
return ((OracleJsonDecimal) oracleJson).bigDecimalValue();
197+
} else if (oracleJson instanceof OracleJsonDouble) {
198+
return ((OracleJsonDouble) oracleJson).doubleValue();
199+
} else if (oracleJson instanceof OracleJsonFloat) {
200+
return ((OracleJsonFloat) oracleJson).floatValue();
201+
} else {
202+
switch (oracleJson.getOracleJsonType()) {
203+
case TRUE:
204+
return Boolean.TRUE;
205+
case FALSE:
206+
return Boolean.FALSE;
207+
case NULL:
208+
return Tuple.JSON_NULL;
209+
default:
210+
return null;
211+
}
212+
}
213+
}
214+
165215
public static boolean isFatal(SQLException e) {
166216
int errorCode = e.getErrorCode();
167217
return errorCode == 28

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/OracleRow.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,20 +138,6 @@ public Numeric getNumeric(int pos) {
138138
return null;
139139
}
140140

141-
/**
142-
* Get a {@link JsonObject} or {@link JsonArray} value.
143-
*/
144-
public Object getJson(int pos) {
145-
Object val = getValue(pos);
146-
if (val instanceof JsonObject) {
147-
return val;
148-
} else if (val instanceof JsonArray) {
149-
return val;
150-
} else {
151-
return null;
152-
}
153-
}
154-
155141
public Character[] getArrayOfChars(int pos) {
156142
Object val = getValue(pos);
157143
if (val instanceof Character[]) {

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/RowReader.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.vertx.sqlclient.Row;
2121
import io.vertx.sqlclient.internal.RowDescriptorBase;
2222
import oracle.jdbc.OracleResultSet;
23+
import oracle.sql.json.OracleJsonValue;
2324

2425
import java.sql.ResultSetMetaData;
2526
import java.sql.SQLException;
@@ -64,7 +65,11 @@ public RowReader(ContextInternal context, Collector<Row, C, R> collector, Oracle
6465
int cols = metaData.getColumnCount();
6566
classes = new ArrayList<>(cols);
6667
for (int i = 1; i <= cols; i++) {
67-
classes.add(getType(metaData.getColumnClassName(i)));
68+
if ("JSON".equals(metaData.getColumnTypeName(i))) {
69+
classes.add(OracleJsonValue.class);
70+
} else {
71+
classes.add(getType(metaData.getColumnClassName(i)));
72+
}
6873
}
6974
Flow.Publisher<Row> publisher = ors.publisherOracle(this);
7075
description = OracleRowDescriptor.create(metaData);

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleCursorQueryCommand.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ protected String query() {
7070

7171
@Override
7272
protected void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
73+
java.sql.ParameterMetaData meta = parameterMetaData(ps);
7374
for (int i = 0; i < params.size(); i++) {
74-
// we must convert types (to comply to JDBC)
75-
Object value = adaptType(conn, params.getValue(i));
75+
Object value = adaptType(conn, params.getValue(i), isJsonParameter(meta, i + 1));
7676
ps.setObject(i + 1, value);
7777
}
7878
}

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedBatchQueryCommand.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ protected String query() {
6363

6464
@Override
6565
protected void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
66+
java.sql.ParameterMetaData meta = parameterMetaData(ps);
6667
for (Tuple params : listParams) {
6768
for (int i = 0; i < params.size(); i++) {
68-
// we must convert types (to comply to JDBC)
69-
Object value = adaptType(conn, params.getValue(i));
69+
Object value = adaptType(conn, params.getValue(i), isJsonParameter(meta, i + 1));
7070
ps.setObject(i + 1, value);
7171
}
7272
ps.addBatch();

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedQueryCommand.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import oracle.jdbc.OraclePreparedStatement;
2323

2424
import java.sql.Connection;
25+
import java.sql.ParameterMetaData;
2526
import java.sql.PreparedStatement;
2627
import java.sql.SQLException;
2728
import java.util.stream.Collector;
@@ -53,9 +54,9 @@ protected String query() {
5354

5455
@Override
5556
protected void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
57+
ParameterMetaData meta = parameterMetaData(ps);
5658
for (int i = 0; i < params.size(); i++) {
57-
// we must convert types (to comply to JDBC)
58-
Object value = adaptType(conn, params.getValue(i));
59+
Object value = adaptType(conn, params.getValue(i), isJsonParameter(meta, i + 1));
5960
ps.setObject(i + 1, value);
6061
}
6162
}

vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleQueryCommand.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,28 @@
1515
import io.vertx.core.VertxException;
1616
import io.vertx.core.buffer.Buffer;
1717
import io.vertx.core.internal.ContextInternal;
18+
import io.vertx.core.internal.logging.Logger;
19+
import io.vertx.core.internal.logging.LoggerFactory;
1820
import io.vertx.core.json.JsonArray;
21+
import io.vertx.core.json.JsonObject;
1922
import io.vertx.oracleclient.OracleConnectOptions;
2023
import io.vertx.oracleclient.OraclePrepareOptions;
2124
import io.vertx.oracleclient.data.Blob;
2225
import io.vertx.oracleclient.impl.Helper;
2326
import io.vertx.oracleclient.impl.OracleRow;
2427
import io.vertx.oracleclient.impl.OracleRowDescriptor;
2528
import io.vertx.sqlclient.Row;
29+
import io.vertx.sqlclient.Tuple;
2630
import io.vertx.sqlclient.desc.ColumnDescriptor;
2731
import io.vertx.sqlclient.internal.RowDescriptorBase;
2832
import oracle.jdbc.OracleConnection;
2933
import oracle.jdbc.OraclePreparedStatement;
3034
import oracle.sql.TIMESTAMPTZ;
35+
import io.vertx.core.json.Json;
36+
import oracle.sql.json.OracleJsonFactory;
37+
import oracle.sql.json.OracleJsonValue;
38+
39+
import java.io.StringReader;
3140

3241
import java.sql.*;
3342
import java.time.Instant;
@@ -40,6 +49,10 @@
4049

4150
public abstract class OracleQueryCommand<C, R> extends OracleCommand<Boolean> {
4251

52+
private static final Logger logger = LoggerFactory.getLogger(OracleQueryCommand.class);
53+
54+
private static final OracleJsonFactory JSON_FACTORY = new OracleJsonFactory();
55+
4356
private final Collector<Row, C, R> collector;
4457
private final OracleConnectOptions connectOptions;
4558

@@ -174,11 +187,49 @@ protected Object adaptType(Connection conn, Object value) throws SQLException {
174187
// -> RAW
175188
Buffer buffer = (Buffer) value;
176189
return buffer.getBytes();
190+
} else if (value instanceof JsonObject) {
191+
return JSON_FACTORY.createJsonTextValue(new StringReader(((JsonObject) value).encode()));
192+
} else if (value instanceof JsonArray) {
193+
return JSON_FACTORY.createJsonTextValue(new StringReader(((JsonArray) value).encode()));
194+
} else if (value == Tuple.JSON_NULL) {
195+
return JSON_FACTORY.createNull();
177196
}
178197

179198
return value;
180199
}
181200

201+
protected static boolean isJsonParameter(ParameterMetaData meta, int oneBasedIndex) {
202+
if (meta == null) {
203+
return false;
204+
}
205+
try {
206+
return "JSON".equals(meta.getParameterTypeName(oneBasedIndex));
207+
} catch (SQLException e) {
208+
if (logger.isDebugEnabled()) {
209+
logger.debug("Failed to get parameter type name for index " + oneBasedIndex, e);
210+
}
211+
return false;
212+
}
213+
}
214+
215+
protected static ParameterMetaData parameterMetaData(PreparedStatement ps) {
216+
try {
217+
return ps.getParameterMetaData();
218+
} catch (SQLException e) {
219+
if (logger.isDebugEnabled()) {
220+
logger.debug("Failed to get parameter metadata", e);
221+
}
222+
return null;
223+
}
224+
}
225+
226+
protected Object adaptType(Connection conn, Object value, boolean jsonTarget) throws SQLException {
227+
if (jsonTarget && (value instanceof String || value instanceof Number || value instanceof Boolean)) {
228+
return JSON_FACTORY.createJsonTextValue(new StringReader(Json.encode(value)));
229+
}
230+
return adaptType(conn, value);
231+
}
232+
182233
protected abstract Future<Boolean> doExecute(OraclePreparedStatement ps, boolean returnAutoGeneratedKeys);
183234

184235
protected OracleResponse<R> decode(Statement statement, boolean returnedResultSet, boolean returnedKeys) throws SQLException {
@@ -238,11 +289,21 @@ private void decodeResultSet(ResultSet rs, OracleResponse<R> response) throws SQ
238289
int size = 0;
239290
ResultSetMetaData metaData = rs.getMetaData();
240291
RowDescriptorBase desc = OracleRowDescriptor.create(metaData);
292+
int columnCount = metaData.getColumnCount();
293+
boolean[] jsonColumns = new boolean[columnCount];
294+
for (int i = 0; i < columnCount; i++) {
295+
jsonColumns[i] = "JSON".equals(metaData.getColumnTypeName(i + 1));
296+
}
241297
while (rs.next()) {
242298
size++;
243299
Row row = new OracleRow(desc);
244-
for (int i = 1; i <= metaData.getColumnCount(); i++) {
245-
Object res = Helper.convertSqlValue(rs.getObject(i));
300+
for (int i = 1; i <= columnCount; i++) {
301+
Object res;
302+
if (jsonColumns[i - 1]) {
303+
res = Helper.convertSqlValue(rs.getObject(i, OracleJsonValue.class));
304+
} else {
305+
res = Helper.convertSqlValue(rs.getObject(i));
306+
}
246307
row.addValue(res);
247308
}
248309
accumulator.accept(container, row);

0 commit comments

Comments
 (0)