Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions vertx-oracle-client/src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ Currently the client supports the following Oracle data types:
| `BLOB`
| `io.vertx.core.buffer.Buffer`
| See <<handling-blob>>

| `JSON`
| `java.lang.Object`
| Native JSON type (Oracle 21c+). See <<handling-json>>
|===

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

[[handling-json]]
=== Handling JSON

Oracle Database 21c and later provide native JSON type support.

IMPORTANT: Requires Oracle Database 21c or later and Oracle JDBC driver 21.1 or later.

The `JSON` data type is represented by the following Java types:

- `io.vertx.core.json.JsonObject`
- `io.vertx.core.json.JsonArray`
- `java.lang.String` (for JSON string scalars)
- `java.math.BigDecimal` (for JSON decimal numbers)
- `java.lang.Double` (for JSON double-precision numbers)
- `java.lang.Boolean`
- `io.vertx.sqlclient.Tuple#JSON_NULL` for the JSON null literal

[source,$lang]
----
{@link examples.OracleClientExamples#jsonExample(io.vertx.sqlclient.Pool)}
----

Oracle distinguishes between SQL NULL and the JSON null literal:

[source,$lang]
----
{@link examples.OracleClientExamples#jsonNullExample(io.vertx.sqlclient.Pool)}
----

== Tracing queries

include::tracing.adoc[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,23 +287,40 @@ public void booleanExample02(SqlClient client) {
});
}

public void jsonExample() {
public void jsonExample(Pool pool) {
JsonObject json = new JsonObject()
.put("name", "Alice")
.put("age", 30);

// Create a tuple
Tuple tuple = Tuple.of(
Tuple.JSON_NULL,
new JsonObject().put("foo", "bar"),
3);

// Retrieving json
Object value = tuple.getValue(0); // Expect JSON_NULL

//
value = tuple.get(JsonObject.class, 1); // Expect JSON object
pool
.preparedQuery("INSERT INTO users (id, data) VALUES (?, ?)")
.execute(Tuple.of(1, json))
.compose(v -> pool
.preparedQuery("SELECT data FROM users WHERE id = ?")
.execute(Tuple.of(1)))
.onComplete(ar -> {
if (ar.succeeded()) {
Row row = ar.result().iterator().next();
JsonObject data = row.getJsonObject(0);
System.out.println("Received: " + data.getString("name"));
}
});
}

//
value = tuple.get(Integer.class, 2); // Expect 3
value = tuple.getInteger(2); // Expect 3
public void jsonNullExample(Pool pool) {
pool
.preparedQuery("INSERT INTO users (id, data) VALUES (?, ?)")
.execute(Tuple.of(2, Tuple.JSON_NULL))
.compose(v -> pool
.preparedQuery("SELECT data FROM users WHERE id = ?")
.execute(Tuple.of(2)))
.onComplete(ar -> {
if (ar.succeeded()) {
Row row = ar.result().iterator().next();
Object value = row.getJson(0);
System.out.println("Is JSON null: " + (value == Tuple.JSON_NULL));
}
});
}

public void numericExample(Row row) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2023 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
Expand All @@ -10,13 +10,22 @@
*/
package io.vertx.oracleclient.impl;

import io.vertx.core.*;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.oracleclient.OracleException;
import io.vertx.sqlclient.Tuple;
import oracle.sql.TIMESTAMPTZ;
import oracle.sql.json.*;

import java.sql.*;
import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

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

if (value instanceof OracleJsonValue) {
return convertOracleJsonValue((OracleJsonValue) value);
}

// fallback to String
return value.toString();
}

private static Object convertOracleJsonValue(OracleJsonValue oracleJson) {
if (oracleJson instanceof OracleJsonObject) {
OracleJsonObject obj = (OracleJsonObject) oracleJson;
Map<String, Object> map = new LinkedHashMap<>(obj.size());
for (Map.Entry<String, OracleJsonValue> entry : obj.entrySet()) {
map.put(entry.getKey(), convertOracleJsonValue(entry.getValue()));
}
return new JsonObject(map);
} else if (oracleJson instanceof OracleJsonArray) {
OracleJsonArray arr = (OracleJsonArray) oracleJson;
List<Object> list = new ArrayList<>(arr.size());
for (OracleJsonValue element : arr) {
list.add(convertOracleJsonValue(element));
}
return new JsonArray(list);
} else if (oracleJson instanceof OracleJsonString) {
return ((OracleJsonString) oracleJson).getString();
} else if (oracleJson instanceof OracleJsonDecimal) {
return ((OracleJsonDecimal) oracleJson).bigDecimalValue();
} else if (oracleJson instanceof OracleJsonDouble) {
return ((OracleJsonDouble) oracleJson).doubleValue();
} else if (oracleJson instanceof OracleJsonFloat) {
return ((OracleJsonFloat) oracleJson).floatValue();
} else {
switch (oracleJson.getOracleJsonType()) {
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case NULL:
return Tuple.JSON_NULL;
default:
return null;
}
}
}

public static boolean isFatal(SQLException e) {
int errorCode = e.getErrorCode();
return errorCode == 28
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,6 @@ public Numeric getNumeric(int pos) {
return null;
}

/**
* Get a {@link JsonObject} or {@link JsonArray} value.
*/
public Object getJson(int pos) {
Object val = getValue(pos);
if (val instanceof JsonObject) {
return val;
} else if (val instanceof JsonArray) {
return val;
} else {
return null;
}
}

public Character[] getArrayOfChars(int pos) {
Object val = getValue(pos);
if (val instanceof Character[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.internal.RowDescriptorBase;
import oracle.jdbc.OracleResultSet;
import oracle.sql.json.OracleJsonValue;

import java.sql.ResultSetMetaData;
import java.sql.SQLException;
Expand Down Expand Up @@ -64,7 +65,11 @@ public RowReader(ContextInternal context, Collector<Row, C, R> collector, Oracle
int cols = metaData.getColumnCount();
classes = new ArrayList<>(cols);
for (int i = 1; i <= cols; i++) {
classes.add(getType(metaData.getColumnClassName(i)));
if ("JSON".equals(metaData.getColumnTypeName(i))) {
classes.add(OracleJsonValue.class);
} else {
classes.add(getType(metaData.getColumnClassName(i)));
}
}
Flow.Publisher<Row> publisher = ors.publisherOracle(this);
description = OracleRowDescriptor.create(metaData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ protected String query() {

@Override
protected void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
java.sql.ParameterMetaData meta = parameterMetaData(ps);
for (int i = 0; i < params.size(); i++) {
// we must convert types (to comply to JDBC)
Object value = adaptType(conn, params.getValue(i));
Object value = adaptType(conn, params.getValue(i), isJsonParameter(meta, i + 1));
ps.setObject(i + 1, value);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ protected String query() {

@Override
protected void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
java.sql.ParameterMetaData meta = parameterMetaData(ps);
for (Tuple params : listParams) {
for (int i = 0; i < params.size(); i++) {
// we must convert types (to comply to JDBC)
Object value = adaptType(conn, params.getValue(i));
Object value = adaptType(conn, params.getValue(i), isJsonParameter(meta, i + 1));
ps.setObject(i + 1, value);
}
ps.addBatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import oracle.jdbc.OraclePreparedStatement;

import java.sql.Connection;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.stream.Collector;
Expand Down Expand Up @@ -53,9 +54,9 @@ protected String query() {

@Override
protected void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
ParameterMetaData meta = parameterMetaData(ps);
for (int i = 0; i < params.size(); i++) {
// we must convert types (to comply to JDBC)
Object value = adaptType(conn, params.getValue(i));
Object value = adaptType(conn, params.getValue(i), isJsonParameter(meta, i + 1));
ps.setObject(i + 1, value);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,28 @@
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.internal.ContextInternal;
import io.vertx.core.internal.logging.Logger;
import io.vertx.core.internal.logging.LoggerFactory;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.oracleclient.OracleConnectOptions;
import io.vertx.oracleclient.OraclePrepareOptions;
import io.vertx.oracleclient.data.Blob;
import io.vertx.oracleclient.impl.Helper;
import io.vertx.oracleclient.impl.OracleRow;
import io.vertx.oracleclient.impl.OracleRowDescriptor;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import io.vertx.sqlclient.desc.ColumnDescriptor;
import io.vertx.sqlclient.internal.RowDescriptorBase;
import oracle.jdbc.OracleConnection;
import oracle.jdbc.OraclePreparedStatement;
import oracle.sql.TIMESTAMPTZ;
import io.vertx.core.json.Json;
import oracle.sql.json.OracleJsonFactory;
import oracle.sql.json.OracleJsonValue;

import java.io.StringReader;

import java.sql.*;
import java.time.Instant;
Expand All @@ -40,6 +49,10 @@

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

private static final Logger logger = LoggerFactory.getLogger(OracleQueryCommand.class);

private static final OracleJsonFactory JSON_FACTORY = new OracleJsonFactory();

private final Collector<Row, C, R> collector;
private final OracleConnectOptions connectOptions;

Expand Down Expand Up @@ -174,11 +187,49 @@ protected Object adaptType(Connection conn, Object value) throws SQLException {
// -> RAW
Buffer buffer = (Buffer) value;
return buffer.getBytes();
} else if (value instanceof JsonObject) {
return JSON_FACTORY.createJsonTextValue(new StringReader(((JsonObject) value).encode()));
} else if (value instanceof JsonArray) {
return JSON_FACTORY.createJsonTextValue(new StringReader(((JsonArray) value).encode()));
} else if (value == Tuple.JSON_NULL) {
return JSON_FACTORY.createNull();
}

return value;
}

protected static boolean isJsonParameter(ParameterMetaData meta, int oneBasedIndex) {
if (meta == null) {
return false;
}
try {
return "JSON".equals(meta.getParameterTypeName(oneBasedIndex));
} catch (SQLException e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to get parameter type name for index " + oneBasedIndex, e);
}
return false;
}
}

protected static ParameterMetaData parameterMetaData(PreparedStatement ps) {
try {
return ps.getParameterMetaData();
} catch (SQLException e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to get parameter metadata", e);
}
return null;
}
}

protected Object adaptType(Connection conn, Object value, boolean jsonTarget) throws SQLException {
if (jsonTarget && (value instanceof String || value instanceof Number || value instanceof Boolean)) {
return JSON_FACTORY.createJsonTextValue(new StringReader(Json.encode(value)));
}
return adaptType(conn, value);
}

protected abstract Future<Boolean> doExecute(OraclePreparedStatement ps, boolean returnAutoGeneratedKeys);

protected OracleResponse<R> decode(Statement statement, boolean returnedResultSet, boolean returnedKeys) throws SQLException {
Expand Down Expand Up @@ -238,11 +289,21 @@ private void decodeResultSet(ResultSet rs, OracleResponse<R> response) throws SQ
int size = 0;
ResultSetMetaData metaData = rs.getMetaData();
RowDescriptorBase desc = OracleRowDescriptor.create(metaData);
int columnCount = metaData.getColumnCount();
boolean[] jsonColumns = new boolean[columnCount];
for (int i = 0; i < columnCount; i++) {
jsonColumns[i] = "JSON".equals(metaData.getColumnTypeName(i + 1));
}
while (rs.next()) {
size++;
Row row = new OracleRow(desc);
for (int i = 1; i <= metaData.getColumnCount(); i++) {
Object res = Helper.convertSqlValue(rs.getObject(i));
for (int i = 1; i <= columnCount; i++) {
Object res;
if (jsonColumns[i - 1]) {
res = Helper.convertSqlValue(rs.getObject(i, OracleJsonValue.class));
} else {
res = Helper.convertSqlValue(rs.getObject(i));
}
row.addValue(res);
}
accumulator.accept(container, row);
Expand Down
Loading