Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import io.vertx.core.Future;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
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.OracleException;
Expand All @@ -30,6 +32,8 @@

public class Helper {

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

public static void closeQuietly(AutoCloseable autoCloseable) {
if (autoCloseable != null) {
try {
Expand Down Expand Up @@ -175,39 +179,54 @@ public static Object convertSqlValue(Object value) throws SQLException {
}

private static Object convertOracleJsonValue(OracleJsonValue oracleJson, boolean topLevel) {
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(), false));
}
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, false));
switch (oracleJson.getOracleJsonType()) {
case OBJECT: {
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(), false));
}
return new JsonObject(map);
}
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 topLevel ? Tuple.JSON_NULL : null;
default:
return null;
case ARRAY: {
OracleJsonArray arr = (OracleJsonArray) oracleJson;
List<Object> list = new ArrayList<>(arr.size());
for (OracleJsonValue element : arr) {
list.add(convertOracleJsonValue(element, false));
}
return new JsonArray(list);
}
case STRING:
return ((OracleJsonString) oracleJson).getString();
case DECIMAL:
return ((OracleJsonDecimal) oracleJson).bigDecimalValue();
case DOUBLE:
return ((OracleJsonDouble) oracleJson).doubleValue();
case FLOAT:
return ((OracleJsonFloat) oracleJson).floatValue();
case TIMESTAMP:
return ((OracleJsonTimestamp) oracleJson).getLocalDateTime().toString();
case TIMESTAMPTZ:
return ((OracleJsonTimestampTZ) oracleJson).getOffsetDateTime().toString();
case DATE:
return ((OracleJsonDate) oracleJson).getLocalDateTime().toString();
case BINARY:
return Buffer.buffer(((OracleJsonBinary) oracleJson).getBytes());
case INTERVALDS:
return ((OracleJsonIntervalDS) oracleJson).getDuration().toString();
case INTERVALYM:
return ((OracleJsonIntervalYM) oracleJson).getPeriod().toString();
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case NULL:
return topLevel ? Tuple.JSON_NULL : null;
default:
if (logger.isWarnEnabled()) {
logger.warn("Unsupported Oracle JSON type: " + oracleJson.getOracleJsonType());
}
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@
import oracle.jdbc.OraclePreparedStatement;
import oracle.sql.TIMESTAMPTZ;
import oracle.sql.json.OracleJsonFactory;
import oracle.sql.json.OracleJsonGenerator;
import oracle.sql.json.OracleJsonValue;

import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.*;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collector;

Expand Down Expand Up @@ -178,17 +184,69 @@ 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 instanceof JsonObject || value instanceof JsonArray) {
return encodeJsonBinary(value);
} else if (value == Tuple.JSON_NULL) {
return JSON_FACTORY.createNull();
}

return value;
}

private static OracleJsonValue encodeJsonBinary(Object value) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (OracleJsonGenerator gen = JSON_FACTORY.createJsonBinaryGenerator(out)) {
writeValue(gen, value);
}
return JSON_FACTORY.createJsonBinaryValue(new ByteArrayInputStream(out.toByteArray()));
}

private static void writeValue(OracleJsonGenerator gen, Object value) {
if (value == null) {
gen.writeNull();
} else if (value instanceof JsonObject) {
gen.writeStartObject();
for (Map.Entry<String, Object> entry : ((JsonObject) value).getMap().entrySet()) {
gen.writeKey(entry.getKey());
writeValue(gen, entry.getValue());
}
gen.writeEnd();
} else if (value instanceof JsonArray) {
gen.writeStartArray();
for (Object element : ((JsonArray) value).getList()) {
writeValue(gen, element);
}
gen.writeEnd();
} else if (value instanceof String) {
gen.write((String) value);
} else if (value instanceof Integer) {
gen.write((int) value);
} else if (value instanceof Long) {
gen.write((long) value);
} else if (value instanceof Float) {
gen.write((float) value);
} else if (value instanceof Double) {
gen.write((double) value);
} else if (value instanceof BigDecimal) {
gen.write((BigDecimal) value);
} else if (value instanceof BigInteger) {
gen.write(new BigDecimal((BigInteger) value));
} else if (value instanceof Boolean) {
gen.write((boolean) value);
} else if (value instanceof Instant) {
gen.write(((Instant) value).atOffset(ZoneOffset.UTC));
} else if (value instanceof byte[]) {
gen.write((byte[]) value);
} else if (value instanceof Buffer) {
gen.write(((Buffer) value).getBytes());
} else {
if (logger.isWarnEnabled()) {
logger.warn("Unsupported JSON value type: " + value.getClass().getName() + ", falling back to toString()");
}
gen.write(value.toString());
}
}

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

protected OracleResponse<R> decode(Statement statement, boolean returnedResultSet, boolean returnedKeys) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import tests.oracleclient.junit.OracleRule;

import java.math.BigDecimal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -328,4 +329,110 @@ public void testDecodeTopLevelJsonNull(TestContext ctx) {
ctx.assertEquals(Tuple.JSON_NULL, row.getJson(0));
}));
}

@Test
public void testDecodeOsonTimestamp(TestContext ctx) {
pool.query("INSERT INTO json_test (id, data) VALUES (52, JSON_OBJECT('ts' VALUE TIMESTAMP '2023-09-21 10:00:00' RETURNING JSON))")
.execute()
.compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(52)))
.onComplete(ctx.asyncAssertSuccess(rows -> {
ctx.assertEquals(1, rows.size());
Row row = rows.iterator().next();
JsonObject obj = row.getJsonObject(0);
ctx.assertTrue(obj.getValue("ts") instanceof String);
ctx.assertEquals("2023-09-21T10:00", obj.getString("ts"));
}));
}

@Test
public void testDecodeOsonTimestampTZ(TestContext ctx) {
pool.query("INSERT INTO json_test (id, data) VALUES (53, JSON_OBJECT('ts' VALUE TIMESTAMP '2023-09-21 10:00:00 +02:00' RETURNING JSON))")
.execute()
.compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(53)))
.onComplete(ctx.asyncAssertSuccess(rows -> {
ctx.assertEquals(1, rows.size());
Row row = rows.iterator().next();
JsonObject obj = row.getJsonObject(0);
ctx.assertTrue(obj.getValue("ts") instanceof String);
java.time.OffsetDateTime expected = java.time.OffsetDateTime.parse("2023-09-21T10:00:00+02:00");
java.time.OffsetDateTime actual = java.time.OffsetDateTime.parse(obj.getString("ts"));
ctx.assertEquals(expected, actual);
}));
}

@Test
public void testDecodeOsonDate(TestContext ctx) {
pool.query("INSERT INTO json_test (id, data) VALUES (54, JSON_OBJECT('d' VALUE DATE '2023-09-21' RETURNING JSON))")
.execute()
.compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(54)))
.onComplete(ctx.asyncAssertSuccess(rows -> {
ctx.assertEquals(1, rows.size());
Row row = rows.iterator().next();
JsonObject obj = row.getJsonObject(0);
ctx.assertTrue(obj.getValue("d") instanceof String);
java.time.LocalDateTime expected = java.time.LocalDateTime.parse("2023-09-21T00:00");
java.time.LocalDateTime actual = java.time.LocalDateTime.parse(obj.getString("d"));
ctx.assertEquals(expected, actual);
}));
}

@Test
public void testDecodeOsonIntervalYearToMonth(TestContext ctx) {
pool.query("INSERT INTO json_test (id, data) VALUES (55, JSON_OBJECT('iv' VALUE TO_YMINTERVAL('P2Y3M') RETURNING JSON))")
.execute()
.compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(55)))
.onComplete(ctx.asyncAssertSuccess(rows -> {
ctx.assertEquals(1, rows.size());
Row row = rows.iterator().next();
JsonObject obj = row.getJsonObject(0);
ctx.assertTrue(obj.getValue("iv") instanceof String);
java.time.Period expected = java.time.Period.parse("P2Y3M");
java.time.Period actual = java.time.Period.parse(obj.getString("iv"));
ctx.assertEquals(expected, actual);
}));
}

@Test
public void testDecodeOsonIntervalDayToSecond(TestContext ctx) {
pool.query("INSERT INTO json_test (id, data) VALUES (56, JSON_OBJECT('iv' VALUE TO_DSINTERVAL('P1DT2H3M4.5S') RETURNING JSON))")
.execute()
.compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(56)))
.onComplete(ctx.asyncAssertSuccess(rows -> {
ctx.assertEquals(1, rows.size());
Row row = rows.iterator().next();
JsonObject obj = row.getJsonObject(0);
ctx.assertTrue(obj.getValue("iv") instanceof String);
Duration expected = Duration.parse("P1DT2H3M4.5S");
Duration actual = Duration.parse(obj.getString("iv"));
ctx.assertEquals(expected, actual);
}));
}

@Test
public void testJsonObjectRoundTrip(TestContext ctx) {
JsonObject expected = new JsonObject()
.put("str", "hello")
.put("num", 42)
.put("dbl", 3.14)
.put("bool", true)
.putNull("nil")
.put("nested", new JsonObject().put("key", "value"))
.put("arr", new JsonArray().add(1).add("two").add(false));
pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)")
.execute(Tuple.of(57, expected))
.compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(57)))
.onComplete(ctx.asyncAssertSuccess(rows -> {
ctx.assertEquals(1, rows.size());
Row row = rows.iterator().next();
JsonObject actual = row.getJsonObject(0);
ctx.assertEquals(expected.getString("str"), actual.getString("str"));
ctx.assertEquals(expected.getBoolean("bool"), actual.getBoolean("bool"));
ctx.assertNull(actual.getValue("nil"));
ctx.assertEquals(expected.getJsonObject("nested"), actual.getJsonObject("nested"));
ctx.assertEquals(expected.getJsonArray("arr").getString(1), actual.getJsonArray("arr").getString(1));
// Verify encode() works without exception
String encoded = actual.encode();
ctx.assertNotNull(encoded);
}));
}
}
Loading