From 42ac3b737895740c88b40ef391dc1a4de2119a7f Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 9 Jul 2026 14:45:08 +0200 Subject: [PATCH] Support Oracle JSON data type for reading and writing Closes #1469 Oracle 21c introduced a native JSON column type. Signed-off-by: Thomas Segismont --- .../src/main/asciidoc/index.adoc | 33 +++ .../java/examples/OracleClientExamples.java | 47 +++- .../io/vertx/oracleclient/impl/Helper.java | 54 +++- .../io/vertx/oracleclient/impl/OracleRow.java | 14 - .../io/vertx/oracleclient/impl/RowReader.java | 7 +- .../commands/OracleCursorQueryCommand.java | 4 +- .../OraclePreparedBatchQueryCommand.java | 4 +- .../commands/OraclePreparedQueryCommand.java | 5 +- .../impl/commands/OracleQueryCommand.java | 65 ++++- .../oracleclient/OracleJsonDataTypeTest.java | 266 ++++++++++++++++++ 10 files changed, 459 insertions(+), 40 deletions(-) create mode 100644 vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java diff --git a/vertx-oracle-client/src/main/asciidoc/index.adoc b/vertx-oracle-client/src/main/asciidoc/index.adoc index 540349e34..744c8de64 100644 --- a/vertx-oracle-client/src/main/asciidoc/index.adoc +++ b/vertx-oracle-client/src/main/asciidoc/index.adoc @@ -200,6 +200,10 @@ Currently the client supports the following Oracle data types: | `BLOB` | `io.vertx.core.buffer.Buffer` | See <> + +| `JSON` +| `java.lang.Object` +| Native JSON type (Oracle 21c+). See <> |=== 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 {@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[] diff --git a/vertx-oracle-client/src/main/java/examples/OracleClientExamples.java b/vertx-oracle-client/src/main/java/examples/OracleClientExamples.java index 944a6c119..5b0cc8d81 100644 --- a/vertx-oracle-client/src/main/java/examples/OracleClientExamples.java +++ b/vertx-oracle-client/src/main/java/examples/OracleClientExamples.java @@ -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) { 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 0fa088094..adb31d68c 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 @@ -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 @@ -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; @@ -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 map = new LinkedHashMap<>(obj.size()); + for (Map.Entry entry : obj.entrySet()) { + map.put(entry.getKey(), convertOracleJsonValue(entry.getValue())); + } + 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)); + } + 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 diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/OracleRow.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/OracleRow.java index 55f4c1ec7..09f8a4a3d 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/OracleRow.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/OracleRow.java @@ -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[]) { diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/RowReader.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/RowReader.java index f54e55ce2..2b7895103 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/RowReader.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/RowReader.java @@ -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; @@ -64,7 +65,11 @@ public RowReader(ContextInternal context, Collector 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 publisher = ors.publisherOracle(this); description = OracleRowDescriptor.create(metaData); diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleCursorQueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleCursorQueryCommand.java index b100c17b1..d65c18e2c 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleCursorQueryCommand.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OracleCursorQueryCommand.java @@ -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); } } diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedBatchQueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedBatchQueryCommand.java index cc2274f32..d186ddc64 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedBatchQueryCommand.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedBatchQueryCommand.java @@ -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(); diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedQueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedQueryCommand.java index 58f119692..0e68fe346 100644 --- a/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedQueryCommand.java +++ b/vertx-oracle-client/src/main/java/io/vertx/oracleclient/impl/commands/OraclePreparedQueryCommand.java @@ -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; @@ -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); } } 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 a0c2a5869..086f98f38 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 @@ -15,7 +15,10 @@ 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; @@ -23,11 +26,17 @@ 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; @@ -40,6 +49,10 @@ public abstract class OracleQueryCommand extends OracleCommand { + private static final Logger logger = LoggerFactory.getLogger(OracleQueryCommand.class); + + private static final OracleJsonFactory JSON_FACTORY = new OracleJsonFactory(); + private final Collector collector; private final OracleConnectOptions connectOptions; @@ -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 doExecute(OraclePreparedStatement ps, boolean returnAutoGeneratedKeys); protected OracleResponse decode(Statement statement, boolean returnedResultSet, boolean returnedKeys) throws SQLException { @@ -238,11 +289,21 @@ private void decodeResultSet(ResultSet rs, OracleResponse 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); diff --git a/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java b/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java new file mode 100644 index 000000000..c69d1bdf7 --- /dev/null +++ b/vertx-oracle-client/src/test/java/tests/oracleclient/OracleJsonDataTypeTest.java @@ -0,0 +1,266 @@ +/* + * 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 + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package tests.oracleclient; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import java.math.BigDecimal; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.oracleclient.OracleBuilder; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.Tuple; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import tests.oracleclient.junit.OracleRule; + +@RunWith(VertxUnitRunner.class) +public class OracleJsonDataTypeTest extends OracleTestBase { + + @ClassRule + public static OracleRule oracle = OracleRule.SHARED_INSTANCE; + + Pool pool; + + @Before + public void setUp(TestContext ctx) { + pool = OracleBuilder.pool(builder -> builder.connectingTo(oracle.options()).using(vertx)); + pool.query("BEGIN EXECUTE IMMEDIATE 'DROP TABLE json_test'; EXCEPTION WHEN OTHERS THEN NULL; END;") + .execute() + .compose(v -> pool.query("CREATE TABLE json_test (id NUMBER PRIMARY KEY, data JSON)").execute()) + .onComplete(ctx.asyncAssertSuccess()); + } + + @After + public void tearDown(TestContext ctx) { + pool.query("DROP TABLE json_test") + .execute() + .compose(v -> pool.close()) + .onComplete(ctx.asyncAssertSuccess()); + } + + @Test + public void testDecodeJsonObject(TestContext ctx) { + JsonObject expected = new JsonObject() + .put("name", "Alice") + .put("age", 30); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(1, expected)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(1))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(expected, row.getJson(0)); + ctx.assertEquals(expected, row.getJsonObject(0)); + })); + } + + @Test + public void testDecodeJsonArray(TestContext ctx) { + JsonArray expected = new JsonArray().add(1).add("two").add(true); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(2, expected)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(2))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(expected, row.getJson(0)); + ctx.assertEquals(expected, row.getJsonArray(0)); + })); + } + + @Test + public void testDecodeJsonLiteralNull(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(3, Tuple.JSON_NULL)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(3))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(Tuple.JSON_NULL, row.getJson(0)); + })); + } + + @Test + public void testDecodeSqlNull(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(4, (Object) null)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(4))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertNull(row.getJson(0)); + ctx.assertNull(row.getJsonObject(0)); + ctx.assertNull(row.getJsonArray(0)); + })); + } + + @Test + public void testDecodeUnicode(TestContext ctx) { + JsonObject expected = new JsonObject() + .put("emoji", "😀") + .put("japanese", "フレームワーク") + .put("arabic", "مرحبا"); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(6, expected)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(6))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(expected, row.getJsonObject(0)); + })); + } + + @Test + public void testDecodeJsonString(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(7, "hello")) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(7))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + ctx.assertEquals("hello", rows.iterator().next().getJson(0)); + })); + } + + @Test + public void testDecodeJsonNumber(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(8, 42)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(8))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + ctx.assertEquals(new BigDecimal("42"), rows.iterator().next().getJson(0)); + })); + } + + @Test + public void testDecodeJsonBooleanTrue(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(9, true)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(9))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + ctx.assertEquals(Boolean.TRUE, rows.iterator().next().getJson(0)); + })); + } + + @Test + public void testDecodeJsonBooleanFalse(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(10, false)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(10))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + ctx.assertEquals(Boolean.FALSE, rows.iterator().next().getJson(0)); + })); + } + + @Test + public void testDecodeNestedJsonObject(TestContext ctx) { + JsonObject expected = new JsonObject() + .put("address", new JsonObject() + .put("city", "New York") + .put("zip", "10001")) + .put("tags", new JsonArray().add("admin").add("user")); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(5, expected)) + .compose(v -> pool.preparedQuery("SELECT data FROM json_test WHERE id = ?").execute(Tuple.of(5))) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(expected, row.getJsonObject(0)); + })); + } + + @Test + public void testDecodeJsonObjectUsingCursor(TestContext ctx) { + JsonObject expected = new JsonObject() + .put("name", "Alice") + .put("age", 30); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(11, expected)) + .compose(v -> pool.getConnection()) + .onComplete(ctx.asyncAssertSuccess(conn -> { + conn.prepare("SELECT data FROM json_test WHERE id = 11") + .onComplete(ctx.asyncAssertSuccess(ps -> { + ps.cursor().read(10).onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(expected, row.getJsonObject(0)); + conn.close(); + })); + })); + })); + } + + @Test + public void testDecodeJsonArrayUsingCursor(TestContext ctx) { + JsonArray expected = new JsonArray().add(1).add("two").add(true); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(12, expected)) + .compose(v -> pool.getConnection()) + .onComplete(ctx.asyncAssertSuccess(conn -> { + conn.prepare("SELECT data FROM json_test WHERE id = 12") + .onComplete(ctx.asyncAssertSuccess(ps -> { + ps.cursor().read(10).onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + ctx.assertEquals(expected, row.getJsonArray(0)); + conn.close(); + })); + })); + })); + } + + @Test + public void testBatchInsertJsonScalars(TestContext ctx) { + List batch = new ArrayList<>(); + batch.add(Tuple.of(20, "hello")); + batch.add(Tuple.of(21, 42)); + batch.add(Tuple.of(22, true)); + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .executeBatch(batch) + .compose(v -> pool.preparedQuery("SELECT id, data FROM json_test WHERE id >= 20 ORDER BY id").execute()) + .onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(3, rows.size()); + List values = new ArrayList<>(); + for (Row row : rows) { + values.add(row.getJson(1)); + } + ctx.assertEquals("hello", values.get(0)); + ctx.assertEquals(new BigDecimal("42"), values.get(1)); + ctx.assertEquals(Boolean.TRUE, values.get(2)); + })); + } + + @Test + public void testCursorInsertJsonScalar(TestContext ctx) { + pool.preparedQuery("INSERT INTO json_test (id, data) VALUES (?, ?)") + .execute(Tuple.of(30, "cursor-test")) + .compose(v -> pool.getConnection()) + .onComplete(ctx.asyncAssertSuccess(conn -> { + conn.prepare("SELECT data FROM json_test WHERE id = ?") + .onComplete(ctx.asyncAssertSuccess(ps -> { + ps.cursor(Tuple.of(30)).read(10).onComplete(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + ctx.assertEquals("cursor-test", rows.iterator().next().getJson(0)); + conn.close(); + })); + })); + })); + } +}