diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d37213f72..90277ff9b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,8 +16,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- profile: [PostgreSQL-9,PostgreSQL-10,PostgreSQL-11,MySQL-8.0,MySQL-5.6,MySQL-5.7,MariaDB-10.4,MSSQL-2017-latest,MSSQL-2019-latest,DB2-11.5,SQL-templates]
- jdk: [8]
+ profile: [PostgreSQL-9,PostgreSQL-10,PostgreSQL-11,MySQL-8.0,MySQL-5.6,MySQL-5.7,MariaDB-10.4,MSSQL-2017-latest,MSSQL-2019-latest,DB2-11.5,Oracle-18,SQL-templates]
+ jdk: [8, 11]
+ exclude:
+ - profile: Oracle-18
+ jdk: 8
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
diff --git a/pom.xml b/pom.xml
index 05e5368df..c6e5f67e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -118,6 +118,7 @@
vertx-mssql-clientvertx-db2-clientvertx-sql-client-templates
+ vertx-oracle-client
@@ -230,6 +231,16 @@
vertx-sql-client-templates
+
+ Oracle-18
+
+ 18-slim
+
+
+ vertx-sql-client
+ vertx-oracle-client
+
+
diff --git a/vertx-oracle-client/pom.xml b/vertx-oracle-client/pom.xml
new file mode 100644
index 000000000..9fa9f1001
--- /dev/null
+++ b/vertx-oracle-client/pom.xml
@@ -0,0 +1,161 @@
+
+
+
+
+ 4.0.0
+
+
+ io.vertx
+ vertx-sql-client-parent
+ 4.2.0-SNAPSHOT
+
+
+ vertx-oracle-client
+
+ Vertx Oracle Client
+ https://github.com/eclipse-vertx/vertx-sql-client
+ The Reactive Oracle Client
+
+
+ false
+ ${project.basedir}/src/main/docs
+ ${project.basedir}/src/main/generated
+
+ 11
+ 11
+
+ 21.1.0.0
+
+
+
+
+
+
+
+ com.oracle.database.jdbc
+ ojdbc11
+ ${ojdbc.version}
+
+
+
+
+ io.vertx
+ vertx-core
+
+
+ io.vertx
+ vertx-codegen
+ true
+
+
+ io.vertx
+ vertx-docgen
+ true
+
+
+ io.vertx
+ vertx-sql-client
+
+
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+ test
+
+
+
+ io.vertx
+ vertx-sql-client
+ test-jar
+ test
+
+
+
+
+
+
+
+
+ maven-surefire-plugin
+
+ -Xmx1024M
+
+ ${project.build.directory}
+
+
+ io/vertx/pgclient/it/**
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ maven-assembly-plugin
+
+
+
+ package-sources
+
+
+ ${project.basedir}/../assembly/sources.xml
+
+
+ none
+
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 11
+ 11
+ 11
+
+
+
+
+
+
+
+
+
+
diff --git a/vertx-oracle-client/src/main/generated/io/vertx/oracle/OracleConnectOptionsConverter.java b/vertx-oracle-client/src/main/generated/io/vertx/oracle/OracleConnectOptionsConverter.java
new file mode 100644
index 000000000..1b8f7f249
--- /dev/null
+++ b/vertx-oracle-client/src/main/generated/io/vertx/oracle/OracleConnectOptionsConverter.java
@@ -0,0 +1,195 @@
+package io.vertx.oracle;
+
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.impl.JsonUtil;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Converter and mapper for {@link io.vertx.oracle.OracleConnectOptions}.
+ * NOTE: This class has been automatically generated from the {@link io.vertx.oracle.OracleConnectOptions} original class using Vert.x codegen.
+ */
+public class OracleConnectOptionsConverter {
+
+
+ public static void fromJson(Iterable> json, OracleConnectOptions obj) {
+ for (java.util.Map.Entry member : json) {
+ switch (member.getKey()) {
+ case "authenticationServices":
+ if (member.getValue() instanceof String) {
+ obj.setAuthenticationServices((String)member.getValue());
+ }
+ break;
+ case "autoGeneratedKeys":
+ if (member.getValue() instanceof Boolean) {
+ obj.setAutoGeneratedKeys((Boolean)member.getValue());
+ }
+ break;
+ case "autoGeneratedKeysIndexes":
+ if (member.getValue() instanceof JsonArray) {
+ obj.setAutoGeneratedKeysIndexes(((JsonArray)member.getValue()).copy());
+ }
+ break;
+ case "catalog":
+ if (member.getValue() instanceof String) {
+ obj.setCatalog((String)member.getValue());
+ }
+ break;
+ case "fetchDirection":
+ if (member.getValue() instanceof String) {
+ obj.setFetchDirection(io.vertx.oracle.FetchDirection.valueOf((String)member.getValue()));
+ }
+ break;
+ case "fetchSize":
+ if (member.getValue() instanceof Number) {
+ obj.setFetchSize(((Number)member.getValue()).intValue());
+ }
+ break;
+ case "keyStore":
+ if (member.getValue() instanceof String) {
+ obj.setKeyStore((String)member.getValue());
+ }
+ break;
+ case "keyStorePassword":
+ if (member.getValue() instanceof String) {
+ obj.setKeyStorePassword((String)member.getValue());
+ }
+ break;
+ case "keyStoreType":
+ if (member.getValue() instanceof String) {
+ obj.setKeyStoreType((String)member.getValue());
+ }
+ break;
+ case "maxRows":
+ if (member.getValue() instanceof Number) {
+ obj.setMaxRows(((Number)member.getValue()).intValue());
+ }
+ break;
+ case "queryTimeout":
+ if (member.getValue() instanceof Number) {
+ obj.setQueryTimeout(((Number)member.getValue()).intValue());
+ }
+ break;
+ case "readOnly":
+ if (member.getValue() instanceof Boolean) {
+ obj.setReadOnly((Boolean)member.getValue());
+ }
+ break;
+ case "resultSetConcurrency":
+ if (member.getValue() instanceof String) {
+ obj.setResultSetConcurrency(io.vertx.oracle.ResultSetConcurrency.valueOf((String)member.getValue()));
+ }
+ break;
+ case "resultSetType":
+ if (member.getValue() instanceof String) {
+ obj.setResultSetType(io.vertx.oracle.ResultSetType.valueOf((String)member.getValue()));
+ }
+ break;
+ case "schema":
+ if (member.getValue() instanceof String) {
+ obj.setSchema((String)member.getValue());
+ }
+ break;
+ case "tnsAdmin":
+ if (member.getValue() instanceof String) {
+ obj.setTnsAdmin((String)member.getValue());
+ }
+ break;
+ case "transactionIsolation":
+ if (member.getValue() instanceof String) {
+ obj.setTransactionIsolation(io.vertx.oracle.TransactionIsolation.valueOf((String)member.getValue()));
+ }
+ break;
+ case "trustStore":
+ if (member.getValue() instanceof String) {
+ obj.setTrustStore((String)member.getValue());
+ }
+ break;
+ case "trustStorePassword":
+ if (member.getValue() instanceof String) {
+ obj.setTrustStorePassword((String)member.getValue());
+ }
+ break;
+ case "trustStoreType":
+ if (member.getValue() instanceof String) {
+ obj.setTrustStoreType((String)member.getValue());
+ }
+ break;
+ case "walletLocation":
+ if (member.getValue() instanceof String) {
+ obj.setWalletLocation((String)member.getValue());
+ }
+ break;
+ case "walletPassword":
+ if (member.getValue() instanceof String) {
+ obj.setWalletPassword((String)member.getValue());
+ }
+ break;
+ }
+ }
+ }
+
+ public static void toJson(OracleConnectOptions obj, JsonObject json) {
+ toJson(obj, json.getMap());
+ }
+
+ public static void toJson(OracleConnectOptions obj, java.util.Map json) {
+ if (obj.getAuthenticationServices() != null) {
+ json.put("authenticationServices", obj.getAuthenticationServices());
+ }
+ json.put("autoGeneratedKeys", obj.isAutoGeneratedKeys());
+ if (obj.getAutoGeneratedKeysIndexes() != null) {
+ json.put("autoGeneratedKeysIndexes", obj.getAutoGeneratedKeysIndexes());
+ }
+ if (obj.getCatalog() != null) {
+ json.put("catalog", obj.getCatalog());
+ }
+ if (obj.getFetchDirection() != null) {
+ json.put("fetchDirection", obj.getFetchDirection().name());
+ }
+ json.put("fetchSize", obj.getFetchSize());
+ if (obj.getKeyStore() != null) {
+ json.put("keyStore", obj.getKeyStore());
+ }
+ if (obj.getKeyStorePassword() != null) {
+ json.put("keyStorePassword", obj.getKeyStorePassword());
+ }
+ if (obj.getKeyStoreType() != null) {
+ json.put("keyStoreType", obj.getKeyStoreType());
+ }
+ json.put("maxRows", obj.getMaxRows());
+ json.put("queryTimeout", obj.getQueryTimeout());
+ json.put("readOnly", obj.isReadOnly());
+ if (obj.getResultSetConcurrency() != null) {
+ json.put("resultSetConcurrency", obj.getResultSetConcurrency().name());
+ }
+ if (obj.getResultSetType() != null) {
+ json.put("resultSetType", obj.getResultSetType().name());
+ }
+ if (obj.getSchema() != null) {
+ json.put("schema", obj.getSchema());
+ }
+ if (obj.getTnsAdmin() != null) {
+ json.put("tnsAdmin", obj.getTnsAdmin());
+ }
+ if (obj.getTransactionIsolation() != null) {
+ json.put("transactionIsolation", obj.getTransactionIsolation().name());
+ }
+ if (obj.getTrustStore() != null) {
+ json.put("trustStore", obj.getTrustStore());
+ }
+ if (obj.getTrustStorePassword() != null) {
+ json.put("trustStorePassword", obj.getTrustStorePassword());
+ }
+ if (obj.getTrustStoreType() != null) {
+ json.put("trustStoreType", obj.getTrustStoreType());
+ }
+ if (obj.getWalletLocation() != null) {
+ json.put("walletLocation", obj.getWalletLocation());
+ }
+ if (obj.getWalletPassword() != null) {
+ json.put("walletPassword", obj.getWalletPassword());
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/FetchDirection.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/FetchDirection.java
new file mode 100644
index 000000000..2ddad7754
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/FetchDirection.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+
+import java.sql.ResultSet;
+
+@VertxGen
+public enum FetchDirection {
+
+ FORWARD(ResultSet.FETCH_FORWARD),
+ REVERSE(ResultSet.FETCH_REVERSE),
+ UNKNOWN(ResultSet.FETCH_UNKNOWN);
+
+ private final int type;
+
+ FetchDirection(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return type;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleClient.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleClient.java
new file mode 100644
index 000000000..a18497779
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleClient.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+import io.vertx.sqlclient.PropertyKind;
+import io.vertx.sqlclient.Row;
+
+/**
+ * An interface to define Oracle specific constants or behaviors.
+ */
+@VertxGen
+public interface OracleClient {
+ /**
+ * The property to be used to retrieve the generated keys
+ */
+ PropertyKind GENERATED_KEYS = PropertyKind.create("generated-keys", Row.class);
+
+ /**
+ * The property to be used to retrieve the output of the callable statement
+ */
+ PropertyKind OUTPUT = PropertyKind.create("callable-statement-output", Boolean.class);
+
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleConnectOptions.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleConnectOptions.java
new file mode 100644
index 000000000..d8d063bfb
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleConnectOptions.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.DataObject;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.SqlConnectOptions;
+
+@DataObject(generateConverter = true)
+public class OracleConnectOptions extends SqlConnectOptions {
+
+ // Support TNS_ADMIN (tnsnames.ora, ojdbc.properties).
+ private String tnsAdmin;
+
+ // Support wallet properties for TCPS/SSL/TLS
+ private String walletLocation;
+ private String walletPassword;
+
+ // Support keystore properties for TCPS/SSL/TLS
+ private String keyStore;
+ private String keyStoreType;
+ private String keyStorePassword;
+
+ // Support truststore properties for TCPS/SSL/TLS
+ private String trustStore;
+ private String trustStoreType;
+ private String trustStorePassword;
+
+ // Support authentication services (RADIUS, KERBEROS, and TCPS)
+ private String authenticationServices;
+ private int connectTimeout;
+ private int idleTimeout;
+
+ // connection
+ private boolean readOnly;
+ private String catalog;
+ private TransactionIsolation transactionIsolation;
+ private ResultSetType resultSetType;
+ private ResultSetConcurrency resultSetConcurrency;
+ // backwards compatibility
+ private boolean autoGeneratedKeys = true;
+ private JsonArray autoGeneratedKeysIndexes;
+ private String schema;
+ // statement
+ private int queryTimeout;
+ private int maxRows;
+ // resultset
+ private FetchDirection fetchDirection;
+ private int fetchSize;
+
+ public OracleConnectOptions(JsonObject toJson) {
+ super(toJson);
+ // TODO Copy
+ }
+
+ public OracleConnectOptions() {
+
+ }
+
+ public OracleConnectOptions(SqlConnectOptions options) {
+ super(options);
+ // TODO Copy
+ }
+
+ // TODO...
+
+ public String getTnsAdmin() {
+ return tnsAdmin;
+ }
+
+ public OracleConnectOptions setTnsAdmin(String tnsAdmin) {
+ this.tnsAdmin = tnsAdmin;
+ return this;
+ }
+
+ public String getWalletLocation() {
+ return walletLocation;
+ }
+
+ public OracleConnectOptions setWalletLocation(String walletLocation) {
+ this.walletLocation = walletLocation;
+ return this;
+ }
+
+ public String getWalletPassword() {
+ return walletPassword;
+ }
+
+ public OracleConnectOptions setWalletPassword(String walletPassword) {
+ this.walletPassword = walletPassword;
+ return this;
+ }
+
+ public String getKeyStore() {
+ return keyStore;
+ }
+
+ public OracleConnectOptions setKeyStore(String keyStore) {
+ this.keyStore = keyStore;
+ return this;
+ }
+
+ public String getKeyStoreType() {
+ return keyStoreType;
+ }
+
+ public OracleConnectOptions setKeyStoreType(String keyStoreType) {
+ this.keyStoreType = keyStoreType;
+ return this;
+ }
+
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public OracleConnectOptions setKeyStorePassword(String keyStorePassword) {
+ this.keyStorePassword = keyStorePassword;
+ return this;
+ }
+
+ public String getTrustStore() {
+ return trustStore;
+ }
+
+ public OracleConnectOptions setTrustStore(String trustStore) {
+ this.trustStore = trustStore;
+ return this;
+ }
+
+ public String getTrustStoreType() {
+ return trustStoreType;
+ }
+
+ public OracleConnectOptions setTrustStoreType(String trustStoreType) {
+ this.trustStoreType = trustStoreType;
+ return this;
+ }
+
+ public String getTrustStorePassword() {
+ return trustStorePassword;
+ }
+
+ public OracleConnectOptions setTrustStorePassword(String trustStorePassword) {
+ this.trustStorePassword = trustStorePassword;
+ return this;
+ }
+
+ public String getAuthenticationServices() {
+ return authenticationServices;
+ }
+
+ public OracleConnectOptions setAuthenticationServices(String authenticationServices) {
+ this.authenticationServices = authenticationServices;
+ return this;
+ }
+
+ @Override
+ public OracleConnectOptions setPort(int port) {
+ super.setPort(port);
+ return this;
+ }
+
+ @Override
+ public OracleConnectOptions setHost(String host) {
+ super.setHost(host);
+ return this;
+ }
+
+ @Override
+ public OracleConnectOptions setDatabase(String db) {
+ super.setDatabase(db);
+ return this;
+ }
+
+ @Override
+ public OracleConnectOptions setUser(String user) {
+ super.setUser(user);
+ return this;
+ }
+
+ @Override
+ public OracleConnectOptions setPassword(String pwd) {
+ super.setPassword(pwd);
+ return this;
+ }
+
+ public int getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ public OracleConnectOptions setConnectTimeout(int connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ public int getIdleTimeout() {
+ return idleTimeout;
+ }
+
+ public OracleConnectOptions setIdleTimeout(int idleTimeout) {
+ this.idleTimeout = idleTimeout;
+ return this;
+ }
+
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ public OracleConnectOptions setReadOnly(boolean readOnly) {
+ this.readOnly = readOnly;
+ return this;
+ }
+
+ public String getCatalog() {
+ return catalog;
+ }
+
+ public OracleConnectOptions setCatalog(String catalog) {
+ this.catalog = catalog;
+ return this;
+ }
+
+ public TransactionIsolation getTransactionIsolation() {
+ return transactionIsolation;
+ }
+
+ public OracleConnectOptions setTransactionIsolation(TransactionIsolation transactionIsolation) {
+ this.transactionIsolation = transactionIsolation;
+ return this;
+ }
+
+ public ResultSetType getResultSetType() {
+ return resultSetType;
+ }
+
+ public OracleConnectOptions setResultSetType(ResultSetType resultSetType) {
+ this.resultSetType = resultSetType;
+ return this;
+ }
+
+ public ResultSetConcurrency getResultSetConcurrency() {
+ return resultSetConcurrency;
+ }
+
+ public OracleConnectOptions setResultSetConcurrency(ResultSetConcurrency resultSetConcurrency) {
+ this.resultSetConcurrency = resultSetConcurrency;
+ return this;
+ }
+
+ public boolean isAutoGeneratedKeys() {
+ return autoGeneratedKeys;
+ }
+
+ public OracleConnectOptions setAutoGeneratedKeys(boolean autoGeneratedKeys) {
+ this.autoGeneratedKeys = autoGeneratedKeys;
+ return this;
+ }
+
+ public JsonArray getAutoGeneratedKeysIndexes() {
+ return autoGeneratedKeysIndexes;
+ }
+
+ public OracleConnectOptions setAutoGeneratedKeysIndexes(JsonArray autoGeneratedKeysIndexes) {
+ this.autoGeneratedKeysIndexes = autoGeneratedKeysIndexes;
+ return this;
+ }
+
+ public String getSchema() {
+ return schema;
+ }
+
+ public OracleConnectOptions setSchema(String schema) {
+ this.schema = schema;
+ return this;
+ }
+
+ public int getQueryTimeout() {
+ return queryTimeout;
+ }
+
+ public OracleConnectOptions setQueryTimeout(int queryTimeout) {
+ this.queryTimeout = queryTimeout;
+ return this;
+ }
+
+ public int getMaxRows() {
+ return maxRows;
+ }
+
+ public OracleConnectOptions setMaxRows(int maxRows) {
+ this.maxRows = maxRows;
+ return this;
+ }
+
+ public FetchDirection getFetchDirection() {
+ return fetchDirection;
+ }
+
+ public OracleConnectOptions setFetchDirection(FetchDirection fetchDirection) {
+ this.fetchDirection = fetchDirection;
+ return this;
+ }
+
+ public int getFetchSize() {
+ return fetchSize;
+ }
+
+ public OracleConnectOptions setFetchSize(int fetchSize) {
+ this.fetchSize = fetchSize;
+ return this;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleConnection.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleConnection.java
new file mode 100644
index 000000000..62a7559b2
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/OracleConnection.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.Fluent;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.impl.ContextInternal;
+import io.vertx.oracle.impl.OracleConnectionImpl;
+import io.vertx.sqlclient.PreparedStatement;
+import io.vertx.sqlclient.SqlConnection;
+
+public interface OracleConnection extends SqlConnection {
+
+ /**
+ * Create a connection to an Oracle Database with the given {@code connectOptions}.
+ *
+ * @param vertx the vertx instance
+ * @param connectOptions the options for the connection
+ * @param handler the handler called with the connection or the failure
+ */
+ static void connect(Vertx vertx, OracleConnectOptions connectOptions,
+ Handler> handler) {
+ Future fut = connect(vertx, connectOptions);
+ if (handler != null) {
+ fut.onComplete(handler);
+ }
+ }
+
+ /**
+ * Like {@link #connect(Vertx, OracleConnectOptions, Handler)} but returns a {@code Future} of the asynchronous result
+ */
+ static Future connect(Vertx vertx, OracleConnectOptions connectOptions) {
+ return OracleConnectionImpl.connect((ContextInternal) vertx.getOrCreateContext(), connectOptions);
+ }
+
+ // TODO URL String parsing
+ // /**
+ // * Like {@link #connect(Vertx, OracleConnectOptions, Handler)} with options built from {@code connectionUri}.
+ // */
+ // static void connect(Vertx vertx, String connectionUri, Handler> handler) {
+ // connect(vertx, fromUri(connectionUri), handler);
+ // }
+ //
+ // /**
+ // * Like {@link #connect(Vertx, String, Handler)} but returns a {@code Future} of the asynchronous result
+ // */
+ // static Future connect(Vertx vertx, String connectionUri) {
+ // return connect(vertx, fromUri(connectionUri));
+ // }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Fluent
+ @Override
+ OracleConnection prepare(String sql, Handler> handler);
+
+ /**
+ * {@inheritDoc}
+ */
+ @Fluent
+ @Override
+ OracleConnection exceptionHandler(Handler handler);
+
+ /**
+ * {@inheritDoc}
+ */
+ @Fluent
+ @Override
+ OracleConnection closeHandler(Handler handler);
+
+ /**
+ * Send a PING command to check if the server is alive.
+ *
+ * @param handler the handler notified when the server responses to client
+ * @return a reference to this, so the API can be used fluently
+ */
+ @Fluent
+ OracleConnection ping(Handler> handler);
+
+ /**
+ * Like {@link #ping(Handler)} but returns a {@code Future} of the asynchronous result
+ */
+ Future ping();
+
+ /**
+ * Cast a {@link SqlConnection} to {@link OracleConnection}.
+ *
+ * This is mostly useful for Vert.x generated APIs like RxJava/Mutiny.
+ *
+ * @param sqlConnection the connection to cast
+ * @return a {@link OracleConnection instance}
+ */
+ static OracleConnection cast(SqlConnection sqlConnection) {
+ return (OracleConnection) sqlConnection;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/OraclePool.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/OraclePool.java
new file mode 100644
index 000000000..8fa24f212
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/OraclePool.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+import io.vertx.core.Vertx;
+import io.vertx.core.VertxOptions;
+import io.vertx.core.impl.VertxInternal;
+import io.vertx.oracle.impl.OraclePoolImpl;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.PoolOptions;
+
+/**
+ * Represents a pool of connection to interact with an Oracle database.
+ */
+@VertxGen
+public interface OraclePool extends Pool {
+
+ // TODO Parse URI
+
+ static OraclePool pool(OracleConnectOptions connectOptions, PoolOptions poolOptions) {
+ if (Vertx.currentContext() != null) {
+ throw new IllegalStateException(
+ "Running in a Vertx context => use OraclePool#pool(Vertx, MySQLConnectOptions, PoolOptions) instead");
+ }
+ VertxOptions vertxOptions = new VertxOptions();
+ // TODO Support domain socket
+ // if (connectOptions.isUsingDomainSocket()) {
+ // vertxOptions.setPreferNativeTransport(true);
+ // }
+ VertxInternal vertx = (VertxInternal) Vertx.vertx(vertxOptions);
+ return OraclePoolImpl.create(vertx, true, connectOptions, poolOptions);
+ }
+
+ /**
+ * Like {@link #pool(OracleConnectOptions, PoolOptions)} with a specific {@link Vertx} instance.
+ */
+ static OraclePool pool(Vertx vertx, OracleConnectOptions connectOptions, PoolOptions poolOptions) {
+ return OraclePoolImpl.create((VertxInternal) vertx, false, connectOptions, poolOptions);
+ }
+
+ // TODO No option version
+ // TODO Version creating the vert.x instance.
+
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/ResultSetConcurrency.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/ResultSetConcurrency.java
new file mode 100644
index 000000000..3597ffe52
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/ResultSetConcurrency.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+
+import java.sql.ResultSet;
+
+/**
+ * Represents the resultset concurrency hint
+ *
+ * @author Paulo Lopes
+ */
+@VertxGen
+public enum ResultSetConcurrency {
+
+ READ_ONLY(ResultSet.CONCUR_READ_ONLY),
+ UPDATABLE(ResultSet.CONCUR_UPDATABLE);
+
+ private final int type;
+
+ ResultSetConcurrency(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return type;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/ResultSetType.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/ResultSetType.java
new file mode 100644
index 000000000..54845ce33
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/ResultSetType.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+
+import java.sql.ResultSet;
+
+/**
+ * Represents the resultset type hint
+ *
+ * @author Paulo Lopes
+ */
+@VertxGen
+public enum ResultSetType {
+ FORWARD_ONLY(ResultSet.TYPE_FORWARD_ONLY),
+ SCROLL_INSENSITIVE(ResultSet.TYPE_SCROLL_INSENSITIVE),
+ SCROLL_SENSITIVE(ResultSet.TYPE_SCROLL_SENSITIVE);
+
+ private final int type;
+
+ ResultSetType(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return type;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/SqlOutParam.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/SqlOutParam.java
new file mode 100644
index 000000000..b0b7651ba
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/SqlOutParam.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+import io.vertx.oracle.impl.SqlOutParamImpl;
+
+import java.sql.JDBCType;
+
+/**
+ * Tag if a parameter is of type OUT or INOUT.
+ *
+ * By default parameters are of type IN as they are provided by the user to the RDBMs engine. There are however cases
+ * where these must be tagged as OUT/INOUT when dealing with stored procedures/functions or complex statements.
+ *
+ * This interface allows marking the type of the param as required by the JDBC API.
+ */
+@VertxGen
+public interface SqlOutParam {
+
+ /**
+ * Factory for a OUT parameter of type {@code out}.
+ *
+ * @param out the kind of the type according to JDBC types.
+ * @return new marker
+ */
+ static SqlOutParam OUT(int out) {
+ return new SqlOutParamImpl(out);
+ }
+
+ /**
+ * Factory for a OUT parameter of type {@code out}.
+ *
+ * @param out the kind of the type according to JDBC types.
+ * @return new marker
+ */
+ static SqlOutParam OUT(String out) {
+ return new SqlOutParamImpl(JDBCType.valueOf(out).getVendorTypeNumber());
+ }
+
+ /**
+ * Factory for a OUT parameter of type {@code out}.
+ *
+ * @param out the kind of the type according to JDBC types.
+ * @return new marker
+ */
+ static SqlOutParam OUT(JDBCType out) {
+ return new SqlOutParamImpl(out.getVendorTypeNumber());
+ }
+
+ /**
+ * Factory for a INOUT parameter of type {@code out}.
+ *
+ * @param in the value to be passed as input.
+ * @param out the kind of the type according to JDBC types.
+ * @return new marker
+ */
+ static SqlOutParam INOUT(Object in, int out) {
+ return new SqlOutParamImpl(in, out);
+ }
+
+ /**
+ * Factory for a INOUT parameter of type {@code out}.
+ *
+ * @param in the value to be passed as input.
+ * @param out the kind of the type according to JDBC types.
+ * @return new marker
+ */
+ static SqlOutParam INOUT(Object in, String out) {
+ return new SqlOutParamImpl(in, JDBCType.valueOf(out).getVendorTypeNumber());
+ }
+
+ /**
+ * Factory for a INOUT parameter of type {@code out}.
+ *
+ * @param in the value to be passed as input.
+ * @param out the kind of the type according to JDBC types.
+ * @return new marker
+ */
+ static SqlOutParam INOUT(Object in, JDBCType out) {
+ return new SqlOutParamImpl(in, out.getVendorTypeNumber());
+ }
+
+ /**
+ * Is this marker {@code IN}?
+ *
+ * @return true if {@code INOUT}
+ */
+ boolean in();
+
+ /**
+ * Get the output type
+ *
+ * @return type
+ */
+ int type();
+
+ /**
+ * Get the input value
+ *
+ * @return input
+ */
+ Object value();
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/TransactionIsolation.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/TransactionIsolation.java
new file mode 100644
index 000000000..f450f469b
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/TransactionIsolation.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle;
+
+import io.vertx.codegen.annotations.VertxGen;
+
+import java.sql.Connection;
+
+/**
+ * Represents a Transaction Isolation Level
+ *
+ * @author Paulo Lopes
+ */
+@VertxGen
+public enum TransactionIsolation {
+
+ /**
+ * Implements dirty read, or isolation level 0 locking, which means that no shared locks are issued and no exclusive
+ * locks are honored. When this option is set, it is possible to read uncommitted or dirty data; values in the data
+ * can be changed and rows can appear or disappear in the data set before the end of the transaction. This is the
+ * least restrictive of the four isolation levels.
+ */
+ READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
+
+ /**
+ * Specifies that shared locks are held while the data is being read to avoid dirty reads, but the data can be changed
+ * before the end of the transaction, resulting in nonrepeatable reads or phantom data.
+ */
+ READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
+
+ /**
+ * Locks are placed on all data that is used in a query, preventing other users from updating the data, but new
+ * phantom rows can be inserted into the data set by another user and are included in later reads in the current
+ * transaction. Because concurrency is lower than the default isolation level, use this option only when necessary.
+ */
+ REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
+
+ /**
+ * Places a range lock on the data set, preventing other users from updating or inserting rows into the data set until
+ * the transaction is complete. This is the most restrictive of the four isolation levels. Because concurrency is
+ * lower, use this option only when necessary.
+ */
+ SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE),
+
+ /**
+ * For engines that support it, none isolation means that each statement would essentially be its own transaction.
+ */
+ NONE(Connection.TRANSACTION_NONE);
+
+ private final int type;
+
+ TransactionIsolation(int type) {
+ this.type = type;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public static TransactionIsolation from(int level) {
+ switch (level) {
+ case Connection.TRANSACTION_READ_COMMITTED:
+ return TransactionIsolation.READ_COMMITTED;
+ case Connection.TRANSACTION_READ_UNCOMMITTED:
+ return TransactionIsolation.READ_UNCOMMITTED;
+ case Connection.TRANSACTION_REPEATABLE_READ:
+ return TransactionIsolation.REPEATABLE_READ;
+ case Connection.TRANSACTION_SERIALIZABLE:
+ return TransactionIsolation.SERIALIZABLE;
+ case Connection.TRANSACTION_NONE:
+ return TransactionIsolation.NONE;
+ default:
+ return null;
+ }
+ }
+
+ public static TransactionIsolation from(String level) {
+ if (level != null) {
+ switch (level.replace('-', ' ').toUpperCase()) {
+ case "READ COMMITTED":
+ return TransactionIsolation.READ_COMMITTED;
+ case "READ UNCOMMITTED":
+ return TransactionIsolation.READ_UNCOMMITTED;
+ case "REPEATABLE READ":
+ return TransactionIsolation.REPEATABLE_READ;
+ case "SERIALIZABLE":
+ return TransactionIsolation.SERIALIZABLE;
+ case "NONE":
+ return TransactionIsolation.NONE;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/CommandHandler.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/CommandHandler.java
new file mode 100644
index 000000000..df6d7f465
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/CommandHandler.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.impl.ContextInternal;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.impl.commands.*;
+import io.vertx.sqlclient.impl.Connection;
+import io.vertx.sqlclient.impl.PreparedStatement;
+import io.vertx.sqlclient.impl.QueryResultHandler;
+import io.vertx.sqlclient.impl.command.CommandBase;
+import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;
+import io.vertx.sqlclient.impl.command.TxCommand;
+import io.vertx.sqlclient.spi.DatabaseMetadata;
+import oracle.jdbc.OracleConnection;
+
+public class CommandHandler implements Connection {
+ private final OracleConnection connection;
+ private final ContextInternal context;
+ private final OracleConnectOptions options;
+ private Holder holder;
+
+ public CommandHandler(ContextInternal ctx, OracleConnectOptions options, OracleConnection oc) {
+ this.context = ctx;
+ this.options = options;
+ this.connection = oc;
+ }
+
+ @Override
+ public void init(Holder holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public boolean isSsl() {
+ return options.isSsl();
+ }
+
+ @Override
+ public DatabaseMetadata getDatabaseMetaData() {
+ return new OracleMetadataImpl(Helper.getOrHandleSQLException(connection::getMetaData));
+ }
+
+ @Override
+ public void close(Holder holder, Promise promise) {
+ Helper.first(Helper.getOrHandleSQLException(connection::closeAsyncOracle), context)
+ .onComplete(x -> holder.handleClosed())
+ .onComplete(promise);
+ }
+
+ @Override
+ public int getProcessId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getSecretKey() {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Future schedule(ContextInternal contextInternal, CommandBase commandBase) {
+ if (commandBase instanceof io.vertx.sqlclient.impl.command.SimpleQueryCommand) {
+ return (Future) handle((io.vertx.sqlclient.impl.command.SimpleQueryCommand) commandBase);
+ } else if (commandBase instanceof io.vertx.sqlclient.impl.command.PrepareStatementCommand) {
+ return (Future) handle((io.vertx.sqlclient.impl.command.PrepareStatementCommand) commandBase);
+ } else if (commandBase instanceof ExtendedQueryCommand) {
+ return (Future) handle((ExtendedQueryCommand>) commandBase);
+ } else if (commandBase instanceof TxCommand) {
+ return handle((TxCommand) commandBase);
+ } else if (commandBase instanceof PingCommand) {
+ return (Future) handle((PingCommand) commandBase);
+ } else {
+ return Future.failedFuture("Not yet implemented " + commandBase);
+ }
+ }
+
+ private Future handle(PingCommand ping) {
+ return ping.execute(connection, context);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private Future handle(io.vertx.sqlclient.impl.command.SimpleQueryCommand command) {
+ QueryCommand, R> action = new SimpleQueryCommand<>(options, command.sql(), command.collector());
+ return handle(action, command.resultHandler());
+ }
+
+ private Future handle(io.vertx.sqlclient.impl.command.PrepareStatementCommand command) {
+ PrepareStatementCommand action = new PrepareStatementCommand(options, command.sql());
+ return action.execute(connection, context);
+ }
+
+ private Future handle(QueryCommand, R> action, QueryResultHandler handler) {
+ Future> fut = action.execute(connection, context);
+ return fut
+ .onSuccess(ar -> ar.handle(handler)).map(false)
+ .onFailure(t -> holder.handleException(t));
+
+ }
+
+ private Future handle(ExtendedQueryCommand command) {
+ if (command.cursorId() != null) {
+ QueryCommand, R> cmd = new OracleCursorQueryCommand<>(options, command, command.params());
+ return cmd.execute(connection, context)
+ .map(false);
+ }
+
+ QueryCommand, R> action =
+ command.isBatch() ?
+ new OraclePreparedBatch<>(options, command, command.collector(), command.paramsList())
+ : new OraclePreparedQuery<>(options, command, command.collector(), command.params());
+
+ return handle(action, command.resultHandler());
+ }
+
+ private Future handle(TxCommand command) {
+ OracleTransactionCommand action = new OracleTransactionCommand<>(command, options);
+ return action.execute(connection, context);
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/Helper.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/Helper.java
new file mode 100644
index 000000000..5f0298f0c
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/Helper.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.VertxException;
+import oracle.jdbc.OraclePreparedStatement;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Flow;
+import java.util.function.Supplier;
+
+public class Helper {
+
+ public static Future completeOrFail(ThrowingSupplier supplier) {
+ try {
+ return Future.succeededFuture(supplier.getOrThrow());
+ } catch (SQLException throwables) {
+ return Future.failedFuture(throwables);
+ }
+ }
+
+ public static void closeQuietly(Statement ps) {
+ if (ps != null) {
+ try {
+ ps.close();
+ } catch (SQLException throwables) {
+ // ignore me.
+ }
+ }
+ }
+
+ public static Future contextualize(CompletionStage stage, Context context) {
+ Promise future = Promise.promise();
+
+ stage.whenComplete((r, f) ->
+ context.runOnContext(x -> {
+ if (f != null) {
+ future.fail(f);
+ } else {
+ future.complete(r);
+ }
+ })
+ );
+
+ return future.future();
+ }
+
+ /**
+ * Returns a {@code PreparedStatement}
+ * {@linkplain Wrapper#unwrap(Class) unwrapped} as an
+ * {@code OraclePreparedStatement}, or throws an {@code R2dbcException} if it
+ * does not wrap or implement the Oracle JDBC interface.
+ *
+ * @param preparedStatement A JDBC prepared statement
+ * @return An Oracle JDBC prepared statement
+ * @throws VertxException If an Oracle JDBC prepared statement is not wrapped.
+ */
+ public static OraclePreparedStatement unwrapOraclePreparedStatement(
+ PreparedStatement preparedStatement) {
+ return getOrHandleSQLException(() ->
+ preparedStatement.unwrap(OraclePreparedStatement.class));
+ }
+
+ /**
+ * Returns the specified {@code supplier}'s output, or throws a
+ * {@link VertxException} if the function throws a {@link SQLException}. This
+ * method serves to improve code readability. For instance:
+ *
+ *
+ * @param supplier Returns a value or throws a {@code SQLException}. Not
+ * null.
+ * @param The output type of the supplier
+ * @return The output of the specified {@code supplier}.
+ * @throws VertxException If the supplier throws a {@code SQLException}.
+ */
+ public static T getOrHandleSQLException(ThrowingSupplier supplier)
+ throws VertxException {
+ try {
+ return supplier.getOrThrow();
+ } catch (SQLException sqlException) {
+ throw new VertxException(sqlException);
+ }
+ }
+
+ public static void runOrHandleSQLException(ThrowingRunnable runnable)
+ throws VertxException {
+ try {
+ runnable.runOrThrow();
+ } catch (SQLException sqlException) {
+ throw new VertxException(sqlException);
+ }
+ }
+
+ public static Future first(Flow.Publisher publisher, Context context) {
+ Promise promise = Promise.promise();
+ publisher.subscribe(new Flow.Subscriber<>() {
+ volatile Flow.Subscription subscription;
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ subscription.request(1);
+ }
+
+ @Override
+ public void onNext(T item) {
+ context.runOnContext(x -> promise.tryComplete(item));
+ subscription.cancel();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ promise.fail(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ // Use tryComplete as the completion signal can be sent even if we cancelled.
+ // Also for Publisher we would get in this case.
+ context.runOnContext(x -> promise.tryComplete(null));
+ }
+ });
+ return promise.future();
+ }
+
+ public static Future> collect(Flow.Publisher publisher, Context context) {
+ Promise> promise = Promise.promise();
+ publisher.subscribe(new Flow.Subscriber<>() {
+ final List list = new ArrayList<>();
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(T item) {
+ list.add(item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ context.runOnContext(x -> promise.fail(throwable));
+ }
+
+ @Override
+ public void onComplete() {
+ context.runOnContext(x -> promise.complete(list));
+ }
+ });
+ return promise.future();
+ }
+
+ /**
+ *
+ * Function type that returns a value or throws a {@link SQLException}. This
+ * functional interface can reference JDBC methods that throw
+ * {@code SQLExceptions}. The standard {@link Supplier} interface cannot
+ * reference methods that throw checked exceptions.
+ *
+ *
+ * @param the type of values supplied by this supplier.
+ */
+ @FunctionalInterface
+ public interface ThrowingSupplier extends Supplier {
+ /**
+ * Returns a value, or throws a {@code SQLException} if an error is
+ * encountered.
+ *
+ * @return the supplied value
+ * @throws SQLException If a value is not returned due to an error.
+ */
+ T getOrThrow() throws SQLException;
+
+ /**
+ * Returns a value, or throws an {@code R2dbcException} if an error is
+ * encountered.
+ *
+ * @throws VertxException If a value is not returned due to an error.
+ * @implNote The default implementation invokes
+ * {@link #getOrHandleSQLException(ThrowingSupplier)} (ThrowingRunnable)}
+ * with this {@code ThrowingSupplier}.
+ */
+ @Override
+ default T get() throws VertxException {
+ return getOrHandleSQLException(this);
+ }
+ }
+
+ /**
+ *
+ * Function type that returns no value or throws a {@link SQLException}.
+ * This functional interface can reference JDBC methods that throw
+ * {@code SQLExceptions}. The standard {@link Runnable} interface cannot
+ * reference methods that throw checked exceptions.
+ *
+ */
+ @FunctionalInterface
+ public interface ThrowingRunnable extends Runnable {
+ /**
+ * Runs to completion and returns normally, or throws a {@code SQLException}
+ * if an error is encountered.
+ *
+ * @throws SQLException If the run does not complete due to an error.
+ */
+ void runOrThrow() throws SQLException;
+
+ /**
+ * Runs to completion and returns normally, or throws an {@code
+ * R2dbcException} if an error is encountered.
+ *
+ * @throws VertxException If the run does not complete due to an error.
+ * @implNote The default implementation invokes
+ * {@link #runOrHandleSQLException(ThrowingRunnable)} with this {@code
+ * ThrowingRunnable}.
+ */
+ @Override
+ default void run() throws VertxException {
+ runOrHandleSQLException(this);
+ }
+ }
+
+ /**
+ * Accessor of column values within a single row from a table of data that
+ * a {@link ResultSet} represents. Instances of {@code JdbcRow} are
+ * supplied as input to row mapping functions, and each instance is valid
+ * only within the scope of a row mapping function's call. Usage outside of
+ * a row mapping function's scope results in an {@code IllegalStateException}.
+ */
+ interface JdbcRow {
+
+ /**
+ * Returns the value of this row for the specified {@code index} as
+ * the specified {@code type}. The value is returned as if by invoking
+ * {@link ResultSet#getObject(int, Class)} on a result set with a cursor
+ * positioned on the table row that this object represents.
+ *
+ * @param index 0-based column index. (The first column's index is 0)
+ * @param type The type of object to return. Not null.
+ * @param The returned type
+ * @return The column value as the specified type.
+ * @throws VertxException If the {@code index} is invalid
+ * @throws IllegalArgumentException If conversion to the specified {@code
+ * type} is not supported.
+ * @throws IllegalStateException If this method is invoked outside of a
+ * row mapping function.
+ */
+ T getObject(int index, Class type);
+
+ /**
+ * Returns a copy of this row. The copy returned by this method is not
+ * backed by the resources of the JDBC connection that created this row.
+ * The copy returned by this method allows the column values of this row
+ * to be accessed after closing the JDBC connection that created this row.
+ *
+ * @return A cached copy of this row.
+ */
+ JdbcRow copy();
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleConnectionFactory.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleConnectionFactory.java
new file mode 100644
index 000000000..1f15c48a1
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleConnectionFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.Promise;
+import io.vertx.core.impl.EventLoopContext;
+import io.vertx.core.impl.VertxInternal;
+import io.vertx.core.impl.future.PromiseInternal;
+import io.vertx.core.net.NetClientOptions;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.sqlclient.SqlConnectOptions;
+import io.vertx.sqlclient.impl.Connection;
+import io.vertx.sqlclient.impl.ConnectionFactory;
+import io.vertx.sqlclient.impl.SqlConnectionFactoryBase;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.datasource.OracleDataSource;
+
+import java.util.concurrent.CompletionStage;
+
+import static io.vertx.oracle.impl.OracleDatabaseHelper.createDataSource;
+
+public class OracleConnectionFactory extends SqlConnectionFactoryBase implements ConnectionFactory {
+
+ private final OracleConnectOptions options;
+ private final OracleDataSource datasource;
+
+ protected OracleConnectionFactory(VertxInternal vertx, OracleConnectOptions options) {
+ super(vertx, options);
+ this.options = options;
+ this.datasource = createDataSource(options);
+ }
+
+ @Override
+ protected void initializeConfiguration(SqlConnectOptions options) {
+
+ }
+
+ @Override
+ protected void configureNetClientOptions(NetClientOptions netClientOptions) {
+
+ }
+
+ @Override
+ protected void doConnectInternal(Promise promise) {
+ PromiseInternal promiseInternal = (PromiseInternal) promise;
+ EventLoopContext context = ConnectionFactory.asEventLoopContext(promiseInternal.context());
+ CompletionStage stage = Helper
+ .getOrHandleSQLException(() -> datasource.createConnectionBuilder().buildAsyncOracle());
+
+ Helper.contextualize(stage, context)
+ .map(c -> new CommandHandler(context, options, c))
+ .onComplete(ar -> promise.handle(ar.map(x -> x)));
+ }
+
+ public OracleConnectOptions options() {
+ return options;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleConnectionImpl.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleConnectionImpl.java
new file mode 100644
index 000000000..96088854f
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleConnectionImpl.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.impl.ContextInternal;
+import io.vertx.core.impl.future.PromiseInternal;
+import io.vertx.core.spi.metrics.ClientMetrics;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.OracleConnection;
+import io.vertx.oracle.impl.commands.PingCommand;
+import io.vertx.sqlclient.impl.Connection;
+import io.vertx.sqlclient.impl.SqlConnectionImpl;
+import io.vertx.sqlclient.impl.tracing.QueryTracer;
+
+public class OracleConnectionImpl extends SqlConnectionImpl implements OracleConnection {
+
+ public static Future connect(ContextInternal ctx, OracleConnectOptions options) {
+ // TODO Add support for domain socket
+ // if (options.isUsingDomainSocket() && !ctx.owner().isNativeTransportEnabled()) {
+ // return ctx.failedFuture("Native transport is not available");
+ // }
+
+ OracleConnectionFactory client;
+ try {
+ client = new OracleConnectionFactory(ctx.owner(), options);
+ } catch (Exception e) {
+ return ctx.failedFuture(e);
+ }
+ ctx.addCloseHook(client);
+ QueryTracer tracer = ctx.tracer() == null ? null : new QueryTracer(ctx.tracer(), options);
+ PromiseInternal promise = ctx.promise();
+ client.connect(promise);
+ return promise.future().map(conn -> {
+ OracleConnectionImpl connection = new OracleConnectionImpl(client, ctx, conn, tracer, null);
+ conn.init(connection);
+ return connection;
+ });
+ }
+
+ private final OracleConnectionFactory factory;
+
+ public OracleConnectionImpl(OracleConnectionFactory factory, ContextInternal context, Connection conn,
+ QueryTracer tracer, ClientMetrics metrics) {
+ super(context, conn, tracer, metrics);
+ this.factory = factory;
+ }
+
+ @Override
+ public OracleConnection ping(Handler> handler) {
+ Future fut = ping();
+ if (handler != null) {
+ fut.onComplete(handler);
+ }
+ return this;
+ }
+
+ @Override
+ public Future ping() {
+ return schedule(context, new PingCommand(factory.options()));
+ }
+
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleDatabaseHelper.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleDatabaseHelper.java
new file mode 100644
index 000000000..de66e8e58
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleDatabaseHelper.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.oracle.OracleConnectOptions;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.datasource.OracleDataSource;
+
+import static io.vertx.oracle.impl.Helper.getOrHandleSQLException;
+import static io.vertx.oracle.impl.Helper.runOrHandleSQLException;
+
+public class OracleDatabaseHelper {
+
+ public static OracleDataSource createDataSource(OracleConnectOptions options) {
+ OracleDataSource oracleDataSource =
+ getOrHandleSQLException(oracle.jdbc.pool.OracleDataSource::new);
+
+ runOrHandleSQLException(() ->
+ oracleDataSource.setURL(composeJdbcUrl(options)));
+ configureStandardOptions(oracleDataSource, options);
+ configureExtendedOptions(oracleDataSource, options);
+ configureJdbcDefaults(oracleDataSource);
+
+ return oracleDataSource;
+ }
+
+ /**
+ * Composes an Oracle JDBC URL from {@code OracleConnectOptions}, as
+ * specified in the javadoc of
+ * {@link #createDataSource(OracleConnectOptions)}
+ *
+ * @param options Oracle Connection options. Must not be {@code null}.
+ * @return An Oracle Connection JDBC URL
+ */
+ private static String composeJdbcUrl(OracleConnectOptions options) {
+ String serviceName = options.getDatabase();
+ String host = options.getHost();
+ int port = options.getPort();
+ boolean isTcps = options.isSsl();
+
+ return String.format("jdbc:oracle:thin:@%s%s%s%s",
+ Boolean.TRUE.equals(isTcps) ? "tcps:" : "",
+ host,
+ port > 0 ? (":" + port) : "",
+ serviceName != null ? ("/" + serviceName) : "");
+
+ }
+
+ /**
+ * Configures an {@code OracleDataSource}.
+ *
+ * @param oracleDataSource An data source to configure
+ * @param options OracleConnectOptions options. Not null.
+ */
+ private static void configureStandardOptions(
+ OracleDataSource oracleDataSource, OracleConnectOptions options) {
+
+ String user = options.getUser();
+ if (user != null) {
+ runOrHandleSQLException(() -> oracleDataSource.setUser(user));
+ }
+
+ CharSequence password = options.getPassword();
+ if (password != null) {
+ runOrHandleSQLException(() ->
+ oracleDataSource.setPassword(password.toString()));
+ }
+
+ int connectTimeout = options.getConnectTimeout();
+ if (connectTimeout > 0) {
+ runOrHandleSQLException(() ->
+ oracleDataSource.setLoginTimeout(connectTimeout));
+ }
+
+ }
+
+ private static void configureExtendedOptions(
+ OracleDataSource oracleDataSource, OracleConnectOptions options) {
+
+ // Handle the short form of the TNS_ADMIN option
+ String tnsAdmin = options.getTnsAdmin();
+ if (tnsAdmin != null) {
+ // Configure using the long form: oracle.net.tns_admin
+ runOrHandleSQLException(() ->
+ oracleDataSource.setConnectionProperty(
+ OracleConnection.CONNECTION_PROPERTY_TNS_ADMIN, tnsAdmin));
+ }
+
+ // TODO Iterate over the other properties.
+ }
+
+ /**
+ * Configures an {@code oracleDataSource} with any connection properties that
+ * this adapter requires by default.
+ *
+ * @param oracleDataSource An data source to configure
+ */
+ private static void configureJdbcDefaults(OracleDataSource oracleDataSource) {
+
+ // Have the Oracle JDBC Driver implement behavior that the JDBC
+ // Specification defines as correct. The javadoc for this property lists
+ // all of it's effects. One effect is to have ResultSetMetaData describe
+ // FLOAT columns as the FLOAT type, rather than the NUMBER type. This
+ // effect allows the Oracle R2DBC Driver obtain correct metadata for
+ // FLOAT type columns. The property is deprecated, but the deprecation note
+ // explains that setting this to "false" is deprecated, and that it
+ // should be set to true; If not set, the 21c driver uses a default value
+ // of false.
+ @SuppressWarnings("deprecation")
+ String enableJdbcSpecCompliance =
+ OracleConnection.CONNECTION_PROPERTY_J2EE13_COMPLIANT;
+ runOrHandleSQLException(() ->
+ oracleDataSource.setConnectionProperty(enableJdbcSpecCompliance, "true"));
+
+ // Have the Oracle JDBC Driver cache PreparedStatements by default.
+ runOrHandleSQLException(() -> {
+ // Don't override a value set by user code
+ String userValue = oracleDataSource.getConnectionProperty(
+ OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE);
+
+ if (userValue == null) {
+ // The default value of the OPEN_CURSORS parameter in the 21c
+ // and 19c databases is 50:
+ // https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/OPEN_CURSORS.html#GUID-FAFD1247-06E5-4E64-917F-AEBD4703CF40
+ // Assuming this default, then a default cache size of 25 will keep
+ // each session at or below 50% of it's cursor capacity, which seems
+ // reasonable.
+ oracleDataSource.setConnectionProperty(
+ OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE,
+ "25");
+ }
+ });
+
+ // TODO: Disable the result set cache? This is needed to support the
+ // SERIALIZABLE isolation level, which requires result set caching to be
+ // disabled.
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleMetadataImpl.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleMetadataImpl.java
new file mode 100644
index 000000000..85785b74a
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleMetadataImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.sqlclient.spi.DatabaseMetadata;
+
+import java.sql.DatabaseMetaData;
+
+public class OracleMetadataImpl implements DatabaseMetadata {
+ private final DatabaseMetaData metadata;
+
+ public OracleMetadataImpl(DatabaseMetaData metadata) {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public String productName() {
+ return Helper.getOrHandleSQLException(metadata::getDatabaseProductName);
+ }
+
+ @Override
+ public String fullVersion() {
+ return Helper.getOrHandleSQLException(metadata::getDatabaseProductVersion);
+ }
+
+ @Override
+ public int majorVersion() {
+ return Helper.getOrHandleSQLException(metadata::getDatabaseMajorVersion);
+ }
+
+ @Override
+ public int minorVersion() {
+ return Helper.getOrHandleSQLException(metadata::getDatabaseMinorVersion);
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OraclePoolImpl.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OraclePoolImpl.java
new file mode 100644
index 000000000..34cdfb7f2
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OraclePoolImpl.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.impl.CloseFuture;
+import io.vertx.core.impl.ContextInternal;
+import io.vertx.core.impl.VertxInternal;
+import io.vertx.core.spi.metrics.ClientMetrics;
+import io.vertx.core.spi.metrics.VertxMetrics;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.OraclePool;
+import io.vertx.sqlclient.PoolOptions;
+import io.vertx.sqlclient.impl.Connection;
+import io.vertx.sqlclient.impl.PoolBase;
+import io.vertx.sqlclient.impl.SqlConnectionImpl;
+import io.vertx.sqlclient.impl.tracing.QueryTracer;
+
+public class OraclePoolImpl extends PoolBase implements OraclePool {
+
+ private final OracleConnectionFactory factory;
+
+ public static OraclePoolImpl create(VertxInternal vertx, boolean closeVertx, OracleConnectOptions connectOptions,
+ PoolOptions poolOptions) {
+ QueryTracer tracer = vertx.tracer() == null ? null : new QueryTracer(vertx.tracer(), connectOptions);
+ VertxMetrics vertxMetrics = vertx.metricsSPI();
+ @SuppressWarnings("rawtypes") ClientMetrics metrics = vertxMetrics != null ?
+ vertxMetrics.createClientMetrics(connectOptions.getSocketAddress(), "sql",
+ connectOptions.getMetricsName()) :
+ null;
+ OraclePoolImpl pool = new OraclePoolImpl(vertx, new OracleConnectionFactory(vertx, connectOptions), tracer,
+ metrics, poolOptions);
+ pool.init();
+ CloseFuture closeFuture = pool.closeFuture();
+ if (closeVertx) {
+ closeFuture.future().onComplete(ar -> vertx.close());
+ } else {
+ ContextInternal ctx = vertx.getContext();
+ if (ctx != null) {
+ ctx.addCloseHook(closeFuture);
+ } else {
+ vertx.addCloseHook(closeFuture);
+ }
+ }
+ return pool;
+ }
+
+ public OraclePoolImpl(VertxInternal vertx, OracleConnectionFactory factory, QueryTracer tracer,
+ ClientMetrics metrics, PoolOptions poolOptions) {
+ super(vertx, factory, tracer, metrics, 1, poolOptions);
+ this.factory = factory;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected SqlConnectionImpl wrap(ContextInternal context, Connection conn) {
+ return new OracleConnectionImpl(factory, context, conn, tracer, metrics);
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleRow.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleRow.java
new file mode 100644
index 000000000..3b1a9e12d
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/OracleRow.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.data.Numeric;
+import io.vertx.sqlclient.impl.ArrayTuple;
+import io.vertx.sqlclient.impl.RowDesc;
+
+import java.time.*;
+import java.util.List;
+import java.util.UUID;
+
+public class OracleRow extends ArrayTuple implements Row {
+
+ private final RowDesc desc;
+
+ public OracleRow(RowDesc desc) {
+ super(desc.columnNames().size());
+ this.desc = desc;
+ }
+
+ public OracleRow(OracleRow row) {
+ super(row);
+ this.desc = row.desc;
+ }
+
+ @Override
+ public String getColumnName(int pos) {
+ List columnNames = desc.columnNames();
+ return pos < 0 || columnNames.size() - 1 < pos ? null : columnNames.get(pos);
+ }
+
+ @Override
+ public int getColumnIndex(String name) {
+ if (name == null) {
+ throw new NullPointerException();
+ }
+ return desc.columnNames().indexOf(name.toUpperCase());
+ }
+
+ @Override
+ public T get(Class type, int pos) {
+ if (type == Boolean.class) {
+ return type.cast(getBoolean(pos));
+ } else if (type == Short.class) {
+ return type.cast(getShort(pos));
+ } else if (type == Integer.class) {
+ return type.cast(getInteger(pos));
+ } else if (type == Long.class) {
+ return type.cast(getLong(pos));
+ } else if (type == Float.class) {
+ return type.cast(getFloat(pos));
+ } else if (type == Double.class) {
+ return type.cast(getDouble(pos));
+ } else if (type == Character.class) {
+ return type.cast(getChar(pos));
+ } else if (type == Numeric.class) {
+ return type.cast(getNumeric(pos));
+ } else if (type == String.class) {
+ return type.cast(getString(pos));
+ } else if (type == Buffer.class) {
+ return type.cast(getBuffer(pos));
+ } else if (type == UUID.class) {
+ return type.cast(getUUID(pos));
+ } else if (type == LocalDate.class) {
+ return type.cast(getLocalDate(pos));
+ } else if (type == LocalTime.class) {
+ return type.cast(getLocalTime(pos));
+ } else if (type == OffsetTime.class) {
+ return type.cast(getOffsetTime(pos));
+ } else if (type == LocalDateTime.class) {
+ return type.cast(getLocalDateTime(pos));
+ } else if (type == OffsetDateTime.class) {
+ return type.cast(getOffsetDateTime(pos));
+ } else if (type == JsonObject.class) {
+ return type.cast(getJson(pos));
+ } else if (type == JsonArray.class) {
+ return type.cast(getJson(pos));
+ } else if (type == Object.class) {
+ return type.cast(getValue(pos));
+ }
+ throw new UnsupportedOperationException("Unsupported type " + type.getName());
+ }
+
+ public Numeric getNumeric(String name) {
+ int pos = desc.columnIndex(name);
+ return pos == -1 ? null : getNumeric(pos);
+ }
+
+ public Object[] getArrayOfJsons(String name) {
+ int pos = desc.columnIndex(name);
+ return pos == -1 ? null : getArrayOfJsons(pos);
+ }
+
+ public Numeric[] getArrayOfNumerics(String name) {
+ int pos = desc.columnIndex(name);
+ return pos == -1 ? null : getArrayOfNumerics(pos);
+ }
+
+ public Character[] getArrayOfChars(String name) {
+ int pos = desc.columnIndex(name);
+ return pos == -1 ? null : getArrayOfChars(pos);
+ }
+
+ public Long getLong(int pos) {
+ Object val = getValue(pos);
+ if (val == null) {
+ return null;
+ } else if (val instanceof String) {
+ return Long.valueOf((String) val);
+ } else if (val instanceof Long) {
+ return (Long) val;
+ } else if (val instanceof Number) {
+ return ((Number) val).longValue();
+ } else if (val instanceof Enum>) {
+ return (long) ((Enum>) val).ordinal();
+ } else {
+ return (Long) val; // Throw CCE
+ }
+ }
+
+ public Character getChar(int pos) {
+ Object val = getValue(pos);
+ if (val instanceof Character) {
+ return (Character) val;
+ } else {
+ return null;
+ }
+ }
+
+ public Numeric getNumeric(int pos) {
+ Object val = getValue(pos);
+ if (val instanceof Numeric) {
+ return (Numeric) val;
+ } else if (val instanceof Number) {
+ return Numeric.parse(val.toString());
+ }
+ 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[]) {
+ return (Character[]) val;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get a {@code Json} array value, the {@code Json} value may be a string, number, JSON object, array, boolean or null.
+ */
+ public Object[] getArrayOfJsons(int pos) {
+ Object val = getValue(pos);
+ if (val instanceof Object[]) {
+ return (Object[]) val;
+ } else {
+ return null;
+ }
+ }
+
+ public Numeric[] getArrayOfNumerics(int pos) {
+ Object val = getValue(pos);
+ if (val instanceof Numeric[]) {
+ return (Numeric[]) val;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/RowReader.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/RowReader.java
new file mode 100644
index 000000000..edb0539a8
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/RowReader.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.PropertyKind;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowIterator;
+import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.desc.ColumnDescriptor;
+import io.vertx.sqlclient.impl.QueryResultHandler;
+import io.vertx.sqlclient.impl.RowDesc;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Flow;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class RowReader implements Flow.Subscriber {
+
+ private final Flow.Publisher publisher;
+ private final Context context;
+ private final RowDesc description;
+ private final QueryResultHandler> handler;
+ private volatile Flow.Subscription subscription;
+ private final Promise subscriptionPromise;
+ private Promise readPromise;
+ private volatile boolean completed;
+ private volatile Throwable failed;
+ private volatile OracleRowSet collector;
+ private final AtomicInteger toRead = new AtomicInteger();
+
+ private final AtomicBoolean wip = new AtomicBoolean();
+
+ public RowReader(Flow.Publisher publisher, RowDesc description, Promise promise,
+ QueryResultHandler> handler,
+ Context context) {
+ this.publisher = publisher;
+ this.description = description;
+ this.subscriptionPromise = promise;
+ this.handler = handler;
+ this.context = context;
+ }
+
+ public static Future create(Flow.Publisher publisher, Context context,
+ QueryResultHandler> handler, RowDesc description) {
+ Promise promise = Promise.promise();
+ RowReader reader = new RowReader(publisher, description, promise, handler, context);
+ reader.subscribe();
+ return promise.future().map(reader);
+ }
+
+ public Future read(int fetchSize) {
+ if (subscription == null) {
+ return Future.failedFuture(new IllegalStateException("Not subscribed"));
+ }
+ if (completed) {
+ return Future.succeededFuture();
+ }
+ if (failed != null) {
+ return Future.failedFuture(failed);
+ }
+ if (wip.compareAndSet(false, true)) {
+ toRead.set(fetchSize);
+ collector = new OracleRowSet(description);
+ readPromise = Promise.promise();
+ subscription.request(fetchSize);
+ return readPromise.future();
+ } else {
+ return Future.failedFuture(new IllegalStateException("Read already in progress"));
+ }
+ }
+
+ private void subscribe() {
+ this.publisher.subscribe(this);
+ }
+
+ @Override
+ public void onSubscribe(Flow.Subscription subscription) {
+ this.subscription = subscription;
+ context.runOnContext(x -> this.subscriptionPromise.complete(null));
+ }
+
+ @Override
+ public void onNext(Row item) {
+ collector.add(item);
+ if (toRead.decrementAndGet() == 0 && wip.compareAndSet(true, false)) {
+ try {
+ handler.handleResult(collector.rowCount(), collector.size(), description, collector, null);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ readPromise.complete();
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (wip.compareAndSet(true, false)) {
+ failed = throwable;
+ handler.handleResult(0, 0, description, null, throwable);
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ if (wip.compareAndSet(true, false)) {
+ completed = true;
+ context.runOnContext(x -> readPromise.complete(null));
+ }
+ }
+
+ private class OracleRowSet implements RowSet {
+
+ private final List rows = new ArrayList<>();
+ private final RowDesc desc;
+
+ private OracleRowSet(RowDesc desc) {
+ this.desc = desc;
+ }
+
+ @Override
+ public RowIterator iterator() {
+ Iterator iterator = rows.iterator();
+ return new RowIterator<>() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public Row next() {
+ return iterator.next();
+ }
+ };
+ }
+
+ @Override
+ public int rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public List columnsNames() {
+ return desc.columnNames();
+ }
+
+ @Override
+ public List columnDescriptors() {
+ return desc.columnDescriptor();
+ }
+
+ @Override
+ public int size() {
+ return rows.size();
+ }
+
+ @Override
+ public V property(PropertyKind propertyKind) {
+ return null; // TODO
+ }
+
+ @Override
+ public RowSet value() {
+ return this;
+ }
+
+ @Override
+ public RowSet next() {
+ return null;
+ }
+
+ public void add(Row item) {
+ rows.add(item);
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/SqlOutParamImpl.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/SqlOutParamImpl.java
new file mode 100644
index 000000000..3551a91be
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/SqlOutParamImpl.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl;
+
+import io.vertx.oracle.SqlOutParam;
+
+public class SqlOutParamImpl implements SqlOutParam {
+
+ private final Object value;
+ private final int type;
+ private final boolean in;
+
+ public SqlOutParamImpl(Object value, int type) {
+ this.value = value;
+ this.type = type;
+ in = true;
+ }
+
+ public SqlOutParamImpl(int type) {
+ this.value = null;
+ this.type = type;
+ in = false;
+ }
+
+ @Override
+ public boolean in() {
+ return in;
+ }
+
+ @Override
+ public int type() {
+ return type;
+ }
+
+ @Override
+ public Object value() {
+ return value;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/AbstractCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/AbstractCommand.java
new file mode 100644
index 000000000..ce81f2072
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/AbstractCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.sqlclient.impl.command.CommandBase;
+import oracle.jdbc.OracleConnection;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public abstract class AbstractCommand extends CommandBase {
+
+ protected final OracleConnectOptions options;
+
+ protected AbstractCommand(OracleConnectOptions options) {
+ this.options = options;
+ }
+
+ public abstract Future execute(OracleConnection conn, Context context);
+
+ protected void applyStatementOptions(Statement statement) throws SQLException {
+ if (options != null) {
+ if (options.getQueryTimeout() > 0) {
+ statement.setQueryTimeout(options.getQueryTimeout());
+ }
+ if (options.getFetchDirection() != null) {
+ //noinspection MagicConstant
+ statement.setFetchDirection(options.getFetchDirection().getType());
+ }
+ if (options.getFetchSize() != 0) {
+ statement.setFetchSize(options.getFetchSize());
+ }
+ if (options.getMaxRows() > 0) {
+ statement.setMaxRows(options.getMaxRows());
+ }
+ }
+ }
+
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleCursorQueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleCursorQueryCommand.java
new file mode 100644
index 000000000..a6f3baf43
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleCursorQueryCommand.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.SqlOutParam;
+import io.vertx.oracle.impl.Helper;
+import io.vertx.oracle.impl.OracleRow;
+import io.vertx.oracle.impl.RowReader;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.Tuple;
+import io.vertx.sqlclient.impl.QueryResultHandler;
+import io.vertx.sqlclient.impl.RowDesc;
+import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.OraclePreparedStatement;
+import oracle.jdbc.OracleResultSet;
+
+import java.sql.*;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Flow;
+
+import static io.vertx.oracle.impl.Helper.unwrapOraclePreparedStatement;
+
+public class OracleCursorQueryCommand extends QueryCommand {
+ private final ExtendedQueryCommand command;
+ private final Tuple params;
+
+ public OracleCursorQueryCommand(OracleConnectOptions options, ExtendedQueryCommand command, Tuple params) {
+ super(options, null);
+ this.command = command;
+ this.params = params;
+ }
+
+ @Override
+ public Future> execute(OracleConnection conn, Context context) {
+ Future future = prepare(command, conn, false, context); // TODO returnAutoGenerateKeys
+ return future
+ .flatMap(ps -> {
+ try {
+ fillStatement(ps, conn);
+ } catch (SQLException throwables) {
+ Helper.closeQuietly(ps);
+ return Future.failedFuture(throwables);
+ }
+
+ return createRowReader(ps, context)
+ .compose(rr -> rr.read(command.fetch()))
+ .map(x -> (OracleResponse) null)
+ .onComplete(ar ->
+ Helper.closeQuietly(ps)
+ );
+ });
+
+ }
+
+ public Future createRowReader(PreparedStatement sqlStatement, Context context) {
+ OraclePreparedStatement oraclePreparedStatement =
+ unwrapOraclePreparedStatement(sqlStatement);
+ try {
+ Flow.Publisher publisher = oraclePreparedStatement.executeQueryAsyncOracle();
+ return Helper.first(publisher, context)
+ .compose(ors -> {
+ try {
+ RowDesc description = createDescription(ors);
+
+ List types = new ArrayList<>();
+ for (int i = 1; i <= ors.getMetaData().getColumnCount(); i++) {
+ types.add(ors.getMetaData().getColumnClassName(i));
+ }
+ return RowReader.create(ors.publisherOracle(
+ or -> Helper.getOrHandleSQLException(() -> transform(types, description, or))),
+ context,
+ (QueryResultHandler>) command.resultHandler(), description);
+ } catch (SQLException e) {
+ return Future.failedFuture(e);
+ }
+ });
+ } catch (SQLException throwables) {
+ return Future.failedFuture(throwables);
+ }
+ }
+
+ private static RowDesc createDescription(OracleResultSet ors) throws SQLException {
+ List columnNames = new ArrayList<>();
+ RowDesc desc = new RowDesc(columnNames);
+ ResultSetMetaData metaData = ors.getMetaData();
+ int cols = metaData.getColumnCount();
+ for (int i = 1; i <= cols; i++) {
+ columnNames.add(metaData.getColumnLabel(i));
+ }
+ return desc;
+ }
+
+ private static Row transform(List ors, RowDesc desc, oracle.jdbc.OracleRow or) throws SQLException {
+ Row row = new OracleRow(desc);
+ for (int i = 1; i <= desc.columnNames().size(); i++) {
+ Object res = QueryCommand.convertSqlValue(or.getObject(i, getType(ors.get(i - 1))));
+ row.addValue(res);
+ }
+ return row;
+ }
+
+ private static Class> getType(String cn) {
+ try {
+ return OraclePreparedQuery.class.getClassLoader().loadClass(cn);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ private void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
+
+ for (int i = 0; i < params.size(); i++) {
+ // we must convert types (to comply to JDBC)
+ Object value = adaptType(conn, params.getValue(i));
+
+ if (value instanceof SqlOutParam) {
+ SqlOutParam outValue = (SqlOutParam) value;
+
+ if (outValue.in()) {
+ ps.setObject(i + 1, adaptType(conn, outValue.value()));
+ }
+
+ ((CallableStatement) ps)
+ .registerOutParameter(i + 1, outValue.type());
+ } else {
+ ps.setObject(i + 1, value);
+ }
+ }
+ }
+
+ private Object adaptType(Connection conn, Object value) throws SQLException {
+ // we must convert types (to comply to JDBC)
+
+ if (value instanceof LocalTime) {
+ // -> java.sql.Time
+ LocalTime time = (LocalTime) value;
+ return Time.valueOf(time);
+ } else if (value instanceof LocalDate) {
+ // -> java.sql.Date
+ LocalDate date = (LocalDate) value;
+ return Date.valueOf(date);
+ } else if (value instanceof Instant) {
+ // -> java.sql.Timestamp
+ Instant timestamp = (Instant) value;
+ return Timestamp.from(timestamp);
+ } else if (value instanceof Buffer) {
+ // -> java.sql.Blob
+ Buffer buffer = (Buffer) value;
+ Blob blob = conn.createBlob();
+ blob.setBytes(1, buffer.getBytes());
+ return blob;
+ }
+
+ return value;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedBatch.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedBatch.java
new file mode 100644
index 000000000..264b1ed75
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedBatch.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.SqlOutParam;
+import io.vertx.oracle.impl.Helper;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.Tuple;
+import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.OraclePreparedStatement;
+
+import java.sql.*;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.stream.Collector;
+
+import static io.vertx.oracle.impl.Helper.closeQuietly;
+import static io.vertx.oracle.impl.Helper.unwrapOraclePreparedStatement;
+
+public class OraclePreparedBatch extends QueryCommand {
+
+ private final ExtendedQueryCommand query;
+ private final List listParams;
+
+ public OraclePreparedBatch(OracleConnectOptions options, ExtendedQueryCommand query,
+ Collector collector, List listParams) {
+ super(options, collector);
+ this.query = query;
+ this.listParams = listParams;
+ }
+
+ @Override
+ public Future> execute(OracleConnection conn, Context context) {
+ boolean returnAutoGeneratedKeys = returnAutoGeneratedKeys(conn);
+ return prepare(query, conn, returnAutoGeneratedKeys, context)
+ .flatMap(ps -> {
+ try {
+ for (Tuple params : listParams) {
+ fillStatement(ps, conn, params);
+ ps.addBatch();
+ }
+ } catch (SQLException e) {
+ closeQuietly(ps);
+ return Future.failedFuture(e);
+ }
+
+ return executeBatch(ps, context)
+ .map(res ->
+ Helper.getOrHandleSQLException(
+ () -> decode(ps, res, returnAutoGeneratedKeys))
+ )
+ .onComplete(ar ->
+ closeQuietly(ps)
+ );
+
+ });
+ }
+
+ public Future executeBatch(
+ PreparedStatement batchUpdateStatement, Context context) {
+ OraclePreparedStatement oraclePreparedStatement =
+ unwrapOraclePreparedStatement(batchUpdateStatement);
+
+ try {
+ return Helper.collect(oraclePreparedStatement.executeBatchAsyncOracle(), context)
+ .map(list -> {
+ int[] res = new int[list.size()];
+ for (int i = 0; i < list.size(); i++) {
+ res[i] = list.get(i).intValue();
+ }
+ return res;
+ });
+ } catch (SQLException throwables) {
+ return Future.failedFuture(throwables);
+ }
+ }
+
+ private void fillStatement(PreparedStatement ps, Connection conn, Tuple params) throws SQLException {
+
+ for (int i = 0; i < params.size(); i++) {
+ // we must convert types (to comply to JDBC)
+ Object value = adaptType(conn, params.getValue(i));
+
+ if (value instanceof SqlOutParam) {
+ throw new SQLException("{out} parameters are not supported in batch mode");
+ } else {
+ ps.setObject(i + 1, value);
+ }
+ }
+ }
+
+ private Object adaptType(Connection conn, Object value) throws SQLException {
+ // we must convert types (to comply to JDBC)
+
+ if (value instanceof LocalTime) {
+ // -> java.sql.Time
+ LocalTime time = (LocalTime) value;
+ return Time.valueOf(time);
+ } else if (value instanceof LocalDate) {
+ // -> java.sql.Date
+ LocalDate date = (LocalDate) value;
+ return Date.valueOf(date);
+ } else if (value instanceof Instant) {
+ // -> java.sql.Timestamp
+ Instant timestamp = (Instant) value;
+ return Timestamp.from(timestamp);
+ } else if (value instanceof Buffer) {
+ // -> java.sql.Blob
+ Buffer blob = (Buffer) value;
+ return conn.createBlob().setBytes(0, blob.getBytes());
+ }
+
+ return value;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedQuery.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedQuery.java
new file mode 100644
index 000000000..1fee26fde
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedQuery.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.SqlOutParam;
+import io.vertx.oracle.impl.Helper;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.Tuple;
+import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.OraclePreparedStatement;
+
+import java.sql.*;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collector;
+
+import static io.vertx.oracle.impl.Helper.completeOrFail;
+import static io.vertx.oracle.impl.Helper.unwrapOraclePreparedStatement;
+
+public class OraclePreparedQuery extends QueryCommand {
+
+ private final ExtendedQueryCommand query;
+ private final Tuple params;
+ private final List outParams;
+
+ private static List countOut(Tuple tuple) {
+ List total = new ArrayList<>();
+ if (tuple != null) {
+ for (int i = 0; i < tuple.size(); i++) {
+ if (tuple.getValue(i) instanceof SqlOutParam) {
+ total.add(i + 1);
+ }
+ }
+ }
+
+ return total;
+ }
+
+ public OraclePreparedQuery(OracleConnectOptions options, ExtendedQueryCommand query,
+ Collector collector, Tuple params) {
+ super(options, collector);
+ this.query = query;
+ this.params = params;
+ this.outParams = countOut(params);
+ }
+
+ @Override
+ public Future> execute(OracleConnection conn, Context context) {
+ boolean returnAutoGeneratedKeys = returnAutoGeneratedKeys(conn);
+
+ Future future = prepare(context, conn, returnAutoGeneratedKeys);
+ return future
+ .flatMap(ps -> {
+ try {
+ fillStatement(ps, conn);
+ } catch (SQLException throwables) {
+ Helper.closeQuietly(ps);
+ return Future.failedFuture(throwables);
+ }
+
+ return execute(ps, context)
+ .map(res -> {
+ return Helper.getOrHandleSQLException(
+ () -> decode(ps, res, returnAutoGeneratedKeys, outParams));
+ })
+ .onComplete(ar ->
+ Helper.closeQuietly(ps)
+ );
+
+ });
+
+ }
+
+ public Future execute(PreparedStatement sqlStatement, Context context) {
+ OraclePreparedStatement oraclePreparedStatement =
+ unwrapOraclePreparedStatement(sqlStatement);
+ try {
+ return Helper.first(oraclePreparedStatement.executeAsyncOracle(), context);
+ } catch (SQLException throwables) {
+ return Future.failedFuture(throwables);
+ }
+ }
+
+ private Future prepare(Context context, Connection conn, boolean returnAutoGeneratedKeys) {
+ final String sql = query.sql();
+ if (outParams.size() > 0) {
+ // TODO Is this blocking?
+ return completeOrFail(() -> conn.prepareCall(sql));
+ } else {
+ return prepare(query, conn, returnAutoGeneratedKeys, context);
+ }
+ }
+
+ private void fillStatement(PreparedStatement ps, Connection conn) throws SQLException {
+
+ for (int i = 0; i < params.size(); i++) {
+ // we must convert types (to comply to JDBC)
+ Object value = adaptType(conn, params.getValue(i));
+
+ if (value instanceof SqlOutParam) {
+ SqlOutParam outValue = (SqlOutParam) value;
+
+ if (outValue.in()) {
+ ps.setObject(i + 1, adaptType(conn, outValue.value()));
+ }
+
+ ((CallableStatement) ps)
+ .registerOutParameter(i + 1, outValue.type());
+ } else {
+ ps.setObject(i + 1, value);
+ }
+ }
+ }
+
+ private Object adaptType(Connection conn, Object value) throws SQLException {
+ // we must convert types (to comply to JDBC)
+
+ if (value instanceof LocalTime) {
+ // -> java.sql.Time
+ LocalTime time = (LocalTime) value;
+ return Time.valueOf(time);
+ } else if (value instanceof LocalDate) {
+ // -> java.sql.Date
+ LocalDate date = (LocalDate) value;
+ return Date.valueOf(date);
+ } else if (value instanceof Instant) {
+ // -> java.sql.Timestamp
+ Instant timestamp = (Instant) value;
+ return Timestamp.from(timestamp);
+ } else if (value instanceof Buffer) {
+ // -> java.sql.Blob
+ Buffer buffer = (Buffer) value;
+ Blob blob = conn.createBlob();
+ blob.setBytes(1, buffer.getBytes());
+ return blob;
+ }
+
+ return value;
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedStatement.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedStatement.java
new file mode 100644
index 000000000..62624974d
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OraclePreparedStatement.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.sqlclient.impl.ParamDesc;
+import io.vertx.sqlclient.impl.RowDesc;
+import io.vertx.sqlclient.impl.TupleInternal;
+
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class OraclePreparedStatement implements io.vertx.sqlclient.impl.PreparedStatement {
+
+ private final String sql;
+ private final RowDesc rowDesc;
+ private final ParamDesc paramDesc;
+
+ public OraclePreparedStatement(String sql, java.sql.PreparedStatement preparedStatement) throws SQLException {
+
+ List columnNames = new ArrayList<>();
+ ResultSetMetaData metaData = preparedStatement.getMetaData();
+ if (metaData != null) {
+ // Not a SELECT
+ int cols = metaData.getColumnCount();
+ for (int i = 1; i <= cols; i++) {
+ columnNames.add(metaData.getColumnLabel(i));
+ }
+ }
+
+ this.sql = sql;
+ this.rowDesc = new RowDesc(columnNames);
+ this.paramDesc = new ParamDesc();
+ }
+
+ @Override
+ public ParamDesc paramDesc() {
+ return paramDesc;
+ }
+
+ @Override
+ public RowDesc rowDesc() {
+ return rowDesc;
+ }
+
+ @Override
+ public String sql() {
+ return sql;
+ }
+
+ @Override
+ public String prepare(TupleInternal values) {
+ return null;
+ }
+
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleResponse.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleResponse.java
new file mode 100644
index 000000000..7362f1dcc
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleResponse.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.oracle.OracleClient;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.impl.QueryResultHandler;
+import io.vertx.sqlclient.impl.RowDesc;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class OracleResponse {
+
+ static class RS {
+ R holder;
+ int size;
+ RowDesc desc;
+
+ RS(R holder, RowDesc desc, int size) {
+ this.holder = holder;
+ this.desc = desc;
+ this.size = size;
+ }
+ }
+
+ private final int update;
+ private List> rs;
+ private Row ids;
+ private List> output;
+ private R empty;
+
+ public OracleResponse(int updateCount) {
+ this.update = updateCount;
+ }
+
+ public void push(R decodeResultSet, RowDesc desc, int size) {
+ if (rs == null) {
+ rs = new ArrayList<>();
+ }
+ rs.add(new RS<>(decodeResultSet, desc, size));
+ }
+
+ public void returnedKeys(Row keys) {
+ this.ids = keys;
+ }
+
+ public void empty(R apply) {
+ this.empty = apply;
+ }
+
+ public void outputs(R decodeResultSet, RowDesc desc, int size) {
+ if (output == null) {
+ output = new ArrayList<>();
+ }
+ output.add(new RS<>(decodeResultSet, desc, size));
+ }
+
+ public void handle(QueryResultHandler handler) {
+ if (rs != null) {
+ for (RS rs : this.rs) {
+ handler.handleResult(update, rs.size, rs.desc, rs.holder, null);
+ if (ids != null) {
+ handler.addProperty(OracleClient.GENERATED_KEYS, ids);
+ }
+ }
+ }
+ if (output != null) {
+ for (RS rs : this.output) {
+ handler.handleResult(update, rs.size, null, rs.holder, null);
+ handler.addProperty(OracleClient.OUTPUT, true);
+ }
+ }
+ if (rs == null && output == null) {
+ handler.handleResult(update, -1, null, empty, null);
+ if (ids != null) {
+ handler.addProperty(OracleClient.GENERATED_KEYS, ids);
+ }
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleTransactionCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleTransactionCommand.java
new file mode 100644
index 000000000..2f85ec116
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/OracleTransactionCommand.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.impl.Helper;
+import io.vertx.sqlclient.impl.command.TxCommand;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.OraclePreparedStatement;
+
+import java.sql.SQLException;
+import java.util.concurrent.Flow;
+
+import static java.sql.Connection.TRANSACTION_READ_COMMITTED;
+import static java.sql.Connection.TRANSACTION_SERIALIZABLE;
+
+public class OracleTransactionCommand extends AbstractCommand {
+
+ private final TxCommand op;
+
+ public OracleTransactionCommand(TxCommand op, OracleConnectOptions options) {
+ super(options);
+ this.op = op;
+ }
+
+ @Override
+ public Future execute(OracleConnection conn, Context context) {
+ if (op.kind == TxCommand.Kind.BEGIN) {
+ return begin(conn, context)
+ .map(x -> op.result);
+ } else if (op.kind == TxCommand.Kind.COMMIT) {
+ return commit(conn, context)
+ .map(x -> op.result)
+ .onComplete(x -> Helper.runOrHandleSQLException(() -> conn.setAutoCommit(false)));
+ } else {
+ return rollback(conn, context)
+ .map(x -> op.result)
+ .onComplete(x -> Helper.runOrHandleSQLException(() -> conn.setAutoCommit(false)));
+ }
+ }
+
+ private Future begin(OracleConnection conn, Context context) {
+ int isolation = Helper.getOrHandleSQLException(conn::getTransactionIsolation);
+ String isolationLevel;
+ switch (isolation) {
+ case TRANSACTION_READ_COMMITTED:
+ isolationLevel = "READ COMMITTED";
+ break;
+ case TRANSACTION_SERIALIZABLE:
+ isolationLevel = "SERIALIZABLE";
+ default:
+ throw new IllegalArgumentException("Invalid isolation level: " + isolation);
+ }
+
+ try {
+ conn.setAutoCommit(false);
+ Flow.Publisher publisher = conn
+ .prepareStatement("SET TRANSACTION ISOLATION LEVEL " + isolationLevel)
+ .unwrap(OraclePreparedStatement.class)
+ .executeAsyncOracle();
+ return Helper.first(publisher, context)
+ .map(x -> null);
+ } catch (SQLException e) {
+ return Future.failedFuture(e);
+ }
+ }
+
+ private Future commit(OracleConnection conn, Context context) {
+ try {
+ if (conn.getAutoCommit()) {
+ return Future.succeededFuture();
+ } else {
+ return Helper.first(conn.commitAsyncOracle(), context);
+ }
+ } catch (SQLException e) {
+ return Future.failedFuture(e);
+ }
+ }
+
+ private Future rollback(OracleConnection conn, Context context) {
+ try {
+ if (conn.getAutoCommit()) {
+ return Future.succeededFuture();
+ } else {
+ return Helper.first(conn.rollbackAsyncOracle(), context);
+ }
+ } catch (SQLException e) {
+ return Future.failedFuture(e);
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/PingCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/PingCommand.java
new file mode 100644
index 000000000..501d32759
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/PingCommand.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.oracle.OracleConnectOptions;
+import oracle.jdbc.OracleConnection;
+
+import java.sql.SQLException;
+
+public class PingCommand extends AbstractCommand {
+ public PingCommand(OracleConnectOptions options) {
+ super(options);
+ }
+
+ @Override
+ public Future execute(OracleConnection conn, Context context) {
+ return context.executeBlocking(p -> {
+ int result;
+ try {
+ result = conn.pingDatabase();
+
+ } catch (SQLException throwables) {
+ p.fail(throwables);
+ return;
+ }
+ p.complete(result);
+ }
+ );
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/PrepareStatementCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/PrepareStatementCommand.java
new file mode 100644
index 000000000..8521d5a87
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/PrepareStatementCommand.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonArray;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.sqlclient.impl.PreparedStatement;
+import oracle.jdbc.OracleConnection;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class PrepareStatementCommand extends AbstractCommand {
+
+ private final String sql;
+
+ public PrepareStatementCommand(OracleConnectOptions options, String sql) {
+ super(options);
+ this.sql = sql;
+ }
+
+ @Override
+ public Future execute(OracleConnection conn, Context context) {
+ boolean autoGeneratedKeys = options == null || options.isAutoGeneratedKeys();
+ boolean autoGeneratedIndexes = options != null && options.getAutoGeneratedKeysIndexes() != null;
+
+ if (autoGeneratedKeys && !autoGeneratedIndexes) {
+ return prepareReturningKey(conn);
+ } else if (autoGeneratedIndexes) {
+ return prepareWithAutoGeneratedIndexes(conn, context);
+ } else {
+ return prepare(conn, context);
+ }
+ }
+
+ private Future prepareWithAutoGeneratedIndexes(OracleConnection conn, Context context) {
+ return context.owner().executeBlocking(p -> {
+ // convert json array to int or string array
+ JsonArray indexes = options.getAutoGeneratedKeysIndexes();
+ try {
+ if (indexes.getValue(0) instanceof Number) {
+ int[] keys = new int[indexes.size()];
+ for (int i = 0; i < keys.length; i++) {
+ keys[i] = indexes.getInteger(i);
+ }
+ OraclePreparedStatement statement = create(conn.prepareStatement(sql, keys));
+ p.complete(statement);
+ } else if (indexes.getValue(0) instanceof String) {
+ String[] keys = new String[indexes.size()];
+ for (int i = 0; i < keys.length; i++) {
+ keys[i] = indexes.getString(i);
+ }
+ OraclePreparedStatement statement = create(conn.prepareStatement(sql, keys));
+ p.complete(statement);
+ } else {
+ p.fail(new SQLException("Invalid type of index, only [int, String] allowed"));
+ }
+ } catch (RuntimeException | SQLException e) {
+ p.fail(e);
+ }
+ });
+ }
+
+ private Future prepareReturningKey(OracleConnection connection) {
+ try {
+ java.sql.PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
+ applyStatementOptions(ps);
+ return Future.succeededFuture(new OraclePreparedStatement(sql, ps));
+ } catch (Exception e) {
+ return Future.failedFuture(e);
+ }
+ }
+
+ private Future prepare(OracleConnection connection, Context context) {
+ return context.owner().executeBlocking(p -> {
+ try {
+ OraclePreparedStatement result = create(connection.prepareStatement(sql));
+ p.complete(result);
+ } catch (Exception e) {
+ p.fail(e);
+ }
+ });
+ }
+
+ private OraclePreparedStatement create(java.sql.PreparedStatement statement) throws SQLException {
+ applyStatementOptions(statement);
+ return new OraclePreparedStatement(sql, statement);
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/QueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/QueryCommand.java
new file mode 100644
index 000000000..a8f643165
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/QueryCommand.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonArray;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.impl.OracleRow;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.Tuple;
+import io.vertx.sqlclient.impl.RowDesc;
+import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;
+
+import java.math.BigDecimal;
+import java.sql.*;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.stream.Collector;
+
+import static io.vertx.oracle.impl.Helper.completeOrFail;
+
+public abstract class QueryCommand extends AbstractCommand> {
+
+ private final Collector collector;
+
+ public QueryCommand(OracleConnectOptions options, Collector collector) {
+ super(options);
+ this.collector = collector;
+ }
+
+ protected OracleResponse decode(Statement statement, boolean returnedResultSet, boolean returnedKeys,
+ List out) throws SQLException {
+
+ final OracleResponse response = new OracleResponse<>(statement.getUpdateCount());
+ if (returnedResultSet) {
+ // normal return only
+ while (returnedResultSet) {
+ try (ResultSet rs = statement.getResultSet()) {
+ decodeResultSet(rs, response);
+ }
+ if (returnedKeys) {
+ decodeReturnedKeys(statement, response);
+ }
+ returnedResultSet = statement.getMoreResults();
+ }
+ } else {
+ collector.accumulator();
+ // first rowset includes the output results
+ C container = collector.supplier().get();
+
+ response.empty(collector.finisher().apply(container));
+ if (returnedKeys) {
+ decodeReturnedKeys(statement, response);
+ }
+ }
+
+ if (out.size() > 0) {
+ decodeOutput((CallableStatement) statement, out, response);
+ }
+
+ return response;
+ }
+
+ // protected OracleResponse decode(Statement statement, RowReader.ReadRows rr, boolean returnedKeys,
+ // List out) throws SQLException {
+ //
+ // final OracleResponse response = new OracleResponse<>(statement.getUpdateCount());
+ // BiConsumer accumulator = collector.accumulator();
+ // C container = collector.supplier().get();
+ //
+ // for (Row row : rr.getRows()) {
+ // accumulator.accept(container, row);
+ // }
+ //
+ // response
+ // .push(collector.finisher().apply(container), rr.getRowDescription(), rr.getRows().size());
+ //
+ // if (returnedKeys) {
+ // decodeReturnedKeys(statement, response);
+ // }
+ //
+ // if (out.size() > 0) {
+ // decodeOutput((CallableStatement) statement, out, response);
+ // }
+ //
+ // return response;
+ // }
+
+ protected OracleResponse decode(Statement statement, int[] returnedBatchResult, boolean returnedKeys)
+ throws SQLException {
+ final OracleResponse response = new OracleResponse<>(returnedBatchResult.length);
+
+ BiConsumer accumulator = collector.accumulator();
+
+ RowDesc desc = new RowDesc(Collections.emptyList());
+ C container = collector.supplier().get();
+ for (int result : returnedBatchResult) {
+ Row row = new OracleRow(desc);
+ row.addValue(result);
+ accumulator.accept(container, row);
+ }
+
+ response
+ .push(collector.finisher().apply(container), desc, returnedBatchResult.length);
+
+ if (returnedKeys) {
+ decodeReturnedKeys(statement, response);
+ }
+
+ return response;
+ }
+
+ private void decodeResultSet(ResultSet rs, OracleResponse response) throws SQLException {
+ BiConsumer accumulator = collector.accumulator();
+
+ List columnNames = new ArrayList<>();
+ RowDesc desc = new RowDesc(columnNames);
+ C container = collector.supplier().get();
+ int size = 0;
+ ResultSetMetaData metaData = rs.getMetaData();
+ int cols = metaData.getColumnCount();
+ for (int i = 1; i <= cols; i++) {
+ columnNames.add(metaData.getColumnLabel(i));
+ }
+ while (rs.next()) {
+ size++;
+ Row row = new OracleRow(desc);
+ for (int i = 1; i <= cols; i++) {
+ Object res = convertSqlValue(rs.getObject(i));
+ row.addValue(res);
+ }
+ accumulator.accept(container, row);
+ }
+
+ response
+ .push(collector.finisher().apply(container), desc, size);
+ }
+
+ private R decodeRawResultSet(ResultSet rs) throws SQLException {
+ BiConsumer accumulator = collector.accumulator();
+
+ List columnNames = new ArrayList<>();
+ RowDesc desc = new RowDesc(columnNames);
+ C container = collector.supplier().get();
+
+ ResultSetMetaData metaData = rs.getMetaData();
+ int cols = metaData.getColumnCount();
+ for (int i = 1; i <= cols; i++) {
+ columnNames.add(metaData.getColumnLabel(i));
+ }
+ while (rs.next()) {
+ Row row = new OracleRow(desc);
+ for (int i = 1; i <= cols; i++) {
+ Object res = convertSqlValue(rs.getObject(i));
+ row.addValue(res);
+ }
+ accumulator.accept(container, row);
+ }
+
+ return collector.finisher().apply(container);
+ }
+
+ private void decodeOutput(CallableStatement cs, List out, OracleResponse output) throws SQLException {
+ BiConsumer accumulator = collector.accumulator();
+
+ // first rowset includes the output results
+ C container = collector.supplier().get();
+
+ // the result is unlabeled
+ Row row = new OracleRow(new RowDesc(Collections.emptyList()));
+ for (Integer idx : out) {
+ if (cs.getObject(idx) instanceof ResultSet) {
+ row.addValue(decodeRawResultSet((ResultSet) cs.getObject(idx)));
+ } else {
+ Object res = convertSqlValue(cs.getObject(idx));
+ row.addValue(res);
+ }
+ }
+
+ accumulator.accept(container, row);
+
+ R result = collector.finisher().apply(container);
+
+ output.outputs(result, null, 1);
+ }
+
+ private void decodeReturnedKeys(Statement statement, OracleResponse response) throws SQLException {
+ Row keys = null;
+
+ ResultSet keysRS = statement.getGeneratedKeys();
+
+ if (keysRS != null) {
+ ResultSetMetaData metaData = keysRS.getMetaData();
+ if (metaData != null) {
+ int cols = metaData.getColumnCount();
+ if (cols > 0) {
+ List keysColumnNames = new ArrayList<>();
+ RowDesc keysDesc = new RowDesc(keysColumnNames);
+ for (int i = 1; i <= cols; i++) {
+ keysColumnNames.add(metaData.getColumnLabel(i));
+ }
+
+ if (keysRS.next()) {
+ keys = new OracleRow(keysDesc);
+ for (int i = 1; i <= cols; i++) {
+ Object res = convertSqlValue(keysRS.getObject(i));
+ keys.addValue(res);
+ }
+ }
+ response.returnedKeys(keys);
+ }
+ }
+ }
+ }
+
+ public static Object convertSqlValue(Object value) throws SQLException {
+ if (value == null) {
+ return null;
+ }
+
+ // valid json types are just returned as is
+ if (value instanceof Boolean || value instanceof String || value instanceof byte[]) {
+ return value;
+ }
+
+ // numeric values
+ if (value instanceof Number) {
+ if (value instanceof BigDecimal) {
+ BigDecimal d = (BigDecimal) value;
+ if (d.scale() == 0) {
+ return ((BigDecimal) value).toBigInteger();
+ } else {
+ // we might loose precision here
+ return ((BigDecimal) value).doubleValue();
+ }
+ }
+
+ return value;
+ }
+
+ // JDBC temporal values
+
+ if (value instanceof Time) {
+ return ((Time) value).toLocalTime();
+ }
+
+ if (value instanceof Date) {
+ return ((Date) value).toLocalDate();
+ }
+
+ if (value instanceof Timestamp) {
+ return ((Timestamp) value).toInstant().atOffset(ZoneOffset.UTC);
+ }
+
+ // large objects
+ if (value instanceof Clob) {
+ Clob c = (Clob) value;
+ try {
+ // result might be truncated due to downcasting to int
+ return c.getSubString(1, (int) c.length());
+ } finally {
+ try {
+ c.free();
+ } catch (AbstractMethodError | SQLFeatureNotSupportedException e) {
+ // ignore since it is an optional feature since 1.6 and non existing before 1.6
+ }
+ }
+ }
+
+ if (value instanceof Blob) {
+ Blob b = (Blob) value;
+ try {
+ // result might be truncated due to downcasting to int
+ return b.getBytes(1, (int) b.length());
+ } finally {
+ try {
+ b.free();
+ } catch (AbstractMethodError | SQLFeatureNotSupportedException e) {
+ // ignore since it is an optional feature since 1.6 and non existing before 1.6
+ }
+ }
+ }
+
+ // arrays
+ if (value instanceof Array) {
+ Array a = (Array) value;
+ try {
+ Object arr = a.getArray();
+ if (arr != null) {
+ int len = java.lang.reflect.Array.getLength(arr);
+ Object[] castedArray = new Object[len];
+ for (int i = 0; i < len; i++) {
+ castedArray[i] = convertSqlValue(java.lang.reflect.Array.get(arr, i));
+ }
+ return castedArray;
+ }
+ } finally {
+ a.free();
+ }
+ }
+
+ // RowId
+ if (value instanceof RowId) {
+ return ((RowId) value).getBytes();
+ }
+
+ // Struct
+ if (value instanceof Struct) {
+ return Tuple.of(((Struct) value).getAttributes());
+ }
+
+ // fallback to String
+ return value.toString();
+ }
+
+ boolean returnAutoGeneratedKeys(Connection conn) {
+ boolean autoGeneratedKeys = options == null || options.isAutoGeneratedKeys();
+ boolean autoGeneratedIndexes = options != null && options.getAutoGeneratedKeysIndexes() != null
+ && options.getAutoGeneratedKeysIndexes().size() > 0;
+ // // even though the user wants it, the DBMS may not support it
+ // if (autoGeneratedKeys || autoGeneratedIndexes) {
+ // try {
+ // DatabaseMetaData dbmd = conn.getMetaData();
+ // if (dbmd != null) {
+ // return dbmd.supportsGetGeneratedKeys();
+ // }
+ // } catch (SQLException e) {
+ // // ignore...
+ // }
+ // }
+ // TODO Oracle does not support this in batch???
+ return false;
+ }
+
+ protected Future prepare(ExtendedQueryCommand query, Connection conn,
+ boolean returnAutoGeneratedKeys, Context context) {
+ boolean autoGeneratedIndexes = options != null && options.getAutoGeneratedKeysIndexes() != null
+ && options.getAutoGeneratedKeysIndexes().size() > 0;
+
+ String sql = query.sql();
+ int fetch = query.fetch();
+ if (returnAutoGeneratedKeys && !autoGeneratedIndexes) {
+ return completeOrFail(() -> {
+ PreparedStatement statement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
+ configureFetch(fetch, statement);
+ if (query.cursorId() != null) {
+ statement.setCursorName(query.cursorId());
+ }
+ return statement;
+ });
+ } else if (autoGeneratedIndexes) {
+ return context.executeBlocking(promise -> createPreparedStatement(conn, sql, fetch, promise));
+ } else {
+ return completeOrFail(() -> {
+ PreparedStatement statement = conn.prepareStatement(sql);
+ configureFetch(fetch, statement);
+ return statement;
+ });
+ }
+ }
+
+ private void configureFetch(int fetch, PreparedStatement statement) throws SQLException {
+ if (fetch > 0) {
+ statement.setFetchSize(fetch);
+ if (options.getFetchDirection() != null) {
+ statement.setFetchDirection(options.getFetchDirection().getType());
+ }
+ }
+ }
+
+ protected void createPreparedStatement(Connection conn, String sql, int fetch,
+ io.vertx.core.Promise promise) {
+ // convert json array to int or string array
+ JsonArray indexes = options.getAutoGeneratedKeysIndexes();
+ try {
+ if (indexes.getValue(0) instanceof Number) {
+ int[] keys = new int[indexes.size()];
+ for (int i = 0; i < keys.length; i++) {
+ keys[i] = indexes.getInteger(i);
+ }
+ promise.complete(conn.prepareStatement(sql, keys));
+ } else if (indexes.getValue(0) instanceof String) {
+ String[] keys = new String[indexes.size()];
+ for (int i = 0; i < keys.length; i++) {
+ keys[i] = indexes.getString(i);
+ }
+ PreparedStatement statement = conn.prepareStatement(sql, keys);
+ configureFetch(fetch, statement);
+ promise.complete(statement);
+ } else {
+ promise.fail(new SQLException("Invalid type of index, only [int, String] allowed"));
+ }
+ } catch (SQLException e) {
+ promise.fail(e);
+ } catch (RuntimeException e) {
+ // any exception due to type conversion
+ promise.fail(new SQLException(e));
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/SimpleQueryCommand.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/SimpleQueryCommand.java
new file mode 100644
index 000000000..ade86fe18
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/commands/SimpleQueryCommand.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.commands;
+
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.impl.Helper;
+import io.vertx.sqlclient.Row;
+import oracle.jdbc.OracleConnection;
+import oracle.jdbc.OraclePreparedStatement;
+
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.stream.Collector;
+
+public class SimpleQueryCommand extends QueryCommand {
+
+ private final String sql;
+
+ public SimpleQueryCommand(OracleConnectOptions options, String sql,
+ Collector collector) {
+ super(options, collector);
+ this.sql = sql;
+ }
+
+ private Future execute(OraclePreparedStatement sqlStatement, Context context) {
+ try {
+ return Helper.first(sqlStatement.executeAsyncOracle(), context);
+ } catch (SQLException throwables) {
+ return Future.failedFuture(throwables);
+ }
+ }
+
+ private void closeQuietly(OraclePreparedStatement c) {
+ if (c == null) {
+ return;
+ }
+ try {
+ c.close();
+ } catch (Exception e) {
+ // Ignore it.
+ }
+ }
+
+ @Override
+ public Future> execute(OracleConnection conn, Context context) {
+ OraclePreparedStatement ps = null;
+ try {
+ ps = (OraclePreparedStatement) conn.prepareStatement(sql);
+ applyStatementOptions(ps);
+ final OraclePreparedStatement ref = ps;
+ return execute(ps, context)
+ .compose(mayBeResult -> {
+ try {
+ return Future.succeededFuture(decode(ref, mayBeResult, false, Collections.emptyList()));
+ } catch (SQLException throwables) {
+ return Future.failedFuture(throwables);
+ } finally {
+ closeQuietly(ref);
+ }
+ });
+ } catch (SQLException e) {
+ closeQuietly(ps);
+ return Future.failedFuture(e);
+ }
+ }
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/spi/OracleDriver.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/spi/OracleDriver.java
new file mode 100644
index 000000000..79f927983
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/impl/spi/OracleDriver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.impl.spi;
+
+import io.vertx.core.Vertx;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.OraclePool;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.PoolOptions;
+import io.vertx.sqlclient.SqlConnectOptions;
+import io.vertx.sqlclient.spi.Driver;
+
+public class OracleDriver implements Driver {
+
+ @Override
+ public Pool createPool(SqlConnectOptions options, PoolOptions poolOptions) {
+ return OraclePool.pool(wrap(options), poolOptions);
+ }
+
+ @Override
+ public Pool createPool(Vertx vertx, SqlConnectOptions options, PoolOptions poolOptions) {
+ return OraclePool.pool(vertx, wrap(options), poolOptions);
+ }
+
+ @Override
+ public boolean acceptsOptions(SqlConnectOptions options) {
+ return options instanceof OracleConnectOptions || SqlConnectOptions.class.equals(options.getClass());
+ }
+
+ private static OracleConnectOptions wrap(SqlConnectOptions options) {
+ if (options instanceof OracleConnectOptions) {
+ return (OracleConnectOptions) options;
+ } else {
+ return new OracleConnectOptions(options);
+ }
+ }
+
+}
diff --git a/vertx-oracle-client/src/main/java/io/vertx/oracle/package-info.java b/vertx-oracle-client/src/main/java/io/vertx/oracle/package-info.java
new file mode 100644
index 000000000..7db0ec46c
--- /dev/null
+++ b/vertx-oracle-client/src/main/java/io/vertx/oracle/package-info.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2011-2021 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
+ */
+@ModuleGen(name = "vertx-oracle-client", groupPackage = "io.vertx")
+package io.vertx.oracle;
+
+import io.vertx.codegen.annotations.ModuleGen;
diff --git a/vertx-oracle-client/src/main/resources/META-INF/MANIFEST.MF b/vertx-oracle-client/src/main/resources/META-INF/MANIFEST.MF
new file mode 100644
index 000000000..2bff83ce9
--- /dev/null
+++ b/vertx-oracle-client/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,2 @@
+Automatic-Module-Name: io.vertx.client.sql.oracle
+
diff --git a/vertx-oracle-client/src/main/resources/META-INF/services/io.vertx.sqlclient.spi.Driver b/vertx-oracle-client/src/main/resources/META-INF/services/io.vertx.sqlclient.spi.Driver
new file mode 100644
index 000000000..ec553691a
--- /dev/null
+++ b/vertx-oracle-client/src/main/resources/META-INF/services/io.vertx.sqlclient.spi.Driver
@@ -0,0 +1 @@
+io.vertx.oracle.impl.spi.OracleDriver
diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/OraclePoolTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/OraclePoolTest.java
new file mode 100644
index 000000000..3212a6387
--- /dev/null
+++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/OraclePoolTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.test;
+
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.OracleConnection;
+import io.vertx.oracle.OraclePool;
+import io.vertx.oracle.test.junit.OracleRule;
+import io.vertx.sqlclient.*;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.List;
+
+public class OraclePoolTest extends OracleTestBase {
+
+ @Rule
+ public OracleRule oracle;
+
+ static final String DROP_TABLE = "DROP TABLE fruits";
+ static final String CREATE_TABLE = "CREATE TABLE fruits (" +
+ "id integer PRIMARY KEY, " +
+ "name VARCHAR(100), " +
+ "quantity INTEGER)";
+ static final String INSERT = "INSERT INTO fruits (id, name, quantity) VALUES (?, ?, ?)";
+
+ @Test
+ public void test() {
+ OraclePool pool = OraclePool.pool(vertx, new OracleConnectOptions()
+ .setHost(OracleRule.getDatabaseHost())
+ .setPort(OracleRule.getDatabasePort())
+ .setUser(OracleRule.getUser())
+ .setPassword(OracleRule.getPassword())
+ .setDatabase(OracleRule.getDatabase()),
+ new PoolOptions().setMaxSize(1)
+ );
+
+ SqlConnection connection = await(pool.getConnection());
+ System.out.println(connection);
+
+ System.out.println(
+ "metadata: " + connection.databaseMetadata().fullVersion() + " " + connection.databaseMetadata()
+ .productName());
+
+ try {
+ await(connection.query(DROP_TABLE).execute());
+ } catch (Exception ignored) {
+
+ }
+
+ await(connection.query(CREATE_TABLE).execute());
+
+ await(connection.prepare(INSERT)
+ .flatMap(ps -> ps.query().execute(Tuple.of(1, "apple", 10))));
+ await(connection.prepare(INSERT)
+ .flatMap(ps -> ps.query().execute(Tuple.of(2, "pear", 5))));
+ await(connection.prepare(INSERT)
+ .flatMap(ps -> ps.query().execute(Tuple.of(3, "mango", 3))));
+
+ RowSet rows = await(connection.query("SELECT * FROM fruits").execute());
+ rows.forEach(row -> System.out.printf("[%d] %s : %d%n", row.get(Integer.class, 0), row.get(String.class, 1),
+ row.get(Integer.class, 2)));
+
+ RowSet res = await(connection.query("SELECT * FROM fruits WHERE id = 1").execute());
+ System.out.println("Select one : " + res.iterator().next().get(String.class, 1));
+
+ // Batch
+ System.out.println("Batch insert:");
+ RowSet set = await(connection.preparedQuery(INSERT)
+ .executeBatch(List.of(
+ Tuple.of(4, "pineapple", 1),
+ Tuple.of(5, "kiwi", 2),
+ Tuple.of(6, "orange", 3),
+ Tuple.of(7, "strawberry", 20)
+ )));
+
+ System.out.println(set.size());
+
+ rows = await(connection.query("SELECT * FROM fruits").execute());
+ rows.forEach(row -> System.out.printf("[%d] %s : %d%n", row.get(Integer.class, 0), row.get(String.class, 1),
+ row.get(Integer.class, 2)));
+
+ System.out.println("Transaction:");
+ await(connection.begin()
+ .flatMap(tx ->
+ connection.prepare(INSERT).flatMap(ps -> ps.query().execute(Tuple.of(20, "olive", 200)))
+ .flatMap(x -> tx.commit())
+ .eventually(x -> tx.rollback())
+ ));
+
+ await(connection.begin()
+ .flatMap(tx ->
+ connection.prepare(INSERT).flatMap(ps -> ps.query().execute(Tuple.of(23, "nope", -2)))
+ .flatMap(x -> tx.rollback())
+ .eventually(x -> tx.rollback())
+ ));
+
+ rows = await(connection.query("SELECT * FROM fruits").execute());
+ rows.forEach(row -> System.out.printf("[%d] %s : %d%n", row.get(Integer.class, 0), row.get(String.class, 1),
+ row.get(Integer.class, 2)));
+
+ System.out.println("Ping");
+ System.out.println(await(((OracleConnection) connection).ping()));
+
+ await(connection.close());
+ }
+
+}
diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/OracleTestBase.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/OracleTestBase.java
new file mode 100644
index 000000000..353d8a96a
--- /dev/null
+++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/OracleTestBase.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.test;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class OracleTestBase {
+
+ public static Vertx vertx;
+
+ @BeforeClass
+ public static void start() {
+ vertx = Vertx.vertx();
+ }
+
+ @AfterClass
+ public static void stop() {
+ await(vertx.close());
+ }
+
+ public static T await(Future future) {
+ try {
+ return future.toCompletionStage().toCompletableFuture().get(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("Interrupted before receiving a result");
+ } catch (ExecutionException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/junit/OracleRule.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/junit/OracleRule.java
new file mode 100644
index 000000000..2a823a102
--- /dev/null
+++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/junit/OracleRule.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.test.junit;
+
+import io.vertx.oracle.OracleConnectOptions;
+import org.junit.rules.ExternalResource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import java.time.Duration;
+
+public class OracleRule extends ExternalResource {
+
+ static final String IMAGE = "gvenzl/oracle-xe";
+ static final String PASSWORD = "vertx";
+ static final int PORT = 1521;
+
+ static final GenericContainer> ORACLE_DB;
+
+ static {
+ String containerVersion = System.getProperty("oracle-container.version");
+ if (containerVersion == null || containerVersion.isEmpty()) {
+ containerVersion = "18-slim";
+ }
+
+ String image = IMAGE + ":" + containerVersion;
+
+ ORACLE_DB = new GenericContainer<>(image)
+ .withEnv("ORACLE_PASSWORD", PASSWORD)
+ .withExposedPorts(PORT)
+ .withFileSystemBind("src/test/resources/tck", "/container-entrypoint-initdb.d")
+ .withLogConsumer(of -> System.out.print("[ORACLE] " + of.getUtf8String()))
+ .waitingFor(
+ Wait.forLogMessage(".*DATABASE IS READY TO USE!.*\\n", 1)
+ )
+ .withStartupTimeout(Duration.ofMinutes(15));
+
+ ORACLE_DB.start();
+ }
+
+ public static String getPassword() {
+ return PASSWORD;
+ }
+
+ public static String getUser() {
+ return "sys as sysdba";
+ }
+
+ public static String getDatabaseHost() {
+ return ORACLE_DB.getHost();
+ }
+
+ public static int getDatabasePort() {
+ return ORACLE_DB.getMappedPort(1521);
+ }
+
+ public static String getDatabase() {
+ return "xe";
+ }
+
+ private OracleConnectOptions options;
+
+ public static final OracleRule SHARED_INSTANCE = new OracleRule();
+
+ public synchronized OracleConnectOptions getOptions() throws Exception {
+ return new OracleConnectOptions()
+ .setPort(getDatabasePort())
+ .setHost(getDatabaseHost())
+ .setUser(getUser())
+ .setPassword(getPassword())
+ .setDatabase(getDatabase());
+ }
+
+ public OracleConnectOptions options() {
+ return new OracleConnectOptions(options);
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ options = getOptions();
+ }
+
+ @Override
+ protected void after() {
+
+ }
+}
diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/ClientConfig.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/ClientConfig.java
new file mode 100644
index 000000000..c500a66c1
--- /dev/null
+++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/ClientConfig.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.test.tck;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.oracle.OracleConnectOptions;
+import io.vertx.oracle.OracleConnection;
+import io.vertx.oracle.OraclePool;
+import io.vertx.sqlclient.PoolOptions;
+import io.vertx.sqlclient.SqlClient;
+import io.vertx.sqlclient.SqlConnectOptions;
+import io.vertx.sqlclient.SqlConnection;
+import io.vertx.sqlclient.tck.Connector;
+
+public enum ClientConfig {
+
+ CONNECT() {
+ @Override
+ Connector connect(Vertx vertx, SqlConnectOptions options) {
+ return new Connector<>() {
+ @Override
+ public void connect(Handler> handler) {
+ OracleConnection.connect(vertx, new OracleConnectOptions(options.toJson()), ar -> {
+ if (ar.succeeded()) {
+ handler.handle(Future.succeededFuture(ar.result()));
+ } else {
+ handler.handle(Future.failedFuture(ar.cause()));
+ }
+ });
+ }
+
+ @Override
+ public void close() {
+ }
+ };
+ }
+ },
+
+ POOLED() {
+ @Override
+ Connector connect(Vertx vertx, SqlConnectOptions options) {
+ OraclePool pool = OraclePool
+ .pool(vertx, new OracleConnectOptions(options.toJson()), new PoolOptions().setMaxSize(1));
+ return new Connector() {
+ @Override
+ public void connect(Handler> handler) {
+ pool.getConnection(ar -> {
+ if (ar.succeeded()) {
+ handler.handle(Future.succeededFuture(ar.result()));
+ } else {
+ handler.handle(Future.failedFuture(ar.cause()));
+ }
+ });
+ }
+
+ @Override
+ public void close() {
+ pool.close();
+ }
+ };
+ }
+ };
+
+ abstract Connector connect(Vertx vertx, SqlConnectOptions options);
+
+}
diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleCollectorTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleCollectorTest.java
new file mode 100644
index 000000000..1abe5db79
--- /dev/null
+++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleCollectorTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2011-2021 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 io.vertx.oracle.test.tck;
+
+import io.vertx.ext.unit.Async;
+import io.vertx.ext.unit.TestContext;
+import io.vertx.ext.unit.junit.VertxUnitRunner;
+import io.vertx.oracle.test.junit.OracleRule;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.tck.CollectorTestBase;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+@RunWith(VertxUnitRunner.class)
+public class OracleCollectorTest extends CollectorTestBase {
+ @ClassRule
+ public static OracleRule rule = OracleRule.SHARED_INSTANCE;
+
+ @Override
+ protected void initConnector() {
+ connector = ClientConfig.CONNECT.connect(vertx, rule.options());
+ }
+
+ @Test
+ public void testSimpleQuery(TestContext ctx) {
+ Async async = ctx.async();
+ Collector> collector = Collectors.toMap((row) -> row.getInteger(0),
+ row -> new TestingCollectorObject(row.getInteger(0), row.getShort(1), row.getInteger(2), row.getLong(3),
+ row.getFloat(4), row.getDouble(5), row.getString(6)));
+
+ TestingCollectorObject expected = new TestingCollectorObject(1, (short) 32767, 2147483647, 9223372036854775807L,
+ 123.456F, 1.234567D, "HELLO,WORLD");
+ this.connector.connect(ctx.asyncAssertSuccess((conn) -> {
+ conn.query("SELECT * FROM test_collector WHERE id = 1").collecting(collector)
+ .execute(ctx.asyncAssertSuccess((result) -> {
+ Map map = result.value();
+ TestingCollectorObject actual = map.get(1);
+ ctx.assertEquals(expected, actual);
+ conn.close();
+ async.complete();
+ }));
+ }));
+ }
+
+ @Test
+ public void testPreparedQuery(TestContext ctx) {
+ Async async = ctx.async();
+ Collector> collector = Collectors.toMap(
+ row -> row.getInteger("id"),
+ row -> new TestingCollectorObject(row.getInteger(0),
+ row.getShort(1),
+ row.getInteger(2),
+ row.getLong(3),
+ row.getFloat(4),
+ row.getDouble(5),
+ row.getString(6))
+ );
+
+ TestingCollectorObject expected = new TestingCollectorObject(1, (short) 32767, 2147483647, 9223372036854775807L,
+ 123.456f, 1.234567d, "HELLO,WORLD");
+
+ connector.connect(ctx.asyncAssertSuccess(conn -> {
+ conn.preparedQuery("SELECT * FROM test_collector WHERE id = 1")
+ .collecting(collector)
+ .execute(ctx.asyncAssertSuccess(result -> {
+ Map map = result.value();
+ TestingCollectorObject actual = map.get(1);
+ ctx.assertEquals(expected, actual);
+ conn.close();
+ async.complete();
+ }));
+ }));
+ }
+
+ @Test
+ public void testCollectorFailureProvidingSupplier(TestContext ctx) {
+ RuntimeException cause = new RuntimeException();
+ testCollectorFailure(ctx.async(), ctx, cause, new CollectorBase() {
+ @Override
+ public Supplier