diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/Helper.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/Helper.java index be607161e..44df3fe22 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/Helper.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/Helper.java @@ -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; @@ -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 { @@ -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 map = new LinkedHashMap<>(obj.size()); - for (Map.Entry 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 list = new ArrayList<>(arr.size()); - for (OracleJsonValue element : arr) { - list.add(convertOracleJsonValue(element, false)); + switch (oracleJson.getOracleJsonType()) { + case OBJECT: { + OracleJsonObject obj = (OracleJsonObject) oracleJson; + Map map = new LinkedHashMap<>(obj.size()); + for (Map.Entry 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 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; } } diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleQueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleQueryCommand.java index 10107ad2f..0e77655aa 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleQueryCommand.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleQueryCommand.java @@ -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; @@ -178,10 +184,8 @@ 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(); } @@ -189,6 +193,60 @@ protected Object adaptType(Connection conn, Object value) throws SQLException { 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 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 doExecute(OraclePreparedStatement ps, boolean returnAutoGeneratedKeys); protected OracleResponse decode(Statement statement, boolean returnedResultSet, boolean returnedKeys) throws SQLException { diff --git a/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java b/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java index 124da72e8..b70d60ed2 100644 --- a/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java +++ b/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java @@ -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; @@ -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); + })); + } }