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-client vertx-db2-client vertx-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 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 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 cmd = new OracleCursorQueryCommand<>(options, command, command.params()); + return cmd.execute(connection, context) + .map(false); + } + + QueryCommand 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: + *

+   *   try {
+   *     return resultSet.getMetaData();
+   *   }
+   *   catch (SQLException sqlException) {
+   *     throw OracleR2dbcExceptions.toR2dbcException(sqlException);
+   *   }
+   * 
+ * Can be expressed more concisely as: + *
+   *   return getOrHandleSQLException(resultSet::getMetaData);
+   * 
+ * + * @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 supplier() { + throw cause; + } + }); + } + + @Test + public void testCollectorFailureInSupplier(TestContext ctx) { + RuntimeException cause = new RuntimeException(); + testCollectorFailure(ctx.async(), ctx, cause, new CollectorBase() { + @Override + public Supplier supplier() { + return () -> { + throw cause; + }; + } + }); + } + + @Test + public void testCollectorFailureProvidingAccumulator(TestContext ctx) { + RuntimeException cause = new RuntimeException(); + testCollectorFailure(ctx.async(), ctx, cause, new CollectorBase() { + @Override + public BiConsumer accumulator() { + throw cause; + } + }); + } + + @Test + public void testCollectorFailureInAccumulator(TestContext ctx) { + RuntimeException cause = new RuntimeException(); + testCollectorFailure(ctx.async(), ctx, cause, new CollectorBase() { + @Override + public BiConsumer accumulator() { + return (o, row) -> { + throw cause; + }; + } + }); + } + + @Test + public void testCollectorFailureProvidingFinisher(TestContext ctx) { + RuntimeException cause = new RuntimeException(); + testCollectorFailure(ctx.async(), ctx, cause, new CollectorBase() { + @Override + public Function finisher() { + throw cause; + } + }); + } + + @Test + public void testCollectorFailureInFinisher(TestContext ctx) { + RuntimeException cause = new RuntimeException(); + testCollectorFailure(ctx.async(), ctx, cause, new CollectorBase() { + @Override + public Function finisher() { + return o -> { + throw cause; + }; + } + }); + } + + private void testCollectorFailure(Async async, TestContext ctx, Throwable cause, + Collector collector) { + connector.connect(ctx.asyncAssertSuccess(conn -> { + conn.query("SELECT * FROM test_collector WHERE id = 1") + .collecting(collector) + .execute(ctx.asyncAssertFailure(result -> { + ctx.assertEquals(cause, result); + conn.close(); + async.complete(); + })); + })); + } + + // this class is for verifying the use of Collector API + private static class TestingCollectorObject { + public int id; + public short int2; + public int int4; + public long int8; + public float floatNum; + public double doubleNum; + public String varchar; + + private TestingCollectorObject(int id, short int2, int int4, long int8, float floatNum, double doubleNum, + String varchar) { + this.id = id; + this.int2 = int2; + this.int4 = int4; + this.int8 = int8; + this.floatNum = floatNum; + this.doubleNum = doubleNum; + this.varchar = varchar; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TestingCollectorObject that = (TestingCollectorObject) o; + + if (id != that.id) { + return false; + } + if (int2 != that.int2) { + return false; + } + if (int4 != that.int4) { + return false; + } + if (int8 != that.int8) { + return false; + } + if (Float.compare(that.floatNum, floatNum) != 0) { + return false; + } + if (Double.compare(that.doubleNum, doubleNum) != 0) { + return false; + } + return varchar != null ? varchar.equals(that.varchar) : that.varchar == null; + } + } + + private static class CollectorBase implements Collector { + @Override + public Supplier supplier() { + return () -> null; + } + + @Override + public BiConsumer accumulator() { + return (a, t) -> { + + }; + } + + @Override + public BinaryOperator combiner() { + return (a, a2) -> null; + } + + @Override + public Function finisher() { + return a -> null; + } + + @Override + public Set characteristics() { + return Collections.emptySet(); + } + } +} diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleConnectionAutoRetryTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleConnectionAutoRetryTest.java new file mode 100644 index 000000000..a971c3ef1 --- /dev/null +++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleConnectionAutoRetryTest.java @@ -0,0 +1,81 @@ +/* + * 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.OracleConnectOptions; +import io.vertx.oracle.test.junit.OracleRule; +import io.vertx.sqlclient.tck.ConnectionAutoRetryTestBase; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class OracleConnectionAutoRetryTest extends ConnectionAutoRetryTestBase { + @ClassRule + public static OracleRule rule = OracleRule.SHARED_INSTANCE; + + @Override + public void setUp() throws Exception { + super.setUp(); + options = rule.options(); + } + + @Override + public void tearDown(TestContext ctx) { + connectionConnector.close(); + poolConnector.close(); + super.tearDown(ctx); + } + + @Override + protected void initialConnector(int proxyPort) { + OracleConnectOptions proxyOptions = new OracleConnectOptions(options); + proxyOptions.setPort(proxyPort); + proxyOptions.setHost("localhost"); + connectionConnector = ClientConfig.CONNECT.connect(vertx, proxyOptions); + poolConnector = ClientConfig.POOLED.connect(vertx, proxyOptions); + } + + @Test + @Ignore("Connection success, but any request get staled") + public void testConnExceedingRetryLimit(TestContext ctx) { + Async async = ctx.async(); + this.options.setReconnectAttempts(1); + this.options.setReconnectInterval(1000L); + UnstableProxyServer unstableProxyServer = new UnstableProxyServer( + 2); + unstableProxyServer.initialize(this.options, ctx.asyncAssertSuccess((v) -> { + this.initialConnector(unstableProxyServer.port()); + this.connectionConnector.connect(s -> { + ctx.assertFalse(s.succeeded()); + async.complete(); + }); + })); + } + + @Test + @Ignore("Connection success, but any request get staled") + public void testPoolExceedingRetryLimit(TestContext ctx) { + this.options.setReconnectAttempts(1); + this.options.setReconnectInterval(1000L); + UnstableProxyServer unstableProxyServer = new UnstableProxyServer( + 2); + unstableProxyServer.initialize(this.options, ctx.asyncAssertSuccess((v) -> { + this.initialConnector(unstableProxyServer.port()); + this.poolConnector.connect(ctx.asyncAssertFailure((throwable) -> { + })); + })); + } +} diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleConnectionTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleConnectionTest.java new file mode 100644 index 000000000..d5dc178da --- /dev/null +++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleConnectionTest.java @@ -0,0 +1,136 @@ +/* + * 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.spi.DatabaseMetadata; +import io.vertx.sqlclient.tck.ConnectionTestBase; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class OracleConnectionTest extends ConnectionTestBase { + @ClassRule + public static OracleRule rule = OracleRule.SHARED_INSTANCE; + + @Override + public void setUp() throws Exception { + super.setUp(); + options = rule.options(); + connector = ClientConfig.CONNECT.connect(vertx, options); + } + + @Override + public void tearDown(TestContext ctx) { + connector.close(); + super.tearDown(ctx); + } + + @Test + public void testConnect(TestContext ctx) { + Async async = ctx.async(); + connect(ctx.asyncAssertSuccess(conn -> async.complete())); + } + + @Test + public void testConnectInvalidDatabase(TestContext ctx) { + Async async = ctx.async(); + options.setDatabase("invalidDatabase"); + connect(ctx.asyncAssertFailure(err -> { + ctx.assertTrue(err.getMessage().contains("ORA-12514")); + async.complete(); + })); + } + + @Test + public void testConnectInvalidPassword(TestContext ctx) { + Async async = ctx.async(); + options.setPassword("invalidPassword"); + connect(ctx.asyncAssertFailure(err -> { + ctx.assertTrue(err.getMessage().contains("ORA-01017")); + async.complete(); + })); + } + + @Test + public void testConnectInvalidUsername(TestContext ctx) { + Async async = ctx.async(); + options.setUser("invalidUsername"); + connect(ctx.asyncAssertFailure(err -> { + ctx.assertTrue(err.getMessage().contains("ORA-01017")); + async.complete(); + })); + } + + @Test + public void testClose(TestContext ctx) { + Async closedAsync = ctx.async(); + Async closeAsync = ctx.async(); + connect(ctx.asyncAssertSuccess(conn -> { + conn.closeHandler(v -> { + closedAsync.complete(); + }); + conn.close(ctx.asyncAssertSuccess(v -> closeAsync.complete())); + })); + closedAsync.await(); + } + + @Test + public void testCloseWithErrorInProgress(TestContext ctx) { + Async async = ctx.async(2); + connect(ctx.asyncAssertSuccess(conn -> { + conn.query("SELECT whatever from DOES_NOT_EXIST").execute(ctx.asyncAssertFailure(err -> async.countDown())); + conn.closeHandler(v -> async.countDown()); + conn.close(); + })); + async.await(); + } + + @Test + public void testCloseWithQueryInProgress(TestContext ctx) { + Async async = ctx.async(2); + connect(ctx.asyncAssertSuccess(conn -> { + conn.query("SELECT id, message from immutable").execute(ctx.asyncAssertFailure(result -> { + ctx.assertEquals(2, async.count()); + async.countDown(); + })); + conn.closeHandler(v -> { + ctx.assertEquals(1, async.count()); + async.countDown(); + }); + conn.close(); + })); + async.await(); + } + + @Test + public void testDatabaseMetaData(TestContext ctx) { + connect(ctx.asyncAssertSuccess(conn -> { + DatabaseMetadata md = conn.databaseMetadata(); + ctx.assertNotNull(md, "DatabaseMetadata should not be null"); + ctx.assertNotNull(md.productName(), "Database product name should not be null"); + ctx.assertNotNull(md.fullVersion(), "Database full version string should not be null"); + ctx.assertTrue(md.majorVersion() >= 1, "Expected DB major version to be >= 1 but was " + md.majorVersion()); + ctx.assertTrue(md.minorVersion() >= 0, "Expected DB minor version to be >= 0 but was " + md.minorVersion()); + validateDatabaseMetaData(ctx, md); + })); + } + + @Override + protected void validateDatabaseMetaData(TestContext ctx, DatabaseMetadata md) { + ctx.assertTrue(md.fullVersion().contains("Oracle")); + ctx.assertTrue(md.productName().contains("Oracle")); + } +} diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleDriverTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleDriverTest.java new file mode 100644 index 000000000..b8660e5b3 --- /dev/null +++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleDriverTest.java @@ -0,0 +1,31 @@ +/* + * 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.junit.VertxUnitRunner; +import io.vertx.oracle.test.junit.OracleRule; +import io.vertx.sqlclient.SqlConnectOptions; +import io.vertx.sqlclient.tck.DriverTestBase; +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class OracleDriverTest extends DriverTestBase { + + @ClassRule + public static OracleRule rule = OracleRule.SHARED_INSTANCE; + + @Override + protected SqlConnectOptions defaultOptions() { + return rule.options(); + } + +} diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OraclePreparedBatchTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OraclePreparedBatchTest.java new file mode 100644 index 000000000..f356b061a --- /dev/null +++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OraclePreparedBatchTest.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.test.tck; + +import io.vertx.core.Future; +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.RowSet; +import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.tck.PreparedBatchTestBase; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(VertxUnitRunner.class) +public class OraclePreparedBatchTest extends PreparedBatchTestBase { + @ClassRule + public static OracleRule rule = OracleRule.SHARED_INSTANCE; + + @Override + protected void initConnector() { + connector = ClientConfig.POOLED.connect(vertx, rule.options()); + } + + @Override + protected String statement(String... parts) { + return String.join(" ?", parts); + } + + @Override + public void cleanTestTable(TestContext ctx) { + connect(ctx.asyncAssertSuccess(conn -> { + conn.preparedQuery("TRUNCATE TABLE mutable").execute(result -> { + conn.close(); + }); + })); + } + + @Test + @Override + public void testInsert(TestContext ctx) { + Async async = ctx.async(); + this.connector.connect(ctx.asyncAssertSuccess((conn) -> { + List batch = new ArrayList<>(); + batch.add(Tuple.of(79991, "batch one")); + batch.add(Tuple.of(79992, "batch two")); + batch.add(Tuple.wrap(Arrays.asList(79993, "batch three"))); + batch.add(Tuple.wrap(Arrays.asList(79994, "batch four"))); + Future> fut = conn.preparedQuery("INSERT INTO MUTABLE (id, val) VALUES (?, ?)") + .executeBatch(batch); + + fut.onComplete(result -> { + ctx.assertFalse(result.failed()); + ctx.assertEquals(4, result.result().rowCount()); + conn.preparedQuery("SELECT * FROM mutable WHERE id=?") + .execute(Tuple.of(79991), ctx.asyncAssertSuccess(rows1 -> { + verify(rows1, ctx, 79991, "batch one"); + conn.preparedQuery("SELECT * FROM mutable WHERE id=?") + .execute(Tuple.of(79992), ctx.asyncAssertSuccess((ar2) -> { + verify(ar2, ctx, 79992, "batch two"); + conn.preparedQuery(this.statement("SELECT * FROM mutable WHERE id=", "")) + .execute(Tuple.of(79993), ctx.asyncAssertSuccess((ar3) -> { + verify(ar3, ctx, 79993, "batch three"); + conn.preparedQuery( + this.statement("SELECT * FROM mutable WHERE id=", "")) + .execute(Tuple.of(79994), ctx.asyncAssertSuccess((ar4) -> { + verify(ar4, ctx, 79994, "batch four"); + async.complete(); + })); + })); + })); + })); + }); + })); + } + + private void verify(RowSet rows, TestContext ctx, int id, String val) { + ctx.assertEquals(1, rows.size()); + Row one = rows.iterator().next(); + ctx.assertEquals(id, one.getInteger(0)); + ctx.assertEquals(val, one.getString(1)); + } + + @Test + @Ignore("Oracle does not support batching queries") + public void testBatchQuery(TestContext ctx) { + + } + + @Test + public void testEmptyBatch(TestContext ctx) { + Async async = ctx.async(); + connector.connect(ctx.asyncAssertSuccess(conn -> { + List batch = new ArrayList<>(); + conn.preparedQuery(statement("SELECT * FROM immutable WHERE id=", "")) + .executeBatch(batch, ctx.asyncAssertFailure(err -> { + async.complete(); + })); + })); + } + + @Test + public void testIncorrectNumBatchArguments(TestContext ctx) { + Async async = ctx.async(); + connector.connect(ctx.asyncAssertSuccess(conn -> { + List batch = new ArrayList<>(); + batch.add(Tuple.of(1, 2)); + conn.preparedQuery(statement("SELECT * FROM immutable WHERE id=", "")) + .executeBatch(batch, ctx.asyncAssertFailure(err -> { + async.complete(); + })); + })); + } + +} diff --git a/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleSimpleQueryTest.java b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleSimpleQueryTest.java new file mode 100644 index 000000000..aea2c7d49 --- /dev/null +++ b/vertx-oracle-client/src/test/java/io/vertx/oracle/test/tck/OracleSimpleQueryTest.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.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.tck.SimpleQueryTestBase; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class OracleSimpleQueryTest extends SimpleQueryTestBase { + @ClassRule + public static OracleRule rule = OracleRule.SHARED_INSTANCE; + + @Override + protected void initConnector() { + connector = ClientConfig.CONNECT.connect(vertx, rule.options()); + } + + @Override + public void cleanTestTable(TestContext ctx) { + connect(ctx.asyncAssertSuccess(conn -> { + conn.preparedQuery("TRUNCATE TABLE mutable").execute(result -> { + conn.close(); + }); + })); + } + + @Test + public void testInsert(TestContext ctx) { + Async async = ctx.async(); + this.connector.connect(ctx.asyncAssertSuccess((conn) -> { + conn.query("INSERT INTO mutable (id, val) VALUES (1, 'Whatever')").execute(ctx.asyncAssertSuccess((r1) -> { + ctx.assertEquals(1, r1.rowCount()); + async.complete(); + })); + })); + async.await(); + } + +} diff --git a/vertx-oracle-client/src/test/resources/tck/import.sql b/vertx-oracle-client/src/test/resources/tck/import.sql new file mode 100644 index 000000000..df7269c4a --- /dev/null +++ b/vertx-oracle-client/src/test/resources/tck/import.sql @@ -0,0 +1,94 @@ +-- Fortune Table +CREATE TABLE Fortune +( + id integer GENERATED by default on null as IDENTITY, + message varchar(2048), + PRIMARY KEY (id) +); +INSERT INTO Fortune (message) +VALUES ('fortune: No such file or directory'); +INSERT INTO Fortune (message) +VALUES ('A computer scientist is someone who fixes things that are not broken.'); +INSERT INTO Fortune (message) +VALUES ('After enough decimal places, nobody gives a damn.'); +INSERT INTO Fortune (message) +VALUES ('A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1'); +INSERT INTO Fortune (message) +VALUES ('A computer program does what you tell it to do, not what you want it to do.'); +INSERT INTO Fortune (message) +VALUES ('Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen'); +INSERT INTO Fortune (message) +VALUES ('Any program that runs right is obsolete.'); +INSERT INTO Fortune (message) +VALUES ('A list is only as strong as its weakest link. — Donald Knuth'); +INSERT INTO Fortune (message) +VALUES ('Feature: A bug with seniority.'); +INSERT INTO Fortune (message) +VALUES ('Computers make very fast, very accurate mistakes.'); +INSERT INTO Fortune (message) +VALUES (''); +INSERT INTO Fortune (message) +VALUES ('フレームワークのベンチマーク'); + +-- immutable table for select query testing -- +-- used by TCK + +CREATE TABLE immutable +( + id integer NOT NULL, + message varchar(2048) NOT NULL, + PRIMARY KEY (id) +); +INSERT INTO immutable (id, message) +VALUES (1, 'fortune: No such file or directory'); +INSERT INTO immutable (id, message) +VALUES (2, 'A computer scientist is someone who fixes things that aren''t broken.'); +INSERT INTO immutable (id, message) +VALUES (3, 'After enough decimal places, nobody gives a damn.'); +INSERT INTO immutable (id, message) +VALUES (4, 'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1'); +INSERT INTO immutable (id, message) +VALUES (5, 'A computer program does what you tell it to do, not what you want it to do.'); +INSERT INTO immutable (id, message) +VALUES (6, 'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen'); +INSERT INTO immutable (id, message) +VALUES (7, 'Any program that runs right is obsolete.'); +INSERT INTO immutable (id, message) +VALUES (8, 'A list is only as strong as its weakest link. — Donald Knuth'); +INSERT INTO immutable (id, message) +VALUES (9, 'Feature: A bug with seniority.'); +INSERT INTO immutable (id, message) +VALUES (10, 'Computers make very fast, very accurate mistakes.'); +INSERT INTO immutable (id, message) +VALUES (11, ''); +INSERT INTO immutable (id, message) +VALUES (12, 'フレームワークのベンチマーク'); + +-- mutable for insert,update,delete query testing -- +-- used by TCK +CREATE TABLE mutable +( + id integer NOT NULL, + val varchar(2048) NOT NULL, + PRIMARY KEY (id) +); + +-- Collector API testing +CREATE TABLE test_collector +( + id INT, + test_int_2 SMALLINT, + test_int_4 INT, + test_int_8 clob, + test_float FLOAT, + test_double NUMBER, + test_varchar VARCHAR(20) +); + +INSERT INTO test_collector +VALUES (1, 32767, 2147483647, 9223372036854775807, 123.456, 1.234567, 'HELLO,WORLD'); +INSERT INTO test_collector +VALUES (2, 32767, 2147483647, 9223372036854775807, 123.456, 1.234567, 'hello,world'); + +-- Don't forget to commit... +COMMIT;