diff --git a/.travis.yml b/.travis.yml index 5842eb9f..faea24e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,24 +4,34 @@ sudo: required dist: trusty jdk: - - oraclejdk11 - - openjdk10 + - openjdk8 - openjdk11 + - openjdk12 + +env: + - TNT_VERSION=1.9 + - TNT_VERSION=1.10 + - TNT_VERSION=2x + - TNT_VERSION=2.2 + +stages: + - checkstyle + - test + +jobs: + include: + - stage: checkstyle + env: [] + jdk: openjdk11 + before_script: skip + script: mvn checkstyle:check + after_success: skip before_script: - - src/test/travis.pre.sh + - .travis/travis.pre.sh script: - - | - if [ "${TRAVIS_JDK_VERSION}" = openjdk11 ]; then - mvn verify jacoco:report - else - mvn verify - fi - - head -n -0 testroot/*.log + - .travis/travis.build.sh after_success: - - | - if [ "${TRAVIS_JDK_VERSION}" = openjdk11 ]; then - mvn coveralls:report -DrepoToken=${COVERALLS_TOKEN} - fi + - .travis/travis.post.sh diff --git a/.travis/travis.build.sh b/.travis/travis.build.sh new file mode 100755 index 00000000..7a187778 --- /dev/null +++ b/.travis/travis.build.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -exu # Strict shell (w/o -o pipefail) + +if [ "${TRAVIS_JDK_VERSION}" = "openjdk11" ] && [ "${TNT_VERSION}" = "2x" ]; then + mvn verify jacoco:report +else + mvn verify +fi + +head -n -0 testroot/*.log diff --git a/.travis/travis.post.sh b/.travis/travis.post.sh new file mode 100755 index 00000000..4fc44267 --- /dev/null +++ b/.travis/travis.post.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -exu # Strict shell (w/o -o pipefail) + +if [ "${TRAVIS_JDK_VERSION}" = "openjdk11" ] && [ "${TNT_VERSION}" = "2x" ]; then + mvn coveralls:report -DrepoToken=${COVERALLS_TOKEN} +fi diff --git a/.travis/travis.pre.sh b/.travis/travis.pre.sh new file mode 100755 index 00000000..e7d8eece --- /dev/null +++ b/.travis/travis.pre.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -exuo pipefail # Strict shell + +curl http://download.tarantool.org/tarantool/${TNT_VERSION}/gpgkey | sudo apt-key add - +RELEASE=`lsb_release -c -s` + +sudo rm -f /etc/apt/sources.list.d/*tarantool*.list +sudo tee /etc/apt/sources.list.d/tarantool_${TNT_VERSION/./_}.list <<- EOF +deb http://download.tarantool.org/tarantool/${TNT_VERSION}/ubuntu/ ${RELEASE} main +deb-src http://download.tarantool.org/tarantool/${TNT_VERSION}/ubuntu/ ${RELEASE} main +EOF + +sudo apt-get update +sudo apt-get -y install tarantool tarantool-common + +sudo tarantoolctl stop example diff --git a/README.md b/README.md index 9600bfb2..d9075e34 100644 --- a/README.md +++ b/README.md @@ -5,33 +5,32 @@ align="right"> # Java connector for Tarantool 1.7.4+ -[![Coverage Status][coveralls-badge]][coveralls-page] - -[coveralls-badge]: https://coveralls.io/repos/github/tarantool/tarantool-java/badge.svg?branch=master -[coveralls-page]: https://coveralls.io/github/tarantool/tarantool-java?branch=master - [![Join the chat at https://gitter.im/tarantool/tarantool-java](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tarantool/tarantool-java?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.tarantool/connector/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.tarantool/connector) +[![Build Status](https://travis-ci.org/tarantool/tarantool-java.svg?branch=master)](https://travis-ci.org/tarantool/tarantool-java) +[![Coverage Status](https://coveralls.io/repos/github/tarantool/tarantool-java/badge.svg?branch=master)](https://coveralls.io/github/tarantool/tarantool-java?branch=master) To get the Java connector for Tarantool 1.6.9, visit [this GitHub page](https://github.com/tarantool/tarantool-java/tree/connector-1.6.9). ## Table of contents * [Getting started](#getting-started) +* [Cluster support](#cluster-support) * [Where to get help](#where-to-get-help) ## Getting started -1. Add a dependency to your `pom.xml` file. +1. Add a dependency to your `pom.xml` file: ```xml org.tarantool connector - 1.7.4 + 1.9.1 ``` -2. Configure `TarantoolClientConfig`. +2. Configure `TarantoolClientConfig`: ```java TarantoolClientConfig config = new TarantoolClientConfig(); @@ -39,67 +38,73 @@ config.username = "test"; config.password = "test"; ``` -3. Implement your `SocketChannelProvider`. - It should return a connected `SocketChannel`. +3. Create a client: ```java -SocketChannelProvider socketChannelProvider = new SocketChannelProvider() { - @Override - public SocketChannel get(int retryNumber, Throwable lastError) { - if (lastError != null) { - lastError.printStackTrace(System.out); - } - try { - return SocketChannel.open(new InetSocketAddress("localhost", 3301)); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - }; +TarantoolClient client = new TarantoolClientImpl("host:3301", config); +``` + +using `TarantoolClientImpl(String, TarantoolClientConfig)` is equivalent to: + +```java +SocketChannelProvider socketChannelProvider = new SingleSocketChannelProviderImpl("host:3301") +TarantoolClient client = new TarantoolClientImpl(socketChannelProvider, config); ``` -Here you could also implement some reconnection or fallback policy. -Remember that `TarantoolClient` adopts a -[fail-fast](https://en.wikipedia.org/wiki/Fail-fast) policy -when a client is not connected. +You could implement your own `SocketChannelProvider`. It should return +a connected `SocketChannel`. Feel free to implement `get(int retryNumber, Throwable lastError)` +using your appropriate strategy to obtain the channel. The strategy can take into +account current attempt number (retryNumber) and the last transient error occurred on +the previous attempt. -The `TarantoolClient` will stop functioning if your implementation of a socket -channel provider raises an exception or returns a null. You will need a new -instance of client to recover. Hence, you should only throw in case you have -met unrecoverable error. +The `TarantoolClient` will be closed if your implementation of a socket +channel provider raises exceptions. However, throwing a `SocketProviderTransientException` +or returning `null` value are handled by the client as recoverable errors. In these cases, +the client will make next attempt to obtain the socket channel. Otherwise, you will need +a new instance of client to recover. Hence, you should only throw an error different +to `SocketProviderTransientException` in case you have met unrecoverable error. -Below is an example of `SocketChannelProvider` implementation that handles short -tarantool restarts. +Below is an example of `SocketChannelProvider` implementation that tries +to connect no more than 3 times, two seconds for each attempt at max. ```java SocketChannelProvider socketChannelProvider = new SocketChannelProvider() { @Override public SocketChannel get(int retryNumber, Throwable lastError) { - long deadline = System.currentTimeMillis() + RESTART_TIMEOUT; - while (!Thread.currentThread().isInterrupted()) { - try { - return SocketChannel.open(new InetSocketAddress("localhost", 3301)); - } catch (IOException e) { - if (deadline < System.currentTimeMillis()) - throw new RuntimeException(e); - try { - Thread.sleep(100); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } + if (retryNumber > 3) { + throw new RuntimeException("Too many attempts"); + } + SocketChannel channel = null; + try { + channel = SocketChannel.open(); + channel.socket().connect(new InetSocketAddress("localhost", 3301), 2000); + return channel; + } catch (IOException e) { + if (channel != null) { + try { + channel.close(); + } catch (IOException ignored) { } } + throw new SocketProviderTransientException("Couldn't connect to server", e); } - throw new RuntimeException(new TimeoutException("Connect timed out.")); } }; ``` -4. Create a client. +Same behaviour can be achieved using built-in `SingleSocketChannelProviderImpl`: ```java +TarantoolClientConfig config = new TarantoolClientConfig(); +config.connectionTimeout = 2_000; // two seconds timeout per attempt +config.retryCount = 3; // three attempts at max + +SocketChannelProvider socketChannelProvider = new SingleSocketChannelProviderImpl("localhost:3301") TarantoolClient client = new TarantoolClientImpl(socketChannelProvider, config); ``` +`SingleSocketChannelProviderImpl` implements `ConfigurableSocketChannelProvider` that +makes possible for the client to configure a socket provider. + > **Notes:** > * `TarantoolClient` is thread-safe and asynchronous, so you should use one > client inside the whole application. @@ -156,6 +161,117 @@ System.out.println(template.query("select * from hello_world where hello=:id", C For more implementation details, see [API documentation](http://tarantool.github.io/tarantool-java/apidocs/index.html). +## Cluster support + +To be more fault-tolerant the connector provides cluster extensions. In +particular `TarantoolClusterClient` and built-in `RoundRobinSocketProviderImpl` +used as a default `SocketProvider` implementation. When currently connected +instance is down then the client will try to reconnect to the first available +instance using strategy defined in a socket provider. You need to supply +a list of nodes which will be used by the cluster client to provide such +ability. Also you can prefer to use a [discovery mechanism](#auto-discovery) +in order to dynamically fetch and apply the node list. + +### The RoundRobinSocketProviderImpl class + +This cluster-aware provider uses addresses pool to connect to DB server. +The provider picks up next address in order the addresses were passed. + +Similar to `SingleSocketChannelProviderImpl` this RR provider also +relies on two options from the config: `TarantoolClientConfig.connectionTimeout` +and `TarantoolClientConfig.retryCount` but in a bit different way. +The latter option says how many times the provider should try to establish a +connection to _one instance_ before failing an attempt. The provider requires +positive retry count to work properly. The socket timeout is used to limit +an interval between connections attempts per instance. In other words, the provider +follows a pattern _connection should succeed after N attempts with M interval between +them at max_. + +### Basic cluster client usage + +1. Configure `TarantoolClusterClientConfig`: + +```java +TarantoolClusterClientConfig config = new TarantoolClusterClientConfig(); +// fill other settings +config.operationExpiryTimeMillis = 2000; +config.executor = Executors.newSingleThreadExecutor(); +``` + +2. Create an instance of `TarantoolClusterClientImpl`. You need to provide +an initial list of nodes: + +```java +String[] nodes = new String[] { "myHost1:3301", "myHost2:3302", "myHost3:3301" }; +TarantoolClusterClient client = new TarantoolClusterClient(config, nodes); +``` + +3. Work with the client using same API as defined in `TarantoolClient`: + +```java +client.syncOps().insert(23, Arrays.asList(1, 1)); +``` + +### Auto-discovery + +Auto-discovery feature allows a cluster client to fetch addresses of +cluster nodes to reflect changes related to the cluster topology. To achieve +this you have to create a Lua function on the server side which returns +a single array result. Client periodically polls the server to obtain a +fresh list and apply it if its content changes. + +1. On the server side create a function which returns nodes: + +```bash +tarantool> function get_cluster_nodes() return { 'host1:3301', 'host2:3302', 'host3:3301' } end +``` + +You need to pay attention to a function contract we are currently supporting: +* The client never passes any arguments to a discovery function. +* A discovery function _should_ return a single result of strings (i.e. single + string `return 'host:3301'` or array of strings `return {'host1:3301', 'host2:3301'}`). +* A discovery function _may_ return multi-results but the client takes + into account only first of them (i.e. `return {'host:3301'}, discovery_delay`, where + the second result is unused). Even more, any extra results __are reserved__ by the client + in order to extend its contract with a backward compatibility. +* A discovery function _should NOT_ return no results, empty result, wrong type result, + and Lua errors. The client discards such kinds of results but it does not affect the discovery + process for next scheduled tasks. + +2. On the client side configure discovery settings in `TarantoolClusterClientConfig`: + +```java +TarantoolClusterClientConfig config = new TarantoolClusterClientConfig(); +// fill other settings +config.clusterDiscoveryEntryFunction = "get_cluster_nodes"; // discovery function used to fetch nodes +config.clusterDiscoveryDelayMillis = 60_000; // how often client polls the discovery server +``` + +3. Create a client using the config made above. + +```java +TarantoolClusterClient client = new TarantoolClusterClient(config); +client.syncOps().insert(45, Arrays.asList(1, 1)); +``` + +### Auto-discovery caveats + +* You need to set _not empty_ value to `clusterDiscoveryEntryFunction` to enable auto-discovery feature. +* There are only two cases when a discovery task runs: just after initialization of the cluster + client and a periodical scheduler timeout defined in `TarantoolClusterClientConfig.clusterDiscoveryDelayMillis`. +* A discovery task always uses an active client connection to get the nodes list. + It's in your responsibility to provide a function availability as well as a consistent + nodes list on all instances you initially set or obtain from the task. +* If some error occurs while a discovery task is running then this task + will be aborted without any after-effects for next task executions. These cases, for instance, are + a wrong function result (see discovery function contract) or a broken connection. + There is an exception if the client is closed then discovery process will stop permanently. +* It's possible to obtain a list which does NOT contain the node we are currently + connected to. It leads the client to try to reconnect to another node from the + new list. It may take some time to graceful disconnect from the current node. + The client does its best to catch the moment when there are no pending responses + and perform a reconnection. + ## Where to get help Got problems or questions? Post them on @@ -164,7 +280,15 @@ Got problems or questions? Post them on base for possible answers and solutions. ## Building -To run tests -``` + +To run unit tests use: + +```bash ./mvnw clean test +``` + +To run integration tests use: + +```bash +./mvnw clean verify ``` diff --git a/pom.xml b/pom.xml index dcd9b41a..f7804286 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,9 @@ jar UTF-8 - 5.3.1 + 5.4.2 + 1.23 + 1.10.19 Tarantool Connector for Java https://github.com/tarantool/tarantool-java @@ -79,12 +81,12 @@ maven-surefire-plugin - 2.22.0 + 3.0.0-M3 org.apache.maven.plugins maven-failsafe-plugin - 2.22.0 + 3.0.0-M3 @@ -136,6 +138,30 @@ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + + com.puppycrawl.tools + checkstyle + 8.19 + + + + src/test/resources/checkstyle.xml + error + true + true + true + true + + + + @@ -148,13 +174,13 @@ org.mockito mockito-all - 1.9.5 + ${mockito.version} test org.yaml snakeyaml - 1.23 + ${snakeyml.version} test diff --git a/src/main/java/org/tarantool/AbstractTarantoolOps.java b/src/main/java/org/tarantool/AbstractTarantoolOps.java index 9d59264e..3d926155 100644 --- a/src/main/java/org/tarantool/AbstractTarantoolOps.java +++ b/src/main/java/org/tarantool/AbstractTarantoolOps.java @@ -1,7 +1,9 @@ package org.tarantool; -public abstract class AbstractTarantoolOps implements TarantoolClientOps { +public abstract class AbstractTarantoolOps + implements TarantoolClientOps { + private Code callCode = Code.OLD_CALL; protected abstract Result exec(Code code, Object... args); @@ -11,7 +13,15 @@ public Result select(Space space, Space index, Tuple key, int offset, int limit, } public Result select(Space space, Space index, Tuple key, int offset, int limit, int iterator) { - return exec(Code.SELECT, Key.SPACE, space, Key.INDEX, index, Key.KEY, key, Key.ITERATOR, iterator, Key.LIMIT, limit, Key.OFFSET, offset); + return exec( + Code.SELECT, + Key.SPACE, space, + Key.INDEX, index, + Key.KEY, key, + Key.ITERATOR, iterator, + Key.LIMIT, limit, + Key.OFFSET, offset + ); } public Result insert(Space space, Tuple tuple) { diff --git a/src/main/java/org/tarantool/Base64.java b/src/main/java/org/tarantool/Base64.java index c1a1817b..2dca0214 100644 --- a/src/main/java/org/tarantool/Base64.java +++ b/src/main/java/org/tarantool/Base64.java @@ -1,8 +1,7 @@ package org.tarantool; public class Base64 { - static String charSet = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + static final String CHAR_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; public static byte[] decode(String s) { int end = 0; @@ -16,7 +15,7 @@ public static byte[] decode(String s) { byte[] result = new byte[len]; int dst = 0; for (int src = 0; src < s.length(); src++) { - int code = charSet.indexOf(s.charAt(src)); + int code = CHAR_SET.indexOf(s.charAt(src)); if (code == -1) { break; } @@ -35,8 +34,10 @@ public static byte[] decode(String s) { case 3: result[dst++] |= (byte) (code & 0x3f); break; + default: + break; } } return result; } -} \ No newline at end of file +} diff --git a/src/main/java/org/tarantool/BaseSocketChannelProvider.java b/src/main/java/org/tarantool/BaseSocketChannelProvider.java new file mode 100644 index 00000000..2b85a934 --- /dev/null +++ b/src/main/java/org/tarantool/BaseSocketChannelProvider.java @@ -0,0 +1,138 @@ +package org.tarantool; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; + +public abstract class BaseSocketChannelProvider implements ConfigurableSocketChannelProvider { + + /** + * Limit of retries. + */ + private int retriesLimit = RETRY_NO_LIMIT; + + /** + * Timeout to establish socket connection with an individual server. + */ + private int connectionTimeout = NO_TIMEOUT; + + /** + * Tries to establish a new connection to the Tarantool instances. + * + * @param retryNumber number of current retry + * @param lastError the last error occurs when reconnecting + * + * @return connected socket channel + * + * @throws CommunicationException if number of retries or socket timeout are exceeded + * @throws SocketProviderTransientException if any I/O errors happen + */ + @Override + public final SocketChannel get(int retryNumber, Throwable lastError) { + try { + return makeAttempt(retryNumber, lastError); + } catch (IOException e) { + throw new SocketProviderTransientException("Couldn't connect to the server", e); + } + } + + /** + * Obtains a connected socket channel. + * + * @param retryNumber reconnection attempt number + * @param lastError reconnection reason + * + * @return opened socket channel + * + * @throws IOException if any I/O errors occur + */ + protected abstract SocketChannel makeAttempt(int retryNumber, Throwable lastError) throws IOException; + + /** + * Sets maximum amount of reconnect attempts to be made before an exception is raised. + * The retry count is maintained by a {@link #get(int, Throwable)} caller + * when a socket level connection was established. + *

+ * Negative value means unlimited attempts. + * + * @param retriesLimit Limit of retries to use. + */ + @Override + public void setRetriesLimit(int retriesLimit) { + if (retriesLimit < 0) { + throw new IllegalArgumentException("Retries count cannot be negative."); + } + this.retriesLimit = retriesLimit; + } + + /** + * Gets limit of attempts to establish connection. + * + * @return Maximum reconnect attempts to make before raising exception. + */ + public int getRetriesLimit() { + return retriesLimit; + } + + /** + * Parse a string address in the form of host[:port] + * and builds a socket address. + * + * @param address Server address. + * + * @return Socket address. + */ + protected InetSocketAddress parseAddress(String address) { + int separatorPosition = address.indexOf(':'); + String host = (separatorPosition < 0) ? address : address.substring(0, separatorPosition); + int port = (separatorPosition < 0) ? 3301 : Integer.parseInt(address.substring(separatorPosition + 1)); + return new InetSocketAddress(host, port); + } + + protected SocketChannel openChannel(InetSocketAddress socketAddress) throws IOException { + SocketChannel channel = null; + try { + channel = SocketChannel.open(); + channel.socket().connect(socketAddress, connectionTimeout); + return channel; + } catch (IOException e) { + if (channel != null) { + try { + channel.close(); + } catch (IOException ignored) { + // No-op. + } + } + throw e; + } + } + + /** + * Gets maximum amount of time to wait for a socket + * connection establishment with an individual server. + * + * @return timeout + */ + public int getConnectionTimeout() { + return connectionTimeout; + } + + /** + * Sets maximum amount of time to wait for a socket connection establishment + * with an individual server. + *

+ * Zero means infinite connectionTimeout. + * + * @param connectionTimeout connectionTimeout value, ms. + * + * @throws IllegalArgumentException if connectionTimeout is negative. + */ + @Override + public void setConnectionTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException("Connection timeout cannot be negative."); + } + this.connectionTimeout = connectionTimeout; + } + +} diff --git a/src/main/java/org/tarantool/Code.java b/src/main/java/org/tarantool/Code.java index 5f6263fa..38cea521 100644 --- a/src/main/java/org/tarantool/Code.java +++ b/src/main/java/org/tarantool/Code.java @@ -2,8 +2,19 @@ public enum Code { - SELECT(1), INSERT(2), REPLACE(3), UPDATE(4), - DELETE(5), OLD_CALL(6), AUTH(7), EVAL(8), UPSERT(9), CALL(10), EXECUTE(11) , PING(64), SUBSCRIBE(66),; + SELECT(1), + INSERT(2), + REPLACE(3), + UPDATE(4), + DELETE(5), + OLD_CALL(6), + AUTH(7), + EVAL(8), + UPSERT(9), + CALL(10), + EXECUTE(11), + PING(64), + SUBSCRIBE(66); int id; diff --git a/src/main/java/org/tarantool/CommunicationException.java b/src/main/java/org/tarantool/CommunicationException.java index 70567fd9..9efb39ba 100644 --- a/src/main/java/org/tarantool/CommunicationException.java +++ b/src/main/java/org/tarantool/CommunicationException.java @@ -1,41 +1,35 @@ package org.tarantool; /** - *

* CommunicationException class. - *

- * + * * @author dgreen * @version $Id: $ */ public class CommunicationException extends RuntimeException { - /** - *

- * Constructor for CommunicationException. - *

- * - * @param message - * a {@link java.lang.String} object. - * @param cause - * a {@link java.lang.Throwable} object. - */ - public CommunicationException(String message, Throwable cause) { - super(message, cause); - } + /** + * Constructor for CommunicationException. + * + * @param message + * a {@link java.lang.String} object. + * @param cause + * a {@link java.lang.Throwable} object. + */ + public CommunicationException(String message, Throwable cause) { + super(message, cause); + } - /** - *

- * Constructor for CommunicationException. - *

- * - * @param message - * a {@link java.lang.String} object. - */ - public CommunicationException(String message) { - super(message); - } + /** + * Constructor for CommunicationException. + * + * @param message + * a {@link java.lang.String} object. + */ + public CommunicationException(String message) { + super(message); + } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; } diff --git a/src/main/java/org/tarantool/ConfigurableSocketChannelProvider.java b/src/main/java/org/tarantool/ConfigurableSocketChannelProvider.java new file mode 100644 index 00000000..2c04ea64 --- /dev/null +++ b/src/main/java/org/tarantool/ConfigurableSocketChannelProvider.java @@ -0,0 +1,23 @@ +package org.tarantool; + +public interface ConfigurableSocketChannelProvider extends SocketChannelProvider { + + int RETRY_NO_LIMIT = 0; + int NO_TIMEOUT = 0; + + /** + * Configures max count of retries. + * + * @param limit max attempts count + */ + void setRetriesLimit(int limit); + + /** + * Configures max time to establish + * a connection per attempt. + * + * @param timeout connection timeout in millis + */ + void setConnectionTimeout(int timeout); + +} diff --git a/src/main/java/org/tarantool/Iterator.java b/src/main/java/org/tarantool/Iterator.java index 4e37d075..4452a744 100644 --- a/src/main/java/org/tarantool/Iterator.java +++ b/src/main/java/org/tarantool/Iterator.java @@ -24,4 +24,4 @@ public enum Iterator { public int getValue() { return value; } -} \ No newline at end of file +} diff --git a/src/main/java/org/tarantool/JDBCBridge.java b/src/main/java/org/tarantool/JDBCBridge.java index 5693493f..3ec18af4 100644 --- a/src/main/java/org/tarantool/JDBCBridge.java +++ b/src/main/java/org/tarantool/JDBCBridge.java @@ -1,89 +1,85 @@ package org.tarantool; +import org.tarantool.protocol.TarantoolPacket; + import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.ListIterator; -import java.util.Map; - -import org.tarantool.jdbc.SQLResultSet; -import org.tarantool.protocol.TarantoolPacket; +@Deprecated public class JDBCBridge { - public static final JDBCBridge EMPTY = new JDBCBridge(Collections.emptyList(), Collections.>emptyList()); + public static final JDBCBridge EMPTY = new JDBCBridge(Collections.emptyList(), Collections.emptyList()); - final List sqlMetadata; - final Map columnsByName; + final List sqlMetadata; final List> rows; protected JDBCBridge(TarantoolPacket pack) { this(SqlProtoUtils.getSQLMetadata(pack), SqlProtoUtils.getSQLData(pack)); } - protected JDBCBridge(List sqlMetadata, List> rows) { + protected JDBCBridge(List sqlMetadata, List> rows) { this.sqlMetadata = sqlMetadata; this.rows = rows; - columnsByName = new LinkedHashMap((int) Math.ceil(sqlMetadata.size() / 0.75), 0.75f); - for (int i = 0; i < sqlMetadata.size(); i++) { - columnsByName.put(sqlMetadata.get(i).getName(), i + 1); - } } - public static JDBCBridge query(TarantoolConnection connection, String sql, Object ... params) { + public static JDBCBridge query(TarantoolConnection connection, String sql, Object... params) { TarantoolPacket pack = connection.sql(sql, params); return new JDBCBridge(pack); } - public static int update(TarantoolConnection connection, String sql, Object ... params) { + public static int update(TarantoolConnection connection, String sql, Object... params) { return connection.update(sql, params).intValue(); } - public static JDBCBridge mock(List fields, List> values) { - List meta = new ArrayList(fields.size()); - for(String field : fields) { - meta.add(new TarantoolBase.SQLMetaData(field)); + /** + * Constructs a JDBCBridge with a predefined data. + * + * @param fields fields metadata + * @param values tuples + * + * @return bridge + */ + public static JDBCBridge mock(List fields, List> values) { + List meta = new ArrayList<>(fields.size()); + for (String field : fields) { + meta.add(new SqlProtoUtils.SQLMetaData(field)); } return new JDBCBridge(meta, values); } - public static Object execute(TarantoolConnection connection, String sql, Object ... params) { + /** + * Constructs a JDBCBridge with a parsed query result. + * + * @param connection connection to be used + * @param sql query string + * @param params query binding parameters + * + * @return bridge + */ + public static Object execute(TarantoolConnection connection, String sql, Object... params) { TarantoolPacket pack = connection.sql(sql, params); Long rowCount = SqlProtoUtils.getSqlRowCount(pack); - if(rowCount == null) { + if (rowCount == null) { return new JDBCBridge(pack); } return rowCount.intValue(); } - - public String getColumnName(int columnIndex) { - return columnIndex > sqlMetadata.size() ? null : sqlMetadata.get(columnIndex - 1).getName(); + public List> getRows() { + return rows; } - public Integer getColumnIndex(String columnName) { - return columnsByName.get(columnName); - } - - public int getColumnCount() { - return columnsByName.size(); - } - - public ListIterator> iterator() { - return rows.listIterator(); - } - - public int size() { - return rows.size(); + public List getSqlMetadata() { + return sqlMetadata; } @Override public String toString() { return "JDBCBridge{" + - "sqlMetadata=" + sqlMetadata + - ", columnsByName=" + columnsByName + - ", rows=" + rows + - '}'; + "sqlMetadata=" + sqlMetadata + + ", rows=" + rows + + '}'; } + } diff --git a/src/main/java/org/tarantool/Key.java b/src/main/java/org/tarantool/Key.java index 32e761f3..f081d497 100644 --- a/src/main/java/org/tarantool/Key.java +++ b/src/main/java/org/tarantool/Key.java @@ -1,22 +1,29 @@ package org.tarantool; - import java.util.concurrent.Callable; public enum Key implements Callable { - //header - CODE(0x00), SYNC(0x01), SCHEMA_ID(0x05), - //body - SPACE(0x10), INDEX(0x11), - LIMIT(0x12), OFFSET(0x13), + // header + CODE(0x00), + SYNC(0x01), + SCHEMA_ID(0x05), + + // body + SPACE(0x10), + INDEX(0x11), + LIMIT(0x12), + OFFSET(0x13), ITERATOR(0x14), KEY(0x20), - TUPLE(0x21), FUNCTION(0x22), - USER_NAME(0x23),EXPRESSION(0x27), + TUPLE(0x21), + FUNCTION(0x22), + USER_NAME(0x23), + EXPRESSION(0x27), UPSERT_OPS(0x28), - DATA(0x30), ERROR(0x31), + DATA(0x30), + ERROR(0x31), SQL_FIELD_NAME(0), SQL_METADATA(0x32), @@ -45,9 +52,9 @@ public static Key getById(int id) { return null; } - @Override public Integer call() throws Exception { return id; } + } diff --git a/src/main/java/org/tarantool/MsgPackLite.java b/src/main/java/org/tarantool/MsgPackLite.java index e6450115..f16f66fa 100644 --- a/src/main/java/org/tarantool/MsgPackLite.java +++ b/src/main/java/org/tarantool/MsgPackLite.java @@ -21,58 +21,58 @@ public class MsgPackLite { public static final MsgPackLite INSTANCE = new MsgPackLite(); - protected final int MAX_4BIT = 0xf; - protected final int MAX_5BIT = 0x1f; - protected final int MAX_7BIT = 0x7f; - protected final int MAX_8BIT = 0xff; - protected final int MAX_15BIT = 0x7fff; - protected final int MAX_16BIT = 0xffff; - protected final int MAX_31BIT = 0x7fffffff; - protected final long MAX_32BIT = 0xffffffffL; + protected static final int MAX_4BIT = 0xf; + protected static final int MAX_5BIT = 0x1f; + protected static final int MAX_7BIT = 0x7f; + protected static final int MAX_8BIT = 0xff; + protected static final int MAX_15BIT = 0x7fff; + protected static final int MAX_16BIT = 0xffff; + protected static final int MAX_31BIT = 0x7fffffff; + protected static final long MAX_32BIT = 0xffffffffL; - protected final BigInteger BI_MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE); - protected final BigInteger BI_MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); - protected final BigInteger BI_MAX_64BIT = BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE); + protected static final BigInteger BI_MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE); + protected static final BigInteger BI_MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE); + protected static final BigInteger BI_MAX_64BIT = BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE); //these values are from http://wiki.msgpack.org/display/MSGPACK/Format+specification - protected final byte MP_NULL = (byte) 0xc0; - protected final byte MP_FALSE = (byte) 0xc2; - protected final byte MP_TRUE = (byte) 0xc3; - protected final byte MP_BIN8 = (byte) 0xc4; - protected final byte MP_BIN16 = (byte) 0xc5; - protected final byte MP_BIN32 = (byte) 0xc6; + protected static final byte MP_NULL = (byte) 0xc0; + protected static final byte MP_FALSE = (byte) 0xc2; + protected static final byte MP_TRUE = (byte) 0xc3; + protected static final byte MP_BIN8 = (byte) 0xc4; + protected static final byte MP_BIN16 = (byte) 0xc5; + protected static final byte MP_BIN32 = (byte) 0xc6; - protected final byte MP_FLOAT = (byte) 0xca; - protected final byte MP_DOUBLE = (byte) 0xcb; + protected static final byte MP_FLOAT = (byte) 0xca; + protected static final byte MP_DOUBLE = (byte) 0xcb; - protected final byte MP_FIXNUM = (byte) 0x00;//last 7 bits is value - protected final byte MP_UINT8 = (byte) 0xcc; - protected final byte MP_UINT16 = (byte) 0xcd; - protected final byte MP_UINT32 = (byte) 0xce; - protected final byte MP_UINT64 = (byte) 0xcf; + protected static final byte MP_FIXNUM = (byte) 0x00;//last 7 bits is value + protected static final byte MP_UINT8 = (byte) 0xcc; + protected static final byte MP_UINT16 = (byte) 0xcd; + protected static final byte MP_UINT32 = (byte) 0xce; + protected static final byte MP_UINT64 = (byte) 0xcf; - protected final byte MP_NEGATIVE_FIXNUM = (byte) 0xe0;//last 5 bits is value - protected final int MP_NEGATIVE_FIXNUM_INT = 0xe0;// /me wishes for signed numbers. - protected final byte MP_INT8 = (byte) 0xd0; - protected final byte MP_INT16 = (byte) 0xd1; - protected final byte MP_INT32 = (byte) 0xd2; - protected final byte MP_INT64 = (byte) 0xd3; + protected static final byte MP_NEGATIVE_FIXNUM = (byte) 0xe0;//last 5 bits is value + protected static final int MP_NEGATIVE_FIXNUM_INT = 0xe0;// /me wishes for signed numbers. + protected static final byte MP_INT8 = (byte) 0xd0; + protected static final byte MP_INT16 = (byte) 0xd1; + protected static final byte MP_INT32 = (byte) 0xd2; + protected static final byte MP_INT64 = (byte) 0xd3; - protected final byte MP_FIXARRAY = (byte) 0x90;//last 4 bits is size - protected final int MP_FIXARRAY_INT = 0x90; - protected final byte MP_ARRAY16 = (byte) 0xdc; - protected final byte MP_ARRAY32 = (byte) 0xdd; + protected static final byte MP_FIXARRAY = (byte) 0x90;//last 4 bits is size + protected static final int MP_FIXARRAY_INT = 0x90; + protected static final byte MP_ARRAY16 = (byte) 0xdc; + protected static final byte MP_ARRAY32 = (byte) 0xdd; - protected final byte MP_FIXMAP = (byte) 0x80;//last 4 bits is size - protected final int MP_FIXMAP_INT = 0x80; - protected final byte MP_MAP16 = (byte) 0xde; - protected final byte MP_MAP32 = (byte) 0xdf; + protected static final byte MP_FIXMAP = (byte) 0x80;//last 4 bits is size + protected static final int MP_FIXMAP_INT = 0x80; + protected static final byte MP_MAP16 = (byte) 0xde; + protected static final byte MP_MAP32 = (byte) 0xdf; - protected final byte MP_FIXSTR = (byte) 0xa0;//last 5 bits is size - protected final int MP_FIXSTR_INT = 0xa0; - protected final byte MP_STR8 = (byte) 0xd9; - protected final byte MP_STR16 = (byte) 0xda; - protected final byte MP_STR32 = (byte) 0xdb; + protected static final byte MP_FIXSTR = (byte) 0xa0;//last 5 bits is size + protected static final int MP_FIXSTR_INT = 0xa0; + protected static final byte MP_STR8 = (byte) 0xd9; + protected static final byte MP_STR16 = (byte) 0xda; + protected static final byte MP_STR32 = (byte) 0xdb; public void pack(Object item, OutputStream os) throws IOException { DataOutputStream out = new DataOutputStream(os); @@ -99,14 +99,16 @@ public void pack(Object item, OutputStream os) throws IOException { BigInteger value = (BigInteger) item; boolean isPositive = value.signum() >= 0; if (isPositive && value.compareTo(BI_MAX_64BIT) > 0 || - value.compareTo(BI_MIN_LONG) < 0) + value.compareTo(BI_MIN_LONG) < 0) { throw new IllegalArgumentException( "Cannot encode BigInteger as MsgPack: out of -2^63..2^64-1 range"); + } if (isPositive && value.compareTo(BI_MAX_LONG) > 0) { byte[] data = value.toByteArray(); // data can contain leading zero bytes - for (int i = 0; i < data.length - 8; ++i) + for (int i = 0; i < data.length - 8; ++i) { assert data[i] == 0; + } out.write(MP_UINT64); out.write(data, data.length - 8, 8); return; @@ -247,26 +249,26 @@ public Object unpack(InputStream is) throws IOException { case MP_DOUBLE: return in.readDouble(); case MP_UINT8: - return in.read();//read single byte, return as int + return in.read(); // read single byte, return as int case MP_UINT16: - return in.readShort() & MAX_16BIT;//read short, trick Java into treating it as unsigned, return int + return in.readShort() & MAX_16BIT; // read short, trick Java into treating it as unsigned, return int case MP_UINT32: - return in.readInt() & MAX_32BIT;//read int, trick Java into treating it as unsigned, return long + return in.readInt() & MAX_32BIT; // read int, trick Java into treating it as unsigned, return long case MP_UINT64: { long v = in.readLong(); if (v >= 0) { return v; } else { - //this is a little bit more tricky, since we don't have unsigned longs - byte[] bytes = new byte[]{ - (byte) ((v >> 56) & 0xff), - (byte) ((v >> 48) & 0xff), - (byte) ((v >> 40) & 0xff), - (byte) ((v >> 32) & 0xff), - (byte) ((v >> 24) & 0xff), - (byte) ((v >> 16) & 0xff), - (byte) ((v >> 8) & 0xff), - (byte) (v & 0xff), + // this is a little bit more tricky, since we don't have unsigned longs + byte[] bytes = new byte[] { + (byte) ((v >> 56) & 0xff), + (byte) ((v >> 48) & 0xff), + (byte) ((v >> 40) & 0xff), + (byte) ((v >> 32) & 0xff), + (byte) ((v >> 24) & 0xff), + (byte) ((v >> 16) & 0xff), + (byte) ((v >> 8) & 0xff), + (byte) (v & 0xff), }; return new BigInteger(1, bytes); } @@ -299,6 +301,8 @@ public Object unpack(InputStream is) throws IOException { return unpackBin(in.readShort() & MAX_16BIT, in); case MP_BIN32: return unpackBin(in.readInt(), in); + default: + break; } if (value >= MP_NEGATIVE_FIXNUM_INT && value <= MP_NEGATIVE_FIXNUM_INT + MAX_5BIT) { @@ -309,7 +313,8 @@ public Object unpack(InputStream is) throws IOException { return unpackMap(value - MP_FIXMAP_INT, in); } else if (value >= MP_FIXSTR_INT && value <= MP_FIXSTR_INT + MAX_5BIT) { return unpackStr(value - MP_FIXSTR_INT, in); - } else if (value <= MAX_7BIT) {//MP_FIXNUM - the value is value as an int + } else if (value <= MAX_7BIT) { + // MP_FIXNUM - the value is value as an int return value; } else { throw new IllegalArgumentException("Input contains invalid type value " + (byte) value); diff --git a/src/main/java/org/tarantool/RefreshableSocketProvider.java b/src/main/java/org/tarantool/RefreshableSocketProvider.java new file mode 100644 index 00000000..a3ccc56f --- /dev/null +++ b/src/main/java/org/tarantool/RefreshableSocketProvider.java @@ -0,0 +1,12 @@ +package org.tarantool; + +import java.net.SocketAddress; +import java.util.Collection; + +public interface RefreshableSocketProvider { + + Collection getAddresses(); + + void refreshAddresses(Collection addresses); + +} diff --git a/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java b/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java index d16c6bf4..1107799c 100644 --- a/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java +++ b/src/main/java/org/tarantool/RoundRobinSocketProviderImpl.java @@ -2,181 +2,218 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.channels.SocketChannel; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; /** * Basic reconnection strategy that changes addresses in a round-robin fashion. * To be used with {@link TarantoolClientImpl}. */ -public class RoundRobinSocketProviderImpl implements SocketChannelProvider { - /** Timeout to establish socket connection with an individual server. */ - private int timeout; // 0 is infinite. - /** Limit of retries. */ - private int retriesLimit = -1; // No-limit. - /** Server addresses as configured. */ - private final String[] addrs; - /** Socket addresses. */ - private final InetSocketAddress[] sockAddrs; - /** Current position within {@link #sockAddrs} array. */ - private int pos; +public class RoundRobinSocketProviderImpl extends BaseSocketChannelProvider implements RefreshableSocketProvider { + + private static final int UNSET_POSITION = -1; + private static final int DEFAULT_RETRIES_PER_CONNECTION = 3; /** - * Constructs an instance. - * - * @param addrs Array of addresses in a form of [host]:[port]. + * Socket addresses pool. */ - public RoundRobinSocketProviderImpl(String... addrs) { - if (addrs == null || addrs.length == 0) - throw new IllegalArgumentException("addrs is null or empty."); + private final List socketAddresses = new ArrayList<>(); - this.addrs = Arrays.copyOf(addrs, addrs.length); - - sockAddrs = new InetSocketAddress[this.addrs.length]; - - for (int i = 0; i < this.addrs.length; i++) { - sockAddrs[i] = parseAddress(this.addrs[i]); - } - } + /** + * Current position within {@link #socketAddresses} list. + *

+ * It is {@link #UNSET_POSITION} when no addresses from + * the {@link #socketAddresses} pool have been processed yet. + *

+ * When this provider receives new addresses it tries + * to look for a new position for the last used address or + * sets the position to {@link #UNSET_POSITION} otherwise. + * + * @see #getLastObtainedAddress() + * @see #refreshAddresses(Collection) + */ + private AtomicInteger currentPosition = new AtomicInteger(UNSET_POSITION); /** - * @return Configured addresses in a form of [host]:[port]. + * Address list lock for a thread-safe access to it + * when a refresh operation occurs. + * + * @see RefreshableSocketProvider#refreshAddresses(Collection) */ - public String[] getAddresses() { - return this.addrs; - } + private ReadWriteLock addressListLock = new ReentrantReadWriteLock(); /** - * Sets maximum amount of time to wait for a socket connection establishment - * with an individual server. + * Constructs an instance. * - * Zero means infinite timeout. + * @param addresses optional array of addresses in a form of host[:port] * - * @param timeout Timeout value, ms. - * @return {@code this}. - * @throws IllegalArgumentException If timeout is negative. + * @throws IllegalArgumentException if addresses aren't provided */ - public RoundRobinSocketProviderImpl setTimeout(int timeout) { - if (timeout < 0) - throw new IllegalArgumentException("timeout is negative."); - - this.timeout = timeout; + public RoundRobinSocketProviderImpl(String... addresses) { + updateAddressList(Arrays.asList(addresses)); + setRetriesLimit(DEFAULT_RETRIES_PER_CONNECTION); + } - return this; + private void updateAddressList(Collection addresses) { + if (addresses == null || addresses.isEmpty()) { + throw new IllegalArgumentException("At least one address must be provided"); + } + Lock writeLock = addressListLock.writeLock(); + writeLock.lock(); + try { + InetSocketAddress lastAddress = getLastObtainedAddress(); + socketAddresses.clear(); + addresses.stream() + .map(this::parseAddress) + .collect(Collectors.toCollection(() -> socketAddresses)); + if (lastAddress != null) { + int recoveredPosition = socketAddresses.indexOf(lastAddress); + currentPosition.set(recoveredPosition); + } else { + currentPosition.set(UNSET_POSITION); + } + } finally { + writeLock.unlock(); + } } /** - * @return Maximum amount of time to wait for a socket connection establishment - * with an individual server. + * Gets parsed and resolved internet addresses. + * + * @return socket addresses */ - public int getTimeout() { - return timeout; + public List getAddresses() { + Lock readLock = addressListLock.readLock(); + readLock.lock(); + try { + return Collections.unmodifiableList(this.socketAddresses); + } finally { + readLock.unlock(); + } } /** - * Sets maximum amount of reconnect attempts to be made before an exception is raised. - * The retry count is maintained by a {@link #get(int, Throwable)} caller - * when a socket level connection was established. - * - * Negative value means unlimited. + * Gets last used address from the pool if it exists. * - * @param retriesLimit Limit of retries to use. - * @return {@code this}. + * @return last obtained address or null + * if {@link #currentPosition} has {@link #UNSET_POSITION} value */ - public RoundRobinSocketProviderImpl setRetriesLimit(int retriesLimit) { - this.retriesLimit = retriesLimit; - - return this; + protected InetSocketAddress getLastObtainedAddress() { + Lock readLock = addressListLock.readLock(); + readLock.lock(); + try { + int index = currentPosition.get(); + return index != UNSET_POSITION ? socketAddresses.get(index) : null; + } finally { + readLock.unlock(); + } } /** - * @return Maximum reconnect attempts to make before raising exception. + * Tries to open a socket channel to a next instance + * for the addresses list. + * + * There are {@link #getRetriesLimit()} attempts per + * call to initiate a connection to the instance. + * + * @param retryNumber reconnection attempt number + * @param lastError reconnection reason + * + * @return opened socket channel + * + * @throws IOException if any IO errors occur + * @throws CommunicationException if retry number exceeds addresses size + * + * @see #setRetriesLimit(int) + * @see #getAddresses() */ - public int getRetriesLimit() { - return retriesLimit; - } - - /** {@inheritDoc} */ @Override - public SocketChannel get(int retryNumber, Throwable lastError) { - if (areRetriesExhausted(retryNumber)) { - throw new CommunicationException("Connection retries exceeded.", lastError); + protected SocketChannel makeAttempt(int retryNumber, Throwable lastError) throws IOException { + if (retryNumber > getAddressCount()) { + throwFatalError("No more connection addresses are left."); } - int attempts = getAddressCount(); - long deadline = System.currentTimeMillis() + timeout * attempts; - while (!Thread.currentThread().isInterrupted()) { - SocketChannel channel = null; + + int retriesLimit = getRetriesLimit(); + InetSocketAddress socketAddress = getNextSocketAddress(); + IOException connectionError = null; + for (int i = 0; i < retriesLimit; i++) { try { - channel = SocketChannel.open(); - InetSocketAddress addr = getNextSocketAddress(); - channel.socket().connect(addr, timeout); - return channel; + return openChannel(socketAddress); } catch (IOException e) { - if (channel != null) { - try { - channel.close(); - } catch (IOException ignored) { - // No-op. - } - } - long now = System.currentTimeMillis(); - if (deadline <= now) { - throw new CommunicationException("Connection time out.", e); - } - if (--attempts == 0) { - // Tried all addresses without any lack, but still have time. - attempts = getAddressCount(); - try { - Thread.sleep((deadline - now) / attempts); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } + connectionError = e; } } - throw new CommunicationException("Thread interrupted.", new InterruptedException()); + throw connectionError; } /** - * @return Number of configured addresses. + * Sets a retries count per instance. + * 0 (infinite) count is not supported by this provider. + * + * @param retriesLimit limit of retries to use. */ - protected int getAddressCount() { - return sockAddrs.length; + @Override + public void setRetriesLimit(int retriesLimit) { + if (retriesLimit == 0) { + throwFatalError("Retries count should be at least 1 or more"); + } + super.setRetriesLimit(retriesLimit); } /** - * @return Socket address to use for the next reconnection attempt. + * Gets size of addresses pool. + * + * @return Number of configured addresses. */ - protected InetSocketAddress getNextSocketAddress() { - InetSocketAddress res = sockAddrs[pos]; - pos = (pos + 1) % sockAddrs.length; - return res; + protected int getAddressCount() { + Lock readLock = addressListLock.readLock(); + readLock.lock(); + try { + return socketAddresses.size(); + } finally { + readLock.unlock(); + } } /** - * Parse a string address in the form of [host]:[port] - * and builds a socket address. + * Gets next address from the pool to be used to connect. * - * @param addr Server address. - * @return Socket address. + * @return Socket address to use for the next reconnection attempt */ - protected InetSocketAddress parseAddress(String addr) { - int idx = addr.indexOf(':'); - String host = (idx < 0) ? addr : addr.substring(0, idx); - int port = (idx < 0) ? 3301 : Integer.parseInt(addr.substring(idx + 1)); - return new InetSocketAddress(host, port); + protected InetSocketAddress getNextSocketAddress() { + Lock readLock = addressListLock.readLock(); + readLock.lock(); + try { + int position = currentPosition.updateAndGet(i -> (i + 1) % socketAddresses.size()); + return socketAddresses.get(position); + } finally { + readLock.unlock(); + } } /** - * Provides a decision on whether retries limit is hit. + * Update addresses pool by new list. + * + * @param addresses list of addresses to be applied * - * @param retries Current count of retries. - * @return {@code true} if retries are exhausted. + * @throws IllegalArgumentException if addresses list is empty */ - private boolean areRetriesExhausted(int retries) { - int limit = getRetriesLimit(); - if (limit < 0) - return false; - return retries >= limit; + public void refreshAddresses(Collection addresses) { + updateAddressList(addresses); } + + private void throwFatalError(String message) { + throw new CommunicationException(message); + } + } diff --git a/src/main/java/org/tarantool/SingleSocketChannelProviderImpl.java b/src/main/java/org/tarantool/SingleSocketChannelProviderImpl.java new file mode 100644 index 00000000..ef1fc59b --- /dev/null +++ b/src/main/java/org/tarantool/SingleSocketChannelProviderImpl.java @@ -0,0 +1,62 @@ +package org.tarantool; + +import org.tarantool.util.StringUtils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; + +/** + * Simple provider that produces a single connection. + * To be used with {@link TarantoolClientImpl}. + */ +public class SingleSocketChannelProviderImpl extends BaseSocketChannelProvider { + + private InetSocketAddress address; + + /** + * Creates a simple provider. + * + * @param address instance address + */ + public SingleSocketChannelProviderImpl(String address) { + setAddress(address); + } + + public SocketAddress getAddress() { + return address; + } + + @Override + protected SocketChannel makeAttempt(int retryNumber, Throwable lastError) throws IOException { + if (areRetriesExhausted(retryNumber)) { + throw new CommunicationException("Connection retries exceeded.", lastError); + } + return openChannel(address); + } + + /** + * Provides a decision on whether retries limit is hit. + * + * @param retryNumber current count of retries. + * + * @return {@code true} if retries are exhausted. + */ + private boolean areRetriesExhausted(int retryNumber) { + int limit = getRetriesLimit(); + if (limit < 1) { + return false; + } + return retryNumber >= limit; + } + + public void setAddress(String address) { + if (StringUtils.isBlank(address)) { + throw new IllegalArgumentException("address must not be empty"); + } + + this.address = parseAddress(address); + } + +} diff --git a/src/main/java/org/tarantool/SocketChannelProvider.java b/src/main/java/org/tarantool/SocketChannelProvider.java index 09112dec..a811bb1f 100644 --- a/src/main/java/org/tarantool/SocketChannelProvider.java +++ b/src/main/java/org/tarantool/SocketChannelProvider.java @@ -1,15 +1,22 @@ package org.tarantool; - +import java.net.SocketAddress; import java.nio.channels.SocketChannel; public interface SocketChannelProvider { + /** * Provides socket channel to init restore connection. - * You could change hosts on fail and sleep between retries in this method - * @param retryNumber number of current retry. Reset after successful connect. + * You could change hosts between retries in this method. + * + * @param retryNumber number of current retry. * @param lastError the last error occurs when reconnecting - * @return the result of SocketChannel open(SocketAddress remote) call + * + * @return the result of {@link SocketChannel#open(SocketAddress)} call + * + * @throws SocketProviderTransientException if recoverable error occurred + * @throws RuntimeException if any other reasons occurred */ SocketChannel get(int retryNumber, Throwable lastError); + } diff --git a/src/main/java/org/tarantool/SocketProviderTransientException.java b/src/main/java/org/tarantool/SocketProviderTransientException.java new file mode 100644 index 00000000..5a378903 --- /dev/null +++ b/src/main/java/org/tarantool/SocketProviderTransientException.java @@ -0,0 +1,9 @@ +package org.tarantool; + +public class SocketProviderTransientException extends RuntimeException { + + public SocketProviderTransientException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/org/tarantool/SqlProtoUtils.java b/src/main/java/org/tarantool/SqlProtoUtils.java index f84b406b..23a339fd 100644 --- a/src/main/java/org/tarantool/SqlProtoUtils.java +++ b/src/main/java/org/tarantool/SqlProtoUtils.java @@ -11,10 +11,10 @@ public abstract class SqlProtoUtils { public static List> readSqlResult(TarantoolPacket pack) { List> data = (List>) pack.getBody().get(Key.DATA.getId()); - List> values = new ArrayList>(data.size()); - List metaData = getSQLMetadata(pack); - LinkedHashMap value = new LinkedHashMap(); + List> values = new ArrayList<>(data.size()); + List metaData = getSQLMetadata(pack); for (List row : data) { + LinkedHashMap value = new LinkedHashMap<>(); for (int i = 0; i < row.size(); i++) { value.put(metaData.get(i).getName(), row.get(i)); } @@ -27,11 +27,11 @@ public static List> getSQLData(TarantoolPacket pack) { return (List>) pack.getBody().get(Key.DATA.getId()); } - public static List getSQLMetadata(TarantoolPacket pack) { + public static List getSQLMetadata(TarantoolPacket pack) { List> meta = (List>) pack.getBody().get(Key.SQL_METADATA.getId()); - List values = new ArrayList(meta.size()); + List values = new ArrayList<>(meta.size()); for (Map c : meta) { - values.add(new TarantoolBase.SQLMetaData((String) c.get(Key.SQL_FIELD_NAME.getId()))); + values.add(new SQLMetaData((String) c.get(Key.SQL_FIELD_NAME.getId()))); } return values; } @@ -44,4 +44,23 @@ public static Long getSqlRowCount(TarantoolPacket pack) { } return null; } + + public static class SQLMetaData { + protected String name; + + public SQLMetaData(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "SQLMetaData{" + + "name='" + name + '\'' + + '}'; + } + } } diff --git a/src/main/java/org/tarantool/TarantoolBase.java b/src/main/java/org/tarantool/TarantoolBase.java index 2babde7a..6856a495 100644 --- a/src/main/java/org/tarantool/TarantoolBase.java +++ b/src/main/java/org/tarantool/TarantoolBase.java @@ -11,8 +11,9 @@ public abstract class TarantoolBase extends AbstractTarantoolOps, Object, Result> { protected String serverVersion; + /** - * Connection state + * Connection state. */ protected MsgPackLite msgPackLite = MsgPackLite.INSTANCE; protected AtomicLong syncId = new AtomicLong(); @@ -31,25 +32,6 @@ public TarantoolBase(String username, String password, Socket socket) { } } - protected static class SQLMetaData { - protected String name; - - public SQLMetaData(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return "SQLMetaData{" + - "name='" + name + '\'' + - '}'; - } - } - protected TarantoolException serverError(long code, Object error) { return new TarantoolException(code, error instanceof String ? (String) error : new String((byte[]) error)); } @@ -59,7 +41,7 @@ protected void closeChannel(SocketChannel channel) { try { channel.close(); } catch (IOException ignored) { - + // no-op } } } diff --git a/src/main/java/org/tarantool/TarantoolClientConfig.java b/src/main/java/org/tarantool/TarantoolClientConfig.java index b83996fc..6ddc40fb 100644 --- a/src/main/java/org/tarantool/TarantoolClientConfig.java +++ b/src/main/java/org/tarantool/TarantoolClientConfig.java @@ -1,53 +1,70 @@ package org.tarantool; - -import java.util.concurrent.TimeUnit; - public class TarantoolClientConfig { /** - * username and password for authorization + * Auth-related data. */ public String username; - public String password; /** - * default ByteArrayOutputStream size when make query serialization + * Default request size when make query serialization. */ public int defaultRequestSize = 4096; /** - * initial size for map which holds futures of sent request + * Initial capacity for the map which holds futures of sent request. */ public int predictedFutures = (int) ((1024 * 1024) / 0.75) + 1; - public int writerThreadPriority = Thread.NORM_PRIORITY; - public int readerThreadPriority = Thread.NORM_PRIORITY; - /** - * shared buffer is place where client collect requests when socket is busy on write + * Shared buffer size (place where client collects requests + * when socket is busy on write). */ public int sharedBufferSize = 8 * 1024 * 1024; + /** - * not put request into the shared buffer if request size is ge directWriteFactor * sharedBufferSize + * Factor to calculate a threshold whether request will be accommodated + * in the shared buffer. + *

+ * if request size exceeds directWriteFactor * sharedBufferSize + * request is sent directly. */ public double directWriteFactor = 0.5d; /** - * Use old call command https://github.com/tarantool/doc/issues/54, - * please ensure that you server supports new call command + * Write operation timeout. + */ + public long writeTimeoutMillis = 60 * 1000L; + + /** + * Use old call command https://github.com/tarantool/doc/issues/54, + * please ensure that you server supports new call command. */ public boolean useNewCall = false; /** - * Any blocking ops timeout + * Max time to establish connection to the server + * and be completely configured (to have an {@code ALIVE} status). + * + * @see TarantoolClient#isAlive() */ - public long initTimeoutMillis = 60*1000L; + public long initTimeoutMillis = 60 * 1000L; - public long writeTimeoutMillis = 60*1000L; + /** + * Connection timeout per attempt. + * {@code 0} means no timeout. + */ + public int connectionTimeout = 2 * 1000; + + /** + * Total attempts number to connect to DB. + * {@code 0} means unlimited attempts. + */ + public int retryCount = 3; } diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index bd7a993d..7c53cfeb 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -2,8 +2,8 @@ import org.tarantool.protocol.ProtoUtils; import org.tarantool.protocol.ReadableViaSelectorChannel; -import org.tarantool.protocol.TarantoolPacket; import org.tarantool.protocol.TarantoolGreeting; +import org.tarantool.protocol.TarantoolPacket; import java.io.IOException; import java.nio.ByteBuffer; @@ -22,44 +22,47 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.ReentrantLock; - public class TarantoolClientImpl extends TarantoolBase> implements TarantoolClient { - public static final CommunicationException NOT_INIT_EXCEPTION = new CommunicationException("Not connected, initializing connection"); + + public static final CommunicationException NOT_INIT_EXCEPTION + = new CommunicationException("Not connected, initializing connection"); + protected TarantoolClientConfig config; /** - * External + * External. */ protected SocketChannelProvider socketProvider; + protected SocketChannel channel; + protected ReadableViaSelectorChannel readChannel; + protected volatile Exception thumbstone; - protected Map> futures; - protected AtomicInteger wait = new AtomicInteger(); + protected Map> futures; + protected AtomicInteger pendingResponsesCount = new AtomicInteger(); + /** - * Write properties + * Write properties. */ - protected SocketChannel channel; - protected ReadableViaSelectorChannel readChannel; - protected ByteBuffer sharedBuffer; - protected ByteBuffer writerBuffer; protected ReentrantLock bufferLock = new ReentrantLock(false); protected Condition bufferNotEmpty = bufferLock.newCondition(); protected Condition bufferEmpty = bufferLock.newCondition(); + + protected ByteBuffer writerBuffer; protected ReentrantLock writeLock = new ReentrantLock(true); /** - * Interfaces + * Interfaces. */ protected SyncOps syncOps; protected FireAndForgetOps fireAndForgetOps; protected ComposableAsyncOps composableAsyncOps; /** - * Inner + * Inner. */ protected TarantoolClientStats stats; protected StateHelper state = new StateHelper(StateHelper.RECONNECT); @@ -70,16 +73,31 @@ public class TarantoolClientImpl extends TarantoolBase> implements Tar @Override public void run() { while (!Thread.currentThread().isInterrupted()) { - if (state.compareAndSet(StateHelper.RECONNECT, 0)) { - reconnect(0, thumbstone); + reconnect(thumbstone); + try { + state.awaitReconnection(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - LockSupport.park(state); } } }); + public TarantoolClientImpl(String address, TarantoolClientConfig config) { + this(new SingleSocketChannelProviderImpl(address), config); + } + public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) { - super(); + initClient(socketProvider, config); + if (socketProvider instanceof ConfigurableSocketChannelProvider) { + ConfigurableSocketChannelProvider configurableProvider = (ConfigurableSocketChannelProvider) socketProvider; + configurableProvider.setConnectionTimeout(config.connectionTimeout); + configurableProvider.setRetriesLimit(config.retryCount); + } + startConnector(config.initTimeoutMillis); + } + + private void initClient(SocketChannelProvider socketProvider, TarantoolClientConfig config) { this.thumbstone = NOT_INIT_EXCEPTION; this.config = config; this.initialRequestSize = config.defaultRequestSize; @@ -99,12 +117,17 @@ public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClient this.fireAndForgetOps.setCallCode(Code.CALL); this.composableAsyncOps.setCallCode(Code.CALL); } + } + + private void startConnector(long initTimeoutMillis) { connector.start(); try { - if (!waitAlive(config.initTimeoutMillis, TimeUnit.MILLISECONDS)) { - CommunicationException e = new CommunicationException(config.initTimeoutMillis + + if (!waitAlive(initTimeoutMillis, TimeUnit.MILLISECONDS)) { + CommunicationException e = new CommunicationException( + initTimeoutMillis + "ms is exceeded when waiting for client initialization. " + - "You could configure init timeout in TarantoolConfig"); + "You could configure init timeout in TarantoolConfig" + ); close(e); throw e; @@ -115,40 +138,44 @@ public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClient } } - protected void reconnect(int retry, Throwable lastError) { - SocketChannel channel; + protected void reconnect(Throwable lastError) { + SocketChannel channel = null; + int retryNumber = 0; while (!Thread.currentThread().isInterrupted()) { try { - channel = socketProvider.get(retry++, lastError == NOT_INIT_EXCEPTION ? null : lastError); + channel = socketProvider.get(retryNumber++, lastError == NOT_INIT_EXCEPTION ? null : lastError); } catch (Exception e) { - close(e); - return; + closeChannel(channel); + lastError = e; + if (!(e instanceof SocketProviderTransientException)) { + close(e); + return; + } } try { - connect(channel); - return; + if (channel != null) { + connect(channel); + return; + } } catch (Exception e) { closeChannel(channel); lastError = e; - if (e instanceof InterruptedException) + if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); + } } } } protected void connect(final SocketChannel channel) throws Exception { try { - TarantoolGreeting greeting = ProtoUtils.connect(channel, - config.username, config.password); + TarantoolGreeting greeting = ProtoUtils.connect(channel, config.username, config.password); this.serverVersion = greeting.getServerVersion(); } catch (IOException e) { - try { - channel.close(); - } catch (IOException ignored) { - } - + closeChannel(channel); throw new CommunicationException("Couldn't connect to tarantool", e); } + channel.configureBlocking(false); this.channel = channel; this.readChannel = new ReadableViaSelectorChannel(channel); @@ -164,42 +191,42 @@ protected void connect(final SocketChannel channel) throws Exception { } protected void startThreads(String threadName) throws InterruptedException { - final CountDownLatch init = new CountDownLatch(2); - reader = new Thread(new Runnable() { - @Override - public void run() { - init.countDown(); - if (state.acquire(StateHelper.READING)) { - try { - readThread(); - } finally { - state.release(StateHelper.READING); - if (state.compareAndSet(0, StateHelper.RECONNECT)) - LockSupport.unpark(connector); + final CountDownLatch ioThreadStarted = new CountDownLatch(2); + final AtomicInteger leftIoThreads = new AtomicInteger(2); + reader = new Thread(() -> { + ioThreadStarted.countDown(); + if (state.acquire(StateHelper.READING)) { + try { + readThread(); + } finally { + state.release(StateHelper.READING); + // only last of two IO-threads can signal for reconnection + if (leftIoThreads.decrementAndGet() == 0) { + state.trySignalForReconnection(); } } } }); - writer = new Thread(new Runnable() { - @Override - public void run() { - init.countDown(); - if (state.acquire(StateHelper.WRITING)) { - try { - writeThread(); - } finally { - state.release(StateHelper.WRITING); - if (state.compareAndSet(0, StateHelper.RECONNECT)) - LockSupport.unpark(connector); + writer = new Thread(() -> { + ioThreadStarted.countDown(); + if (state.acquire(StateHelper.WRITING)) { + try { + writeThread(); + } finally { + state.release(StateHelper.WRITING); + // only last of two IO-threads can signal for reconnection + if (leftIoThreads.decrementAndGet() == 0) { + state.trySignalForReconnection(); } } } }); + state.release(StateHelper.RECONNECT); configureThreads(threadName); reader.start(); writer.start(); - init.await(); + ioThreadStarted.await(); } protected void configureThreads(String threadName) { @@ -216,42 +243,43 @@ protected Future exec(Code code, Object... args) { protected CompletableFuture doExec(Code code, Object[] args) { validateArgs(args); long sid = syncId.incrementAndGet(); - CompletableFuture q = new CompletableFuture<>(); + TarantoolOp future = new TarantoolOp<>(code); - if (isDead(q)) { - return q; + if (isDead(future)) { + return future; } - futures.put(sid, q); - if (isDead(q)) { + futures.put(sid, future); + if (isDead(future)) { futures.remove(sid); - return q; + return future; } try { write(code, sid, null, args); } catch (Exception e) { futures.remove(sid); - fail(q, e); + fail(future, e); } - return q; + return future; } protected synchronized void die(String message, Exception cause) { if (thumbstone != null) { return; } - final CommunicationException err = new CommunicationException(message, cause); - this.thumbstone = err; + final CommunicationException error = new CommunicationException(message, cause); + this.thumbstone = error; while (!futures.isEmpty()) { - Iterator>> iterator = futures.entrySet().iterator(); + Iterator>> iterator = futures.entrySet().iterator(); while (iterator.hasNext()) { - Map.Entry> elem = iterator.next(); + Map.Entry> elem = iterator.next(); if (elem != null) { - CompletableFuture future = elem.getValue(); - fail(future, err); + TarantoolOp future = elem.getValue(); + fail(future, error); } iterator.remove(); } } + pendingResponsesCount.set(0); bufferLock.lock(); try { @@ -268,7 +296,7 @@ public void ping() { } protected void write(Code code, Long syncId, Long schemaId, Object... args) - throws Exception { + throws Exception { ByteBuffer buffer = ProtoUtils.createPacket(code, syncId, schemaId, args); if (directWrite(buffer)) { @@ -293,14 +321,18 @@ protected void sharedWrite(ByteBuffer buffer) throws InterruptedException, Timeo try { if (remaining < 1 || !bufferEmpty.await(remaining, TimeUnit.MILLISECONDS)) { stats.sharedEmptyAwaitTimeouts++; - throw new TimeoutException(config.writeTimeoutMillis + "ms is exceeded while waiting for empty buffer you could configure write timeout it in TarantoolConfig"); + throw new TimeoutException( + config.writeTimeoutMillis + + "ms is exceeded while waiting for empty buffer. " + + "You could configure write timeout it in TarantoolConfig" + ); } } catch (InterruptedException e) { throw new CommunicationException("Interrupted", e); } } sharedBuffer.put(buffer); - wait.incrementAndGet(); + pendingResponsesCount.incrementAndGet(); bufferNotEmpty.signalAll(); stats.buffered++; } finally { @@ -308,7 +340,11 @@ protected void sharedWrite(ByteBuffer buffer) throws InterruptedException, Timeo } } else { stats.sharedWriteLockTimeouts++; - throw new TimeoutException(config.writeTimeoutMillis + "ms is exceeded while waiting for shared buffer lock you could configure write timeout in TarantoolConfig"); + throw new TimeoutException( + config.writeTimeoutMillis + + "ms is exceeded while waiting for shared buffer lock. " + + "You could configure write timeout in TarantoolConfig" + ); } } @@ -323,39 +359,39 @@ private boolean directWrite(ByteBuffer buffer) throws InterruptedException, IOEx } writeFully(channel, buffer); stats.directWrite++; - wait.incrementAndGet(); + pendingResponsesCount.incrementAndGet(); } finally { writeLock.unlock(); } return true; } else { stats.directWriteLockTimeouts++; - throw new TimeoutException(config.writeTimeoutMillis + "ms is exceeded while waiting for channel lock you could configure write timeout in TarantoolConfig"); + throw new TimeoutException( + config.writeTimeoutMillis + + "ms is exceeded while waiting for channel lock. " + + "You could configure write timeout in TarantoolConfig" + ); } } return false; } protected void readThread() { - try { - while (!Thread.currentThread().isInterrupted()) { - try { - TarantoolPacket packet = ProtoUtils.readPacket(readChannel); + while (!Thread.currentThread().isInterrupted()) { + try { + TarantoolPacket packet = ProtoUtils.readPacket(readChannel); - Map headers = packet.getHeaders(); + Map headers = packet.getHeaders(); - Long syncId = (Long) headers.get(Key.SYNC.getId()); - CompletableFuture future = futures.remove(syncId); - stats.received++; - wait.decrementAndGet(); - complete(packet, future); - } catch (Exception e) { - die("Cant read answer", e); - return; - } + Long syncId = (Long) headers.get(Key.SYNC.getId()); + TarantoolOp future = futures.remove(syncId); + stats.received++; + pendingResponsesCount.decrementAndGet(); + complete(packet, future); + } catch (Exception e) { + die("Cant read answer", e); + return; } - } catch (Exception e) { - die("Cant init thread", e); } } @@ -395,36 +431,35 @@ protected void fail(CompletableFuture q, Exception e) { q.completeExceptionally(e); } - protected void complete(TarantoolPacket packet, CompletableFuture q) { - if (q != null) { + protected void complete(TarantoolPacket packet, TarantoolOp future) { + if (future != null) { long code = packet.getCode(); if (code == 0) { - - if (code == Code.EXECUTE.getId()) { - completeSql(q, packet); + if (future.getCode() == Code.EXECUTE) { + completeSql(future, packet); } else { - ((CompletableFuture) q).complete(packet.getBody().get(Key.DATA.getId())); + ((CompletableFuture) future).complete(packet.getBody().get(Key.DATA.getId())); } } else { Object error = packet.getBody().get(Key.ERROR.getId()); - fail(q, serverError(code, error)); + fail(future, serverError(code, error)); } } } - protected void completeSql(CompletableFuture q, TarantoolPacket pack) { + protected void completeSql(CompletableFuture future, TarantoolPacket pack) { Long rowCount = SqlProtoUtils.getSqlRowCount(pack); - if (rowCount!=null) { - ((CompletableFuture) q).complete(rowCount); + if (rowCount != null) { + ((CompletableFuture) future).complete(rowCount); } else { List> values = SqlProtoUtils.readSqlResult(pack); - ((CompletableFuture) q).complete(values); + ((CompletableFuture) future).complete(values); } } - protected T syncGet(Future r) { + protected T syncGet(Future result) { try { - return r.get(); + return result.get(); } catch (ExecutionException e) { if (e.getCause() instanceof CommunicationException) { throw (CommunicationException) e.getCause(); @@ -455,7 +490,6 @@ public void close() { protected void close(Exception e) { if (state.close()) { connector.interrupt(); - die(e.getMessage(), e); } } @@ -469,9 +503,9 @@ protected void stopIO() { } if (readChannel != null) { try { - readChannel.close();//also closes this.channel + readChannel.close(); // also closes this.channel } catch (IOException ignored) { - + // no-op } } closeChannel(channel); @@ -499,7 +533,7 @@ public TarantoolClientOps, Object, List> syncOps() { @Override public TarantoolClientOps, Object, Future>> asyncOps() { - return (TarantoolClientOps)this; + return (TarantoolClientOps) this; } @Override @@ -515,7 +549,7 @@ public TarantoolClientOps, Object, Long> fireAndForgetOps() { @Override public TarantoolSQLOps>> sqlSyncOps() { - return new TarantoolSQLOps>>() { + return new TarantoolSQLOps>>() { @Override public Long update(String sql, Object... bind) { @@ -531,7 +565,7 @@ public List> query(String sql, Object... bind) { @Override public TarantoolSQLOps, Future>>> sqlAsyncOps() { - return new TarantoolSQLOps, Future>>>() { + return new TarantoolSQLOps, Future>>>() { @Override public Future update(String sql, Object... bind) { return (Future) exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, bind); @@ -555,9 +589,11 @@ public List exec(Code code, Object... args) { public void close() { throw new IllegalStateException("You should close TarantoolClient instead."); } + } protected class FireAndForgetOps extends AbstractTarantoolOps, Object, Long> { + @Override public Long exec(Code code, Object... args) { if (thumbstone == null) { @@ -577,9 +613,12 @@ public Long exec(Code code, Object... args) { public void close() { throw new IllegalStateException("You should close TarantoolClient instead."); } + } - protected class ComposableAsyncOps extends AbstractTarantoolOps, Object, CompletionStage>> { + protected class ComposableAsyncOps + extends AbstractTarantoolOps, Object, CompletionStage>> { + @Override public CompletionStage> exec(Code code, Object... args) { return (CompletionStage>) TarantoolClientImpl.this.doExec(code, args); @@ -589,10 +628,11 @@ public CompletionStage> exec(Code code, Object... args) { public void close() { TarantoolClientImpl.this.close(); } + } protected boolean isDead(CompletableFuture q) { - if (TarantoolClientImpl.this.thumbstone != null) { + if (this.thumbstone != null) { fail(q, new CommunicationException("Connection is dead", thumbstone)); return true; } @@ -619,6 +659,8 @@ public TarantoolClientStats getStats() { * Manages state changes. */ protected final class StateHelper { + + static final int UNINITIALIZED = 0; static final int READING = 1; static final int WRITING = 2; static final int ALIVE = READING | WRITING; @@ -628,10 +670,22 @@ protected final class StateHelper { private final AtomicInteger state; private final AtomicReference nextAliveLatch = - new AtomicReference(new CountDownLatch(1)); + new AtomicReference<>(new CountDownLatch(1)); private final CountDownLatch closedLatch = new CountDownLatch(1); + /** + * The condition variable to signal a reconnection is needed from reader / + * writer threads and waiting for that signal from the reconnection thread. + *

+ * The lock variable to access this condition. + * + * @see #awaitReconnection() + * @see #trySignalForReconnection() + */ + protected final ReentrantLock connectorLock = new ReentrantLock(); + protected final Condition reconnectRequired = connectorLock.newCondition(); + protected StateHelper(int state) { this.state = new AtomicInteger(state); } @@ -640,35 +694,62 @@ protected int getState() { return state.get(); } + /** + * Set CLOSED state, drop RECONNECT state. + */ protected boolean close() { - for (;;) { - int st = getState(); - if ((st & CLOSED) == CLOSED) + for (; ; ) { + int currentState = getState(); + + /* CLOSED is the terminal state. */ + if ((currentState & CLOSED) == CLOSED) { return false; - if (compareAndSet(st, (st & ~RECONNECT) | CLOSED)) + } + + /* Drop RECONNECT, set CLOSED. */ + if (compareAndSet(currentState, (currentState & ~RECONNECT) | CLOSED)) { return true; + } } } + /** + * Move from a current state to a give one. + *

+ * Some moves are forbidden. + */ protected boolean acquire(int mask) { - for (;;) { - int st = getState(); - if ((st & CLOSED) == CLOSED) + for (; ; ) { + int currentState = getState(); + + /* CLOSED is the terminal state. */ + if ((currentState & CLOSED) == CLOSED) { return false; + } + + /* Don't move to READING, WRITING or ALIVE from RECONNECT. */ + if ((currentState & RECONNECT) > mask) { + return false; + } - if ((st & mask) != 0) + /* Cannot move from a state to the same state. */ + if ((currentState & mask) != 0) { throw new IllegalStateException("State is already " + mask); + } - if (compareAndSet(st, st | mask)) + /* Set acquired state. */ + if (compareAndSet(currentState, currentState | mask)) { return true; + } } } protected void release(int mask) { - for (;;) { - int st = getState(); - if (compareAndSet(st, st & ~mask)) + for (; ; ) { + int currentState = getState(); + if (compareAndSet(currentState, currentState & ~mask)) { return; + } } } @@ -687,10 +768,18 @@ protected boolean compareAndSet(int expect, int update) { return true; } + /** + * Reconnection uses another way to await state via receiving a signal + * instead of latches. + */ protected void awaitState(int state) throws InterruptedException { - CountDownLatch latch = getStateLatch(state); - if (latch != null) { - latch.await(); + if (state == RECONNECT) { + awaitReconnection(); + } else { + CountDownLatch latch = getStateLatch(state); + if (latch != null) { + latch.await(); + } } } @@ -710,9 +799,60 @@ private CountDownLatch getStateLatch(int state) { CountDownLatch latch = nextAliveLatch.get(); /* It may happen so that an error is detected but the state is still alive. Wait for the 'next' alive state in such cases. */ - return (getState() == ALIVE && thumbstone == null) ? null : latch; + return (getState() == ALIVE && thumbstone == null) ? null : latch; } return null; } + + /** + * Blocks until a reconnection signal will be received. + * + * @see #trySignalForReconnection() + */ + private void awaitReconnection() throws InterruptedException { + connectorLock.lock(); + try { + while (getState() != StateHelper.RECONNECT) { + reconnectRequired.await(); + } + } finally { + connectorLock.unlock(); + } + } + + /** + * Signals to the connector that reconnection process can be performed. + * + * @see #awaitReconnection() + */ + private void trySignalForReconnection() { + if (compareAndSet(StateHelper.UNINITIALIZED, StateHelper.RECONNECT)) { + connectorLock.lock(); + try { + reconnectRequired.signal(); + } finally { + connectorLock.unlock(); + } + } + } + } + + protected static class TarantoolOp extends CompletableFuture { + + /** + * Tarantool binary protocol operation code. + */ + private final Code code; + + public TarantoolOp(Code code) { + this.code = code; + } + + public Code getCode() { + return code; + } + + } + } diff --git a/src/main/java/org/tarantool/TarantoolClusterClient.java b/src/main/java/org/tarantool/TarantoolClusterClient.java index 3a6c243a..7b71f10e 100644 --- a/src/main/java/org/tarantool/TarantoolClusterClient.java +++ b/src/main/java/org/tarantool/TarantoolClusterClient.java @@ -1,50 +1,95 @@ package org.tarantool; +import org.tarantool.cluster.TarantoolClusterDiscoverer; +import org.tarantool.cluster.TarantoolClusterStoredFunctionDiscoverer; +import org.tarantool.protocol.TarantoolPacket; +import org.tarantool.util.StringUtils; + +import java.io.IOException; +import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collection; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; - -import static org.tarantool.TarantoolClientImpl.StateHelper.CLOSED; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.StampedLock; /** * Basic implementation of a client that may work with the cluster * of tarantool instances in fault-tolerant way. - * + *

* Failed operations will be retried once connection is re-established * unless the configured expiration time is over. */ public class TarantoolClusterClient extends TarantoolClientImpl { - /* Need some execution context to retry writes. */ + + /** + * Need some execution context to retry writes. + */ private Executor executor; - /* Collection of operations to be retried. */ - private ConcurrentHashMap> retries = new ConcurrentHashMap>(); + /** + * Discovery activity. + */ + private ScheduledExecutorService instancesDiscoveryExecutor; + private Runnable instancesDiscovererTask; + private StampedLock discoveryLock = new StampedLock(); + + /** + * Collection of operations to be retried. + */ + private ConcurrentHashMap> retries = new ConcurrentHashMap<>(); /** - * @param config Configuration. - * @param addrs Array of addresses in the form of [host]:[port]. + * Constructs a new cluster client. + * + * @param config Configuration. + * @param addresses Array of addresses in the form of host[:port]. */ - public TarantoolClusterClient(TarantoolClusterClientConfig config, String... addrs) { - this(config, new RoundRobinSocketProviderImpl(addrs).setTimeout(config.operationExpiryTimeMillis)); + public TarantoolClusterClient(TarantoolClusterClientConfig config, String... addresses) { + this(config, makeClusterSocketProvider(addresses)); } /** + * Constructs a new cluster client. + * * @param provider Socket channel provider. - * @param config Configuration. + * @param config Configuration. */ public TarantoolClusterClient(TarantoolClusterClientConfig config, SocketChannelProvider provider) { super(provider, config); - this.executor = config.executor == null ? - Executors.newSingleThreadExecutor() : config.executor; + this.executor = config.executor == null + ? Executors.newSingleThreadExecutor() + : config.executor; + + if (StringUtils.isNotBlank(config.clusterDiscoveryEntryFunction)) { + this.instancesDiscovererTask = + createDiscoveryTask(new TarantoolClusterStoredFunctionDiscoverer(config, this)); + this.instancesDiscoveryExecutor + = Executors.newSingleThreadScheduledExecutor(new TarantoolThreadDaemonFactory("tarantoolDiscoverer")); + int delay = config.clusterDiscoveryDelayMillis > 0 + ? config.clusterDiscoveryDelayMillis + : TarantoolClusterClientConfig.DEFAULT_CLUSTER_DISCOVERY_DELAY_MILLIS; + + // todo: it's better to start a job later (out of ctor) + this.instancesDiscoveryExecutor.scheduleWithFixedDelay( + this.instancesDiscovererTask, + 0, + delay, + TimeUnit.MILLISECONDS + ); + } } @Override protected boolean isDead(CompletableFuture q) { - if ((state.getState() & CLOSED) != 0) { + if ((state.getState() & StateHelper.CLOSED) != 0) { q.completeExceptionally(new CommunicationException("Connection is dead", thumbstone)); return true; } @@ -59,23 +104,42 @@ protected boolean isDead(CompletableFuture q) { protected CompletableFuture doExec(Code code, Object[] args) { validateArgs(args); long sid = syncId.incrementAndGet(); - CompletableFuture q = makeFuture(sid, code, args); + ExpirableOp future = makeFuture(sid, code, args); + return registerOperation(future); + } - if (isDead(q)) { - return q; - } - futures.put(sid, q); - if (isDead(q)) { - futures.remove(sid); - return q; - } + /** + * Registers a new async operation which will be resolved later. + * Registration is discovery-aware in term of synchronization and + * it may be blocked util the discovery finishes its work. + * + * @param future operation to be performed + * + * @return registered operation + */ + private CompletableFuture registerOperation(ExpirableOp future) { + long stamp = discoveryLock.readLock(); try { - write(code, sid, null, args); - } catch (Exception e) { - futures.remove(sid); - fail(q, e); + if (isDead(future)) { + return future; + } + futures.put(future.getId(), future); + if (isDead(future)) { + futures.remove(future.getId()); + return future; + } + + try { + write(future.getCode(), future.getId(), null, future.getArgs()); + } catch (Exception e) { + futures.remove(future.getId()); + fail(future, e); + } + + return future; + } finally { + discoveryLock.unlock(stamp); } - return q; } @Override @@ -85,12 +149,12 @@ protected void fail(CompletableFuture q, Exception e) { protected boolean checkFail(CompletableFuture q, Exception e) { assert q instanceof ExpirableOp; - if (!isTransientError(e) || ((ExpirableOp)q).hasExpired(System.currentTimeMillis())) { + if (!isTransientError(e) || ((ExpirableOp) q).hasExpired(System.currentTimeMillis())) { q.completeExceptionally(e); return true; } else { assert retries != null; - retries.put(((ExpirableOp) q).getId(), (ExpirableOp)q); + retries.put(((ExpirableOp) q).getId(), (ExpirableOp) q); return false; } } @@ -99,6 +163,10 @@ protected boolean checkFail(CompletableFuture q, Exception e) { protected void close(Exception e) { super.close(e); + if (instancesDiscoveryExecutor != null) { + instancesDiscoveryExecutor.shutdownNow(); + } + if (retries == null) { // May happen within constructor. return; @@ -114,12 +182,12 @@ protected boolean isTransientError(Exception e) { return true; } if (e instanceof TarantoolException) { - return ((TarantoolException)e).isTransient(); + return ((TarantoolException) e).isTransient(); } return false; } - protected CompletableFuture makeFuture(long id, Code code, Object...args) { + private ExpirableOp makeFuture(long id, Code code, Object... args) { int expireTime = ((TarantoolClusterClientConfig) config).operationExpiryTimeMillis; return new ExpirableOp(id, expireTime, code, args); } @@ -133,58 +201,129 @@ protected void onReconnect() { // First call is before the constructor finished. Skip it. return; } - Collection> futsToRetry = new ArrayList>(retries.values()); + Collection> futuresToRetry = new ArrayList<>(retries.values()); retries.clear(); long now = System.currentTimeMillis(); - for (final ExpirableOp fut : futsToRetry) { - if (!fut.hasExpired(now)) { - executor.execute(new Runnable() { - @Override - public void run() { - futures.put(fut.getId(), fut); - try { - write(fut.getCode(), fut.getId(), null, fut.getArgs()); - } catch (Exception e) { - futures.remove(fut.getId()); - fail(fut, e); - } - } - }); + for (final ExpirableOp future : futuresToRetry) { + if (!future.hasExpired(now)) { + executor.execute(() -> registerOperation(future)); + } + } + } + + @Override + protected void complete(TarantoolPacket packet, TarantoolOp future) { + super.complete(packet, future); + RefreshableSocketProvider provider = getRefreshableSocketProvider(); + if (provider != null) { + renewConnectionIfRequired(provider.getAddresses()); + } + } + + protected void onInstancesRefreshed(Set instances) { + RefreshableSocketProvider provider = getRefreshableSocketProvider(); + if (provider != null) { + provider.refreshAddresses(instances); + renewConnectionIfRequired(provider.getAddresses()); + } + } + + private RefreshableSocketProvider getRefreshableSocketProvider() { + return socketProvider instanceof RefreshableSocketProvider + ? (RefreshableSocketProvider) socketProvider + : null; + } + + private void renewConnectionIfRequired(Collection addresses) { + if (pendingResponsesCount.get() > 0 || !isAlive()) { + return; + } + SocketAddress addressInUse = getCurrentAddressOrNull(); + if (!(addressInUse == null || addresses.contains(addressInUse))) { + long stamp = discoveryLock.tryWriteLock(); + if (!discoveryLock.validate(stamp)) { + return; + } + try { + if (pendingResponsesCount.get() == 0) { + stopIO(); + } + } finally { + discoveryLock.unlock(stamp); } } } + private SocketAddress getCurrentAddressOrNull() { + try { + return channel.getRemoteAddress(); + } catch (IOException ignored) { + return null; + } + } + + public void refreshInstances() { + if (instancesDiscovererTask != null) { + instancesDiscovererTask.run(); + } + } + + private static RoundRobinSocketProviderImpl makeClusterSocketProvider(String[] addresses) { + return new RoundRobinSocketProviderImpl(addresses); + } + + private Runnable createDiscoveryTask(TarantoolClusterDiscoverer serviceDiscoverer) { + return new Runnable() { + + private Set lastInstances; + + @Override + public synchronized void run() { + try { + Set freshInstances = serviceDiscoverer.getInstances(); + if (!(freshInstances.isEmpty() || Objects.equals(lastInstances, freshInstances))) { + lastInstances = freshInstances; + onInstancesRefreshed(lastInstances); + } + } catch (Exception ignored) { + // no-op + } + } + }; + } + /** * Holds operation code and arguments for retry. */ - private class ExpirableOp extends CompletableFuture { - /** Moment in time when operation is not considered for retry. */ - final private long deadline; + private class ExpirableOp extends TarantoolOp { /** - * A task identifier used in {@link TarantoolClientImpl#futures}. + * Moment in time when operation is not considered for retry. */ - final private long id; + private final long deadline; /** - * Tarantool binary protocol operation code. + * A task identifier used in {@link TarantoolClientImpl#futures}. */ - final private Code code; + private final long id; - /** Arguments of operation. */ - final private Object[] args; + /** + * Arguments of operation. + */ + private final Object[] args; /** + * Constructs a new Expirable operation. * - * @param id Sync. + * @param id Sync. * @param expireTime Expiration time (relative) in ms. - * @param code Tarantool operation code. - * @param args Operation arguments. + * @param code Tarantool operation code. + * @param args Operation arguments. */ - ExpirableOp(long id, int expireTime, Code code, Object...args) { + ExpirableOp(long id, int expireTime, Code code, Object... args) { + super(code); this.id = id; this.deadline = System.currentTimeMillis() + expireTime; - this.code = code; this.args = args; } @@ -196,12 +335,9 @@ public long getId() { return id; } - public Code getCode() { - return code; - } - public Object[] getArgs() { return args; } } + } diff --git a/src/main/java/org/tarantool/TarantoolClusterClientConfig.java b/src/main/java/org/tarantool/TarantoolClusterClientConfig.java index 423896b3..81f67cbb 100644 --- a/src/main/java/org/tarantool/TarantoolClusterClientConfig.java +++ b/src/main/java/org/tarantool/TarantoolClusterClientConfig.java @@ -6,9 +6,30 @@ * Configuration for the {@link TarantoolClusterClient}. */ public class TarantoolClusterClientConfig extends TarantoolClientConfig { - /* Amount of time (in milliseconds) the operation is eligible for retry. */ - public int operationExpiryTimeMillis = 500; - /* Executor service that will be used as a thread of execution to retry writes. */ - public Executor executor = null; + public static final int DEFAULT_OPERATION_EXPIRY_TIME_MILLIS = 500; + public static final int DEFAULT_CLUSTER_DISCOVERY_DELAY_MILLIS = 60_000; + + /** + * Period for the operation is eligible for retry. + */ + public int operationExpiryTimeMillis = DEFAULT_OPERATION_EXPIRY_TIME_MILLIS; + + /** + * Executor that will be used as a thread of + * execution to retry writes. + */ + public Executor executor; + + /** + * Gets a name of the stored function to be used + * to fetch list of instances. + */ + public String clusterDiscoveryEntryFunction; + + /** + * Scan period for refreshing a new list of instances. + */ + public int clusterDiscoveryDelayMillis = DEFAULT_CLUSTER_DISCOVERY_DELAY_MILLIS; + } diff --git a/src/main/java/org/tarantool/TarantoolConnection.java b/src/main/java/org/tarantool/TarantoolConnection.java index b51ba0ad..56e5abb7 100644 --- a/src/main/java/org/tarantool/TarantoolConnection.java +++ b/src/main/java/org/tarantool/TarantoolConnection.java @@ -12,12 +12,13 @@ import java.util.List; import java.util.Map; -public class TarantoolConnection extends TarantoolBase> implements TarantoolSQLOps>> { +public class TarantoolConnection extends TarantoolBase> + implements TarantoolSQLOps>> { + protected InputStream in; protected OutputStream out; protected Socket socket; - public TarantoolConnection(String username, String password, Socket socket) throws IOException { super(username, password, socket); this.socket = socket; @@ -65,11 +66,14 @@ public void rollback() { call("box.rollback"); } + /** + * Closes current connection. + */ public void close() { try { socket.close(); } catch (IOException ignored) { - + // No-op } } diff --git a/src/main/java/org/tarantool/TarantoolException.java b/src/main/java/org/tarantool/TarantoolException.java index 3778bccf..aed93b14 100644 --- a/src/main/java/org/tarantool/TarantoolException.java +++ b/src/main/java/org/tarantool/TarantoolException.java @@ -2,78 +2,69 @@ /** * A remote server error with error code and message. - * + * * @author dgreen * @version $Id: $ */ public class TarantoolException extends RuntimeException { - /* taken from src/box/errcode.h */ - public final static int ERR_READONLY = 7; - public final static int ERR_TIMEOUT = 78; - public final static int ERR_LOADING = 116; - public final static int ERR_LOCAL_INSTANCE_ID_IS_READ_ONLY = 128; + /* taken from src/box/errcode.h */ + public static final int ERR_READONLY = 7; + public static final int ERR_TIMEOUT = 78; + public static final int ERR_LOADING = 116; + public static final int ERR_LOCAL_INSTANCE_ID_IS_READ_ONLY = 128; - private static final long serialVersionUID = 1L; - long code; + private static final long serialVersionUID = 1L; + long code; - /** - *

- * Getter for the field code. - *

- * - * @return a int. - */ - public long getCode() { - return code; - } + /** + * Getter for the field code. + * + * @return error code + */ + public long getCode() { + return code; + } - /** - *

- * Constructor for TarantoolException. - *

- * - * @param code - * a int. - * @param message - * a {@link java.lang.String} object. - * @param cause - * a {@link java.lang.Throwable} object. - */ - public TarantoolException(long code, String message, Throwable cause) { - super(message, cause); - this.code = code; + /** + * Constructor for TarantoolException. + * + * @param code a int. + * @param message a {@link java.lang.String} object. + * @param cause a {@link java.lang.Throwable} object. + */ + public TarantoolException(long code, String message, Throwable cause) { + super(message, cause); + this.code = code; - } + } - /** - *

- * Constructor for TarantoolException. - *

- * - * @param code - * a int. - * @param message - * a {@link java.lang.String} object. - */ - public TarantoolException(long code, String message) { - super(message); - this.code = code; + /** + * Constructor for TarantoolException. + * + * @param code a int. + * @param message a {@link java.lang.String} object. + */ + public TarantoolException(long code, String message) { + super(message); + this.code = code; - } + } - /** - * - * @return {@code true} if retry can possibly help to overcome this error. - */ - boolean isTransient() { - switch ((int)code) { - case ERR_READONLY: - case ERR_TIMEOUT: - case ERR_LOADING: - case ERR_LOCAL_INSTANCE_ID_IS_READ_ONLY: - return true; - default: - return false; - } - } + /** + * Determines whether this error was caused under transient + * circumstances or not. + * + * @return {@code true} if retry can possibly help to overcome this error. + */ + boolean isTransient() { + switch ((int) code) { + case ERR_READONLY: + case ERR_TIMEOUT: + case ERR_LOADING: + case ERR_LOCAL_INSTANCE_ID_IS_READ_ONLY: + return true; + default: + return false; + } + } } diff --git a/src/main/java/org/tarantool/TarantoolThreadDaemonFactory.java b/src/main/java/org/tarantool/TarantoolThreadDaemonFactory.java new file mode 100644 index 00000000..f1b4dfb5 --- /dev/null +++ b/src/main/java/org/tarantool/TarantoolThreadDaemonFactory.java @@ -0,0 +1,23 @@ +package org.tarantool; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class TarantoolThreadDaemonFactory implements ThreadFactory { + + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public TarantoolThreadDaemonFactory(String namePrefix) { + this.namePrefix = namePrefix + "-" + POOL_NUMBER.incrementAndGet() + "-thread-"; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, namePrefix + threadNumber.incrementAndGet()); + thread.setDaemon(true); + + return thread; + } +} diff --git a/src/main/java/org/tarantool/cluster/IllegalDiscoveryFunctionResult.java b/src/main/java/org/tarantool/cluster/IllegalDiscoveryFunctionResult.java new file mode 100644 index 00000000..41f8dd82 --- /dev/null +++ b/src/main/java/org/tarantool/cluster/IllegalDiscoveryFunctionResult.java @@ -0,0 +1,13 @@ +package org.tarantool.cluster; + +/** + * Raised when {@link TarantoolClusterStoredFunctionDiscoverer} validates + * a function result as unsupported. + */ +public class IllegalDiscoveryFunctionResult extends RuntimeException { + + public IllegalDiscoveryFunctionResult(String message) { + super(message); + } + +} diff --git a/src/main/java/org/tarantool/cluster/TarantoolClusterDiscoverer.java b/src/main/java/org/tarantool/cluster/TarantoolClusterDiscoverer.java new file mode 100644 index 00000000..26e5da3c --- /dev/null +++ b/src/main/java/org/tarantool/cluster/TarantoolClusterDiscoverer.java @@ -0,0 +1,21 @@ +package org.tarantool.cluster; + +import java.util.Set; + +/** + * Discovery strategy to obtain a list of the cluster nodes. + * This one can be used by {@link org.tarantool.RefreshableSocketProvider} + * to provide support for fault tolerance property. + * + * @see org.tarantool.RefreshableSocketProvider + */ +public interface TarantoolClusterDiscoverer { + + /** + * Gets nodes addresses in host[:port] format. + * + * @return list of the cluster nodes + */ + Set getInstances(); + +} diff --git a/src/main/java/org/tarantool/cluster/TarantoolClusterStoredFunctionDiscoverer.java b/src/main/java/org/tarantool/cluster/TarantoolClusterStoredFunctionDiscoverer.java new file mode 100644 index 00000000..c25b578d --- /dev/null +++ b/src/main/java/org/tarantool/cluster/TarantoolClusterStoredFunctionDiscoverer.java @@ -0,0 +1,63 @@ +package org.tarantool.cluster; + +import org.tarantool.TarantoolClient; +import org.tarantool.TarantoolClientOps; +import org.tarantool.TarantoolClusterClientConfig; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A cluster nodes discoverer based on calling a predefined function + * which returns list of nodes. + * + * The function has to have no arguments and return list of + * the strings which follow host[:port] format + */ +public class TarantoolClusterStoredFunctionDiscoverer implements TarantoolClusterDiscoverer { + + private TarantoolClient client; + private String entryFunction; + + public TarantoolClusterStoredFunctionDiscoverer(TarantoolClusterClientConfig clientConfig, TarantoolClient client) { + this.client = client; + this.entryFunction = clientConfig.clusterDiscoveryEntryFunction; + } + + @Override + public Set getInstances() { + TarantoolClientOps, Object, List> syncOperations = client.syncOps(); + + List list = syncOperations.call(entryFunction); + // discoverer expects a single array result from the function now; + // in order to protect this contract the discoverer does a strict + // validation against the data returned; + // this strict-mode allows us to extend the contract in a non-breaking + // way for old clients just reserve an extra return value in + // terms of LUA multi-result support. + checkResult(list); + + List funcResult = (List) list.get(0); + return funcResult.stream() + .map(Object::toString) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Check whether the result follows the contract or not. + * The contract is a mandatory single array of strings + * + * @param result result to be validated + */ + private void checkResult(List result) { + if (result == null || result.isEmpty()) { + throw new IllegalDiscoveryFunctionResult("Discovery function returned no data"); + } + if (!((List)result.get(0)).stream().allMatch(item -> item instanceof String)) { + throw new IllegalDiscoveryFunctionResult("The first value must be an array of strings"); + } + } + +} diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index f6334abe..a8bcd3fb 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -1,8 +1,15 @@ package org.tarantool.jdbc; +import static org.tarantool.jdbc.SQLDriver.PROP_HOST; +import static org.tarantool.jdbc.SQLDriver.PROP_PASSWORD; +import static org.tarantool.jdbc.SQLDriver.PROP_PORT; +import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; +import static org.tarantool.jdbc.SQLDriver.PROP_USER; + import org.tarantool.CommunicationException; import org.tarantool.JDBCBridge; import org.tarantool.TarantoolConnection; +import org.tarantool.util.JdbcConstants; import org.tarantool.util.SQLStates; import java.io.IOException; @@ -12,6 +19,7 @@ import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; +import java.sql.ClientInfoStatus; import java.sql.Clob; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -29,21 +37,23 @@ import java.sql.Statement; import java.sql.Struct; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; -import static org.tarantool.jdbc.SQLDriver.PROP_HOST; -import static org.tarantool.jdbc.SQLDriver.PROP_PASSWORD; -import static org.tarantool.jdbc.SQLDriver.PROP_PORT; -import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; -import static org.tarantool.jdbc.SQLDriver.PROP_USER; - -@SuppressWarnings("Since15") +/** + * Tarantool {@link Connection} implementation. + *

+ * Supports creating {@link Statement} and {@link PreparedStatement} instances + */ public class SQLConnection implements Connection { private static final int UNSET_HOLDABILITY = 0; + private static final String PING_QUERY = "SELECT 1"; private final TarantoolConnection connection; @@ -72,8 +82,9 @@ public class SQLConnection implements Connection { // No-op. } } - if (e instanceof SQLException) + if (e instanceof SQLException) { throw (SQLException) e; + } throw new SQLException("Couldn't initiate connection using " + SQLDriver.diagProperties(properties), e); } } @@ -91,6 +102,7 @@ public class SQLConnection implements Connection { * to honor this timeout for the following read/write operations as well. * * @return Connected socket. + * * @throws SQLException if failed. */ protected Socket getConnectedSocket() throws SQLException { @@ -140,13 +152,17 @@ protected Socket makeSocket() { * @param user User name. * @param pass Password. * @param socket Connected socket. + * * @return Native tarantool connection. + * * @throws IOException if failed. */ protected TarantoolConnection makeConnection(String user, String pass, Socket socket) throws IOException { - return new TarantoolConnection(user, pass, socket) {{ - msgPackLite = SQLMsgPackLite.INSTANCE; - }}; + return new TarantoolConnection(user, pass, socket) { + { + msgPackLite = SQLMsgPackLite.INSTANCE; + } + }; } @Override @@ -154,42 +170,137 @@ public Statement createStatement() throws SQLException { return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); } + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return createStatement(resultSetType, resultSetConcurrency, getHoldability()); + } + + @Override + public Statement createStatement(int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkNotClosed(); + checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); + return new SQLStatement(this, resultSetType, resultSetConcurrency, resultSetHoldability); + } + @Override public PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); } + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return prepareStatement(sql, resultSetType, resultSetConcurrency, getHoldability()); + } + + @Override + public PreparedStatement prepareStatement(String sql, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkNotClosed(); + checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); + return new SQLPreparedStatement(this, sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new SQLFeatureNotSupportedException(); + } + return prepareStatement(sql); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); + } + @Override public CallableStatement prepareCall(String sql) throws SQLException { + return prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return prepareCall(sql, resultSetType, resultSetConcurrency, getHoldability()); + } + + @Override + public CallableStatement prepareCall(String sql, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) + throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public String nativeSQL(String sql) throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public void setAutoCommit(boolean autoCommit) throws SQLException { - if (autoCommit == false) { + checkNotClosed(); + if (!autoCommit) { throw new SQLFeatureNotSupportedException(); } } @Override public boolean getAutoCommit() throws SQLException { + checkNotClosed(); return true; } @Override public void commit() throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot commit when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } throw new SQLFeatureNotSupportedException(); } @Override public void rollback() throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot rollback when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } throw new SQLFeatureNotSupportedException(); + } + @Override + public void rollback(Savepoint savepoint) throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot roll back to a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + throw new SQLFeatureNotSupportedException(); } @Override @@ -213,25 +324,30 @@ public DatabaseMetaData getMetaData() throws SQLException { @Override public void setReadOnly(boolean readOnly) throws SQLException { - + checkNotClosed(); + throw new SQLFeatureNotSupportedException(); } @Override public boolean isReadOnly() throws SQLException { + checkNotClosed(); return false; } @Override public void setCatalog(String catalog) throws SQLException { + checkNotClosed(); } @Override public String getCatalog() throws SQLException { + checkNotClosed(); return null; } @Override public void setTransactionIsolation(int level) throws SQLException { + checkNotClosed(); if (level != Connection.TRANSACTION_NONE) { throw new SQLFeatureNotSupportedException(); } @@ -239,42 +355,30 @@ public void setTransactionIsolation(int level) throws SQLException { @Override public int getTransactionIsolation() throws SQLException { + checkNotClosed(); return Connection.TRANSACTION_NONE; } @Override public SQLWarning getWarnings() throws SQLException { + checkNotClosed(); return null; } @Override public void clearWarnings() throws SQLException { - - } - - @Override - public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { - return createStatement(resultSetType, resultSetConcurrency, getHoldability()); - } - - @Override - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) - throws SQLException { - return prepareStatement(sql, resultSetType, resultSetConcurrency, getHoldability()); - } - - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); } @Override public Map> getTypeMap() throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public void setTypeMap(Map> map) throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @@ -296,130 +400,189 @@ public int getHoldability() throws SQLException { @Override public Savepoint setSavepoint() throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot set a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } throw new SQLFeatureNotSupportedException(); } @Override public Savepoint setSavepoint(String name) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void rollback(Savepoint savepoint) throws SQLException { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot set a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } throw new SQLFeatureNotSupportedException(); } @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override - public Statement createStatement(int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) throws SQLException { + public Clob createClob() throws SQLException { checkNotClosed(); - checkHoldabilitySupport(resultSetHoldability); - return new SQLStatement(this, resultSetType, resultSetConcurrency, resultSetHoldability); + throw new SQLFeatureNotSupportedException(); } @Override - public PreparedStatement prepareStatement(String sql, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) throws SQLException { + public Blob createBlob() throws SQLException { checkNotClosed(); - checkHoldabilitySupport(resultSetHoldability); - return new SQLPreparedStatement(this, sql, resultSetType, resultSetConcurrency, resultSetHoldability); - } - - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) - throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + public NClob createNClob() throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override - public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + public SQLXML createSQLXML() throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } + /** + * {@inheritDoc} + * + * @param timeout temporally ignored param + * + * @return connection activity status + */ @Override - public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public boolean isValid(int timeout) throws SQLException { + if (timeout < 0) { + throw new SQLNonTransientException( + "Timeout cannot be negative", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + if (isClosed()) { + return false; + } + return checkConnection(timeout); } - @Override - public Clob createClob() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } + private boolean checkConnection(int timeout) { + ResultSet resultSet = null; + try (Statement pingStatement = createStatement()) { + // todo: before use timeout we need to provide query timeout per statement - @Override - public Blob createBlob() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } + resultSet = pingStatement.executeQuery(PING_QUERY); + boolean isValid = resultSet.next() && resultSet.getInt(1) == 1; + resultSet.close(); - @Override - public NClob createNClob() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return isValid; + } catch (SQLException e) { + return false; + } } @Override - public SQLXML createSQLXML() throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void setClientInfo(String name, String value) throws SQLClientInfoException { + try { + checkNotClosed(); + } catch (SQLException cause) { + throwUnknownReasonClientProperties("Connection is closed", Collections.singleton(name), cause); + } + throwUnknownClientProperties(Collections.singleton(name)); } @Override - public boolean isValid(int timeout) throws SQLException { - return true; + public void setClientInfo(Properties properties) throws SQLClientInfoException { + try { + checkNotClosed(); + } catch (SQLException cause) { + throwUnknownReasonClientProperties("Connection is closed", properties.keySet(), cause); + } + throwUnknownClientProperties(properties.keySet()); } - @Override - public void setClientInfo(String name, String value) throws SQLClientInfoException { - throw new SQLClientInfoException(); + /** + * Throws an exception caused by {@code cause} and marks all properties + * as {@link ClientInfoStatus#REASON_UNKNOWN}. + * + * @param reason reason mesage + * @param properties client properties + * @param cause original cause + * + * @throws SQLClientInfoException wrapped exception + */ + private void throwUnknownReasonClientProperties(String reason, + Collection properties, + SQLException cause) throws SQLClientInfoException { + Map failedProperties = new HashMap<>(); + properties.forEach(property -> { + failedProperties.put(property.toString(), ClientInfoStatus.REASON_UNKNOWN); + }); + throw new SQLClientInfoException(reason, cause.getSQLState(), failedProperties, cause); } - @Override - public void setClientInfo(Properties properties) throws SQLClientInfoException { - throw new SQLClientInfoException(); + /** + * Throws exception for unrecognizable properties. + * + * @param properties unknown property names. + * + * @throws SQLClientInfoException wrapped exception + */ + private void throwUnknownClientProperties(Collection properties) throws SQLClientInfoException { + Map failedProperties = new HashMap<>(); + properties.forEach(property -> { + failedProperties.put(property.toString(), ClientInfoStatus.REASON_UNKNOWN_PROPERTY); + }); + throw new SQLClientInfoException(failedProperties); } @Override public String getClientInfo(String name) throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public Properties getClientInfo() throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + checkNotClosed(); throw new SQLFeatureNotSupportedException(); } @Override public void setSchema(String schema) throws SQLException { + checkNotClosed(); } @Override public String getSchema() throws SQLException { + checkNotClosed(); return null; } @Override public void abort(Executor executor) throws SQLException { + if (isClosed()) { + return; + } throw new SQLFeatureNotSupportedException(); } @@ -427,8 +590,9 @@ public void abort(Executor executor) throws SQLException { public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { checkNotClosed(); - if (milliseconds < 0) + if (milliseconds < 0) { throw new SQLException("Network timeout cannot be negative."); + } try { connection.setSocketTimeout(milliseconds); @@ -491,7 +655,7 @@ protected int executeUpdate(String sql, Object... args) throws SQLException { } protected List nativeSelect(Integer space, Integer index, List key, int offset, int limit, int iterator) - throws SQLException { + throws SQLException { checkNotClosed(); try { return connection.select(space, index, key, offset, limit, iterator); @@ -506,14 +670,17 @@ protected String getServerVersion() { } /** + * Checks connection close status. + * * @throws SQLException If connection is closed. */ protected void checkNotClosed() throws SQLException { - if (isClosed()) + if (isClosed()) { throw new SQLNonTransientConnectionException( - "Connection is closed.", - SQLStates.CONNECTION_DOES_NOT_EXIST.getSqlState() + "Connection is closed.", + SQLStates.CONNECTION_DOES_NOT_EXIST.getSqlState() ); + } } String getUrl() { @@ -531,7 +698,7 @@ Properties getProperties() { */ private void handleException(Exception e) { if (CommunicationException.class.isAssignableFrom(e.getClass()) || - IOException.class.isAssignableFrom(e.getClass())) { + IOException.class.isAssignableFrom(e.getClass())) { try { close(); } catch (SQLException ignored) { @@ -541,17 +708,70 @@ private void handleException(Exception e) { } /** - * Checks whether holdability is supported + * Checks all params required to make statements. + * + * @param resultSetType scroll type + * @param resultSetConcurrency concurrency level + * @param resultSetHoldability holdability type + * + * @throws SQLFeatureNotSupportedException if any param is not supported + * @throws SQLNonTransientException if any param has an invalid value + */ + private void checkStatementParams(int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkResultSetType(resultSetType); + checkResultSetConcurrency(resultSetType, resultSetConcurrency); + checkHoldabilitySupport(resultSetHoldability); + } + + /** + * Checks whether resultSetType is supported. + * + * @param resultSetType param to be checked * - * @param holdability param to be checked * @throws SQLFeatureNotSupportedException param is not supported - * @throws SQLNonTransientException param has invalid value + * @throws SQLNonTransientException param has invalid value */ - private void checkHoldabilitySupport(int holdability) throws SQLException { - if (holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT - && holdability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { + private void checkResultSetType(int resultSetType) throws SQLException { + if (resultSetType != ResultSet.TYPE_FORWARD_ONLY && + resultSetType != ResultSet.TYPE_SCROLL_INSENSITIVE && + resultSetType != ResultSet.TYPE_SCROLL_SENSITIVE) { throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); } + if (!getMetaData().supportsResultSetType(resultSetType)) { + throw new SQLFeatureNotSupportedException(); + } + } + + /** + * Checks whether resultSetType is supported. + * + * @param resultSetConcurrency param to be checked + * + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkResultSetConcurrency(int resultSetType, int resultSetConcurrency) throws SQLException { + if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY && + resultSetConcurrency != ResultSet.CONCUR_UPDATABLE) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + if (!getMetaData().supportsResultSetConcurrency(resultSetType, resultSetConcurrency)) { + throw new SQLFeatureNotSupportedException(); + } + } + + /** + * Checks whether holdability is supported. + * + * @param holdability param to be checked + * + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkHoldabilitySupport(int holdability) throws SQLException { + JdbcConstants.checkHoldabilityConstant(holdability); if (!getMetaData().supportsResultSetHoldability(holdability)) { throw new SQLFeatureNotSupportedException(); } @@ -562,9 +782,11 @@ private void checkHoldabilitySupport(int holdability) throws SQLException { * * @param sql SQL Text. * @param params Parameters of the SQL statement. + * * @return Formatted error message. */ private static String formatError(String sql, Object... params) { return "Failed to execute SQL: " + sql + ", params: " + Arrays.deepToString(params); } + } diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index d8f3c2bf..1605fd86 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -8,7 +8,7 @@ import java.sql.ResultSet; import java.sql.RowIdLifetime; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLNonTransientException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; @@ -36,17 +36,17 @@ public SQLNullResultSet(JDBCBridge bridge, SQLStatement ownerStatement) throws S } @Override - protected Object getRaw(int columnIndex) { - return columnIndex > getCurrentRow().size() ? null : getCurrentRow().get(columnIndex - 1); + protected Object getRaw(int columnIndex) throws SQLException { + List row = getCurrentRow(); + return columnIndex > row.size() ? null : row.get(columnIndex - 1); } @Override - protected Integer getColumnIndex(String columnLabel) { - Integer idx = super.getColumnIndex(columnLabel); - return idx == null ? Integer.MAX_VALUE : idx; + protected int findColumnIndex(String columnLabel) throws SQLException { + int index = super.findColumnIndex(columnLabel); + return index == 0 ? Integer.MAX_VALUE : index; } - } public SQLDatabaseMetadata(SQLConnection connection) { @@ -80,22 +80,22 @@ public boolean isReadOnly() throws SQLException { @Override public boolean nullsAreSortedHigh() throws SQLException { - return true; + return false; } @Override public boolean nullsAreSortedLow() throws SQLException { - return !nullsAreSortedHigh(); + return true; } @Override public boolean nullsAreSortedAtStart() throws SQLException { - return true; + return false; } @Override public boolean nullsAreSortedAtEnd() throws SQLException { - return !nullsAreSortedAtStart(); + return false; } @Override @@ -645,13 +645,16 @@ public boolean dataDefinitionIgnoredInTransactions() throws SQLException { @Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) - throws SQLException { + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @Override - public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) - throws SQLException { + public ResultSet getProcedureColumns(String catalog, + String schemaPattern, + String procedureNamePattern, + String columnNamePattern) + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @@ -678,8 +681,10 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam connection.checkNotClosed(); return asMetadataResultSet(JDBCBridge.EMPTY); } - String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); - List> spaces = (List>) connection.nativeSelect(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + String[] parts = tableNamePattern == null ? new String[] { "" } : tableNamePattern.split("%"); + List> spaces = (List>) connection.nativeSelect( + _VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0 + ); List> rows = new ArrayList>(); for (List space : spaces) { String tableName = (String) space.get(NAME_IDX); @@ -693,18 +698,19 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam } } List columnNames = Arrays.asList( - "TABLE_NAME", "TABLE_TYPE", - //nulls - "REMARKS", "TABLE_CAT", - "TABLE_SCHEM", "TABLE_TYPE", - "TYPE_CAT", "TYPE_SCHEM", - "TYPE_NAME", "SELF_REFERENCING_COL_NAME", - "REF_GENERATION" + "TABLE_NAME", "TABLE_TYPE", + //nulls + "REMARKS", "TABLE_CAT", + "TABLE_SCHEM", "TABLE_TYPE", + "TYPE_CAT", "TYPE_SCHEM", + "TYPE_NAME", "SELF_REFERENCING_COL_NAME", + "REF_GENERATION" ); return sqlNullResultSet(columnNames, rows); } catch (Exception e) { - throw new SQLException("Failed to retrieve table(s) description: " + - "tableNamePattern=\"" + tableNamePattern + "\".", e); + throw new SQLException( + "Failed to retrieve table(s) description: " + + "tableNamePattern=\"" + tableNamePattern + "\".", e); } } @@ -713,6 +719,11 @@ public ResultSet getSchemas() throws SQLException { return rowOfNullsResultSet(); } + @Override + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return rowOfNullsResultSet(); + } + @Override public ResultSet getCatalogs() throws SQLException { return rowOfNullsResultSet(); @@ -720,17 +731,19 @@ public ResultSet getCatalogs() throws SQLException { @Override public ResultSet getTableTypes() throws SQLException { - return asMetadataResultSet(JDBCBridge.mock(Arrays.asList("TABLE_TYPE"), Arrays.asList(Arrays.asList("TABLE")))); + return asMetadataResultSet(JDBCBridge.mock(Arrays.asList("TABLE_TYPE"), Arrays.asList(Arrays.asList("TABLE")))); } @Override public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) - throws SQLException { + throws SQLException { try { - String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); - String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); - List> spaces = (List>) connection.nativeSelect(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); - List> rows = new ArrayList>(); + String[] tableParts = tableNamePattern == null ? new String[] { "" } : tableNamePattern.split("%"); + String[] colParts = columnNamePattern == null ? new String[] { "" } : columnNamePattern.split("%"); + List> spaces = (List>) connection.nativeSelect( + _VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0 + ); + List> rows = new ArrayList<>(); for (List space : spaces) { String tableName = (String) space.get(NAME_IDX); List> format = (List>) space.get(FORMAT_IDX); @@ -744,53 +757,60 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa String columnName = (String) f.get("name"); String dbType = (String) f.get("type"); if (like(columnName, colParts)) { - rows.add(Arrays.asList(tableName, columnName, columnIdx, Types.OTHER, dbType, 10, 1, "YES", Types.OTHER, "NO", "NO")); + rows.add(Arrays.asList( + tableName, columnName, + columnIdx, Types.OTHER, + dbType, 10, 1, + "YES", Types.OTHER, + "NO", "NO") + ); } } } } List columnNames = Arrays.asList( - "TABLE_NAME", "COLUMN_NAME", - "ORDINAL_POSITION", "DATA_TYPE", - "TYPE_NAME", "NUM_PREC_RADIX", - "NULLABLE", "IS_NULLABLE", - "SOURCE_DATA_TYPE", "IS_AUTOINCREMENT", - "IS_GENERATEDCOLUMN", - //nulls - "TABLE_CAT", "TABLE_SCHEM", - "COLUMN_SIZE", "BUFFER_LENGTH", - "DECIMAL_DIGITS", "REMARKS", - "COLUMN_DEF", "SQL_DATA_TYPE", - "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", - "SCOPE_CATALOG", "SCOPE_SCHEMA", - "SCOPE_TABLE" + "TABLE_NAME", "COLUMN_NAME", + "ORDINAL_POSITION", "DATA_TYPE", + "TYPE_NAME", "NUM_PREC_RADIX", + "NULLABLE", "IS_NULLABLE", + "SOURCE_DATA_TYPE", "IS_AUTOINCREMENT", + "IS_GENERATEDCOLUMN", + //nulls + "TABLE_CAT", "TABLE_SCHEM", + "COLUMN_SIZE", "BUFFER_LENGTH", + "DECIMAL_DIGITS", "REMARKS", + "COLUMN_DEF", "SQL_DATA_TYPE", + "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", + "SCOPE_CATALOG", "SCOPE_SCHEMA", + "SCOPE_TABLE" ); return sqlNullResultSet( - columnNames, - rows); + columnNames, + rows); } catch (Exception e) { - throw new SQLException("Error processing table column metadata: " + - "tableNamePattern=\"" + tableNamePattern + "\"; " + - "columnNamePattern=\"" + columnNamePattern + "\".", e); + throw new SQLException( + "Error processing table column metadata: " + + "tableNamePattern=\"" + tableNamePattern + "\"; " + + "columnNamePattern=\"" + columnNamePattern + "\".", e); } } @Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) - throws SQLException { + throws SQLException { return rowOfNullsResultSet(); } @Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) - throws SQLException { + throws SQLException { return rowOfNullsResultSet(); } @Override public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) - throws SQLException { + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @@ -802,7 +822,8 @@ public ResultSet getVersionColumns(String catalog, String schema, String table) @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { final List colNames = Arrays.asList( - "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"); + "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME" + ); if (table == null || table.isEmpty()) { connection.checkNotClosed(); @@ -812,8 +833,9 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr try { List spaces = connection.nativeSelect(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); - if (spaces == null || spaces.size() == 0) + if (spaces == null || spaces.size() == 0) { return emptyResultSet(colNames); + } List space = ensureType(List.class, spaces.get(0)); List fields = ensureType(List.class, space.get(FORMAT_IDX)); @@ -822,13 +844,14 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr List primaryKey = ensureType(List.class, indexes.get(0)); List parts = ensureType(List.class, primaryKey.get(INDEX_FORMAT_IDX)); - List> rows = new ArrayList>(); + List> rows = new ArrayList<>(); for (int i = 0; i < parts.size(); i++) { // For native spaces, the 'parts' is 'List of Lists'. // We only accept SQL spaces, for which the parts is 'List of Maps'. Map part = checkType(Map.class, parts.get(i)); - if (part == null) + if (part == null) { return emptyResultSet(colNames); + } int ordinal = ensureType(Number.class, part.get("field")).intValue(); Map field = ensureType(Map.class, fields.get(ordinal)); @@ -862,8 +885,13 @@ public ResultSet getExportedKeys(String catalog, String schema, String table) th } @Override - public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) - throws SQLException { + public ResultSet getCrossReference(String parentCatalog, + String parentSchema, + String parentTable, + String foreignCatalog, + String foreignSchema, + String foreignTable) + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @@ -874,18 +902,19 @@ public ResultSet getTypeInfo() throws SQLException { @Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) - throws SQLException { + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @Override public boolean supportsResultSetType(int type) throws SQLException { - return false; + return type == ResultSet.TYPE_FORWARD_ONLY || + type == ResultSet.TYPE_SCROLL_INSENSITIVE; } @Override public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { - return false; + return supportsResultSetType(type) && concurrency == ResultSet.CONCUR_READ_ONLY; } @Override @@ -940,7 +969,7 @@ public boolean supportsBatchUpdates() throws SQLException { @Override public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) - throws SQLException { + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @@ -980,14 +1009,17 @@ public ResultSet getSuperTables(String catalog, String schemaPattern, String tab } @Override - public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) - throws SQLException { + public ResultSet getAttributes(String catalog, + String schemaPattern, + String typeNamePattern, + String attributeNamePattern) + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } /** * {@inheritDoc} - * + *

* Support of {@link ResultSet#CLOSE_CURSORS_AT_COMMIT} is not * available now because it requires cursor transaction support. */ @@ -1041,11 +1073,6 @@ public RowIdLifetime getRowIdLifetime() throws SQLException { return RowIdLifetime.ROWID_UNSUPPORTED; } - @Override - public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { - return rowOfNullsResultSet(); - } - @Override public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { return false; @@ -1063,19 +1090,25 @@ public ResultSet getClientInfoProperties() throws SQLException { @Override public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) - throws SQLException { + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @Override - public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) - throws SQLException { + public ResultSet getFunctionColumns(String catalog, + String schemaPattern, + String functionNamePattern, + String columnNamePattern) + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @Override - public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) - throws SQLException { + public ResultSet getPseudoColumns(String catalog, + String schemaPattern, + String tableNamePattern, + String columnNamePattern) + throws SQLException { return asMetadataResultSet(JDBCBridge.EMPTY); } @@ -1085,13 +1118,16 @@ public boolean generatedKeyAlwaysReturned() throws SQLException { } @Override - public T unwrap(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("SQLDatabaseMetadata does not wrap " + type.getName()); } @Override - public boolean isWrapperFor(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); } private ResultSet asMetadataResultSet(JDBCBridge jdbcBridge) throws SQLException { @@ -1105,7 +1141,7 @@ private SQLStatement createMetadataStatement() throws SQLException { private static T ensureType(Class cls, Object v) throws Exception { if (v == null || !cls.isAssignableFrom(v.getClass())) { throw new Exception(String.format("Wrong value type '%s', expected '%s'.", - v == null ? "null" : v.getClass().getName(), cls.getName())); + v == null ? "null" : v.getClass().getName(), cls.getName())); } return cls.cast(v); } diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java index 6ca97866..c73ae96e 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDriver.java +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -12,13 +12,12 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; -@SuppressWarnings("Since15") public class SQLDriver implements Driver { static { try { java.sql.DriverManager.registerDriver(new SQLDriver()); - } catch (SQLException E) { + } catch (SQLException e) { throw new RuntimeException("Can't register driver!"); } } @@ -31,11 +30,13 @@ public class SQLDriver implements Driver { public static final String PROP_SOCKET_TIMEOUT = "socketTimeout"; // Define default values once here. - final static Properties defaults = new Properties() {{ - setProperty(PROP_HOST, "localhost"); - setProperty(PROP_PORT, "3301"); - setProperty(PROP_SOCKET_TIMEOUT, "0"); - }}; + static final Properties defaults = new Properties() { + { + setProperty(PROP_HOST, "localhost"); + setProperty(PROP_PORT, "3301"); + setProperty(PROP_SOCKET_TIMEOUT, "0"); + } + }; private final Map providerCache = new ConcurrentHashMap(); @@ -45,8 +46,9 @@ public Connection connect(String url, Properties info) throws SQLException { final Properties urlProperties = parseQueryString(uri, info); String providerClassName = urlProperties.getProperty(PROP_SOCKET_PROVIDER); - if (providerClassName == null) + if (providerClassName == null) { return new SQLConnection(url, urlProperties); + } final SQLSocketProvider provider = getSocketProviderInstance(providerClassName); @@ -54,8 +56,9 @@ public Connection connect(String url, Properties info) throws SQLException { @Override protected Socket getConnectedSocket() throws SQLException { Socket socket = provider.getConnectedSocket(uri, urlProperties); - if (socket == null) + if (socket == null) { throw new SQLException("The socket provider returned null socket"); + } return socket; } }; @@ -94,8 +97,9 @@ protected Properties parseQueryString(URI uri, Properties info) throws SQLExcept // We need to convert port to string otherwise getProperty() will not see it. urlProperties.setProperty(PROP_PORT, String.valueOf(uri.getPort())); } - if (info != null) + if (info != null) { urlProperties.putAll(info); + } // Validate properties. int port; @@ -128,7 +132,7 @@ protected SQLSocketProvider getSocketProviderInstance(String className) throws S try { Class cls = Class.forName(className); if (SQLSocketProvider.class.isAssignableFrom(cls)) { - provider = (SQLSocketProvider)cls.newInstance(); + provider = (SQLSocketProvider) cls.newInstance(); providerCache.put(className, provider); } } catch (Exception e) { @@ -172,19 +176,19 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws password.description = "password"; DriverPropertyInfo socketProvider = new DriverPropertyInfo( - PROP_SOCKET_PROVIDER, properties.getProperty(PROP_SOCKET_PROVIDER)); + PROP_SOCKET_PROVIDER, properties.getProperty(PROP_SOCKET_PROVIDER)); socketProvider.required = false; socketProvider.description = "SocketProvider class implements org.tarantool.jdbc.SQLSocketProvider"; DriverPropertyInfo socketTimeout = new DriverPropertyInfo( - PROP_SOCKET_TIMEOUT, properties.getProperty(PROP_SOCKET_TIMEOUT)); + PROP_SOCKET_TIMEOUT, properties.getProperty(PROP_SOCKET_TIMEOUT)); socketTimeout.required = false; socketTimeout.description = "The number of milliseconds to wait before a timeout is occurred on a socket" + - " connect or read. The default value is 0, which means infinite timeout."; + " connect or read. The default value is 0, which means infinite timeout."; - return new DriverPropertyInfo[]{host, port, user, password, socketProvider, socketTimeout}; + return new DriverPropertyInfo[] { host, port, user, password, socketProvider, socketTimeout }; } catch (Exception e) { throw new SQLException(e); } @@ -215,17 +219,19 @@ public Logger getParentLogger() throws SQLFeatureNotSupportedException { * along with their sanitized values. * * @param props Connection properties. + * * @return Comma-separated pairs of property names and values. */ protected static String diagProperties(Properties props) { StringBuilder sb = new StringBuilder(); for (Map.Entry e : props.entrySet()) { - if (sb.length() > 0) + if (sb.length() > 0) { sb.append(", "); + } sb.append(e.getKey()); sb.append('='); - sb.append(PROP_USER.equals(e.getKey()) || PROP_PASSWORD.equals(e.getKey()) ? - "*****" : e.getValue().toString()); + sb.append((PROP_USER.equals(e.getKey()) || PROP_PASSWORD.equals(e.getKey())) + ? "*****" : e.getValue().toString()); } return sb.toString(); } diff --git a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java index 15068774..36fe82d8 100644 --- a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java +++ b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java @@ -1,13 +1,13 @@ package org.tarantool.jdbc; +import org.tarantool.MsgPackLite; + import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; +import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; -import java.sql.Date; - -import org.tarantool.MsgPackLite; public class SQLMsgPackLite extends MsgPackLite { @@ -15,14 +15,14 @@ public class SQLMsgPackLite extends MsgPackLite { @Override public void pack(Object item, OutputStream os) throws IOException { - if(item instanceof Date) { - super.pack(((Date)item).getTime(), os); - } else if(item instanceof Time) { - super.pack(((Time)item).getTime(), os); - } else if(item instanceof Timestamp) { - super.pack(((Timestamp)item).getTime(), os); - } else if(item instanceof BigDecimal) { - super.pack(((BigDecimal)item).toPlainString(), os); + if (item instanceof Date) { + super.pack(((Date) item).getTime(), os); + } else if (item instanceof Time) { + super.pack(((Time) item).getTime(), os); + } else if (item instanceof Timestamp) { + super.pack(((Timestamp) item).getTime(), os); + } else if (item instanceof BigDecimal) { + super.pack(((BigDecimal) item).toPlainString(), os); } else { super.pack(item, os); } diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index f0748c06..760aa569 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -26,7 +26,7 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatement { - final static String INVALID_CALL_MSG = "The method cannot be called on a PreparedStatement."; + static final String INVALID_CALL_MSG = "The method cannot be called on a PreparedStatement."; final String sql; final Map params; @@ -54,6 +54,11 @@ public ResultSet executeQuery() throws SQLException { return createResultSet(connection.executeQuery(sql, getParams())); } + @Override + public ResultSet executeQuery(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + protected Object[] getParams() throws SQLException { Object[] objects = new Object[params.size()]; for (int i = 1; i <= params.size(); i++) { @@ -73,11 +78,21 @@ public int executeUpdate() throws SQLException { return connection.executeUpdate(sql, getParams()); } + @Override + public int executeUpdate(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { setParameter(parameterIndex, null); } + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + setParameter(parameterIndex, null); + } + @Override public void setBoolean(int parameterIndex, boolean parameterValue) throws SQLException { setParameter(parameterIndex, parameterValue); @@ -88,6 +103,11 @@ public void setByte(int parameterIndex, byte parameterValue) throws SQLException setParameter(parameterIndex, parameterValue); } + @Override + public void setBytes(int parameterIndex, byte[] parameterValue) throws SQLException { + setParameter(parameterIndex, parameterValue); + } + @Override public void setShort(int parameterIndex, short parameterValue) throws SQLException { setParameter(parameterIndex, parameterValue); @@ -124,12 +144,12 @@ public void setString(int parameterIndex, String parameterValue) throws SQLExcep } @Override - public void setBytes(int parameterIndex, byte[] parameterValue) throws SQLException { + public void setDate(int parameterIndex, Date parameterValue) throws SQLException { setParameter(parameterIndex, parameterValue); } @Override - public void setDate(int parameterIndex, Date parameterValue) throws SQLException { + public void setDate(int parameterIndex, Date parameterValue, Calendar calendar) throws SQLException { setParameter(parameterIndex, parameterValue); } @@ -139,184 +159,176 @@ public void setTime(int parameterIndex, Time parameterValue) throws SQLException } @Override - public void setTimestamp(int parameterIndex, Timestamp parameterValue) throws SQLException { + public void setTime(int parameterIndex, Time parameterValue, Calendar calendar) throws SQLException { setParameter(parameterIndex, parameterValue); } @Override - public void setAsciiStream(int parameterIndex, InputStream parameterValue, int length) throws SQLException { + public void setTimestamp(int parameterIndex, Timestamp parameterValue) throws SQLException { setParameter(parameterIndex, parameterValue); } @Override - public void setUnicodeStream(int parameterIndex, InputStream parameterValue, int length) throws SQLException { + public void setTimestamp(int parameterIndex, Timestamp parameterValue, Calendar calendar) throws SQLException { setParameter(parameterIndex, parameterValue); } @Override - public void setBinaryStream(int parameterIndex, InputStream parameterValue, int length) throws SQLException { + public void setAsciiStream(int parameterIndex, InputStream parameterValue, int length) throws SQLException { setParameter(parameterIndex, parameterValue); } @Override - public void clearParameters() throws SQLException { - params.clear(); - } - - @Override - public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { - setObject(parameterIndex, x, targetSqlType, -1); + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public void setObject(int parameterIndex, Object value) throws SQLException { - setParameter(parameterIndex, value); - } - - private void setParameter(int parameterIndex, Object value) throws SQLException { - checkNotClosed(); - params.put(parameterIndex, value); + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public boolean execute() throws SQLException { - checkNotClosed(); - discardLastResults(); - return handleResult(connection.execute(sql, getParams())); + public void setUnicodeStream(int parameterIndex, InputStream parameterValue, int length) throws SQLException { + setParameter(parameterIndex, parameterValue); } @Override - public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void setBinaryStream(int parameterIndex, InputStream parameterValue, int length) throws SQLException { + setParameter(parameterIndex, parameterValue); } @Override - public void setRef(int parameterIndex, Ref x) throws SQLException { + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setBlob(int parameterIndex, Blob x) throws SQLException { + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setClob(int parameterIndex, Clob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void clearParameters() throws SQLException { + params.clear(); } @Override - public void setArray(int parameterIndex, Array x) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + setObject(parameterIndex, x, targetSqlType, -1); } @Override - public ResultSetMetaData getMetaData() throws SQLException { - return getResultSet().getMetaData(); + public void setObject(int parameterIndex, Object value) throws SQLException { + setParameter(parameterIndex, value); } @Override - public void setDate(int parameterIndex, Date parameterValue, Calendar calendar) throws SQLException { + public void setObject(int parameterIndex, + Object parameterValue, + int targetSqlType, + int scaleOrLength) throws SQLException { setParameter(parameterIndex, parameterValue); } - @Override - public void setTime(int parameterIndex, Time parameterValue, Calendar calendar) throws SQLException { - setParameter(parameterIndex, parameterValue); + private void setParameter(int parameterIndex, Object value) throws SQLException { + checkNotClosed(); + params.put(parameterIndex, value); } @Override - public void setTimestamp(int parameterIndex, Timestamp parameterValue, Calendar calendar) throws SQLException { - setParameter(parameterIndex, parameterValue); + public boolean execute() throws SQLException { + checkNotClosed(); + return executeInternal(sql, getParams()); } @Override - public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { - setParameter(parameterIndex, null); + public boolean execute(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); } @Override - public void setURL(int parameterIndex, URL parameterValue) throws SQLException { - setParameter(parameterIndex, parameterValue.toString()); + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public ParameterMetaData getParameterMetaData() throws SQLException { - return null; + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public void setRowId(int parameterIndex, RowId x) throws SQLException { + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setNString(int parameterIndex, String parameterValue) throws SQLException { - setParameter(parameterIndex, parameterValue); + public void setRef(int parameterIndex, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + public void setBlob(int parameterIndex, Blob x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setNClob(int parameterIndex, NClob value) throws SQLException { + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + public void setClob(int parameterIndex, Clob x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + public void setClob(int parameterIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setObject(int parameterIndex, Object parameterValue, int targetSqlType, int scaleOrLength) throws SQLException { - setParameter(parameterIndex, parameterValue); + public void setArray(int parameterIndex, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public ResultSetMetaData getMetaData() throws SQLException { + return getResultSet().getMetaData(); } @Override - public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void setURL(int parameterIndex, URL parameterValue) throws SQLException { + setParameter(parameterIndex, parameterValue.toString()); } @Override - public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public ParameterMetaData getParameterMetaData() throws SQLException { + return null; } @Override - public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + public void setRowId(int parameterIndex, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void setNString(int parameterIndex, String parameterValue) throws SQLException { + setParameter(parameterIndex, parameterValue); } @Override - public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @@ -326,12 +338,12 @@ public void setNCharacterStream(int parameterIndex, Reader value) throws SQLExce } @Override - public void setClob(int parameterIndex, Reader reader) throws SQLException { + public void setNClob(int parameterIndex, NClob value) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @@ -341,17 +353,12 @@ public void setNClob(int parameterIndex, Reader reader) throws SQLException { } @Override - public void addBatch(String sql) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int[] executeBatch() throws SQLException { + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void clearBatch() throws SQLException { + public void addBatch(String sql) throws SQLException { throw new SQLFeatureNotSupportedException(); } @@ -361,17 +368,13 @@ public void addBatch() throws SQLException { } @Override - public ResultSet executeQuery(String sql) throws SQLException { - throw new SQLException(INVALID_CALL_MSG); + public int[] executeBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public int executeUpdate(String sql) throws SQLException { - throw new SQLException(INVALID_CALL_MSG); + public void clearBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); } - @Override - public boolean execute(String sql) throws SQLException { - throw new SQLException(INVALID_CALL_MSG); - } } diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index f3a54566..de7a78ac 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -1,6 +1,10 @@ package org.tarantool.jdbc; import org.tarantool.JDBCBridge; +import org.tarantool.jdbc.cursor.CursorIterator; +import org.tarantool.jdbc.cursor.InMemoryForwardCursorIteratorImpl; +import org.tarantool.jdbc.cursor.InMemoryScrollableCursorIteratorImpl; +import org.tarantool.util.SQLStates; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -28,61 +32,66 @@ import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; +import java.util.LinkedHashMap; import java.util.List; -import java.util.ListIterator; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; -@SuppressWarnings("Since15") public class SQLResultSet implements ResultSet { - private ListIterator> iterator; - private JDBCBridge bridge; + private final CursorIterator> iterator; private final SQLResultSetMetaData metaData; + private Map columnByNameLookups; + private final Statement statement; - private int maxRows; - private List row = null; + private final int maxRows; + + private AtomicBoolean isClosed = new AtomicBoolean(false); - private final int type; + private final int scrollType; private final int concurrencyLevel; private final int holdability; public SQLResultSet(JDBCBridge bridge, SQLStatement ownerStatement) throws SQLException { - this.bridge = bridge; - iterator = bridge.iterator(); - metaData = new SQLResultSetMetaData(bridge); + metaData = new SQLResultSetMetaData(bridge.getSqlMetadata()); statement = ownerStatement; - type = statement.getResultSetType(); + scrollType = statement.getResultSetType(); concurrencyLevel = statement.getResultSetConcurrency(); holdability = statement.getResultSetHoldability(); + this.maxRows = statement.getMaxRows(); + + List> fetchedRows = bridge.getRows(); + List> rows = maxRows == 0 || maxRows >= fetchedRows.size() + ? fetchedRows + : fetchedRows.subList(0, maxRows); + + switch (scrollType) { + case ResultSet.TYPE_FORWARD_ONLY: + iterator = new InMemoryForwardCursorIteratorImpl(rows); + break; + case ResultSet.TYPE_SCROLL_INSENSITIVE: + iterator = new InMemoryScrollableCursorIteratorImpl(rows); + break; + default: + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } } public int getMaxRows() { return maxRows; } - public void setMaxRows(int maxRows) { - this.maxRows = maxRows; - } - - List getCurrentRow() { - return row; - } - - @Override - public boolean next() throws SQLException { + public List getCurrentRow() throws SQLException { checkNotClosed(); - if (iterator.hasNext() && (maxRows == 0 || iterator.nextIndex() < maxRows)) { - row = iterator.next(); - return true; - } - row = null; - return false; + return iterator.getItem(); } @Override public void close() throws SQLException { - + if (isClosed.compareAndSet(false, true)) { + iterator.close(); + } } @Override @@ -90,42 +99,65 @@ public boolean wasNull() throws SQLException { return false; } + protected Object getRaw(int columnIndex) throws SQLException { + checkNotClosed(); + metaData.checkColumnIndex(columnIndex); + List row = getCurrentRow(); + return row.get(columnIndex - 1); + } + @Override public String getString(int columnIndex) throws SQLException { Object raw = getRaw(columnIndex); return raw == null ? null : String.valueOf(raw); } - protected Object getRaw(int columnIndex) { - return row.get(columnIndex - 1); - } - - protected Integer getColumnIndex(String columnLabel) { - return bridge.getColumnIndex(columnLabel); + @Override + public String getString(String columnLabel) throws SQLException { + return getString(findColumn(columnLabel)); } - @Override public boolean getBoolean(int columnIndex) throws SQLException { return Boolean.TRUE.equals(getRaw(columnIndex)); } + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getBoolean(findColumn(columnLabel)); + } + @Override public byte getByte(int columnIndex) throws SQLException { return (getNumber(columnIndex)).byteValue(); } + @Override + public byte getByte(String columnLabel) throws SQLException { + return getByte(findColumn(columnLabel)); + } + @Override public short getShort(int columnIndex) throws SQLException { return (getNumber(columnIndex)).shortValue(); } + @Override + public short getShort(String columnLabel) throws SQLException { + return getShort(findColumn(columnLabel)); + } + @Override public int getInt(int columnIndex) throws SQLException { return getNumber(columnIndex).intValue(); } - private Number getNumber(int columnIndex) { + @Override + public int getInt(String columnLabel) throws SQLException { + return getInt(findColumn(columnLabel)); + } + + private Number getNumber(int columnIndex) throws SQLException { Number raw = (Number) getRaw(columnIndex); return raw == null ? 0 : raw; } @@ -135,16 +167,31 @@ public long getLong(int columnIndex) throws SQLException { return (getNumber(columnIndex)).longValue(); } + @Override + public long getLong(String columnLabel) throws SQLException { + return getLong(findColumn(columnLabel)); + } + @Override public float getFloat(int columnIndex) throws SQLException { return (getNumber(columnIndex)).floatValue(); } + @Override + public float getFloat(String columnLabel) throws SQLException { + return getFloat(findColumn(columnLabel)); + } + @Override public double getDouble(int columnIndex) throws SQLException { return (getNumber(columnIndex)).doubleValue(); } + @Override + public double getDouble(String columnLabel) throws SQLException { + return getDouble(findColumn(columnLabel)); + } + @Override public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { BigDecimal bigDecimal = new BigDecimal(getString(columnIndex)); @@ -152,263 +199,281 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException } @Override - public byte[] getBytes(int columnIndex) throws SQLException { - return (byte[]) getRaw(columnIndex); + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(findColumn(columnLabel)); } @Override - public Date getDate(int columnIndex) throws SQLException { - return new java.sql.Date(getLong(columnIndex)); + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return getBigDecimal(columnIndex, -1); } @Override - public Time getTime(int columnIndex) throws SQLException { - return new java.sql.Time(getLong(columnIndex)); + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getBigDecimal(columnLabel, -1); } @Override - public Timestamp getTimestamp(int columnIndex) throws SQLException { - return new java.sql.Timestamp(getLong(columnIndex)); + public byte[] getBytes(int columnIndex) throws SQLException { + return (byte[]) getRaw(columnIndex); } @Override - public InputStream getAsciiStream(int columnIndex) throws SQLException { - return new ByteArrayInputStream(getString(columnIndex).getBytes(Charset.forName("ASCII"))); + public byte[] getBytes(String columnLabel) throws SQLException { + return getBytes(findColumn(columnLabel)); } @Override - public InputStream getUnicodeStream(int columnIndex) throws SQLException { - return new ByteArrayInputStream(getString(columnIndex).getBytes(Charset.forName("UTF-8"))); + public Date getDate(int columnIndex) throws SQLException { + return new java.sql.Date(getLong(columnIndex)); } @Override - public InputStream getBinaryStream(int columnIndex) throws SQLException { - return new ByteArrayInputStream(getBytes(columnIndex)); + public Date getDate(String columnLabel) throws SQLException { + return getDate(findColumn(columnLabel)); } @Override - public String getString(String columnLabel) throws SQLException { - return getString(getColumnIndex(columnLabel)); + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public boolean getBoolean(String columnLabel) throws SQLException { - return getBoolean(getColumnIndex(columnLabel)); + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public byte getByte(String columnLabel) throws SQLException { - return getByte(getColumnIndex(columnLabel)); + public Time getTime(int columnIndex) throws SQLException { + return new java.sql.Time(getLong(columnIndex)); } @Override - public short getShort(String columnLabel) throws SQLException { - return getShort(getColumnIndex(columnLabel)); + public Time getTime(String columnLabel) throws SQLException { + return getTime(findColumn(columnLabel)); } @Override - public int getInt(String columnLabel) throws SQLException { - return getInt(getColumnIndex(columnLabel)); + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public long getLong(String columnLabel) throws SQLException { - return getLong(getColumnIndex(columnLabel)); + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public float getFloat(String columnLabel) throws SQLException { - return getFloat(getColumnIndex(columnLabel)); + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return new java.sql.Timestamp(getLong(columnIndex)); } @Override - public double getDouble(String columnLabel) throws SQLException { - return getDouble(getColumnIndex(columnLabel)); + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getTimestamp(findColumn(columnLabel)); } @Override - public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { - return getBigDecimal(getColumnIndex(columnLabel)); + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public byte[] getBytes(String columnLabel) throws SQLException { - return getBytes(getColumnIndex(columnLabel)); + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); } + @Override - public Date getDate(String columnLabel) throws SQLException { - return getDate(getColumnIndex(columnLabel)); + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return new ByteArrayInputStream(getString(columnIndex).getBytes(Charset.forName("ASCII"))); } @Override - public Time getTime(String columnLabel) throws SQLException { - return getTime(getColumnIndex(columnLabel)); + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return getAsciiStream(findColumn(columnLabel)); } @Override - public Timestamp getTimestamp(String columnLabel) throws SQLException { - return getTimestamp(getColumnIndex(columnLabel)); + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return new ByteArrayInputStream(getString(columnIndex).getBytes(Charset.forName("UTF-8"))); } @Override - public InputStream getAsciiStream(String columnLabel) throws SQLException { - return getAsciiStream(getColumnIndex(columnLabel)); + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return getUnicodeStream(findColumn(columnLabel)); } @Override - public InputStream getUnicodeStream(String columnLabel) throws SQLException { - return getUnicodeStream(getColumnIndex(columnLabel)); + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return new ByteArrayInputStream(getBytes(columnIndex)); } @Override public InputStream getBinaryStream(String columnLabel) throws SQLException { - return getBinaryStream(getColumnIndex(columnLabel)); + return getBinaryStream(findColumn(columnLabel)); } @Override - public SQLWarning getWarnings() throws SQLException { - return null; + public Reader getCharacterStream(int columnIndex) throws SQLException { + return new StringReader(getString(columnIndex)); } @Override - public void clearWarnings() throws SQLException { + public Reader getCharacterStream(String columnLabel) throws SQLException { + return new StringReader(getString(columnLabel)); + } + @Override + public Object getObject(int columnIndex) throws SQLException { + return getRaw(columnIndex); } @Override - public String getCursorName() throws SQLException { + public Object getObject(String columnLabel) throws SQLException { + return getRaw(findColumn(columnLabel)); + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public ResultSetMetaData getMetaData() throws SQLException { - return metaData; + public Object getObject(String columnLabel, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public Object getObject(int columnIndex) throws SQLException { - return getRaw(columnIndex); + public T getObject(int columnIndex, Class type) throws SQLException { + return type.cast(getRaw(columnIndex)); } @Override - public Object getObject(String columnLabel) throws SQLException { - return getRaw(getColumnIndex(columnLabel)); + public T getObject(String columnLabel, Class type) throws SQLException { + return type.cast(getRaw(findColumn(columnLabel))); } @Override - public int findColumn(String columnLabel) throws SQLException { - return getColumnIndex(columnLabel); + public SQLWarning getWarnings() throws SQLException { + return null; } @Override - public Reader getCharacterStream(int columnIndex) throws SQLException { - return new StringReader(getString(columnIndex)); + public void clearWarnings() throws SQLException { + } @Override - public Reader getCharacterStream(String columnLabel) throws SQLException { - return new StringReader(getString(columnLabel)); + public String getCursorName() throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - return getBigDecimal(columnIndex, -1); + public ResultSetMetaData getMetaData() throws SQLException { + return metaData; } @Override - public BigDecimal getBigDecimal(String columnLabel) throws SQLException { - return getBigDecimal(columnLabel, -1); + public int findColumn(String columnLabel) throws SQLException { + return findColumnIndex(columnLabel); + } + + protected int findColumnIndex(String columnLabel) throws SQLException { + if (columnByNameLookups == null) { + columnByNameLookups = new LinkedHashMap<>(); + // Spec quote: Column labels supplied to getter methods are case insensitive. + // If a select list contains the same column more than once, the first instance + // of the column will be returned. + for (int i = metaData.getColumnCount(); i > 0; i--) { + columnByNameLookups.put(metaData.getColumnLabel(i).toUpperCase(), i); + } + } + return columnByNameLookups.getOrDefault(columnLabel.toUpperCase(), 0); + } + + //region Cursor movement API + + @Override + public boolean next() throws SQLException { + checkNotClosed(); + return iterator.next(); } @Override public boolean isBeforeFirst() throws SQLException { checkNotClosed(); - return row == null && iterator.previousIndex() == -1; + return iterator.isBeforeFirst(); } @Override public boolean isAfterLast() throws SQLException { checkNotClosed(); - return iterator.nextIndex() == bridge.size() && row == null; + return iterator.isAfterLast(); } @Override public boolean isFirst() throws SQLException { checkNotClosed(); - return iterator.previousIndex() == 0; + return iterator.isFirst(); } @Override public boolean isLast() throws SQLException { checkNotClosed(); - return iterator.nextIndex() == bridge.size(); + return iterator.isLast(); } @Override public void beforeFirst() throws SQLException { checkNotClosed(); - row = null; - iterator = bridge.iterator(); + iterator.beforeFirst(); } @Override public void afterLast() throws SQLException { checkNotClosed(); - while (next()) { - } + iterator.afterLast(); } @Override public boolean first() throws SQLException { - beforeFirst(); - return next(); + checkNotClosed(); + return iterator.first(); } @Override public boolean last() throws SQLException { checkNotClosed(); - while (iterator.hasNext()) { - next(); - } - return row != null; + return iterator.last(); } @Override - public int getRow() throws SQLException { + public boolean absolute(int row) throws SQLException { checkNotClosed(); - return iterator.previousIndex() + 1; + return iterator.absolute(row); } @Override - public boolean absolute(int row) throws SQLException { - beforeFirst(); - for (int i = 0; i < row && iterator.hasNext(); i++) { - next(); - } - return !(isAfterLast() || isBeforeFirst()); - + public boolean relative(int rows) throws SQLException { + checkNotClosed(); + return iterator.relative(rows); } @Override - public boolean relative(int rows) throws SQLException { + public boolean previous() throws SQLException { checkNotClosed(); - for (int i = 0; i < rows && iterator.hasNext(); i++) { - next(); - } - return !(isAfterLast() || isBeforeFirst()); + return iterator.previous(); } @Override - public boolean previous() throws SQLException { + public int getRow() throws SQLException { checkNotClosed(); - if (iterator.hasPrevious()) { - iterator.previous(); - return true; - } - return false; + return iterator.getRow(); } + //endregion + @Override public void setFetchDirection(int direction) throws SQLException { checkNotClosed(); @@ -436,7 +501,7 @@ public int getFetchSize() throws SQLException { @Override public int getType() throws SQLException { checkNotClosed(); - return type; + return scrollType; } @Override @@ -465,308 +530,329 @@ public void updateNull(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override + public void updateNull(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + @Override public void updateBoolean(int columnIndex, boolean x) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + @Override public void updateByte(int columnIndex, byte x) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + @Override public void updateShort(int columnIndex, short x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateInt(int columnIndex, int x) throws SQLException { + public void updateShort(String columnLabel, short x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateLong(int columnIndex, long x) throws SQLException { + public void updateInt(int columnIndex, int x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateFloat(int columnIndex, float x) throws SQLException { + public void updateInt(String columnLabel, int x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateDouble(int columnIndex, double x) throws SQLException { + public void updateLong(int columnIndex, long x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + public void updateLong(String columnLabel, long x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateString(int columnIndex, String x) throws SQLException { + public void updateFloat(int columnIndex, float x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBytes(int columnIndex, byte[] x) throws SQLException { + public void updateFloat(String columnLabel, float x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateDate(int columnIndex, Date x) throws SQLException { + public void updateDouble(int columnIndex, double x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateTime(int columnIndex, Time x) throws SQLException { + public void updateDouble(String columnLabel, double x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + public void updateString(int columnIndex, String x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + public void updateString(String columnLabel, String x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + public void updateBytes(int columnIndex, byte[] x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateObject(int columnIndex, Object x) throws SQLException { + public void updateBytes(String columnLabel, byte[] x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateNull(String columnLabel) throws SQLException { + public void updateDate(int columnIndex, Date x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBoolean(String columnLabel, boolean x) throws SQLException { + public void updateDate(String columnLabel, Date x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateByte(String columnLabel, byte x) throws SQLException { + public void updateTime(int columnIndex, Time x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateShort(String columnLabel, short x) throws SQLException { + public void updateTime(String columnLabel, Time x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateInt(String columnLabel, int x) throws SQLException { + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateLong(String columnLabel, long x) throws SQLException { + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateFloat(String columnLabel, float x) throws SQLException { + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateDouble(String columnLabel, double x) throws SQLException { + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateString(String columnLabel, String x) throws SQLException { + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBytes(String columnLabel, byte[] x) throws SQLException { + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateDate(String columnLabel, Date x) throws SQLException { + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateTime(String columnLabel, Time x) throws SQLException { + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateObject(String columnLabel, Object x) throws SQLException { + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void insertRow() throws SQLException { + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateRow() throws SQLException { + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void deleteRow() throws SQLException { + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void refreshRow() throws SQLException { + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void cancelRowUpdates() throws SQLException { + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void moveToInsertRow() throws SQLException { + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void moveToCurrentRow() throws SQLException { + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Statement getStatement() throws SQLException { + public void updateObject(int columnIndex, Object x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Object getObject(int columnIndex, Map> map) throws SQLException { + public void updateObject(String columnLabel, Object x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Ref getRef(int columnIndex) throws SQLException { + public void insertRow() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Blob getBlob(int columnIndex) throws SQLException { + public void updateRow() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Clob getClob(int columnIndex) throws SQLException { + public void deleteRow() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Array getArray(int columnIndex) throws SQLException { + public void refreshRow() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Object getObject(String columnLabel, Map> map) throws SQLException { + public void cancelRowUpdates() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Ref getRef(String columnLabel) throws SQLException { + public void moveToInsertRow() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Blob getBlob(String columnLabel) throws SQLException { + public void moveToCurrentRow() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Clob getClob(String columnLabel) throws SQLException { + public Statement getStatement() throws SQLException { + checkNotClosed(); + return statement; + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Array getArray(String columnLabel) throws SQLException { + public Ref getRef(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Date getDate(int columnIndex, Calendar cal) throws SQLException { + public Blob getBlob(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Date getDate(String columnLabel, Calendar cal) throws SQLException { + public Blob getBlob(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Time getTime(int columnIndex, Calendar cal) throws SQLException { + public Clob getClob(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Time getTime(String columnLabel, Calendar cal) throws SQLException { + public Clob getClob(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + public Array getArray(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + public Array getArray(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @@ -809,183 +895,113 @@ public void updateBlob(String columnLabel, Blob x) throws SQLException { } @Override - public void updateClob(int columnIndex, Clob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateClob(String columnLabel, Clob x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateArray(int columnIndex, Array x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateArray(String columnLabel, Array x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public RowId getRowId(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public RowId getRowId(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateRowId(int columnIndex, RowId x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateRowId(String columnLabel, RowId x) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int getHoldability() throws SQLException { - checkNotClosed(); - return holdability; - } - - @Override - public boolean isClosed() throws SQLException { - return statement.isClosed(); - } - - @Override - public void updateNString(int columnIndex, String nString) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNString(String columnLabel, String nString) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(int columnIndex, NClob nClob) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public void updateNClob(String columnLabel, NClob nClob) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public NClob getNClob(int columnIndex) throws SQLException { + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public NClob getNClob(String columnLabel) throws SQLException { + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public SQLXML getSQLXML(int columnIndex) throws SQLException { + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public SQLXML getSQLXML(String columnLabel) throws SQLException { + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + public void updateClob(int columnIndex, Clob x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + public void updateClob(String columnLabel, Clob x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public String getNString(int columnIndex) throws SQLException { + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public String getNString(String columnLabel) throws SQLException { + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Reader getNCharacterStream(int columnIndex) throws SQLException { + public void updateClob(int columnIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public Reader getNCharacterStream(String columnLabel) throws SQLException { + public void updateClob(String columnLabel, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + public void updateArray(int columnIndex, Array x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + public void updateArray(String columnLabel, Array x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + public RowId getRowId(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + public RowId getRowId(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + public void updateRowId(int columnIndex, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + public void updateRowId(String columnLabel, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public int getHoldability() throws SQLException { + checkNotClosed(); + return holdability; } @Override - public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public boolean isClosed() throws SQLException { + return isClosed.get() || statement.isClosed(); } @Override - public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + public void updateNString(int columnIndex, String string) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + public void updateNString(String columnLabel, String string) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + public void updateNClob(int columnIndex, NClob clob) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + public void updateNClob(String columnLabel, NClob clob) throws SQLException { throw new SQLFeatureNotSupportedException(); } @@ -1000,102 +1016,107 @@ public void updateNClob(String columnLabel, Reader reader, long length) throws S } @Override - public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + public void updateNClob(int columnIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + public void updateNClob(String columnLabel, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + public NClob getNClob(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + public NClob getNClob(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + public SQLXML getSQLXML(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + public SQLXML getSQLXML(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException(); } - @Override - public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + public String getNString(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + public String getNString(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateClob(int columnIndex, Reader reader) throws SQLException { + public Reader getNCharacterStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateClob(String columnLabel, Reader reader) throws SQLException { + public Reader getNCharacterStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateNClob(int columnIndex, Reader reader) throws SQLException { + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public void updateNClob(String columnLabel, Reader reader) throws SQLException { + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override - public T getObject(int columnIndex, Class type) throws SQLException { - return type.cast(getRaw(columnIndex)); + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public T getObject(String columnLabel, Class type) throws SQLException { - return type.cast(getRaw(getColumnIndex(columnLabel))); + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); } @Override - public T unwrap(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("ResultSet does not wrap " + type.getName()); } @Override - public boolean isWrapperFor(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); } @Override public String toString() { return "SQLResultSet{" + - "metaData=" + metaData + - ", row=" + row + - '}'; + "metaData=" + metaData + + ", statement=" + statement + + ", scrollType=" + scrollType + + ", concurrencyLevel=" + concurrencyLevel + + ", holdability=" + holdability + + '}'; } protected void checkNotClosed() throws SQLException { diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java index b3053d24..513679c9 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -1,22 +1,26 @@ package org.tarantool.jdbc; +import org.tarantool.SqlProtoUtils; +import org.tarantool.util.SQLStates; + import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLNonTransientException; import java.sql.Types; - -import org.tarantool.JDBCBridge; +import java.util.List; public class SQLResultSetMetaData implements ResultSetMetaData { - protected final JDBCBridge JDBCBridgeExecutor; - public SQLResultSetMetaData(JDBCBridge JDBCBridgeExecutor) { - this.JDBCBridgeExecutor = JDBCBridgeExecutor; + private final List sqlMetadata; + + public SQLResultSetMetaData(List sqlMetaData) { + this.sqlMetadata = sqlMetaData; } @Override public int getColumnCount() throws SQLException { - return JDBCBridgeExecutor.getColumnCount(); + return sqlMetadata.size(); } @Override @@ -56,12 +60,14 @@ public int getColumnDisplaySize(int column) throws SQLException { @Override public String getColumnLabel(int column) throws SQLException { - return JDBCBridgeExecutor.getColumnName(column); + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getName(); } @Override public String getColumnName(int column) throws SQLException { - return JDBCBridgeExecutor.getColumnName(column); + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getName(); } @Override @@ -120,19 +126,31 @@ public String getColumnClassName(int column) throws SQLException { } @Override - public T unwrap(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("ResultSetMetadata does not wrap " + type.getName()); } @Override - public boolean isWrapperFor(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); + } + + void checkColumnIndex(int columnIndex) throws SQLException { + if (columnIndex < 1 || columnIndex > getColumnCount()) { + throw new SQLNonTransientException( + String.format("Column index %d is out of range. Max index is %d", columnIndex, getColumnCount()), + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } } @Override public String toString() { return "SQLResultSetMetaData{" + - "bridge=" + JDBCBridgeExecutor + + "sqlMetadata=" + sqlMetadata + '}'; } } diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index 658d2487..a4dfdaf1 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -1,6 +1,8 @@ package org.tarantool.jdbc; import org.tarantool.JDBCBridge; +import org.tarantool.util.JdbcConstants; +import org.tarantool.util.SQLStates; import java.sql.Connection; import java.sql.ResultSet; @@ -9,20 +11,33 @@ import java.sql.SQLNonTransientException; import java.sql.SQLWarning; import java.sql.Statement; - -@SuppressWarnings("Since15") +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Tarantool {@link Statement} implementation. + *

+ * Supports {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#TYPE_SCROLL_INSENSITIVE} + * types of cursors. + * Supports only {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} holdability type. + */ public class SQLStatement implements Statement { protected final SQLConnection connection; + /** + * Current result set / update count associated to this statement. + */ private SQLResultSet resultSet; + private int updateCount; + private final int resultSetType; private final int resultSetConcurrency; private final int resultSetHoldability; - private int updateCount; private int maxRows; + private final AtomicBoolean isClosed = new AtomicBoolean(false); + protected SQLStatement(SQLConnection sqlConnection) throws SQLException { this.connection = sqlConnection; this.resultSetType = ResultSet.TYPE_FORWARD_ONLY; @@ -43,20 +58,50 @@ protected SQLStatement(SQLConnection sqlConnection, @Override public ResultSet executeQuery(String sql) throws SQLException { checkNotClosed(); - discardLastResults(); - return createResultSet(connection.executeQuery(sql)); + if (!executeInternal(sql)) { + throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); + } + return resultSet; } @Override public int executeUpdate(String sql) throws SQLException { checkNotClosed(); - discardLastResults(); - return connection.executeUpdate(sql); + if (executeInternal(sql)) { + throw new SQLException( + "Result was returned but nothing was expected", + SQLStates.TOO_MANY_RESULTS.getSqlState() + ); + } + return updateCount; } @Override - public void close() throws SQLException { + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new SQLFeatureNotSupportedException(); + } + return executeUpdate(sql); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + @Override + public void close() throws SQLException { + if (isClosed.compareAndSet(false, true)) { + cancel(); + discardLastResults(); + } } @Override @@ -82,9 +127,6 @@ public void setMaxRows(int maxRows) throws SQLException { throw new SQLNonTransientException("Max rows parameter can't be a negative value"); } this.maxRows = maxRows; - if (resultSet != null) { - resultSet.setMaxRows(this.maxRows); - } } @Override @@ -126,27 +168,39 @@ public void setCursorName(String name) throws SQLException { public boolean execute(String sql) throws SQLException { checkNotClosed(); discardLastResults(); - return handleResult(connection.execute(sql)); + return executeInternal(sql); } @Override - public ResultSet getResultSet() throws SQLException { + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { checkNotClosed(); - try { - return resultSet; - } finally { - resultSet = null; + JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new SQLFeatureNotSupportedException(); } + return execute(sql); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public ResultSet getResultSet() throws SQLException { + checkNotClosed(); + return resultSet; } @Override public int getUpdateCount() throws SQLException { checkNotClosed(); - try { - return updateCount; - } finally { - updateCount = -1; - } + return updateCount; } @Override @@ -155,6 +209,12 @@ public boolean getMoreResults() throws SQLException { return false; } + @Override + public boolean getMoreResults(int current) throws SQLException { + checkNotClosed(); + return false; + } + @Override public void setFetchDirection(int direction) throws SQLException { checkNotClosed(); @@ -212,45 +272,10 @@ public Connection getConnection() throws SQLException { return connection; } - @Override - public boolean getMoreResults(int current) throws SQLException { - checkNotClosed(); - return false; - } - @Override public ResultSet getGeneratedKeys() throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public int executeUpdate(String sql, String[] columnNames) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean execute(String sql, int[] columnIndexes) throws SQLException { - throw new SQLFeatureNotSupportedException(); - } - - @Override - public boolean execute(String sql, String[] columnNames) throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + return new SQLResultSet(JDBCBridge.EMPTY, this); } @Override @@ -261,7 +286,7 @@ public int getResultSetHoldability() throws SQLException { @Override public boolean isClosed() throws SQLException { - return connection.isClosed(); + return isClosed.get() || connection.isClosed(); } @Override @@ -301,7 +326,8 @@ public boolean isWrapperFor(Class type) throws SQLException { /** * Clears the results of the most recent execution. */ - protected void discardLastResults() { + protected void discardLastResults() throws SQLException { + clearWarnings(); updateCount = -1; if (resultSet != null) { try { @@ -313,16 +339,29 @@ protected void discardLastResults() { } } + /** + * Performs query execution. + * + * @param sql query + * @param params optional params + * + * @return {@code true}, if the result is a ResultSet object; + */ + protected boolean executeInternal(String sql, Object... params) throws SQLException { + discardLastResults(); + return handleResult(connection.execute(sql, params)); + } + /** * Sets the internals according to the result of last execution. * * @param result The result of SQL statement execution. + * * @return {@code true}, if the result is a ResultSet object. */ protected boolean handleResult(Object result) throws SQLException { if (result instanceof JDBCBridge) { resultSet = createResultSet((JDBCBridge) result); - resultSet.setMaxRows(maxRows); updateCount = -1; return true; } else { @@ -333,10 +372,12 @@ protected boolean handleResult(Object result) throws SQLException { } /** - * Returns {@link ResultSet} which will be initialized by data + * Returns {@link ResultSet} which will be initialized by data. * * @param data predefined result to be wrapped by {@link ResultSet} + * * @return wrapped result + * * @throws SQLException if a database access error occurs or * this method is called on a closed Statement */ @@ -354,4 +395,5 @@ protected void checkNotClosed() throws SQLException { throw new SQLNonTransientException("Statement is closed."); } } + } diff --git a/src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java b/src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java new file mode 100644 index 00000000..7c29080a --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java @@ -0,0 +1,40 @@ +package org.tarantool.jdbc.cursor; + +import java.sql.SQLException; + +/** + * Extracted interface for a cursor traversal part of {@link java.sql.ResultSet}. + */ +public interface CursorIterator { + + boolean isBeforeFirst() throws SQLException; + + boolean isAfterLast() throws SQLException; + + boolean isFirst() throws SQLException; + + boolean isLast() throws SQLException; + + void beforeFirst() throws SQLException; + + void afterLast() throws SQLException; + + boolean first() throws SQLException; + + boolean last() throws SQLException; + + boolean absolute(int row) throws SQLException; + + boolean relative(int rows) throws SQLException; + + boolean next() throws SQLException; + + boolean previous() throws SQLException; + + int getRow() throws SQLException; + + T getItem() throws SQLException; + + void close(); + +} diff --git a/src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java b/src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java new file mode 100644 index 00000000..1b735968 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java @@ -0,0 +1,137 @@ +package org.tarantool.jdbc.cursor; + +import org.tarantool.util.SQLStates; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Forward only iterator to support {@link java.sql.ResultSet#TYPE_FORWARD_ONLY} + * result set semantic. + */ +public class InMemoryForwardCursorIteratorImpl implements CursorIterator> { + + protected final List> results = new ArrayList<>(); + protected int currentPosition = -1; + + public InMemoryForwardCursorIteratorImpl(List> results) { + if (results == null) { + throw new IllegalArgumentException("Results list cannot be null"); + } + this.results.addAll(results); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return hasResults() && currentPosition == -1; + } + + @Override + public boolean isAfterLast() throws SQLException { + return hasResults() && currentPosition == results.size(); + } + + @Override + public boolean isFirst() throws SQLException { + return hasResults() && currentPosition == 0; + } + + @Override + public boolean isLast() throws SQLException { + return hasResults() && currentPosition == results.size() - 1; + } + + @Override + public void beforeFirst() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public void afterLast() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean first() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean last() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean absolute(int row) throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean relative(int rows) throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean next() throws SQLException { + if (!hasResults() || isAfterLast()) { + return false; + } + currentPosition++; + return !isAfterLast(); + } + + @Override + public boolean previous() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public int getRow() throws SQLException { + return !hasResults() || isBeforeFirst() || isAfterLast() ? 0 : currentPosition + 1; + } + + @Override + public List getItem() throws SQLException { + int row = getRow(); + if (row > 0) { + return results.get(row - 1); + } + throw new SQLException( + "Cursor is out of range. Try to call next() or previous() before.", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + protected boolean hasResults() { + return !results.isEmpty(); + } + + @Override + public void close() { + results.clear(); + currentPosition = -1; + } + +} diff --git a/src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java b/src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java new file mode 100644 index 00000000..0561cc37 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java @@ -0,0 +1,100 @@ +package org.tarantool.jdbc.cursor; + +import java.sql.SQLException; +import java.util.List; + +/** + * Scrollable iterator to support {@link java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE} + * result set type semantic. + */ +public class InMemoryScrollableCursorIteratorImpl extends InMemoryForwardCursorIteratorImpl { + + public InMemoryScrollableCursorIteratorImpl(List> results) { + super(results); + } + + @Override + public void beforeFirst() throws SQLException { + moveIfHasResults(-1); + } + + @Override + public void afterLast() throws SQLException { + moveIfHasResults(results.size()); + } + + @Override + public boolean first() throws SQLException { + return moveIfHasResults(0); + } + + @Override + public boolean last() throws SQLException { + return moveIfHasResults(results.size() - 1); + } + + @Override + public boolean absolute(int row) throws SQLException { + if (!hasResults()) { + return false; + } + if (row == 0) { + beforeFirst(); + return false; + } + if (row > results.size()) { + afterLast(); + return false; + } + if (row < -results.size()) { + beforeFirst(); + return false; + } + + currentPosition = (row > 0) ? row - 1 : results.size() + row; + return true; + } + + @Override + public boolean relative(int rows) throws SQLException { + if (!hasResults()) { + return false; + } + if (rows == 0) { + return !(isBeforeFirst() || isAfterLast()); + } + if (currentPosition + rows >= results.size()) { + afterLast(); + return false; + } + if (currentPosition + rows <= -1) { + beforeFirst(); + return false; + } + + return absolute(currentPosition + rows + 1); + } + + @Override + public boolean previous() throws SQLException { + if (!hasResults() || isBeforeFirst()) { + return false; + } + currentPosition--; + return !isBeforeFirst(); + } + + /** + * Moves to the target position if results is not empty. + * + * @param position target position + * @return successful operation status + */ + private boolean moveIfHasResults(int position) { + if (!hasResults()) { + return false; + } + currentPosition = position; + return true; + } +} diff --git a/src/main/java/org/tarantool/protocol/ByteBufferBackedInputStream.java b/src/main/java/org/tarantool/protocol/ByteBufferBackedInputStream.java index 2ff9df44..b2f83090 100644 --- a/src/main/java/org/tarantool/protocol/ByteBufferBackedInputStream.java +++ b/src/main/java/org/tarantool/protocol/ByteBufferBackedInputStream.java @@ -4,13 +4,15 @@ import java.nio.ByteBuffer; /** - * Input stream based on ByteBuffer + * Input stream based on ByteBuffer. */ class ByteBufferBackedInputStream extends InputStream { private final ByteBuffer buf; /** + * Constructs a new wrapper-stream for {@link ByteBuffer}. + * * @param buf a buffer that have to be ready for read (flipped) */ public ByteBufferBackedInputStream(ByteBuffer buf) { diff --git a/src/main/java/org/tarantool/protocol/ProtoUtils.java b/src/main/java/org/tarantool/protocol/ProtoUtils.java index a1ba90ee..a0724279 100644 --- a/src/main/java/org/tarantool/protocol/ProtoUtils.java +++ b/src/main/java/org/tarantool/protocol/ProtoUtils.java @@ -16,6 +16,7 @@ import java.net.SocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; +import java.nio.channels.NonReadableChannelException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SocketChannel; import java.security.MessageDigest; @@ -26,15 +27,20 @@ import java.util.Map; public abstract class ProtoUtils { - private final static int DEFAULT_INITIAL_REQUEST_SIZE = 4096; + + public static final int LENGTH_OF_SIZE_MESSAGE = 5; + + private static final int DEFAULT_INITIAL_REQUEST_SIZE = 4096; private static final String WELCOME = "Tarantool "; /** - * Reads tarantool binary protocol's packet from {@code inputStream} + * Reads tarantool binary protocol's packet from {@code inputStream}. * * @param inputStream ready to use input stream - * @return Nonnull instance of packet. - * @throws IOException in case of any io-error. + * + * @return Nonnull instance of packet + * + * @throws IOException in case of any io-error */ public static TarantoolPacket readPacket(InputStream inputStream) throws IOException { CountInputStreamImpl msgStream = new CountInputStreamImpl(inputStream); @@ -52,13 +58,71 @@ public static TarantoolPacket readPacket(InputStream inputStream) throws IOExcep return new TarantoolPacket(headers, body); } + /** + * Reads a tarantool's binary protocol packet from the reader. + * + * @param bufferReader readable channel that have to be in blocking mode + * or instance of {@link ReadableViaSelectorChannel} + * + * @return tarantool binary protocol message wrapped by instance of {@link TarantoolPacket} + * + * @throws IOException if any IO-error occurred during read from the channel + * @throws CommunicationException input stream bytes constitute msg pack message in wrong format + * @throws NonReadableChannelException If this channel was not opened for reading + */ + public static TarantoolPacket readPacket(ReadableByteChannel bufferReader) + throws CommunicationException, IOException { + + ByteBuffer buffer = ByteBuffer.allocate(LENGTH_OF_SIZE_MESSAGE); + bufferReader.read(buffer); + + buffer.flip(); + int size = ((Number) getMsgPackLite().unpack(new ByteBufferBackedInputStream(buffer))).intValue(); + + buffer = ByteBuffer.allocate(size); + bufferReader.read(buffer); + + buffer.flip(); + ByteBufferBackedInputStream msgBytesStream = new ByteBufferBackedInputStream(buffer); + Object unpackedHeaders = getMsgPackLite().unpack(msgBytesStream); + if (!(unpackedHeaders instanceof Map)) { + //noinspection ConstantConditions + throw new CommunicationException( + "Error while unpacking headers of tarantool response: " + + "expected type Map but was " + + unpackedHeaders != null ? unpackedHeaders.getClass().toString() : "null" + ); + } + //noinspection unchecked (checked above) + Map headers = (Map) unpackedHeaders; + + Map body = null; + if (msgBytesStream.hasAvailable()) { + Object unpackedBody = getMsgPackLite().unpack(msgBytesStream); + if (!(unpackedBody instanceof Map)) { + //noinspection ConstantConditions + throw new CommunicationException( + "Error while unpacking body of tarantool response: " + + "expected type Map but was " + + unpackedBody != null ? unpackedBody.getClass().toString() : "null" + ); + } + //noinspection unchecked (checked above) + body = (Map) unpackedBody; + } + + return new TarantoolPacket(headers, body); + } + /** * Connects to a tarantool node described by {@code socket}. Performs an authentication if required * * @param socket a socket channel to a tarantool node * @param username auth username * @param password auth password + * * @return object with information about a connection/ + * * @throws IOException in case of any IO fails * @throws CommunicationException when welcome string is invalid * @throws TarantoolException in case of failed authentication @@ -97,7 +161,9 @@ public static TarantoolGreeting connect(Socket socket, * @param channel a socket channel to tarantool node. The channel have to be in blocking mode * @param username auth username * @param password auth password + * * @return object with information about a connection/ + * * @throws IOException in case of any IO fails * @throws CommunicationException when welcome string is invalid * @throws TarantoolException in case of failed authentication @@ -110,7 +176,7 @@ public static TarantoolGreeting connect(SocketChannel channel, String firstLine = new String(welcomeBytes.array()); assertCorrectWelcome(firstLine, channel.getRemoteAddress()); - String serverVersion = firstLine.substring(WELCOME.length()); + final String serverVersion = firstLine.substring(WELCOME.length()); welcomeBytes.clear(); channel.read(welcomeBytes); @@ -129,8 +195,10 @@ public static TarantoolGreeting connect(SocketChannel channel, private static void assertCorrectWelcome(String firstLine, SocketAddress remoteAddress) { if (!firstLine.startsWith(WELCOME)) { - String errMsg = "Failed to connect to node " + remoteAddress.toString() + ":" + - " Welcome message should starts with tarantool but starts with '" + firstLine + "'"; + String errMsg = "Failed to connect to node " + remoteAddress.toString() + + ": Welcome message should starts with tarantool but starts with '" + + firstLine + + "'"; throw new CommunicationException(errMsg, new IllegalStateException("Invalid welcome packet")); } } @@ -158,58 +226,6 @@ public static void writeFully(SocketChannel channel, ByteBuffer buffer) throws I } } - public static final int LENGTH_OF_SIZE_MESSAGE = 5; - - /** - * Reads a tarantool's binary protocol packet from the reader - * - * @param bufferReader readable channel that have to be in blocking mode - * or instance of {@link ReadableViaSelectorChannel} - * @return tarantool binary protocol message wrapped by instance of {@link TarantoolPacket}. - * @throws IOException if any IO-error occurred during read from the channel - * @throws CommunicationException input stream bytes consitute msg pack message in wrong format. - * @throws java.nio.channels.NonReadableChannelException – If this channel was not opened for reading - */ - public static TarantoolPacket readPacket(ReadableByteChannel bufferReader) - throws CommunicationException, IOException { - - ByteBuffer buffer = ByteBuffer.allocate(LENGTH_OF_SIZE_MESSAGE); - bufferReader.read(buffer); - - buffer.flip(); - int size = ((Number) getMsgPackLite().unpack(new ByteBufferBackedInputStream(buffer))).intValue(); - - buffer = ByteBuffer.allocate(size); - bufferReader.read(buffer); - - buffer.flip(); - ByteBufferBackedInputStream msgBytesStream = new ByteBufferBackedInputStream(buffer); - Object unpackedHeaders = getMsgPackLite().unpack(msgBytesStream); - if (!(unpackedHeaders instanceof Map)) { - //noinspection ConstantConditions - throw new CommunicationException("Error while unpacking headers of tarantool response: " + - "expected type Map but was " + - unpackedHeaders != null ? unpackedHeaders.getClass().toString() : "null"); - } - //noinspection unchecked (checked above) - Map headers = (Map) unpackedHeaders; - - Map body = null; - if (msgBytesStream.hasAvailable()) { - Object unpackedBody = getMsgPackLite().unpack(msgBytesStream); - if (!(unpackedBody instanceof Map)) { - //noinspection ConstantConditions - throw new CommunicationException("Error while unpacking body of tarantool response: " + - "expected type Map but was " + - unpackedBody != null ? unpackedBody.getClass().toString() : "null"); - } - //noinspection unchecked (checked above) - body = (Map) unpackedBody; - } - - return new TarantoolPacket(headers, body); - } - public static ByteBuffer createAuthPacket(String username, final String password, String salt) throws IOException { @@ -237,7 +253,7 @@ public static ByteBuffer createAuthPacket(String username, auth.add(p); return createPacket(DEFAULT_INITIAL_REQUEST_SIZE, Code.AUTH, 0L, null, - Key.USER_NAME, username, Key.TUPLE, auth); + Key.USER_NAME, username, Key.TUPLE, auth); } public static ByteBuffer createPacket(Code code, Long syncId, Long schemaId, Object... args) throws IOException { @@ -260,7 +276,7 @@ public static ByteBuffer createPacket(int initialRequestSize, Object... args) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(initialRequestSize); bos.write(new byte[5]); - DataOutputStream ds = new DataOutputStream(bos); + final DataOutputStream ds = new DataOutputStream(bos); Map header = new EnumMap<>(Key.class); Map body = new EnumMap<>(Key.class); header.put(Key.CODE, code); diff --git a/src/main/java/org/tarantool/protocol/ReadableViaSelectorChannel.java b/src/main/java/org/tarantool/protocol/ReadableViaSelectorChannel.java index 2a7a623d..f6565ba5 100644 --- a/src/main/java/org/tarantool/protocol/ReadableViaSelectorChannel.java +++ b/src/main/java/org/tarantool/protocol/ReadableViaSelectorChannel.java @@ -31,7 +31,9 @@ public ReadableViaSelectorChannel(SocketChannel channel) throws IOException { @Override public int read(ByteBuffer buffer) throws IOException { - int count, n; + int count; + int n; + count = n = channel.read(buffer); if (n < 0) { diff --git a/src/main/java/org/tarantool/protocol/TarantoolPacket.java b/src/main/java/org/tarantool/protocol/TarantoolPacket.java index d8c30707..ca131177 100644 --- a/src/main/java/org/tarantool/protocol/TarantoolPacket.java +++ b/src/main/java/org/tarantool/protocol/TarantoolPacket.java @@ -23,9 +23,11 @@ public Long getCode() { if (!(potenticalCode instanceof Long)) { //noinspection ConstantConditions - throw new IllegalStateException("A value contained in the header by key '" + Key.CODE.name() + "'" + - " is not instance of Long class: " + - potenticalCode != null ? potenticalCode.getClass().toString() : "null"); + throw new IllegalStateException( + "A value contained in the header by key '" + Key.CODE.name() + + "' is not instance of Long class: " + + potenticalCode != null ? potenticalCode.getClass().toString() : "null" + ); } return (Long) potenticalCode; diff --git a/src/main/java/org/tarantool/util/JdbcConstants.java b/src/main/java/org/tarantool/util/JdbcConstants.java new file mode 100644 index 00000000..dfbad572 --- /dev/null +++ b/src/main/java/org/tarantool/util/JdbcConstants.java @@ -0,0 +1,24 @@ +package org.tarantool.util; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLNonTransientException; +import java.sql.Statement; + +public class JdbcConstants { + + public static void checkGeneratedKeysConstant(int autoGeneratedKeys) throws SQLException { + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS && + autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + } + + public static void checkHoldabilityConstant(int holdability) throws SQLException { + if (holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT && + holdability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + } + +} diff --git a/src/main/java/org/tarantool/util/SQLStates.java b/src/main/java/org/tarantool/util/SQLStates.java index 48f7f332..89ac309d 100644 --- a/src/main/java/org/tarantool/util/SQLStates.java +++ b/src/main/java/org/tarantool/util/SQLStates.java @@ -2,8 +2,12 @@ public enum SQLStates { + TOO_MANY_RESULTS("0100E"), + NO_DATA("02000"), + CONNECTION_DOES_NOT_EXIST("08003"), INVALID_PARAMETER_VALUE("22023"), - CONNECTION_DOES_NOT_EXIST("08003"); + INVALID_CURSOR_STATE("24000"), + INVALID_TRANSACTION_STATE("25000"); private final String sqlState; diff --git a/src/main/java/org/tarantool/util/StringUtils.java b/src/main/java/org/tarantool/util/StringUtils.java new file mode 100644 index 00000000..7a289a3f --- /dev/null +++ b/src/main/java/org/tarantool/util/StringUtils.java @@ -0,0 +1,21 @@ +package org.tarantool.util; + +public class StringUtils { + + public static boolean isEmpty(String string) { + return (string == null) || (string.isEmpty()); + } + + public static boolean isNotEmpty(String string) { + return !isEmpty(string); + } + + public static boolean isBlank(String string) { + return (string == null) || (string.trim().isEmpty()); + } + + public static boolean isNotBlank(String string) { + return !isBlank(string); + } + +} diff --git a/src/test/java/org/tarantool/AbstractAsyncClientOperationsIT.java b/src/test/java/org/tarantool/AbstractAsyncClientOperationsIT.java index 8813c0ce..ec94eeb7 100644 --- a/src/test/java/org/tarantool/AbstractAsyncClientOperationsIT.java +++ b/src/test/java/org/tarantool/AbstractAsyncClientOperationsIT.java @@ -1,5 +1,11 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,8 +20,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static org.junit.jupiter.api.Assertions.*; - /** * Abstract class with test cases for asynchronous operations provided by getOps() method of a child class. */ @@ -87,8 +91,9 @@ public void testOperations() throws ExecutionException, InterruptedException, Ti futs.add(ops.call("box.space.basic_test:delete", Collections.singletonList(30))); // Wait completion of all operations. - for (Future> f : futs) + for (Future> f : futs) { f.get(TIMEOUT, TimeUnit.MILLISECONDS); + } // Check the effects. checkTupleResult(consoleSelect(SPACE_NAME, 10), Arrays.asList(10, "ten")); diff --git a/src/test/java/org/tarantool/AbstractSocketProviderTest.java b/src/test/java/org/tarantool/AbstractSocketProviderTest.java new file mode 100644 index 00000000..526e55b8 --- /dev/null +++ b/src/test/java/org/tarantool/AbstractSocketProviderTest.java @@ -0,0 +1,50 @@ +package org.tarantool; + +import static org.mockito.Mockito.anyObject; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; +import java.util.Collection; +import java.util.stream.Collectors; + +public class AbstractSocketProviderTest { + + protected String extractRawHostAndPortString(SocketAddress socketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + return inetSocketAddress.getAddress().getHostName() + ":" + inetSocketAddress.getPort(); + } + + protected Iterable asRawHostAndPort(Collection addresses) { + return addresses.stream() + .map(this::extractRawHostAndPortString) + .collect(Collectors.toList()); + } + + protected T wrapWithMockChannelProvider(T source) throws IOException { + T wrapper = spy(source); + doReturn(makeSocketChannel()).when(wrapper).openChannel(anyObject()); + return wrapper; + } + + protected T wrapWithMockErroredChannelProvider(T source) throws IOException { + T wrapper = spy(source); + doThrow(IOException.class).when(wrapper).openChannel(anyObject()); + return wrapper; + } + + private SocketChannel makeSocketChannel() { + SocketChannel socketChannel = mock(SocketChannel.class); + when(socketChannel.socket()).thenReturn(mock(Socket.class)); + + return socketChannel; + } + +} diff --git a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java index 739a1cf3..950bd131 100644 --- a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java +++ b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java @@ -1,11 +1,16 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.TestUtils.makeInstanceEnv; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.opentest4j.AssertionFailedError; -import java.math.BigInteger; import java.io.IOException; +import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; @@ -15,16 +20,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import static org.tarantool.TestUtils.makeInstanceEnv; - /** * Abstract test. Provides environment control and frequently used functions. */ public abstract class AbstractTarantoolConnectorIT { + protected static final String host = System.getProperty("tntHost", "localhost"); protected static final int port = Integer.parseInt(System.getProperty("tntPort", "3301")); protected static final int consolePort = Integer.parseInt(System.getProperty("tntConsolePort", "3313")); @@ -37,8 +37,8 @@ public abstract class AbstractTarantoolConnectorIT { protected static final int TIMEOUT = 500; protected static final int RESTART_TIMEOUT = 2000; - protected static final SocketChannelProvider socketChannelProvider = new TestSocketChannelProvider(host, port, - RESTART_TIMEOUT); + protected static final SocketChannelProvider socketChannelProvider = + new TestSocketChannelProvider(host, port, RESTART_TIMEOUT); protected static TarantoolControl control; protected static TarantoolConsole console; @@ -124,7 +124,7 @@ private static void executeLua(String[] exprs) { protected void checkTupleResult(Object res, List tuple) { assertNotNull(res); assertTrue(List.class.isAssignableFrom(res.getClass())); - List list = (List)res; + List list = (List) res; assertEquals(1, list.size()); assertNotNull(list.get(0)); assertTrue(List.class.isAssignableFrom(list.get(0).getClass())); @@ -143,19 +143,19 @@ protected static TarantoolClientConfig makeClientConfig() { return fillClientConfig(new TarantoolClientConfig()); } - protected static TarantoolClusterClientConfig makeClusterClientConfig() { + public static TarantoolClusterClientConfig makeClusterClientConfig() { TarantoolClusterClientConfig config = fillClientConfig(new TarantoolClusterClientConfig()); config.executor = null; config.operationExpiryTimeMillis = TIMEOUT; return config; } - private static T fillClientConfig(TarantoolClientConfig config) { + private static T fillClientConfig(T config) { config.username = username; config.password = password; config.initTimeoutMillis = RESTART_TIMEOUT; config.sharedBufferSize = 128; - return (T)config; + return (T) config; } protected static TarantoolConsole openConsole() { @@ -163,7 +163,7 @@ protected static TarantoolConsole openConsole() { } protected static TarantoolConsole openConsole(String instance) { - return TarantoolConsole.open(control.tntCtlWorkDir, instance); + return TarantoolConsole.open(TarantoolControl.tntCtlWorkDir, instance); } protected TarantoolConnection openConnection() { @@ -194,8 +194,9 @@ private void appendKey(StringBuilder sb, Object key) { if (List.class.isAssignableFrom(key.getClass())) { List parts = (List) key; for (int i = 0; i < parts.size(); i++) { - if (i != 0) + if (i != 0) { sb.append(", "); + } Object k = parts.get(i); if (k instanceof BigInteger) { appendBigInteger(sb, (BigInteger) k); @@ -234,12 +235,10 @@ protected List consoleDelete(String spaceName, Object key) { protected static void stopTarantool(String instance) { control.stop(instance); - control.waitStopped("jdk-testing"); } protected static void startTarantool(String instance) { control.start(instance); - control.waitStarted("jdk-testing"); } /** @@ -247,7 +246,7 @@ protected static void startTarantool(String instance) { * * @param timeout Timeout in ms. * @param message Error message. - * @param r Runnable. + * @param r Runnable. */ protected void assertTimeoutPreemptively(int timeout, String message, Runnable r) { ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -264,4 +263,5 @@ protected void assertTimeoutPreemptively(int timeout, String message, Runnable r executorService.shutdownNow(); } } + } diff --git a/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java b/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java index 5576a338..b39b4d75 100644 --- a/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java +++ b/src/test/java/org/tarantool/AbstractTarantoolOpsIT.java @@ -1,5 +1,11 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.tarantool.TestAssumptions.assumeMaximalServerVersion; +import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -8,10 +14,6 @@ import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - /** * Tests operations available in {@link TarantoolClientOps} interface. */ @@ -172,8 +174,7 @@ public void testUpdateMultiPart() { Arrays.asList("=", 2, "thirty")); } - private void checkUpdate(String space, int spaceId, List key, List initTuple, List expectedTuple, - Object ... ops) { + private void checkUpdate(String space, int spaceId, List key, List initTuple, List expectedTuple, Object... ops) { // Try update non-existing key. List res = getOps().update(spaceId, key, ops); assertNotNull(res); @@ -214,8 +215,7 @@ public void testUpsertMultiPart() { Arrays.asList("=", 2, "fourty")); } - private void checkUpsert(String space, int spaceId, List key, List defTuple, List expectedTuple, - Object ... ops) { + private void checkUpsert(String space, int spaceId, List key, List defTuple, List expectedTuple, Object... ops) { // Check that key doesn't exist. assertEquals(Collections.emptyList(), consoleSelect(space, key)); @@ -373,23 +373,38 @@ public void execute() throws Throwable { } @Test - public void testInsertInvalidData() { + void testInsertInvalidData() { // Invalid types. - TarantoolException ex = assertThrows(TarantoolException.class, new Executable() { - @Override - public void execute() throws Throwable { - getOps().insert(SPACE_ID, Arrays.asList("one", 1)); - } - }); + TarantoolException ex = assertThrows( + TarantoolException.class, + () -> getOps().insert(SPACE_ID, Arrays.asList("one", 1)) + ); assertEquals("Tuple field 1 type does not match one required by operation: expected integer", ex.getMessage()); + } + + @Test + public void testInsertInvalidTupleSize2xVersion() { + assumeMinimalServerVersion(console, ServerVersion.V_2_1); // Invalid tuple size. - ex = assertThrows(TarantoolException.class, new Executable() { - @Override - public void execute() throws Throwable { - getOps().insert(SPACE_ID, Collections.singletonList(101)); - } - }); + TarantoolException ex = assertThrows( + TarantoolException.class, + () -> getOps().insert(SPACE_ID, Collections.singletonList(101)) + ); assertEquals("Tuple field 2 required by space format is missing", ex.getMessage()); } + + @Test + public void testInsertInvalidTupleSize1xVersion() { + assumeMaximalServerVersion(console, ServerVersion.V_1_10); + + // Invalid tuple size. + TarantoolException ex = assertThrows( + TarantoolException.class, + () -> getOps().insert(SPACE_ID, Collections.singletonList(101)) + ); + assertEquals("Tuple field count 1 is less than required by space format or defined indexes " + + "(expected at least 2)", ex.getMessage()); + } + } diff --git a/src/test/java/org/tarantool/AbstractTarantoolSQLConnectorIT.java b/src/test/java/org/tarantool/AbstractTarantoolSQLConnectorIT.java new file mode 100644 index 00000000..46805575 --- /dev/null +++ b/src/test/java/org/tarantool/AbstractTarantoolSQLConnectorIT.java @@ -0,0 +1,117 @@ +package org.tarantool; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.tarantool.TestUtils.makeInstanceEnv; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Abstract test. Provides environment control and frequently used functions which are related to SQL. + */ +public abstract class AbstractTarantoolSQLConnectorIT { + + protected static final String HOST = System.getProperty("tntHost", "localhost"); + protected static final int PORT = Integer.parseInt(System.getProperty("tntPort", "3301")); + protected static final int CONSOLE_PORT = Integer.parseInt(System.getProperty("tntConsolePort", "3313")); + protected static final String USERNAME = System.getProperty("tntUser", "test_admin"); + protected static final String PASSWORD = System.getProperty("tntPass", "4pWBZmLEgkmKK5WP"); + + protected static final String LUA_FILE = "jdk-testing.lua"; + protected static final int LISTEN = 3301; + protected static final int ADMIN = 3313; + protected static final int RESTART_TIMEOUT = 2000; + + protected static final SocketChannelProvider socketChannelProvider = new TestSocketChannelProvider( + HOST, PORT, RESTART_TIMEOUT + ); + + protected static TarantoolControl control; + protected static TarantoolConsole console; + + @BeforeAll + public static void setupEnv() { + control = new TarantoolControl(); + control.createInstance("jdk-testing", LUA_FILE, makeInstanceEnv(LISTEN, ADMIN)); + startTarantool("jdk-testing"); + console = openConsole(); + } + + @AfterAll + public static void cleanupEnv() { + try { + console.close(); + } finally { + stopTarantool("jdk-testing"); + } + } + + protected static void executeLua(String[] exprs) { + for (String expr : exprs) { + console.exec(expr); + } + } + + protected void checkTupleResult(List> expected, List> actual) { + assertNotNull(expected); + assertEquals(expected, actual); + } + + protected List> asResult(Object[][] tuples) { + List> result = new ArrayList<>(); + if (tuples != null) { + for (int i = 0; i < tuples.length; i++) { + Object[] tuple = tuples[i]; + if (tuple.length % 2 != 0) { + continue; + } + Map row = new HashMap<>(); + for (int j = 0; j <= tuple.length / 2; j += 2) { + row.put(tuple[j].toString(), tuple[j + 1]); + } + result.add(row); + } + } + return result; + } + + protected TarantoolClient makeClient() { + return new TarantoolClientImpl(socketChannelProvider, makeClientConfig()); + } + + protected TarantoolClient makeClient(SocketChannelProvider provider) { + return new TarantoolClientImpl(provider, makeClientConfig()); + } + + protected static TarantoolClientConfig makeClientConfig() { + TarantoolClientConfig config = new TarantoolClientConfig(); + config.username = USERNAME; + config.password = PASSWORD; + config.initTimeoutMillis = RESTART_TIMEOUT; + config.sharedBufferSize = 128; + return config; + } + + protected static TarantoolConsole openConsole() { + return TarantoolConsole.open(HOST, CONSOLE_PORT); + } + + protected static TarantoolConsole openConsole(String instance) { + return TarantoolConsole.open(control.tntCtlWorkDir, instance); + } + + protected static void stopTarantool(String instance) { + control.stop(instance); + } + + protected static void startTarantool(String instance) { + control.start(instance); + } + +} diff --git a/src/test/java/org/tarantool/AbstractTarantoolSQLOpsIT.java b/src/test/java/org/tarantool/AbstractTarantoolSQLOpsIT.java new file mode 100644 index 00000000..2e84a857 --- /dev/null +++ b/src/test/java/org/tarantool/AbstractTarantoolSQLOpsIT.java @@ -0,0 +1,86 @@ +package org.tarantool; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +/** + * Tests operations available in {@link TarantoolSQLOps} interface. + */ +public abstract class AbstractTarantoolSQLOpsIT extends AbstractTarantoolSQLConnectorIT { + + private static final String[] SETUP_SCRIPT = new String[] { + "\\set language sql", + + "CREATE TABLE sql_test (id INTEGER PRIMARY KEY, val VARCHAR(100));", + "CREATE UNIQUE INDEX sql_test_val_index_unique ON sql_test (val);", + + "INSERT INTO sql_test VALUES (1, 'A');", + "INSERT INTO sql_test VALUES (2, 'B');", + "INSERT INTO sql_test VALUES (3, 'C');" + }; + + private static final String[] CLEAN_SCRIPT = new String[] { + "DROP TABLE sql_test;", + "\\set language lua" + }; + + protected abstract TarantoolSQLOps>> getSQLOps(); + + @BeforeEach + void setUpTest() { + assumeMinimalServerVersion(console, ServerVersion.V_2_1); + executeLua(SETUP_SCRIPT); + } + + @AfterEach + void tearDownTest() { + executeLua(CLEAN_SCRIPT); + } + + @Test + public void testSelectOne() { + List> result = getSQLOps().query("SELECT id, val FROM sql_test WHERE id = 1"); + checkTupleResult( + asResult(new Object[][] { {"ID", 1, "VAL", "A"} }), + result + ); + } + + @Test + public void testSelectMany() { + List> result = getSQLOps().query("SELECT id, val FROM sql_test WHERE id = 1 or val = 'B'"); + checkTupleResult( + asResult(new Object[][] { {"ID", 1, "VAL", "A"}, {"ID", 2, "VAL", "B"} }), + result + ); + } + + @Test + public void testSelectEmpty() { + List> result = getSQLOps().query("SELECT id, val FROM sql_test WHERE val = 'AB'"); + checkTupleResult( + asResult(new Object[][] { }), + result + ); + } + + @Test + public void testInsertOneRecord() { + Long rowsAffected = getSQLOps().update("INSERT INTO sql_test VALUES (27, 'Z');"); + assertEquals(1L, (long) rowsAffected); + } + + @Test + public void testInsertDuplication() { + assertThrows(TarantoolException.class, () -> getSQLOps().update("INSERT INTO sql_test VALUES (1, 'A');")); + } + +} diff --git a/src/test/java/org/tarantool/ClientComposableAsyncOpsIT.java b/src/test/java/org/tarantool/ClientComposableAsyncOpsIT.java index b1d75e31..04d0059e 100644 --- a/src/test/java/org/tarantool/ClientComposableAsyncOpsIT.java +++ b/src/test/java/org/tarantool/ClientComposableAsyncOpsIT.java @@ -9,11 +9,14 @@ */ public class ClientComposableAsyncOpsIT extends AbstractAsyncClientOperationsIT { - private static class Composable2FutureClientOpsAdapter implements TarantoolClientOps, Object, Future>> { + private static class Composable2FutureClientOpsAdapter + implements TarantoolClientOps, Object, Future>> { private final TarantoolClientOps, Object, CompletionStage>> originOps; - private Composable2FutureClientOpsAdapter(TarantoolClientOps, Object, CompletionStage>> originOps) { + private Composable2FutureClientOpsAdapter( + TarantoolClientOps, Object, CompletionStage>> originOps) { + this.originOps = originOps; } @@ -23,7 +26,12 @@ public Future> select(Integer space, Integer index, List key, int off } @Override - public Future> select(Integer space, Integer index, List key, int offset, int limit, Iterator iterator) { + public Future> select(Integer space, + Integer index, + List key, + int offset, + int limit, + Iterator iterator) { return originOps.select(space, index, key, offset, limit, iterator).toCompletableFuture(); } diff --git a/src/test/java/org/tarantool/ClientOperationsIT.java b/src/test/java/org/tarantool/ClientOperationsIT.java index 22d1965a..f752bd48 100644 --- a/src/test/java/org/tarantool/ClientOperationsIT.java +++ b/src/test/java/org/tarantool/ClientOperationsIT.java @@ -1,5 +1,8 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -7,9 +10,6 @@ import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - /** * Tests for synchronous operations of {@link TarantoolClientImpl} class. * diff --git a/src/test/java/org/tarantool/ClientReconnectClusterIT.java b/src/test/java/org/tarantool/ClientReconnectClusterIT.java index 35737a32..fefdd337 100644 --- a/src/test/java/org/tarantool/ClientReconnectClusterIT.java +++ b/src/test/java/org/tarantool/ClientReconnectClusterIT.java @@ -1,27 +1,43 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.tarantool.AbstractTarantoolConnectorIT.makeClusterClientConfig; +import static org.tarantool.TestUtils.makeDiscoveryFunction; +import static org.tarantool.TestUtils.makeInstanceEnv; + +import org.tarantool.cluster.ClusterServiceStoredFunctionDiscovererIT; + import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.tarantool.AbstractTarantoolConnectorIT.makeClusterClientConfig; -import static org.tarantool.TestUtils.makeInstanceEnv; - +@DisplayName("A cluster client") public class ClientReconnectClusterIT { + + private static final String SCHEMA_PATTERN = + "return box.schema.space.create('%1$s').id, box.space.%1$s:create_index('primary').id"; + private static final int TIMEOUT = 500; private static final String LUA_FILE = "jdk-testing.lua"; private static final String SRV1 = "replica1"; private static final String SRV2 = "replica2"; private static final String SRV3 = "replica3"; - private static final int[] PORTS = {3302, 3303, 3304}; - private static final int[] CONSOLE_PORTS = {3312, 3313, 3314}; + private static final int[] PORTS = { 3401, 3402, 3403 }; + private static final int[] CONSOLE_PORTS = { 3501, 3502, 3503 }; private static TarantoolControl control; private static String REPLICATION_CONFIG = TestUtils.makeReplicationString( @@ -29,7 +45,8 @@ public class ClientReconnectClusterIT { AbstractTarantoolConnectorIT.password, "localhost:" + PORTS[0], "localhost:" + PORTS[1], - "localhost:" + PORTS[2]); + "localhost:" + PORTS[2] + ); // Resume replication faster in case of temporary failure to fit TIMEOUT. private static double REPLICATION_TIMEOUT = 0.1; @@ -38,10 +55,13 @@ public class ClientReconnectClusterIT { public static void setupEnv() { control = new TarantoolControl(); int idx = 0; - for (String name: Arrays.asList(SRV1, SRV2, SRV3)) { + for (String name : Arrays.asList(SRV1, SRV2, SRV3)) { control.createInstance(name, LUA_FILE, - makeInstanceEnv(PORTS[idx], CONSOLE_PORTS[idx], REPLICATION_CONFIG, - REPLICATION_TIMEOUT)); + makeInstanceEnv( + PORTS[idx], CONSOLE_PORTS[idx], + REPLICATION_CONFIG, REPLICATION_TIMEOUT + ) + ); idx++; } } @@ -57,61 +77,407 @@ public static void tearDownEnv() { } } - @Test - public void testRoundRobinReconnect() { - control.start(SRV1); - control.start(SRV2); - control.start(SRV3); + @BeforeEach + public void setUpTest() { + startInstancesAndAwait(SRV1, SRV2, SRV3); + } - control.waitStarted(SRV1); - control.waitStarted(SRV2); - control.waitStarted(SRV3); + @AfterEach + public void tearDownTest() { + stopInstancesAndAwait(SRV1, SRV2, SRV3); + } - final TarantoolClientImpl client = makeClient( + @Test + @DisplayName("reconnected to another node after the current node had disappeared") + public void testRoundRobinReconnect() { + final TarantoolClientImpl client = makeClusterClient( "localhost:" + PORTS[0], "127.0.0.1:" + PORTS[1], - "localhost:" + PORTS[2]); + "localhost:" + PORTS[2] + ); + + int[] ids = makeAndFillTestSpace(client, "rr_test1"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV1); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV2); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV3); + expectDisconnected(client, spaceId, pkId); + } + + /** + * Before fetch client = { srv1 } + * After fetch client = { srv1, srv2 } + *

+ * 1. fetch nodes - ok (client will apply { srv1, srv2 } as a new nodes list) + * 2. shutdown srv1 - ok (client will reconnect to srv2) + * 3. shutdown srv2 - fail (there are no available nodes anymore) + */ + @Test + @DisplayName("applied new nodes and reconnected to another node") + void testUpdateExtendedNodeList() { + String service1Address = "localhost:" + PORTS[0]; + String service2Address = "127.0.0.1:" + PORTS[1]; + String service3Address = "localhost:" + PORTS[2]; + + CyclicBarrier barrier = new CyclicBarrier(2); + + String infoFunctionName = "getAddresses"; + String infoFunctionScript = + makeDiscoveryFunction(infoFunctionName, Arrays.asList(service1Address, service2Address)); + + control.openConsole(SRV1).exec(infoFunctionScript); - List ids = client.syncOps().eval( - "return box.schema.space.create('rr_test').id, " + - "box.space.rr_test:create_index('primary').id"); + final TarantoolClusterClient client = makeClientWithDiscoveryFeature( + infoFunctionName, + 0, + (ignored) -> tryAwait(barrier), + service1Address + ); - final int spaceId = ((Number)ids.get(0)).intValue(); - final int pkId = ((Number)ids.get(1)).intValue(); + int[] ids = makeAndFillTestSpace(client, "rr_test2"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + tryAwait(barrier); // client = { srv1 }; wait for { srv1, srv2 } + + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV1); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV2); + expectDisconnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV3); + } + + /** + * Before fetch client = { srv1, srv2 } + * After fetch client = { srv1 } + *

+ * 1. fetch nodes - ok (client will apply the narrowed { srv1 } + * 2. shutdown srv1 - fail (client will not reconnect to srv2 because latest is out of the list) + */ + @Test + @DisplayName("applied new nodes and stayed connected to the current node") + void testUpdateNarrowNodeList() { + String service1Address = "localhost:" + PORTS[0]; + String service2Address = "127.0.0.1:" + PORTS[1]; + + CyclicBarrier barrier = new CyclicBarrier(2); + + String infoFunctionName = "getAddresses"; + String infoFunctionScript = makeDiscoveryFunction(infoFunctionName, Collections.singletonList(service1Address)); + + control.openConsole(SRV1).exec(infoFunctionScript); + + final TarantoolClusterClient client = makeClientWithDiscoveryFeature( + infoFunctionName, + 0, + (ignored) -> tryAwait(barrier), + service1Address, + service2Address + ); + + int[] ids = makeAndFillTestSpace(client, "rr_test3"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + tryAwait(barrier); // client = { srv1, srv2 }; wait for { srv1 } + + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV1); + expectDisconnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV2); + stopInstancesAndAwait(SRV3); + } + + /** + * Before fetch client = { srv1, srv2, srv3 } + * After fetch client = { srv1, srv2, srv3 } + *

+ * 1. fetch nodes - ok (client will ignore the same list) + * 2. shutdown srv1 - ok + * 3. shutdown srv2 - ok + * 4. shutdown srv3 - fail + */ + @Test + @DisplayName("applied empty list and stayed connected to the current node") + void testUpdateEmptyNodeList() { + String service1Address = "localhost:" + PORTS[0]; + String service2Address = "127.0.0.1:" + PORTS[1]; + String service3Address = "localhost:" + PORTS[2]; + + String infoFunctionName = "getAddresses"; + String infoFunctionScript = makeDiscoveryFunction(infoFunctionName, Collections.emptyList()); + + control.openConsole(SRV1).exec(infoFunctionScript); + + final TarantoolClusterClient client = makeClientWithDiscoveryFeature( + infoFunctionName, + service1Address, + service2Address, + service3Address + ); + + int[] ids = makeAndFillTestSpace(client, "rr_test4"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV1); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV2); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV3); + expectDisconnected(client, spaceId, pkId); + } + + /** + * Before fetch client = { srv1, srv2, srv3 } + * After fetch client = { srv1, srv2, srv3 } + *

+ * 1. fetch with an exception (i.e. missing/error-prone function) - ok (client will ignore the failure) + * 2. shutdown srv1 - ok + * 3. shutdown srv2 - ok + * 4. shutdown srv3 - fail + * + * @see ClusterServiceStoredFunctionDiscovererIT#testFunctionWithError() + */ + @Test + @DisplayName("applied nothing and stayed connected to the current node") + void testWrongConfigFetch() { + String service1Address = "localhost:" + PORTS[0]; + String service2Address = "127.0.0.1:" + PORTS[1]; + String service3Address = "localhost:" + PORTS[2]; + String infoFunctionName = "getAddresses"; + String infoFunctionScript = makeDiscoveryFunction(infoFunctionName, Collections.emptyList()); + + control.openConsole(SRV1).exec(infoFunctionScript); + + final TarantoolClusterClient client = makeClientWithDiscoveryFeature( + infoFunctionName + "wrongSuffix", + service1Address, + service2Address, + service3Address + ); + + int[] ids = makeAndFillTestSpace(client, "rr_test5"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV1); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV2); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV3); + expectDisconnected(client, spaceId, pkId); + } + + /** + * Before fetch client = { srv1, srv2, srv3 } + * After fetch client = { srv1, srv2, srv3 } + *

+ * 1. fetch an improper result - ok (client will ignore the awkward data) + * 2. shutdown srv1 - ok + * 3. shutdown srv2 - ok + * 4. shutdown srv3 - fail + */ + @Test + @DisplayName("ignored an wrong function result and stayed connected to the current node") + void testWrongFunctionResultFetch() { + String service1Address = "localhost:" + PORTS[0]; + String service2Address = "127.0.0.1:" + PORTS[1]; + String service3Address = "localhost:" + PORTS[2]; + + String infoFunctionName = "getWhateverExceptAddressesListFunction"; + String infoFunctionScript = makeDiscoveryFunction(infoFunctionName, 42); + + control.openConsole(SRV1).exec(infoFunctionScript); + + final TarantoolClusterClient client = makeClientWithDiscoveryFeature( + infoFunctionName, + service1Address, + service2Address, + service3Address + ); + + int[] ids = makeAndFillTestSpace(client, "rr_test6"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV1); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV2); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV3); + expectDisconnected(client, spaceId, pkId); + } + + /** + * Before fetch client = { srv1 } + * After fetch ph1 client = { srv1 } + * After fetch ph2 client = { srv2 } + * After fetch ph3 client = { srv3 } + *

+ * 1. fetch an initial result (ph1) - ok (client will ignore the same data) + * 2. fetch the 2nd result (ph2) - ok (client will reconnect to srv2) + * 3. shutdown srv1 - ok + * 4. fetch the 3rd result (ph3) - ok (client will reconnect to srv3) + * 5. shutdown srv2 - ok + * 6. shutdown srv3 - fail + */ + @Test + @DisplayName("applied each second a new cluster node and reconnected to it") + void testDelayFunctionResultFetch() { + String service1Address = "localhost:" + PORTS[0]; + String service2Address = "127.0.0.1:" + PORTS[1]; + String service3Address = "localhost:" + PORTS[2]; + + CyclicBarrier barrier = new CyclicBarrier(2); + + String infoFunctionName = "getAddressesFunction"; + String functionBody = Stream.of(service1Address, service2Address) + .map(address -> "coroutine.yield('" + address + "');") + .collect(Collectors.joining(" ")); + + control.openConsole(SRV1) + .exec("co = coroutine.create(function() " + functionBody + " end)"); + control.openConsole(SRV1) + .exec("function getAddressesFunction() local c, r = coroutine.resume(co); return r end"); + + String infoFunctionScript = makeDiscoveryFunction(infoFunctionName, Collections.singletonList(service3Address)); + control.openConsole(SRV2).exec(infoFunctionScript); + + final TarantoolClusterClient client = makeClientWithDiscoveryFeature( + infoFunctionName, + 3000, + (ignored) -> tryAwait(barrier), + service1Address + ); + + int[] ids = makeAndFillTestSpace(client, "rr_test7"); + final int spaceId = ids[0]; + final int pkId = ids[1]; + + tryAwait(barrier); // client = { srv1 }; wait for { srv1 } + + expectConnected(client, spaceId, pkId); + + tryAwait(barrier); // client = { srv1 }; wait for { srv2 } + + stopInstancesAndAwait(SRV1); + expectConnected(client, spaceId, pkId); + + tryAwait(barrier); // client = { srv2 }; wait for { srv3 } + + stopInstancesAndAwait(SRV2); + expectConnected(client, spaceId, pkId); + + stopInstancesAndAwait(SRV3); + expectDisconnected(client, spaceId, pkId); + } + + private void tryAwait(CyclicBarrier barrier) { + try { + barrier.await(6000, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private void startInstancesAndAwait(String... instances) { + for (String instance : instances) { + control.start(instance, false); + } + for (String instance : instances) { + control.waitStarted(instance); + } + } + + private void stopInstancesAndAwait(String... instances) { + for (String instance : instances) { + control.stop(instance); + } + } + + private void expectConnected(TarantoolClientImpl client, int spaceId, int pkId) { final List key = Collections.singletonList(1); final List tuple = Arrays.asList(1, 1); - client.syncOps().insert(spaceId, tuple); - control.waitReplication(SRV1, TIMEOUT); - List res = client.syncOps().select(spaceId, pkId, key, 0, 1, Iterator.EQ); assertEquals(res.get(0), tuple); + } - control.stop(SRV1); + private void expectDisconnected(TarantoolClientImpl client, int spaceId, int pkId) { + final List key = Collections.singletonList(1); - res = client.syncOps().select(spaceId, pkId, key, 0, 1, Iterator.EQ); - assertEquals(res.get(0), Arrays.asList(1, 1)); + assertThrows( + CommunicationException.class, + () -> client.syncOps().select(spaceId, pkId, key, 0, 1, Iterator.EQ) + ); + } - control.stop(SRV2); + private int[] makeAndFillTestSpace(TarantoolClientImpl client, String spaceName) { + List ids = client.syncOps().eval(String.format(SCHEMA_PATTERN, spaceName)); - res = client.syncOps().select(spaceId, pkId, key, 0, 1, Iterator.EQ); - assertEquals(res.get(0), Arrays.asList(1, 1)); + final int spaceId = ((Number) ids.get(0)).intValue(); + final int pkId = ((Number) ids.get(1)).intValue(); - control.stop(SRV3); + client.syncOps().insert(spaceId, Arrays.asList(1, 1)); + control.waitReplication(SRV1, TIMEOUT); - CommunicationException e = assertThrows(CommunicationException.class, new Executable() { - @Override - public void execute() throws Throwable { - client.syncOps().select(spaceId, pkId, key, 0, 1, Iterator.EQ); - } - }); + return new int[] { spaceId, pkId }; + } + + private TarantoolClusterClient makeClusterClient(String... addresses) { + return makeClientWithDiscoveryFeature(null, addresses); + } - assertEquals("Connection time out.", e.getMessage()); + private TarantoolClusterClient makeClientWithDiscoveryFeature(String entryFunction, + String... addresses) { + return makeClientWithDiscoveryFeature(entryFunction, 0, null, addresses); } - private TarantoolClientImpl makeClient(String...addrs) { + private TarantoolClusterClient makeClientWithDiscoveryFeature(String entryFunction, + int entryDelayMillis, + Consumer> consumer, + String... addresses) { TarantoolClusterClientConfig config = makeClusterClientConfig(); - return new TarantoolClusterClient(config, addrs); + config.clusterDiscoveryEntryFunction = entryFunction; + config.clusterDiscoveryDelayMillis = entryDelayMillis; + + return new TarantoolClusterClient(config, addresses) { + @Override + protected void onInstancesRefreshed(Set instances) { + super.onInstancesRefreshed(instances); + if (consumer != null) { + consumer.accept(instances); + } + } + }; } + } diff --git a/src/test/java/org/tarantool/ClientReconnectIT.java b/src/test/java/org/tarantool/ClientReconnectIT.java index 59c4011a..b0a3e80e 100644 --- a/src/test/java/org/tarantool/ClientReconnectIT.java +++ b/src/test/java/org/tarantool/ClientReconnectIT.java @@ -1,5 +1,12 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -18,15 +25,8 @@ import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.concurrent.locks.LockSupport; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - public class ClientReconnectIT extends AbstractTarantoolConnectorIT { + private static final String INSTANCE_NAME = "jdk-testing"; private TarantoolClient client; @@ -100,7 +100,7 @@ public SocketChannel get(int retryNumber, Throwable lastError) { client.syncOps().ping(); // The park() will return inside connector thread. - LockSupport.unpark(((TarantoolClientImpl)client).connector); + LockSupport.unpark(((TarantoolClientImpl) client).connector); // Wait on latch as a proof that reconnect did not happen. // In case of a failure, latch will reach 0 before timeout occurs. @@ -214,11 +214,54 @@ public void run() { }); } + // DO NOT REMOVE THIS TEST + // Motivation: this test checks start/stop correctness + // of TarantoolControl class which is used by other tests. + // This test is commented out because the class is used + // for internal purposes only and isn't related to + // the connector testing. + // @Test + // @DisplayName("follow up the issue #164") + // void testStartStopTarantoolInstance() throws InterruptedException { + // int numberOfParallelInstances = 4; + // CountDownLatch finished = new CountDownLatch(numberOfParallelInstances); + // List instancesNames = new ArrayList<>(numberOfParallelInstances); + // + // for (int i = 0; i < numberOfParallelInstances; i++) { + // String instance = "startStop" + (i + 1); + // instancesNames.add(instance); + // control.createInstance( + // instancesNames.get(i), + // LUA_FILE, + // makeInstanceEnv(3401 + i + 1, 3501 + i + 1) + // ); + // startTarantool(instancesNames.get(i)); + // new Thread(() -> { + // for (int j = 0; j < 100; j++) { + // stopTarantool(instance); + // startTarantool(instance); + // if (j % 10 == 0) { + // System.out.println( + // Thread.currentThread().getName() + ": " + j + "% completed" + // ); + // } + // } + // finished.countDown(); + // }, "Thread" + (i + 1)).start(); + // } + // + // assertTrue(finished.await(2, TimeUnit.MINUTES)); + // + // for (int i = 0; i < numberOfParallelInstances; i++) { + // stopTarantool(instancesNames.get(i)); + // } + // } + /** * Test concurrent operations, reconnects and close. - * + *

* Expected situation is nothing gets stuck. - * + *

* The test sets SO_LINGER to 0 for outgoing connections to avoid producing * many TIME_WAIT sockets, because an available port range can be * exhausted. @@ -227,13 +270,13 @@ public void run() { public void testLongParallelCloseReconnects() { int numThreads = 4; int numClients = 4; - int timeBudget = 30*1000; + int timeBudget = 30 * 1000; SocketChannelProvider provider = new TestSocketChannelProvider(host, port, RESTART_TIMEOUT).setSoLinger(0); final AtomicReferenceArray clients = - new AtomicReferenceArray(numClients); + new AtomicReferenceArray<>(numClients); for (int idx = 0; idx < clients.length(); idx++) { clients.set(idx, makeClient(provider)); @@ -250,11 +293,8 @@ public void testLongParallelCloseReconnects() { threads[idx] = new Thread(new Runnable() { @Override public void run() { - while (!Thread.currentThread().isInterrupted() && - deadline > System.currentTimeMillis()) { - + while (!Thread.currentThread().isInterrupted() && deadline > System.currentTimeMillis()) { int idx = rnd.nextInt(clients.length()); - try { TarantoolClient cli = clients.get(idx); @@ -293,15 +333,17 @@ public void run() { fail(e); } if (deadline > System.currentTimeMillis()) { - System.out.println("testLongParallelCloseReconnects: " + - (deadline - System.currentTimeMillis()) / 1000 + - "s remain"); + System.out.println( + "testLongParallelCloseReconnects: " + + (deadline - System.currentTimeMillis()) / 1000 + + "s remain" + ); } } // Wait for all threads to finish. try { - assertTrue(latch.await(RESTART_TIMEOUT, TimeUnit.MILLISECONDS)); + assertTrue(latch.await(RESTART_TIMEOUT * 2, TimeUnit.MILLISECONDS)); } catch (InterruptedException e) { fail(e); } @@ -318,7 +360,7 @@ public void run() { * Verify that we don't exceed a file descriptor limit (and so likely don't * leak file descriptors) when trying to connect to an existing node with * wrong authentification credentials. - * + *

* The test sets SO_LINGER to 0 for outgoing connections to avoid producing * many TIME_WAIT sockets, because an available port range can be * exhausted. @@ -331,9 +373,9 @@ public void testReconnectWrongAuth() throws Exception { config.initTimeoutMillis = 100; config.password = config.password + 'x'; for (int i = 0; i < 100; ++i) { - if (i % 10 == 0) - System.out.println("testReconnectWrongAuth: " + (100 - i) + - " iterations remain"); + if (i % 10 == 0) { + System.out.println("testReconnectWrongAuth: " + (100 - i) + " iterations remain"); + } CommunicationException e = assertThrows(CommunicationException.class, new Executable() { @Override @@ -344,7 +386,8 @@ public void execute() throws Throwable { ); assertEquals(e.getMessage(), "100ms is exceeded when waiting " + "for client initialization. You could configure init " + - "timeout in TarantoolConfig"); + "timeout in TarantoolConfig" + ); } /* @@ -355,4 +398,5 @@ public void execute() throws Throwable { client.syncOps().ping(); client.close(); } + } diff --git a/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java b/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java index 3d81fbe2..335c54b5 100644 --- a/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java +++ b/src/test/java/org/tarantool/FireAndForgetClientOperationsIT.java @@ -1,5 +1,9 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,10 +15,6 @@ import java.util.List; import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; - /** * Test "fire & forget" operations available in {@link TarantoolClientImpl} class. */ diff --git a/src/test/java/org/tarantool/IteratorTest.java b/src/test/java/org/tarantool/IteratorTest.java index 9b4be7d9..7fd68b61 100644 --- a/src/test/java/org/tarantool/IteratorTest.java +++ b/src/test/java/org/tarantool/IteratorTest.java @@ -1,13 +1,14 @@ package org.tarantool; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; -import static org.mockito.Mockito.*; - - class IteratorTest { protected class MockOps extends AbstractTarantoolOps, Object, List> { @@ -31,4 +32,4 @@ void testSelectWithIteratorInsteadOfInteger() { verify(spyOps, times(1)).select(1, 1, new ArrayList(), 0, 1, 0); } -} \ No newline at end of file +} diff --git a/src/test/java/org/tarantool/RoundRobinSocketProviderImplTest.java b/src/test/java/org/tarantool/RoundRobinSocketProviderImplTest.java new file mode 100644 index 00000000..ee1f2563 --- /dev/null +++ b/src/test/java/org/tarantool/RoundRobinSocketProviderImplTest.java @@ -0,0 +1,155 @@ +package org.tarantool; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@DisplayName("A RR socket provider") +public class RoundRobinSocketProviderImplTest extends AbstractSocketProviderTest { + + @Test + @DisplayName("initialized with a right addresses count") + public void testAddressesCount() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost:3301", "127.0.0.1:3302", "10.0.0.10:3303"); + assertEquals(3, socketProvider.getAddressCount()); + + socketProvider.refreshAddresses(Collections.singletonList("10.0.0.1")); + assertEquals(1, socketProvider.getAddressCount()); + } + + @Test + @DisplayName("initialized with a right addresses values") + public void testAddresses() { + String[] addresses = {"localhost:3301", "127.0.0.2:3302", "10.0.0.10:3303"}; + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl(addresses); + assertIterableEquals(Arrays.asList(addresses), asRawHostAndPort(socketProvider.getAddresses())); + + List strings = Collections.singletonList("10.0.0.1:3310"); + socketProvider.refreshAddresses(strings); + assertIterableEquals(strings, asRawHostAndPort(socketProvider.getAddresses())); + } + + @Test + @DisplayName("initialized failed when an empty addresses list is provided") + public void testEmptyAddresses() { + assertThrows(IllegalArgumentException.class, RoundRobinSocketProviderImpl::new); + } + + @Test + @DisplayName("changed addresses list with a failure when a new list is empty") + public void testResultWithEmptyAddresses() throws IOException { + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockChannelProvider(new RoundRobinSocketProviderImpl("localhost:3301")); + + assertThrows(IllegalArgumentException.class, () -> socketProvider.refreshAddresses(Collections.emptyList())); + } + + @Test + @DisplayName("changed addresses list with a failure when a new list is null") + public void testResultWithWrongAddress() throws IOException { + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockChannelProvider(new RoundRobinSocketProviderImpl("localhost:3301")); + + assertThrows(IllegalArgumentException.class, () -> socketProvider.refreshAddresses(null)); + } + + @Test + @DisplayName("initialized with a default timeout") + public void testDefaultTimeout() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost"); + assertEquals(RoundRobinSocketProviderImpl.NO_TIMEOUT, socketProvider.getConnectionTimeout()); + } + + @Test + @DisplayName("changed its timeout to new value") + public void testChangingTimeout() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost"); + int expectedTimeout = 10_000; + socketProvider.setConnectionTimeout(expectedTimeout); + assertEquals(expectedTimeout, socketProvider.getConnectionTimeout()); + } + + @Test + @DisplayName("changed to negative timeout with a failure") + public void testWrongChangingTimeout() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost"); + int negativeValue = -200; + assertThrows(IllegalArgumentException.class, () -> socketProvider.setConnectionTimeout(negativeValue)); + } + + @Test + @DisplayName("produced socket channels using a ring pool") + public void testAddressRingPool() throws IOException { + String[] addresses = {"localhost:3301", "10.0.0.1:3302", "10.0.0.2:3309"}; + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockChannelProvider(new RoundRobinSocketProviderImpl(addresses)); + + for (int i = 0; i < 27; i++) { + socketProvider.get(0, null); + assertEquals(addresses[i % 3], extractRawHostAndPortString(socketProvider.getLastObtainedAddress())); + } + } + + @Test + @DisplayName("produced socket channels for the same instance") + public void testOneAddressPool() throws IOException { + String expectedAddress = "10.0.0.1:3301"; + String[] addresses = {expectedAddress}; + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockChannelProvider(new RoundRobinSocketProviderImpl(addresses)); + + for (int i = 0; i < 5; i++) { + socketProvider.get(0, null); + assertEquals(expectedAddress, extractRawHostAndPortString(socketProvider.getLastObtainedAddress())); + } + } + + @Test + @DisplayName("produced socket channel with an exception when an attempt number is over") + public void testTooManyAttempts() throws IOException { + String expectedAddress = "10.0.0.1:3301"; + String[] addresses = {expectedAddress}; + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockChannelProvider(new RoundRobinSocketProviderImpl(addresses)); + + int retriesLimit = addresses.length + 1; + socketProvider.setRetriesLimit(retriesLimit); + + for (int i = 0; i < retriesLimit; i++) { + socketProvider.get(i, null); + assertEquals(expectedAddress, extractRawHostAndPortString(socketProvider.getLastObtainedAddress())); + } + + assertThrows(CommunicationException.class, () -> socketProvider.get(retriesLimit, null)); + } + + @Test + @DisplayName("produced a socket channel with a failure when an unreachable address is provided") + public void testWrongAddress() throws IOException { + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockErroredChannelProvider(new RoundRobinSocketProviderImpl("unreachable-host:3301")); + assertThrows(SocketProviderTransientException.class, () -> socketProvider.get(0, null)); + } + + @Test + @DisplayName("produced a socket channel with a failure with an unreachable address after refresh") + public void testWrongRefreshAddress() throws IOException { + RoundRobinSocketProviderImpl socketProvider + = wrapWithMockErroredChannelProvider(new RoundRobinSocketProviderImpl("unreachable-host:3301")); + assertThrows(SocketProviderTransientException.class, () -> socketProvider.get(0, null)); + } + +} diff --git a/src/test/java/org/tarantool/SQLOperationsIT.java b/src/test/java/org/tarantool/SQLOperationsIT.java new file mode 100644 index 00000000..05b85906 --- /dev/null +++ b/src/test/java/org/tarantool/SQLOperationsIT.java @@ -0,0 +1,35 @@ +package org.tarantool; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.util.List; +import java.util.Map; + +/** + * Tests for synchronous operations of {@link TarantoolClientImpl#sqlSyncOps()} implementation. + *

+ * Actual tests reside in base class. + */ +public class SQLOperationsIT extends AbstractTarantoolSQLOpsIT { + + private TarantoolClient client; + + @BeforeEach + public void setup() { + client = makeClient(); + } + + @AfterEach + public void tearDown() { + if (client != null) { + client.close(); + } + } + + @Override + protected TarantoolSQLOps>> getSQLOps() { + return client.sqlSyncOps(); + } + +} diff --git a/src/test/java/org/tarantool/ServerVersion.java b/src/test/java/org/tarantool/ServerVersion.java new file mode 100644 index 00000000..5877bcca --- /dev/null +++ b/src/test/java/org/tarantool/ServerVersion.java @@ -0,0 +1,74 @@ +package org.tarantool; + +import java.util.function.BiFunction; + +public enum ServerVersion { + + V_1_9("1", "9", "0"), + V_1_10("1", "10", "0"), + V_2_1("2", "1", "0"), + V_2_2("2", "2", "0"); + + private final String majorVersion; + private final String minorVersion; + private final String patchVersion; + + ServerVersion(String majorVersion, + String minorVersion, String patchVersion) { + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + this.patchVersion = patchVersion; + } + + public String getMajorVersion() { + return majorVersion; + } + + public String getMinorVersion() { + return minorVersion; + } + + public String getPatchVersion() { + return patchVersion; + } + + public boolean haveMinimalVersion(String versionString) { + return compareVersions(versionString, (server, minimal) -> server >= minimal); + } + + public boolean haveMaximalVersion(String versionString) { + return compareVersions(versionString, (server, maximal) -> server <= maximal); + } + + private boolean compareVersions(String versionString, BiFunction comparator) { + int parsedVersion = toNumber(splitVersionParts(versionString)); + int thisVersion = toNumber(new String[] { majorVersion, minorVersion, patchVersion }); + return comparator.apply(parsedVersion, thisVersion); + } + + /** + * Translates version parts to format XXXYYYZZZ. + * For example, {@code 1.2.1} translates to number {@code 1002001} + * + * @param parts version parts + * @return version as number + */ + private int toNumber(String[] parts) { + int version = 0; + for (int i = 0; i < 3; i++) { + version = (version + Integer.parseInt(parts[i])) * 1000; + } + return version / 1000; + } + + /** + * Splits Tarantool version string into parts. + * For example, {@code 2.1.1-423-g4007436aa} => {@code [2, 1, 1, 423, g4007436aa]}. + * + * @param version Tarantool version string + * @return split parts + */ + private String[] splitVersionParts(String version) { + return version.split("[.\\-]"); + } +} diff --git a/src/test/java/org/tarantool/SingleSocketChannelProviderImplTest.java b/src/test/java/org/tarantool/SingleSocketChannelProviderImplTest.java new file mode 100644 index 00000000..9c7f54ff --- /dev/null +++ b/src/test/java/org/tarantool/SingleSocketChannelProviderImplTest.java @@ -0,0 +1,69 @@ +package org.tarantool; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +@DisplayName("A single socket provider") +class SingleSocketChannelProviderImplTest extends AbstractSocketProviderTest { + + @Test + @DisplayName("initialized with a right address") + public void testAddressesCount() { + String expectedAddress = "localhost:3301"; + SingleSocketChannelProviderImpl socketProvider + = new SingleSocketChannelProviderImpl(expectedAddress); + assertEquals(expectedAddress, extractRawHostAndPortString(socketProvider.getAddress())); + } + + @Test + @DisplayName("poorly initialized with an empty address") + public void testEmptyAddresses() { + assertThrows(IllegalArgumentException.class, () -> new SingleSocketChannelProviderImpl(null)); + } + + @Test + @DisplayName("initialized with a default timeout") + public void testDefaultTimeout() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost"); + assertEquals(RoundRobinSocketProviderImpl.NO_TIMEOUT, socketProvider.getConnectionTimeout()); + } + + @Test + @DisplayName("changed its timeout to new value") + public void testChangingTimeout() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost"); + int expectedTimeout = 10_000; + socketProvider.setConnectionTimeout(expectedTimeout); + assertEquals(expectedTimeout, socketProvider.getConnectionTimeout()); + } + + @Test + @DisplayName("changed to negative timeout with a failure") + public void testWrongChangingTimeout() { + RoundRobinSocketProviderImpl socketProvider + = new RoundRobinSocketProviderImpl("localhost"); + int negativeValue = -100; + assertThrows(IllegalArgumentException.class, () -> socketProvider.setConnectionTimeout(negativeValue)); + } + + @Test + @DisplayName("produced sockets with same address") + public void testMultipleChannelGetting() throws IOException { + String expectedAddresss = "localhost:3301"; + SingleSocketChannelProviderImpl socketProvider + = wrapWithMockChannelProvider(new SingleSocketChannelProviderImpl(expectedAddresss)); + + for (int i = 0; i < 10; i++) { + socketProvider.get(0, null); + assertEquals(expectedAddresss, extractRawHostAndPortString(socketProvider.getAddress())); + } + } + +} diff --git a/src/test/java/org/tarantool/TarantoolConsole.java b/src/test/java/org/tarantool/TarantoolConsole.java index f664f86b..e59715c0 100644 --- a/src/test/java/org/tarantool/TarantoolConsole.java +++ b/src/test/java/org/tarantool/TarantoolConsole.java @@ -17,25 +17,68 @@ /** * Blocking console connection for test control purposes. - * + *

* Provides the means of lua commands evaluation given * the host and port or the instance name of tarantool. */ public abstract class TarantoolConsole implements Closeable { - private final static Pattern GREETING_PATTERN = Pattern.compile("^Tarantool.+\n.+\n"); - private final static Pattern CONNECTED_PATTERN = Pattern.compile("^connected to (.*)\n"); - private final static Pattern REPLY_PATTERN = Pattern.compile("^.*\\n\\.{3}\\n", + + private static final int TIMEOUT = 2000; + private static final Pattern GREETING_PATTERN = Pattern.compile("^Tarantool.+\n.+\n"); + private static final Pattern CONNECTED_PATTERN = Pattern.compile("^connected to (.*)\n"); + private static final Pattern REPLY_PATTERN = Pattern.compile("^.*\\n\\.{3}\\n", Pattern.UNIX_LINES | Pattern.DOTALL); - private final static int TIMEOUT = 2000; private final StringBuilder unmatched = new StringBuilder(); protected BufferedReader reader; protected OutputStreamWriter writer; + private static void appendApplyBackspaces(StringBuilder sb, char[] buf, int len) { + for (int i = 0; i < len; i++) { + char c = buf[i]; + if (c == '\b') { + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + } else { + sb.append(c); + } + } + } + + /** + * A direct tarantool console connection. + * + * @param host Tarantool host name. + * @param port Console port of tarantool instance. + * + * @return Console connection object. + */ + public static TarantoolConsole open(String host, int port) { + return new TarantoolTcpConsole(host, port); + } + + /** + * An indirect tarantool console connection via tarantoolctl utility. + *

+ * > tarantoolctl enter <instance> + *

+ * This facility is aimed at support of multi-instance tests in future. + * + * @param workDir Directory where .tarantoolctl file is located. + * @param instance Tarantool instance name as per command. + * + * @return Console connection object. + */ + public static TarantoolConsole open(String workDir, String instance) { + return new TarantoolLocalConsole(workDir, instance); + } + private Matcher checkMatch(Pattern p) { - if (unmatched.length() == 0) + if (unmatched.length() == 0) { return null; + } Matcher m = p.matcher(unmatched.toString()); @@ -49,8 +92,9 @@ private Matcher checkMatch(Pattern p) { protected Matcher expect(Pattern p) { Matcher result = checkMatch(p); - if (result != null) + if (result != null) { return result; + } char[] buf = new char[4096]; int rc; @@ -59,8 +103,9 @@ protected Matcher expect(Pattern p) { appendApplyBackspaces(unmatched, buf, rc); result = checkMatch(p); - if (result != null) + if (result != null) { return result; + } } } catch (SocketTimeoutException e) { throw new RuntimeException("Timeout occurred. Unmatched: " + unmatched.toString() + ", pattern:" + p, e); @@ -70,19 +115,6 @@ protected Matcher expect(Pattern p) { throw new RuntimeException("Unexpected end of input."); } - private static void appendApplyBackspaces(StringBuilder sb, char[] buf, int len) { - for (int i = 0; i < len ; i++) { - char c = buf[i]; - if (c == '\b') { - if (sb.length() > 0) { - sb.deleteCharAt(sb.length() - 1); - } - } else { - sb.append(c); - } - } - } - protected void write(String expr) { try { writer.write(expr + '\n'); @@ -96,13 +128,13 @@ protected void write(String expr) { public void close() { try { reader.close(); - } catch (IOException e) { - // No-op. + } catch (IOException ignored) { + // no-op. } try { writer.close(); - } catch (IOException e) { - // No-op. + } catch (IOException ignored) { + // no-op. } } @@ -135,32 +167,6 @@ public List evalList(String expr) { return yaml.load(m.group(0)); } - /** - * A direct tarantool console connection. - * - * @param host Tarantool host name. - * @param port Console port of tarantool instance. - * @return Console connection object. - */ - public static TarantoolConsole open(String host, int port) { - return new TarantoolTcpConsole(host, port); - } - - /** - * An indirect tarantool console connection via tarantoolctl utility. - * - * > tarantoolctl enter <instance> - * - * This facility is aimed at support of multi-instance tests in future. - * - * @param workDir Directory where .tarantoolctl file is located. - * @param instance Tarantool instance name as per command. - * @return Console connection object. - */ - public static TarantoolConsole open(String workDir, String instance) { - return new TarantoolLocalConsole(workDir, instance); - } - /** * A direct tarantool console connection (via TCP connection). */ @@ -189,8 +195,8 @@ public void close() { try { socket.close(); - } catch (IOException e) { - // No-op. + } catch (IOException ignored) { + // no-op. } } } diff --git a/src/test/java/org/tarantool/TarantoolControl.java b/src/test/java/org/tarantool/TarantoolControl.java index 38028eba..551b3d43 100644 --- a/src/test/java/org/tarantool/TarantoolControl.java +++ b/src/test/java/org/tarantool/TarantoolControl.java @@ -1,40 +1,49 @@ package org.tarantool; +import java.io.BufferedReader; import java.io.File; -import java.io.IOException; -import java.nio.channels.FileChannel; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.util.HashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.Map; -import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Wrapper around tarantoolctl utility. */ public class TarantoolControl { - public class TarantoolControlException extends RuntimeException { + + public static class TarantoolControlException extends RuntimeException { + int code; String stdout; String stderr; TarantoolControlException(int code, String stdout, String stderr) { - super("returned exitcode " + code + "\n" + - "[stdout]\n" + stdout + "\n[stderr]\n" + stderr); + super( + "returned exitcode " + code + "\n" + + "[stdout]\n" + stdout + + "\n[stderr]\n" + stderr + ); this.code = code; this.stdout = stdout; this.stderr = stderr; } + } protected static final String tntCtlWorkDir = System.getProperty("tntCtlWorkDir", new File("testroot").getAbsolutePath()); - protected static final String instanceDir = new File("src/test").getAbsolutePath(); - protected static final String tarantoolCtlConfig = new File("src/test/.tarantoolctl").getAbsolutePath(); + protected static final String instanceDir = new File("src/test/resources").getAbsolutePath(); + protected static final String tarantoolCtlConfig = new File("src/test/resources/.tarantoolctl").getAbsolutePath(); protected static final int RESTART_TIMEOUT = 2000; // Per-instance environment. protected final Map> instanceEnv = new HashMap>(); @@ -55,17 +64,20 @@ protected static void setupWorkDirectory() throws IOException { } mkdir(tntCtlWorkDir); - for (File c : new File(instanceDir).listFiles()) - if (c.getName().endsWith(".lua")) + for (File c : new File(instanceDir).listFiles()) { + if (c.getName().endsWith(".lua")) { copyFile(c, tntCtlWorkDir); + } + } copyFile(tarantoolCtlConfig, tntCtlWorkDir); } // Based on https://stackoverflow.com/a/779529 private static void rmdir(File f) throws IOException { if (f.isDirectory()) { - for (File c : f.listFiles()) + for (File c : f.listFiles()) { rmdir(c); + } } f.delete(); } @@ -83,8 +95,9 @@ private static void mkdir(String f) throws IOException { } private static void copyFile(File source, File dest) throws IOException { - if (dest.isDirectory()) + if (dest.isDirectory()) { dest = new File(dest, source.getName()); + } FileChannel sourceChannel = null; FileChannel destChannel = null; try { @@ -113,87 +126,75 @@ private static String loadStream(InputStream s) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(s)); StringBuilder sb = new StringBuilder(); String line; - while ((line = br.readLine()) != null) + while ((line = br.readLine()) != null) { sb.append(line).append("\n"); + } return sb.toString(); } /** - * Control the given tarantool instance via tarantoolctl utility. + * Executes a command of the given tarantool instance via + * tarantoolctl utility. * - * @param command A tarantoolctl utility command. - * @param instanceName Name of tarantool instance to control. + * @param command tarantoolctl utility command. + * @param instanceName name of tarantool instance to control. */ - protected void executeCommand(String command, String instanceName) { - ProcessBuilder builder = new ProcessBuilder("env", "tarantoolctl", command, instanceName); - builder.directory(new File(tntCtlWorkDir)); - Map env = builder.environment(); - env.putAll(buildInstanceEnvironment(instanceName)); - - final Process process; - try { - process = builder.start(); - } catch (IOException e) { - throw new RuntimeException("environment failure", e); - } - - final CountDownLatch latch = new CountDownLatch(1); - // The thread below is necessary to organize timed wait on the process. - // We cannot use Process.waitFor(long, TimeUnit) because we're on java 6. - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - process.waitFor(); - } catch (InterruptedException e) { - // No-op. - } - latch.countDown(); - } - }); - - thread.start(); - - boolean res; - try { - res = latch.await(RESTART_TIMEOUT, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new RuntimeException("wait interrupted", e); - } - - if (!res) { - thread.interrupt(); - process.destroy(); - - throw new RuntimeException("timeout"); + protected void executeControlCommand(String command, String instanceName) { + ProcessCommand controlCommand = + new ProcessCommand(buildInstanceEnvironment(instanceName), "env", "tarantoolctl", command, instanceName); + if (!controlCommand.execute()) { + throw new TarantoolControlException( + controlCommand.getExitCode(), + controlCommand.getOutput(), + controlCommand.getError() + ); } + } - int code = process.exitValue(); + /** + * Sends start command using tarantoolctl + * and blocks until the process will be up. + *

+ * Calling this method has the same effect as + * {@code start(instanceName, true)} + * + * @param instanceName target instance name + * + * @see #start(String, boolean) + */ + public void start(String instanceName) { + start(instanceName, true); + } - if (code != 0) { - String stdout = ""; - String stderr = ""; - try { - stdout = loadStream(process.getInputStream()); - stderr = loadStream(process.getErrorStream()); - } catch (IOException e) { - /* No-op. */ - } - throw new TarantoolControlException(code, stdout, stderr); + /** + * Sends start command using tarantoolctl + * and optionally blocks until the process will be up. + *

+ * The block includes real connection establishment using + * {@link org.tarantool.TarantoolConsole}. + * + * @param instanceName target instance name + * @see #waitStarted(String) + */ + public void start(String instanceName, boolean wait) { + executeControlCommand("start", instanceName); + if (wait) { + waitStarted(instanceName); } } /** - * Wait until the instance will be started. - * + * Waits until the instance will be started. + *

* Use tarantoolctl status instanceName. - * + *

* Then test the instance with TarantoolTcpConsole (ADMIN environment * variable is set) or TarantoolLocalConsole. */ public void waitStarted(String instanceName) { - while (status(instanceName) != 0) + while (status(instanceName) != 0) { sleep(); + } while (true) { try { @@ -207,35 +208,54 @@ public void waitStarted(String instanceName) { } /** - * Wait until the instance will be stopped. + * Sends stop command using tarantoolctl + * and blocks until the process will be down. * - * Use tarantoolctl status instanceName. + * @param instanceName target instance name */ - public void waitStopped(String instanceName) { - while (status(instanceName) != 1) - sleep(); + public void stop(String instanceName) { + try { + Path path = Paths.get(tntCtlWorkDir, instanceName + ".pid"); + if (Files.exists(path)) { + String pid = new String(Files.readAllBytes(path)); + executeControlCommand("stop", instanceName); + waitStopped(pid, instanceName); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - public void start(String instanceName) { - executeCommand("start", instanceName); + /** + * Waits until the instance will be stopped. + * + * @param pid instance PID + * @param instanceName target instance name + */ + private void waitStopped(String pid, String instanceName) { + while (status(instanceName) != 1 || isProcessAlive(pid)) { + sleep(); + } } - public void stop(String instanceName) { - executeCommand("stop", instanceName); + private boolean isProcessAlive(String pid) { + ProcessCommand sendSignal0Command = + new ProcessCommand(Collections.emptyMap(), "kill", "-0", pid); + return sendSignal0Command.execute(); } /** * Wrapper for `tarantoolctl status instanceName`. - * + *

* Return exit code of the command: - * + *

* * 0 -- started; * * 1 -- stopped; * * 2 -- pid file exists, control socket inaccessible. */ public int status(String instanceName) { try { - executeCommand("status", instanceName); + executeControlCommand("status", instanceName); } catch (TarantoolControlException e) { return e.code; } @@ -243,7 +263,7 @@ public int status(String instanceName) { return 0; } - public Map buildInstanceEnvironment(String instanceName) { + public Map buildInstanceEnvironment(String instanceName) { Map env = new HashMap(); env.put("PWD", tntCtlWorkDir); env.put("TEST_WORKDIR", tntCtlWorkDir); @@ -257,8 +277,9 @@ public Map buildInstanceEnvironment(String instanceName) { public void createInstance(String instanceName, String luaFile, Map env) { File src = new File(instanceDir, luaFile.endsWith(".lua") ? luaFile : luaFile.concat(".lua")); - if (!src.exists()) + if (!src.exists()) { throw new RuntimeException("Lua file " + src + " doesn't exist."); + } File dst = new File(tntCtlWorkDir, instanceName + ".lua"); try { @@ -300,8 +321,9 @@ public void waitReplication(String instanceName, int timeout) { */ public TarantoolConsole openConsole(String instanceName) { Map env = instanceEnv.get(instanceName); - if (env == null) - throw new RuntimeException("No such instance '" + instanceName +"'."); + if (env == null) { + throw new RuntimeException("No such instance '" + instanceName + "'."); + } String admin = env.get("ADMIN"); if (admin == null) { @@ -309,7 +331,7 @@ public TarantoolConsole openConsole(String instanceName) { } else { int idx = admin.indexOf(':'); return TarantoolConsole.open(idx < 0 ? "localhost" : admin.substring(0, idx), - Integer.valueOf(idx < 0 ? admin : admin.substring(idx + 1))); + Integer.valueOf(idx < 0 ? admin : admin.substring(idx + 1))); } } @@ -320,4 +342,67 @@ public static void sleep() { throw new RuntimeException(e); } } + + static class ProcessCommand { + + final ProcessBuilder processBuilder; + + String lastOutput = ""; + String lastError = ""; + int lastExitCode = -1; + + public ProcessCommand(Map environment, String... commands) { + processBuilder = new ProcessBuilder(commands); + processBuilder.directory(new File(tntCtlWorkDir)); + Map env = processBuilder.environment(); + env.putAll(environment); + } + + boolean execute() { + final Process process; + try { + process = processBuilder.start(); + } catch (IOException e) { + throw new RuntimeException("environment failure", e); + } + + boolean res; + try { + res = process.waitFor(RESTART_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("wait interrupted", e); + } + + if (!res) { + process.destroy(); + throw new RuntimeException("timeout"); + } + + lastExitCode = process.exitValue(); + try { + lastOutput = loadStream(process.getInputStream()).trim(); + lastError = loadStream(process.getErrorStream()).trim(); + } catch (IOException ignored) { + lastOutput = ""; + lastError = ""; + } + + return lastExitCode == 0; + } + + String getOutput() { + return lastOutput; + } + + String getError() { + return lastError; + } + + int getExitCode() { + return lastExitCode; + } + + } + } + diff --git a/src/test/java/org/tarantool/TestAssumptions.java b/src/test/java/org/tarantool/TestAssumptions.java new file mode 100644 index 00000000..b62af6b7 --- /dev/null +++ b/src/test/java/org/tarantool/TestAssumptions.java @@ -0,0 +1,15 @@ +package org.tarantool; + +import org.junit.jupiter.api.Assumptions; + +public class TestAssumptions { + + public static void assumeMinimalServerVersion(TarantoolConsole console, ServerVersion version) { + Assumptions.assumeTrue(version.haveMinimalVersion(TestUtils.getTarantoolVersion(console))); + } + + public static void assumeMaximalServerVersion(TarantoolConsole console, ServerVersion version) { + Assumptions.assumeTrue(version.haveMaximalVersion(TestUtils.getTarantoolVersion(console))); + } + +} diff --git a/src/test/java/org/tarantool/TestSocketChannelProvider.java b/src/test/java/org/tarantool/TestSocketChannelProvider.java index 57391f70..44465288 100644 --- a/src/test/java/org/tarantool/TestSocketChannelProvider.java +++ b/src/test/java/org/tarantool/TestSocketChannelProvider.java @@ -1,8 +1,9 @@ package org.tarantool; +import static java.net.StandardSocketOptions.SO_LINGER; + import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; -import static java.net.StandardSocketOptions.SO_LINGER; /** * Socket channel provider to be used throughout the tests. @@ -39,8 +40,9 @@ public SocketChannel get(int retryNumber, Throwable lastError) { channel.connect(new InetSocketAddress(host, port)); return channel; } catch (Exception e) { - if (budget < System.currentTimeMillis()) + if (budget < System.currentTimeMillis()) { throw new RuntimeException(e); + } try { Thread.sleep(100); } catch (InterruptedException ex) { diff --git a/src/test/java/org/tarantool/TestUtils.java b/src/test/java/org/tarantool/TestUtils.java index 5d5bf7b2..9744e77f 100644 --- a/src/test/java/org/tarantool/TestUtils.java +++ b/src/test/java/org/tarantool/TestUtils.java @@ -1,14 +1,49 @@ package org.tarantool; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class TestUtils { - final static String replicationInfoRequest = "return " + - "box.info.id, " + - "box.info.lsn, " + - "box.info.replication"; + + public static String makeDiscoveryFunction(String functionName, Collection addresses) { + String functionResult = addresses.stream() + .map(address -> "'" + address + "'") + .collect(Collectors.joining(",", "{", "}")); + return makeDiscoveryFunction(functionName, functionResult); + } + + public static String makeDiscoveryFunction(String functionName, Object result) { + return makeDiscoveryFunction(functionName, result.toString()); + } + + public static String makeDiscoveryFunction(String functionName, String body) { + return "function " + functionName + "() return " + body + " end"; + } + + static final String replicationInfoRequest = "return " + + "box.info.id, " + + "box.info.lsn, " + + "box.info.replication"; + + @FunctionalInterface + public interface ThrowingAction { + + void run() throws X; + + } + + public static Runnable throwingWrapper(ThrowingAction action) { + return () -> { + try { + action.run(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + } public static String makeReplicationString(String user, String pass, String... addrs) { StringBuilder sb = new StringBuilder(); @@ -40,16 +75,44 @@ public static Map makeInstanceEnv(int port, int consolePort, Str return env; } + protected static String getTarantoolVersion(TarantoolConsole console) { + return console.eval("return box.info.version"); + } + + /** + * See waitReplication(TarantoolClientImpl client, int timeout). + */ + protected static void waitReplication(TarantoolConsole console, int timeout) { + long deadline = System.currentTimeMillis() + timeout; + for (; ; ) { + List v = console.evalList(replicationInfoRequest); + + if (parseAndCheckReplicationStatus(v)) { + return; + } + + if (deadline < System.currentTimeMillis()) { + throw new RuntimeException("Test failure: timeout waiting for replication."); + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + /** * Wait until all replicas will be in sync with master's log. - * + *

* It is useful to wait until the last data modification performed on * **that** instance will be applied on its replicas. It does not take care * to modifications performed on instances, which are master's of that one. */ public static void waitReplication(TarantoolClientImpl client, int timeout) { long deadline = System.currentTimeMillis() + timeout; - for (;;) { + for (; ; ) { List v; try { v = client.syncOps().eval(replicationInfoRequest); @@ -57,11 +120,13 @@ public static void waitReplication(TarantoolClientImpl client, int timeout) { continue; } - if (parseAndCheckReplicationStatus(v)) + if (parseAndCheckReplicationStatus(v)) { return; + } - if (deadline < System.currentTimeMillis()) + if (deadline < System.currentTimeMillis()) { throw new RuntimeException("Test failure: timeout waiting for replication."); + } try { Thread.sleep(100); @@ -83,55 +148,36 @@ public static byte[] fromHex(String hex) { assert hex.length() % 2 == 0; byte[] data = new byte[hex.length() / 2]; for (int i = 0; i < data.length; i++) { - data[i] = Integer.decode("0x" + hex.charAt(i*2) + hex.charAt(i*2+1)).byteValue(); + data[i] = Integer.decode("0x" + hex.charAt(i * 2) + hex.charAt(i * 2 + 1)).byteValue(); } return data; } - /** - * See waitReplication(TarantoolClientImpl client, int timeout). - */ - protected static void waitReplication(TarantoolConsole console, int timeout) { - long deadline = System.currentTimeMillis() + timeout; - for (;;) { - List v = console.evalList(replicationInfoRequest); - - if (parseAndCheckReplicationStatus(v)) - return; - - if (deadline < System.currentTimeMillis()) - throw new RuntimeException("Test failure: timeout waiting for replication."); - - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - private static boolean parseAndCheckReplicationStatus(List data) { - if (data == null || data.size() != 3) + if (data == null || data.size() != 3) { throw new IllegalStateException("Unexpected format of replication status."); + } Number masterId = ensureType(Number.class, data.get(0)); Number masterLsn = ensureType(Number.class, data.get(1)); - Map replInfo = ensureTypeOrNull(Map.class, data.get(2)); + Map replInfo = ensureTypeOrNull(Map.class, data.get(2)); - if (replInfo == null || replInfo.size() < 2) + if (replInfo == null || replInfo.size() < 2) { return false; + } for (Object info : replInfo.values()) { Map replItems = ensureTypeOrNull(Map.class, info); - Map downstreamInfo = ensureTypeOrNull(Map.class, replItems.get("downstream")); + Map downstreamInfo = ensureTypeOrNull(Map.class, replItems.get("downstream")); if (downstreamInfo != null) { - Map replica_vclock = ensureTypeOrNull(Map.class, downstreamInfo.get("vclock")); + Map replicaVClock = ensureTypeOrNull(Map.class, downstreamInfo.get("vclock")); - if (replica_vclock == null) + if (replicaVClock == null) { return false; + } - Number replicaLsn = ensureTypeOrNull(Number.class, replica_vclock.get(masterId)); + Number replicaLsn = ensureTypeOrNull(Number.class, replicaVClock.get(masterId)); if (replicaLsn == null || replicaLsn.longValue() < masterLsn.longValue()) { return false; @@ -152,4 +198,5 @@ private static T ensureType(Class cls, Object v) { } return cls.cast(v); } + } diff --git a/src/test/java/org/tarantool/cluster/ClusterServiceStoredFunctionDiscovererIT.java b/src/test/java/org/tarantool/cluster/ClusterServiceStoredFunctionDiscovererIT.java new file mode 100644 index 00000000..fa0ac22f --- /dev/null +++ b/src/test/java/org/tarantool/cluster/ClusterServiceStoredFunctionDiscovererIT.java @@ -0,0 +1,194 @@ +package org.tarantool.cluster; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.TestUtils.makeDiscoveryFunction; +import static org.tarantool.TestUtils.makeInstanceEnv; + +import org.tarantool.AbstractTarantoolConnectorIT; +import org.tarantool.CommunicationException; +import org.tarantool.TarantoolClient; +import org.tarantool.TarantoolClientImpl; +import org.tarantool.TarantoolClusterClientConfig; +import org.tarantool.TarantoolControl; +import org.tarantool.TarantoolException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@DisplayName("A cluster discoverer") +public class ClusterServiceStoredFunctionDiscovererIT { + + protected static final int INSTANCE_LISTEN_PORT = 3301; + protected static final int INSTANCE_ADMIN_PORT = 3313; + private static final String LUA_FILE = "jdk-testing.lua"; + + private static final String INSTANCE_NAME = "jdk-testing"; + private static TarantoolControl control; + + private static String ENTRY_FUNCTION_NAME = "getAddresses"; + + private TarantoolClusterClientConfig clusterConfig; + private TarantoolClient client; + + @BeforeAll + public static void setupEnv() { + control = new TarantoolControl(); + control.createInstance(INSTANCE_NAME, LUA_FILE, makeInstanceEnv(INSTANCE_LISTEN_PORT, INSTANCE_ADMIN_PORT)); + + control.start(INSTANCE_NAME); + } + + @BeforeEach + public void setupTest() { + clusterConfig = AbstractTarantoolConnectorIT.makeClusterClientConfig(); + clusterConfig.clusterDiscoveryEntryFunction = ENTRY_FUNCTION_NAME; + + client = new TarantoolClientImpl("localhost:" + INSTANCE_LISTEN_PORT, clusterConfig); + } + + @AfterAll + public static void tearDownEnv() { + control.stop(INSTANCE_NAME); + } + + @Test + @DisplayName("fetched list of addresses") + public void testSuccessfulAddressParsing() { + List addresses = Arrays.asList("localhost:3311", "127.0.0.1:3301"); + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, addresses); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + Set instances = discoverer.getInstances(); + + assertNotNull(instances); + assertEquals(2, instances.size()); + assertTrue(instances.contains(addresses.get(0))); + assertTrue(instances.contains(addresses.get(1))); + } + + @Test + @DisplayName("fetched duplicated addresses") + public void testSuccessfulUniqueAddressParsing() { + List addresses = Arrays.asList("localhost:3311", "127.0.0.1:3301", "127.0.0.2:3301", "localhost:3311"); + + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, addresses); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + Set instances = discoverer.getInstances(); + + assertNotNull(instances); + assertEquals(3, instances.size()); + assertTrue(instances.contains(addresses.get(0))); + assertTrue(instances.contains(addresses.get(1))); + assertTrue(instances.contains(addresses.get(3))); + } + + + @Test + @DisplayName("fetched empty address list") + public void testFunctionReturnedEmptyList() { + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, Collections.emptyList()); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + Set instances = discoverer.getInstances(); + + assertNotNull(instances); + assertTrue(instances.isEmpty()); + } + + @Test + @DisplayName("fetched with an exception using wrong function name") + public void testWrongFunctionName() { + clusterConfig.clusterDiscoveryEntryFunction = "wrongFunction"; + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + assertThrows(TarantoolException.class, discoverer::getInstances); + } + + @Test + @DisplayName("fetched with an exception using a broken client") + public void testWrongInstanceAddress() { + clusterConfig.initTimeoutMillis = 1000; + + client.close(); + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + assertThrows(CommunicationException.class, discoverer::getInstances); + } + + @Test + @DisplayName("fetched with an exception when wrong data type returned") + public void testWrongTypeResultData() { + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, 42); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + assertThrows(IllegalDiscoveryFunctionResult.class, discoverer::getInstances); + } + + @Test + @DisplayName("fetched with an exception using no return function") + public void testFunctionWithNoReturn() { + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, ""); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + assertThrows(IllegalDiscoveryFunctionResult.class, discoverer::getInstances); + } + + @Test + @DisplayName("fetched first result as a list and ignored other multi-results") + public void testWrongMultiResultData() { + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, "{'host1'}, 'host2', 423"); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + Set instances = discoverer.getInstances(); + + assertNotNull(instances); + assertEquals(1, instances.size()); + assertTrue(instances.contains("host1")); + } + + @Test + @DisplayName("fetched with an exception using error-prone function") + public void testFunctionWithError() { + String functionCode = makeDiscoveryFunction(ENTRY_FUNCTION_NAME, "error('msg')"); + control.openConsole(INSTANCE_NAME).exec(functionCode); + + TarantoolClusterStoredFunctionDiscoverer discoverer = + new TarantoolClusterStoredFunctionDiscoverer(clusterConfig, client); + + assertThrows(TarantoolException.class, discoverer::getInstances); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java index f0c0fda7..0966b221 100644 --- a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -1,10 +1,18 @@ package org.tarantool.jdbc; +import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; +import static org.tarantool.TestUtils.makeInstanceEnv; +import static org.tarantool.jdbc.SqlTestUtils.getCreateTableSQL; + +import org.tarantool.ServerVersion; +import org.tarantool.TarantoolConnection; +import org.tarantool.TarantoolConsole; +import org.tarantool.TarantoolControl; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.tarantool.TarantoolConnection; import java.io.IOException; import java.net.InetSocketAddress; @@ -16,14 +24,9 @@ import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.tarantool.TestUtils.makeInstanceEnv; -import static org.tarantool.jdbc.SqlTestUtils.getCreateTableSQL; - -import org.tarantool.TarantoolControl; - //mvn -DtntHost=localhost -DtntPort=3301 -DtntUser=test -DtntPass=test verify public abstract class AbstractJdbcIT { + private static final String host = System.getProperty("tntHost", "localhost"); private static final Integer port = Integer.valueOf(System.getProperty("tntPort", "3301")); private static final String user = System.getProperty("tntUser", "test_admin"); @@ -35,16 +38,19 @@ public abstract class AbstractJdbcIT { protected static final int ADMIN = 3313; private static String[] initSql = new String[] { - "CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100))", - "INSERT INTO test VALUES (1, 'one'), (2, 'two'), (3, 'three')", - "CREATE TABLE test_compound(id1 INT, id2 INT, val VARCHAR(100), PRIMARY KEY (id2, id1))", - getCreateTableSQL("test_types", TntSqlType.values()) + "CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100))", + "INSERT INTO test VALUES (1, 'one'), (2, 'two'), (3, 'three')", + "CREATE TABLE test_compound(id1 INT, id2 INT, val VARCHAR(100), PRIMARY KEY (id2, id1))", + "CREATE TABLE test_nulls(id INT PRIMARY KEY, val VARCHAR(100))", + "INSERT INTO test_nulls VALUES (1, 'a'), (2, 'b'), (3, 'c'), (4, NULL), (5, NULL), (6, NULL)", + getCreateTableSQL("test_types", TntSqlType.values()) }; private static String[] cleanSql = new String[] { - "DROP TABLE IF EXISTS test", - "DROP TABLE IF EXISTS test_types", - "DROP TABLE IF EXISTS test_compound" + "DROP TABLE IF EXISTS test", + "DROP TABLE IF EXISTS test_types", + "DROP TABLE IF EXISTS test_compound", + "DROP TABLE IF EXISTS test_nulls" }; protected static TarantoolControl control; @@ -55,39 +61,37 @@ public static void setupEnv() throws Exception { control = new TarantoolControl(); control.createInstance("jdk-testing", LUA_FILE, makeInstanceEnv(LISTEN, ADMIN)); control.start("jdk-testing"); - control.waitStarted("jdk-testing"); - - sqlExec(cleanSql); - sqlExec(initSql); } @AfterAll public static void teardownEnv() throws Exception { - try { - sqlExec(cleanSql); - } finally { - control.stop("jdk-testing"); - control.waitStopped("jdk-testing"); - } + control.stop("jdk-testing"); } @BeforeEach - public void setUpConnection() throws SQLException { + public void setUpTest() throws SQLException { + assumeMinimalServerVersion(TarantoolConsole.open(host, ADMIN), ServerVersion.V_2_1); + conn = DriverManager.getConnection(URL); - assertNotNull(conn); + sqlExec(cleanSql); + sqlExec(initSql); } @AfterEach - public void tearDownConnection() throws SQLException { - if (conn != null && !conn.isClosed()) + public void tearDownTest() throws SQLException { + assumeMinimalServerVersion(TarantoolConsole.open(host, ADMIN), ServerVersion.V_2_1); + sqlExec(cleanSql); + if (conn != null && !conn.isClosed()) { conn.close(); + } } protected static void sqlExec(String... text) { TarantoolConnection con = makeConnection(); try { - for (String cmd : text) - con.eval("box.sql.execute(\"" + cmd + "\")"); + for (String cmd : text) { + con.eval("box.execute(\"" + cmd + "\")"); + } } finally { con.close(); } @@ -119,4 +123,5 @@ static TarantoolConnection makeConnection() { throw new RuntimeException(e); } } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java index 270eb6a8..f67b4a39 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -1,26 +1,32 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.tarantool.jdbc.SqlAssertions.assertSqlExceptionHasStatus; + +import org.tarantool.TarantoolConnection; +import org.tarantool.util.SQLStates; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; -import org.tarantool.TarantoolConnection; import java.lang.reflect.Field; import java.net.Socket; +import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; +import java.util.Collections; +import java.util.Properties; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -@SuppressWarnings("Since15") public class JdbcConnectionIT extends AbstractJdbcIT { @Test @@ -88,23 +94,23 @@ public void testClosedConnection() throws SQLException { @Override public void execute() throws Throwable { switch (step) { - case 0: - conn.createStatement(); - break; - case 1: - conn.prepareStatement("TEST"); - break; - case 2: - conn.getMetaData(); - break; - case 3: - conn.getNetworkTimeout(); - break; - case 4: - conn.setNetworkTimeout(null, 1000); - break; - default: - fail(); + case 0: + conn.createStatement(); + break; + case 1: + conn.prepareStatement("TEST"); + break; + case 2: + conn.getMetaData(); + break; + case 3: + conn.getNetworkTimeout(); + break; + case 4: + conn.setNetworkTimeout(null, 1000); + break; + default: + fail(); } } }); @@ -113,6 +119,15 @@ public void execute() throws Throwable { assertEquals(5, i); } + @Test + void testIsValidCheck() throws SQLException { + assertTrue(conn.isValid(2000)); + assertThrows(SQLException.class, () -> conn.isValid(-1000)); + + conn.close(); + assertFalse(conn.isValid(2000)); + } + @Test public void testConnectionUnwrap() throws SQLException { assertEquals(conn, conn.unwrap(SQLConnection.class)); @@ -136,13 +151,11 @@ public void testSetAndGetHoldability() throws SQLException { conn.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, conn.getHoldability()); - assertThrows(SQLFeatureNotSupportedException.class, () -> conn.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT)); + assertThrows( + SQLFeatureNotSupportedException.class, + () -> conn.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT) + ); assertThrows(SQLException.class, () -> conn.setHoldability(Integer.MAX_VALUE)); - - assertThrows(SQLException.class, () -> { - conn.close(); - conn.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); - }); } @Test @@ -153,18 +166,36 @@ public void testCreateHoldableStatement() throws SQLException { statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); - statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + statement = conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); + } + + @Test + public void testCreateUnsupportedHoldableStatement() throws SQLException { + assertThrows( + SQLFeatureNotSupportedException.class, + () -> conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.CLOSE_CURSORS_AT_COMMIT + )); + } + @Test + public void testCreateWrongHoldableStatement() throws SQLException { assertThrows(SQLException.class, () -> { conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, Integer.MAX_VALUE); }); - assertThrows(SQLFeatureNotSupportedException.class, () -> { - conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); - }); assertThrows(SQLException.class, () -> { - conn.close(); - conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + -65 + ); }); } @@ -177,19 +208,399 @@ public void testPrepareHoldableStatement() throws SQLException { statement = conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); - statement = conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + statement = conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); + } + + @Test + public void testPrepareUnsupportedHoldableStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, + () -> { + String sqlString = "SELECT * FROM TEST"; + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.CLOSE_CURSORS_AT_COMMIT + ); + }); + } + + @Test + public void testPrepareWrongHoldableStatement() throws SQLException { + String sqlString = "SELECT * FROM TEST"; + assertThrows(SQLException.class, + () -> { + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + Integer.MAX_VALUE + ); + }); + assertThrows(SQLException.class, + () -> { + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, -190 + ); + }); + } + + @Test + public void testCreateScrollableStatement() throws SQLException { + Statement statement = conn.createStatement(); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, statement.getResultSetType()); + + statement = conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, statement.getResultSetType()); + } + + @Test + public void testCreateUnsupportedScrollableStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, () -> { + conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + }); + assertThrows(SQLFeatureNotSupportedException.class, () -> { + conn.createStatement( + ResultSet.TYPE_SCROLL_SENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testCreateWrongScrollableStatement() { + assertThrows(SQLException.class, () -> { + conn.createStatement(Integer.MAX_VALUE, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + }); + assertThrows(SQLException.class, () -> { + conn.createStatement(-47, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + }); + } + + @Test + public void testPrepareScrollableStatement() throws SQLException { + String sqlString = "TEST"; + Statement statement = conn.prepareStatement(sqlString); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + } + + @Test + public void testPrepareUnsupportedScrollableStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, () -> { + String sqlString = "SELECT * FROM TEST"; + conn.prepareStatement(sqlString, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + }); + assertThrows(SQLFeatureNotSupportedException.class, () -> { + String sqlString = "SELECT * FROM TEST"; + conn.prepareStatement( + sqlString, + ResultSet.TYPE_SCROLL_SENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.CLOSE_CURSORS_AT_COMMIT + ); + }); + } + @Test + public void testPrepareWrongScrollableStatement() throws SQLException { + String sqlString = "SELECT * FROM TEST"; + assertThrows(SQLException.class, + () -> { + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + Integer.MAX_VALUE + ); + }); assertThrows(SQLException.class, () -> { - conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, Integer.MAX_VALUE); + conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, -90); }); + } + + @Test + public void testCreateConcurrentStatement() throws SQLException { + Statement statement = conn.createStatement(); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + + statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + + statement = conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + } + + @Test + public void testCreateUnsupportedConcurrentStatement() throws SQLException { assertThrows(SQLFeatureNotSupportedException.class, () -> { - conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + }); + assertThrows(SQLFeatureNotSupportedException.class, + () -> { + conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_UPDATABLE, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testCreateWrongConcurrentStatement() { + assertThrows(SQLException.class, () -> { + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, Integer.MAX_VALUE, ResultSet.HOLD_CURSORS_OVER_COMMIT); }); assertThrows(SQLException.class, () -> { - conn.close(); - conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, -7213, ResultSet.HOLD_CURSORS_OVER_COMMIT); }); } + @Test + public void testCreateStatementWithClosedConnection() { + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testPrepareStatementWithClosedConnection() { + String sqlString = "SELECT * FROM TEST"; + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.prepareStatement( + sqlString, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testGeneratedKeys() throws SQLException { + String sql = "SELECT * FROM test"; + PreparedStatement preparedStatement = conn.prepareStatement(sql, Statement.NO_GENERATED_KEYS); + assertNotNull(preparedStatement); + preparedStatement.close(); + + assertThrows(SQLFeatureNotSupportedException.class, () -> conn.prepareStatement(sql, new int[] { 1 })); + assertThrows(SQLFeatureNotSupportedException.class, () -> conn.prepareStatement(sql, new String[] { "id" })); + + assertThrows(SQLException.class, () -> conn.prepareStatement(sql, Integer.MAX_VALUE)); + assertThrows(SQLException.class, () -> conn.prepareStatement(sql, Integer.MIN_VALUE)); + assertThrows(SQLException.class, () -> conn.prepareStatement(sql, -76)); + assertThrows( + SQLFeatureNotSupportedException.class, + () -> conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) + ); + } + + @Test + public void testUnavailableMethodsAfterClose() throws SQLException { + conn.close(); + + SQLException sqlException; + sqlException = assertThrows(SQLException.class, () -> conn.clearWarnings()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.getWarnings()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.createArrayOf("INTEGER", new Object[] { 1, 2, 3 })); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.createBlob()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.createClob()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.createNClob()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.createSQLXML()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.createStatement()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> + conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.prepareStatement("")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> + conn.prepareStatement("", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> + conn.prepareStatement( + "", + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.prepareStatement("", Statement.NO_GENERATED_KEYS)); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.prepareStatement("", new int[] { })); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.prepareStatement("", new String[] { })); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.prepareCall("")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> + conn.prepareCall("", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> + conn.prepareCall( + "", + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.getHoldability()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getMetaData()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getNetworkTimeout()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.nativeSQL("")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.commit()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.rollback()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.rollback(null)); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setAutoCommit(true)); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getAutoCommit()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setSavepoint()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setSavepoint("")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.isReadOnly()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setReadOnly(true)); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getTransactionIsolation()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows( + SQLException.class, + () -> conn.setTransactionIsolation(Connection.TRANSACTION_NONE) + ); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLClientInfoException.class, () -> conn.setClientInfo(new Properties())); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLClientInfoException.class, () -> conn.setClientInfo("key", "value")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getClientInfo("param1")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getClientInfo()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setNetworkTimeout(null, 1000)); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.getSchema()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.getCatalog()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setCatalog("")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setSchema("")); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + + sqlException = assertThrows(SQLException.class, () -> conn.getTypeMap()); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + sqlException = assertThrows(SQLException.class, () -> conn.setTypeMap(Collections.emptyMap())); + assertSqlExceptionHasStatus(sqlException, SQLStates.CONNECTION_DOES_NOT_EXIST); + } + } + diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java index dcf90126..99ccf284 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -1,5 +1,13 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -11,15 +19,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { + private DatabaseMetaData meta; @BeforeEach @@ -41,7 +42,7 @@ public void testGetTableTypes() throws SQLException { @Test public void testGetAllTables() throws SQLException { - ResultSet rs = meta.getTables(null, null, null, new String[] {"TABLE"}); + ResultSet rs = meta.getTables(null, null, null, new String[] { "TABLE" }); assertNotNull(rs); assertTrue(rs.next()); @@ -50,6 +51,9 @@ public void testGetAllTables() throws SQLException { assertTrue(rs.next()); assertEquals("TEST_COMPOUND", rs.getString("TABLE_NAME")); + assertTrue(rs.next()); + assertEquals("TEST_NULLS", rs.getString("TABLE_NAME")); + assertTrue(rs.next()); assertEquals("TEST_TYPES", rs.getString("TABLE_NAME")); @@ -60,7 +64,7 @@ public void testGetAllTables() throws SQLException { @Test public void testGetTable() throws SQLException { - ResultSet rs = meta.getTables(null, null, "TEST", new String[] {"TABLE"}); + ResultSet rs = meta.getTables(null, null, "TEST", new String[] { "TABLE" }); assertNotNull(rs); assertTrue(rs.next()); assertEquals("TEST", rs.getString("TABLE_NAME")); @@ -124,7 +128,7 @@ public void testGetPrimaryKeysCompound() throws SQLException { @Test public void testGetPrimaryKeysIgnoresCatalogSchema() throws SQLException { - String[] vals = new String[] {null, "", "IGNORE"}; + String[] vals = new String[] { null, "", "IGNORE" }; for (String cat : vals) { for (String schema : vals) { ResultSet rs = meta.getPrimaryKeys(cat, schema, "TEST"); @@ -140,7 +144,7 @@ public void testGetPrimaryKeysIgnoresCatalogSchema() throws SQLException { @Test public void testGetPrimaryKeysNotFound() throws SQLException { - String[] tables = new String[] {null, "", "NOSUCHTABLE"}; + String[] tables = new String[] { null, "", "NOSUCHTABLE" }; for (String t : tables) { ResultSet rs = meta.getPrimaryKeys(null, null, t); assertNotNull(rs); @@ -200,14 +204,17 @@ public void testClosedConnection() throws SQLException { @Override public void execute() throws Throwable { switch (step) { - case 0: meta.getTables(null, null, null, new String[]{"TABLE"}); - break; - case 1: meta.getColumns(null, null, "TEST", null); - break; - case 2: meta.getPrimaryKeys(null, null, "TEST"); - break; - default: - fail(); + case 0: + meta.getTables(null, null, null, new String[] { "TABLE" }); + break; + case 1: + meta.getColumns(null, null, "TEST", null); + break; + case 2: + meta.getPrimaryKeys(null, null, "TEST"); + break; + default: + fail(); } } }); @@ -220,8 +227,6 @@ public void execute() throws Throwable { public void testGetDriverNameVersion() throws SQLException { String name = meta.getDriverName(); String version = meta.getDriverVersion(); - int majorVersion = meta.getDriverMajorVersion(); - int minorVersion = meta.getDriverMinorVersion(); // Verify driver name. assertEquals("tarantool-java", name); @@ -235,6 +240,9 @@ public void testGetDriverNameVersion() throws SQLException { // Verify the full version matches major/minor ones. int majorVersionMatched = Integer.parseInt(m.group("major")); int minorVersionMatched = Integer.parseInt(m.group("minor")); + int majorVersion = meta.getDriverMajorVersion(); + int minorVersion = meta.getDriverMinorVersion(); + assertEquals(majorVersion, majorVersionMatched); assertEquals(minorVersion, minorVersionMatched); } @@ -254,4 +262,113 @@ public void testSupportsResultSetHoldability() throws SQLException { assertFalse(meta.supportsResultSetHoldability(42)); } + @Test + public void testUnwrap() throws SQLException { + assertEquals(meta, meta.unwrap(SQLDatabaseMetadata.class)); + assertThrows(SQLException.class, () -> meta.unwrap(Integer.class)); + } + + @Test + public void testIsWrapperFor() throws SQLException { + assertTrue(meta.isWrapperFor(SQLDatabaseMetadata.class)); + assertFalse(meta.isWrapperFor(Integer.class)); + } + + @Test + public void testSupportGeneratedKeys() throws SQLException { + assertFalse(meta.supportsGetGeneratedKeys()); + } + + @Test + public void testNullsAreSortedProperties() throws SQLException { + assertTrue(meta.nullsAreSortedLow()); + assertFalse(meta.nullsAreSortedHigh()); + + assertFalse(meta.nullsAreSortedAtStart()); + assertFalse(meta.nullsAreSortedAtEnd()); + } + + @Test + public void testSupportsResultSetType() throws SQLException { + assertTrue(meta.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY)); + assertTrue(meta.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertFalse(meta.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE)); + assertFalse(meta.supportsResultSetType(Integer.MAX_VALUE)); + assertFalse(meta.supportsResultSetType(Integer.MIN_VALUE)); + assertFalse(meta.supportsResultSetType(54)); + } + + @Test + public void testSupportsResultSetConcurrency() throws SQLException { + // valid combinations + assertTrue(meta.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)); + assertTrue(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)); + + // everything else is invalid + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)); + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)); + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY)); + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)); + + // bad inputs are also unsupported + assertFalse(meta.supportsResultSetConcurrency(Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertFalse(meta.supportsResultSetConcurrency(Integer.MIN_VALUE, Integer.MAX_VALUE)); + assertFalse(meta.supportsResultSetConcurrency(54, -45)); + } + + @Test + public void testInsertDetectionSupport() throws SQLException { + int[] types = new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE, + -23, + Integer.MIN_VALUE, + Integer.MAX_VALUE + }; + + for (int type : types) { + assertFalse(meta.othersInsertsAreVisible(type)); + assertFalse(meta.ownInsertsAreVisible(type)); + assertFalse(meta.insertsAreDetected(type)); + } + + } + + @Test + public void testUpdateDetectionSupport() throws SQLException { + int[] types = new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE, + -23, + Integer.MIN_VALUE, + Integer.MAX_VALUE + }; + + for (int type : types) { + assertFalse(meta.othersUpdatesAreVisible(type)); + assertFalse(meta.ownUpdatesAreVisible(type)); + assertFalse(meta.updatesAreDetected(type)); + } + } + + @Test + public void testDeleteDetectionSupport() throws SQLException { + int[] types = new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE, + -23, + Integer.MIN_VALUE, + Integer.MAX_VALUE + }; + + for (int type : types) { + assertFalse(meta.othersDeletesAreVisible(type)); + assertFalse(meta.ownDeletesAreVisible(type)); + assertFalse(meta.deletesAreDetected(type)); + } + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java index 38ca2d4c..37750135 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java @@ -1,21 +1,5 @@ package org.tarantool.jdbc; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.tarantool.CommunicationException; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.net.URI; - -import java.sql.Driver; -import java.sql.DriverManager; -import java.sql.DriverPropertyInfo; -import java.sql.SQLException; -import java.util.Properties; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -30,7 +14,24 @@ import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; import static org.tarantool.jdbc.SQLDriver.PROP_USER; +import org.tarantool.CommunicationException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.util.Properties; + public class JdbcDriverTest { + @Test public void testParseQueryString() throws Exception { SQLDriver drv = new SQLDriver(); @@ -40,9 +41,10 @@ public void testParseQueryString() throws Exception { prop.setProperty(PROP_PASSWORD, "secret"); URI uri = new URI(String.format( - "tarantool://server.local:3302?%s=%s&%s=%d", - PROP_SOCKET_PROVIDER, "some.class", - PROP_SOCKET_TIMEOUT, 5000)); + "tarantool://server.local:3302?%s=%s&%s=%d", + PROP_SOCKET_PROVIDER, "some.class", + PROP_SOCKET_TIMEOUT, 5000) + ); Properties res = drv.parseQueryString(uri, prop); assertNotNull(res); @@ -70,7 +72,11 @@ public void testParseQueryStringUserInfoInURI() throws Exception { public void testParseQueryStringValidations() { // Check non-number port checkParseQueryStringValidation("tarantool://0", - new Properties() {{setProperty(PROP_PORT, "nan");}}, + new Properties() { + { + setProperty(PROP_PORT, "nan"); + } + }, "Port must be a valid number."); // Check zero port @@ -81,11 +87,11 @@ public void testParseQueryStringValidations() { // Check non-number timeout checkParseQueryStringValidation(String.format("tarantool://0:3301?%s=nan", PROP_SOCKET_TIMEOUT), null, - "Timeout must be a valid number."); + "Timeout must be a valid number."); // Check negative timeout checkParseQueryStringValidation(String.format("tarantool://0:3301?%s=-100", PROP_SOCKET_TIMEOUT), null, - "Timeout must not be negative."); + "Timeout must not be negative."); } @Test @@ -96,7 +102,7 @@ public void testGetPropertyInfo() throws SQLException { assertNotNull(info); assertEquals(6, info.length); - for (DriverPropertyInfo e: info) { + for (DriverPropertyInfo e : info) { assertNotNull(e.name); assertNull(e.choices); assertNotNull(e.description); @@ -119,24 +125,25 @@ public void testGetPropertyInfo() throws SQLException { } else if (PROP_SOCKET_TIMEOUT.equals(e.name)) { assertFalse(e.required); assertEquals("0", e.value); - } else + } else { fail("Unknown property '" + e.name + "'"); + } } } @Test public void testCustomSocketProviderFail() throws SQLException { checkCustomSocketProviderFail("nosuchclassexists", - "Couldn't instantiate socket provider"); + "Couldn't instantiate socket provider"); checkCustomSocketProviderFail(Integer.class.getName(), - "The socket provider java.lang.Integer does not implement org.tarantool.jdbc.SQLSocketProvider"); + "The socket provider java.lang.Integer does not implement org.tarantool.jdbc.SQLSocketProvider"); checkCustomSocketProviderFail(TestSQLProviderThatReturnsNull.class.getName(), - "The socket provider returned null socket"); + "The socket provider returned null socket"); checkCustomSocketProviderFail(TestSQLProviderThatThrows.class.getName(), - "Couldn't initiate connection using"); + "Couldn't initiate connection using"); } @Test diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index dd8cd69b..c34d5bed 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -1,12 +1,31 @@ package org.tarantool.jdbc; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.junit.jupiter.api.function.ThrowingConsumer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACES_MAX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; +import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; + import org.tarantool.CommunicationException; import org.tarantool.TarantoolConnection; import org.tarantool.protocol.TarantoolPacket; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingConsumer; + import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; @@ -21,25 +40,8 @@ import java.util.HashMap; import java.util.Properties; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; -import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; -import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; -import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACES_MAX; -import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; -import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; -import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; - public class JdbcExceptionHandlingTest { + /** * Simulates meta parsing error: missing "name" field in a space format for the primary key. * @@ -48,21 +50,26 @@ public class JdbcExceptionHandlingTest { @Test public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException { TarantoolConnection tntCon = mock(TarantoolConnection.class); - SQLConnection conn = buildTestSQLConnection(tntCon, "", SQLDriver.defaults); + final SQLConnection conn = buildTestSQLConnection(tntCon, "", SQLDriver.defaults); Object[] spc = new Object[7]; spc[FORMAT_IDX] = Collections.singletonList(new HashMap()); spc[SPACE_ID_IDX] = 1000; doReturn(Collections.singletonList(Arrays.asList(spc))).when(tntCon) - .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); + .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); Object[] idx = new Object[6]; idx[INDEX_FORMAT_IDX] = Collections.singletonList( - new HashMap() {{ put("field", 0);}}); + new HashMap() { + { + put("field", 0); + } + } + ); doReturn(Collections.singletonList(Arrays.asList(idx))).when(tntCon) - .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); + .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); final DatabaseMetaData meta = conn.getMetaData(); @@ -125,7 +132,7 @@ public void testDatabaseMetaDataCommunicationException() throws SQLException { checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { @Override public void accept(DatabaseMetaData meta) throws Throwable { - meta.getTables(null, null, null, new String[] {"TABLE"}); + meta.getTables(null, null, null, new String[] { "TABLE" }); } }, "Failed to retrieve table(s) description: tableNamePattern=\"null\"."); @@ -232,11 +239,11 @@ public void execute() throws Throwable { } private void checkPreparedStatementCommunicationException(final ThrowingConsumer consumer) - throws SQLException { + throws SQLException { TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); final PreparedStatement prep = new SQLPreparedStatement( - buildTestSQLConnection(mockCon, "tarantool://0:0", SQLDriver.defaults), "TEST"); + buildTestSQLConnection(mockCon, "tarantool://0:0", SQLDriver.defaults), "TEST"); Exception ex = new CommunicationException("TEST"); doThrow(ex).when(mockCon).sql("TEST", new Object[0]); @@ -256,7 +263,7 @@ public void execute() throws Throwable { } private void checkDatabaseMetaDataCommunicationException(final ThrowingConsumer consumer, - String msg) throws SQLException { + String msg) throws SQLException { TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); SQLConnection conn = buildTestSQLConnection(mockCon, "tarantool://0:0", new Properties(SQLDriver.defaults)); final DatabaseMetaData meta = conn.getMetaData(); @@ -287,7 +294,7 @@ protected Socket makeSocket() { } @Override - protected TarantoolConnection makeConnection (String user, String pass, Socket socket) { + protected TarantoolConnection makeConnection(String user, String pass, Socket socket) { return tntCon; } }; @@ -297,6 +304,7 @@ class TestTarantoolConnection extends TarantoolConnection { TestTarantoolConnection() throws IOException { super(null, null, mock(Socket.class)); } + @Override protected TarantoolPacket sql(String sql, Object[] bind) { return super.sql(sql, bind); diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index 3d4f967d..24e4a539 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -1,5 +1,13 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -9,22 +17,17 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import java.sql.Statement; public class JdbcPreparedStatementIT extends JdbcTypesIT { + private PreparedStatement prep; @AfterEach public void tearDown() throws SQLException { - if (prep != null && !prep.isClosed()) + if (prep != null && !prep.isClosed()) { prep.close(); + } } @Test @@ -104,9 +107,12 @@ public void testExecuteReturnsUpdateCount() throws Exception { assertEquals("ten", getRow("test", 10).get(1)); assertEquals("twenty", getRow("test", 20).get(1)); + + conn.createStatement().execute("DELETE FROM test WHERE id IN (10, 20)"); } - @Test void testForbiddenMethods() throws SQLException { + @Test + void testForbiddenMethods() throws SQLException { prep = conn.prepareStatement("TEST"); int i = 0; @@ -116,14 +122,17 @@ public void testExecuteReturnsUpdateCount() throws Exception { @Override public void execute() throws Throwable { switch (step) { - case 0: prep.executeQuery("TEST"); - break; - case 1: prep.executeUpdate("TEST"); - break; - case 2: prep.execute("TEST"); - break; - default: - fail(); + case 0: + prep.executeQuery("TEST"); + break; + case 1: + prep.executeUpdate("TEST"); + break; + case 2: + prep.execute("TEST"); + break; + default: + fail(); } } }); @@ -145,14 +154,17 @@ public void testClosedConnection() throws SQLException { @Override public void execute() throws Throwable { switch (step) { - case 0: prep.executeQuery(); - break; - case 1: prep.executeUpdate(); - break; - case 2: prep.execute(); - break; - default: - fail(); + case 0: + prep.executeQuery(); + break; + case 1: + prep.executeUpdate(); + break; + case 2: + prep.execute(); + break; + default: + fail(); } } }); @@ -161,75 +173,110 @@ public void execute() throws Throwable { assertEquals(3, i); } + @Test + public void testUnwrap() throws SQLException { + prep = conn.prepareStatement("SELECT val FROM test"); + assertEquals(prep, prep.unwrap(SQLPreparedStatement.class)); + assertEquals(prep, prep.unwrap(SQLStatement.class)); + assertThrows(SQLException.class, () -> prep.unwrap(Integer.class)); + } + + @Test + public void testIsWrapperFor() throws SQLException { + prep = conn.prepareStatement("SELECT val FROM test"); + assertTrue(prep.isWrapperFor(SQLPreparedStatement.class)); + assertTrue(prep.isWrapperFor(SQLStatement.class)); + assertFalse(prep.isWrapperFor(Integer.class)); + } + + @Test + public void testSupportGeneratedKeys() throws SQLException { + prep = conn.prepareStatement("INSERT INTO test values (50, 'fifty')", Statement.NO_GENERATED_KEYS); + assertFalse(prep.execute()); + assertEquals(1, prep.getUpdateCount()); + + ResultSet generatedKeys = prep.getGeneratedKeys(); + assertNotNull(generatedKeys); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, generatedKeys.getType()); + assertEquals(ResultSet.CONCUR_READ_ONLY, generatedKeys.getConcurrency()); + } + + @Test + void testStatementConnection() throws SQLException { + Statement statement = conn.prepareStatement("SELECT * FROM TEST"); + assertEquals(conn, statement.getConnection()); + } + @Test public void testSetByte() throws SQLException { makeHelper(Byte.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(BYTE_VALS) - .testSetParameter(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(BYTE_VALS) + .testSetParameter(); } @Test public void testSetInt() throws SQLException { makeHelper(Integer.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(INT_VALS) - .testSetParameter(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(INT_VALS) + .testSetParameter(); } @Test public void testSetLong() throws SQLException { makeHelper(Long.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(LONG_VALS) - .testSetParameter(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(LONG_VALS) + .testSetParameter(); } @Test public void testSetString() throws SQLException { makeHelper(String.class) - .setColumns(TntSqlType.VARCHAR, TntSqlType.TEXT) - .setValues(STRING_VALS) - .testSetParameter(); + .setColumns(TntSqlType.VARCHAR, TntSqlType.TEXT) + .setValues(STRING_VALS) + .testSetParameter(); } @Test public void testSetFloat() throws SQLException { makeHelper(Float.class) - .setColumns(TntSqlType.REAL) - .setValues(FLOAT_VALS) - .testSetParameter(); + .setColumns(TntSqlType.REAL) + .setValues(FLOAT_VALS) + .testSetParameter(); } @Test public void testSetDouble() throws SQLException { makeHelper(Double.class) - .setColumns(TntSqlType.FLOAT, TntSqlType.DOUBLE) - .setValues(DOUBLE_VALS) - .testSetParameter(); + .setColumns(TntSqlType.FLOAT, TntSqlType.DOUBLE) + .setValues(DOUBLE_VALS) + .testSetParameter(); } @Test public void testSetBigDecimal() throws SQLException { makeHelper(BigDecimal.class) - .setColumns(TntSqlType.REAL, TntSqlType.FLOAT, TntSqlType.DOUBLE) - .setValues(BIGDEC_VALS) - .testSetParameter(); + .setColumns(TntSqlType.REAL, TntSqlType.FLOAT, TntSqlType.DOUBLE) + .setValues(BIGDEC_VALS) + .testSetParameter(); } @Test public void testSetByteArray() throws SQLException { makeHelper(byte[].class) - .setColumns(TntSqlType.SCALAR) - .setValues(BINARY_VALS) - .testSetParameter(); + .setColumns(TntSqlType.SCALAR) + .setValues(BINARY_VALS) + .testSetParameter(); } @Test public void testSetDate() throws SQLException { makeHelper(Date.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(DATE_VALS) - .testSetParameter(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(DATE_VALS) + .testSetParameter(); } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java index 4d8ac44c..944c4198 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java @@ -1,5 +1,12 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -7,15 +14,12 @@ import java.math.BigDecimal; import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class JdbcResultSetIT extends JdbcTypesIT { + private Statement stmt; private DatabaseMetaData metaData; @@ -27,8 +31,9 @@ public void setUp() throws Exception { @AfterEach public void tearDown() throws Exception { - if (stmt != null && !stmt.isClosed()) + if (stmt != null && !stmt.isClosed()) { stmt.close(); + } } @Test @@ -56,73 +61,114 @@ public void testIteration() throws SQLException { @Test public void testGetByteColumn() throws SQLException { makeHelper(Byte.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(BYTE_VALS) - .testGetColumn(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(BYTE_VALS) + .testGetColumn(); } @Test public void testGetShortColumn() throws SQLException { makeHelper(Short.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(SHORT_VALS) - .testGetColumn(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(SHORT_VALS) + .testGetColumn(); } @Test public void testGetIntColumn() throws SQLException { makeHelper(Integer.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(INT_VALS) - .testGetColumn(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(INT_VALS) + .testGetColumn(); } @Test public void testGetLongColumn() throws SQLException { makeHelper(Long.class) - .setColumns(TntSqlType.INT, TntSqlType.INTEGER) - .setValues(LONG_VALS) - .testGetColumn(); + .setColumns(TntSqlType.INT, TntSqlType.INTEGER) + .setValues(LONG_VALS) + .testGetColumn(); } @Test public void testGetBigDecimalColumn() throws SQLException { makeHelper(BigDecimal.class) - .setColumns(TntSqlType.REAL, TntSqlType.FLOAT, TntSqlType.DOUBLE) - .setValues(BIGDEC_VALS) - .testGetColumn(); + .setColumns(TntSqlType.REAL, TntSqlType.FLOAT, TntSqlType.DOUBLE) + .setValues(BIGDEC_VALS) + .testGetColumn(); } @Test public void testGetFloatColumn() throws SQLException { makeHelper(Float.class) - .setColumns(TntSqlType.REAL) - .setValues(FLOAT_VALS) - .testGetColumn(); + .setColumns(TntSqlType.REAL) + .setValues(FLOAT_VALS) + .testGetColumn(); } @Test public void testGetDoubleColumn() throws SQLException { makeHelper(Double.class) - .setColumns(TntSqlType.FLOAT, TntSqlType.DOUBLE) - .setValues(DOUBLE_VALS) - .testGetColumn(); + .setColumns(TntSqlType.FLOAT, TntSqlType.DOUBLE) + .setValues(DOUBLE_VALS) + .testGetColumn(); } @Test public void testGetStringColumn() throws SQLException { makeHelper(String.class) - .setColumns(TntSqlType.VARCHAR, TntSqlType.TEXT) - .setValues(STRING_VALS) - .testGetColumn(); + .setColumns(TntSqlType.VARCHAR, TntSqlType.TEXT) + .setValues(STRING_VALS) + .testGetColumn(); } @Test public void testGetByteArrayColumn() throws SQLException { makeHelper(byte[].class) - .setColumns(TntSqlType.SCALAR) - .setValues(BINARY_VALS) - .testGetColumn(); + .setColumns(TntSqlType.SCALAR) + .setValues(BINARY_VALS) + .testGetColumn(); + } + + @Test + public void testDefaultScrollType() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + assertEquals(stmt.getResultSetType(), resultSet.getType()); + + stmt.close(); + assertThrows(SQLException.class, resultSet::getType); + } + + @Test + public void testSelectedScrollType() throws SQLException { + Statement statement = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + ResultSet resultSet = statement.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + assertEquals(statement.getResultSetType(), resultSet.getType()); + + statement.close(); + assertThrows(SQLException.class, resultSet::getType); + } + + @Test + public void testOwnedResultSet() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + assertEquals(stmt, resultSet.getStatement()); + + stmt.close(); + assertThrows(SQLException.class, resultSet::getStatement); + } + + @Test + public void testResultSetMetadataAfterClose() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertNotNull(metaData); + resultSet.close(); + assertEquals(metaData, resultSet.getMetaData()); } @Test @@ -132,4 +178,148 @@ public void testHoldability() throws SQLException { assertEquals(metaData.getResultSetHoldability(), resultSet.getHoldability()); } + @Test + public void testUnwrap() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertEquals(resultSet, resultSet.unwrap(SQLResultSet.class)); + assertThrows(SQLException.class, () -> resultSet.unwrap(Integer.class)); + } + + @Test + public void testIsWrapperFor() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertTrue(resultSet.isWrapperFor(SQLResultSet.class)); + assertFalse(resultSet.isWrapperFor(Integer.class)); + } + + @Test + public void testNullsSortingAsc() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_nulls ORDER BY val ASC"); + for (int i = 0; i < 3; i++) { + assertTrue(resultSet.next()); + assertNull(resultSet.getString(2)); + } + for (int i = 0; i < 3; i++) { + assertTrue(resultSet.next()); + assertNotNull(resultSet.getString(2)); + } + assertFalse(resultSet.next()); + } + + @Test + public void testNullsSortingDesc() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_nulls ORDER BY val DESC"); + for (int i = 0; i < 3; i++) { + assertTrue(resultSet.next()); + assertNotNull(resultSet.getString(2)); + } + for (int i = 0; i < 3; i++) { + assertTrue(resultSet.next()); + assertNull(resultSet.getString(2)); + } + assertFalse(resultSet.next()); + } + + @Test + public void testFindUniqueColumnLabels() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertEquals(1, resultSet.findColumn("f1")); + assertEquals(2, resultSet.findColumn("f2")); + } + + @Test + public void testFindDuplicatedColumnLabels() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f1 FROM test"); + assertNotNull(resultSet); + assertEquals(1, resultSet.findColumn("f1")); + } + + @Test + public void testMaxRows() throws SQLException { + stmt.setMaxRows(1); + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertTrue(resultSet.next()); + assertTrue(resultSet.getInt("f1") > 0); + assertFalse(resultSet.next()); + } + + @Test + public void testForwardTraversal() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertTrue(resultSet.isBeforeFirst()); + assertEquals(0, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertTrue(resultSet.isFirst()); + assertEquals(1, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertEquals(2, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertFalse(resultSet.next()); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isAfterLast()); + + stmt.close(); + assertThrows(SQLException.class, resultSet::isAfterLast); + } + + @Test + public void testTraversal() throws SQLException { + Statement statement = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + ResultSet resultSet = statement.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertTrue(resultSet.isBeforeFirst()); + assertEquals(0, resultSet.getRow()); + + assertTrue(resultSet.last()); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertTrue(resultSet.first()); + assertEquals(1, resultSet.getRow()); + assertTrue(resultSet.isFirst()); + + assertFalse(resultSet.relative(-1)); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isBeforeFirst()); + + assertTrue(resultSet.relative(1)); + assertEquals(1, resultSet.getRow()); + assertTrue(resultSet.isFirst()); + + assertTrue(resultSet.absolute(-1)); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertTrue(resultSet.absolute(1)); + assertEquals(1, resultSet.getRow()); + assertTrue(resultSet.isFirst()); + + resultSet.beforeFirst(); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isBeforeFirst()); + + resultSet.afterLast(); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isAfterLast()); + + assertTrue(resultSet.previous()); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertTrue(resultSet.first()); + assertEquals(1, resultSet.getRow()); + + assertFalse(resultSet.previous()); + assertEquals(0, resultSet.getRow()); + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java index 53c09158..b5b66228 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java @@ -1,5 +1,12 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.sql.ResultSet; @@ -7,11 +14,11 @@ import java.sql.SQLException; import java.sql.Statement; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - +@DisplayName("A resultSet metadata") public class JdbcResultSetMetaDataIT extends AbstractJdbcIT { + @Test + @DisplayName("returned correct column names") public void testColumnNames() throws SQLException { Statement stmt = conn.createStatement(); assertNotNull(stmt); @@ -31,4 +38,98 @@ public void testColumnNames() throws SQLException { rs.close(); stmt.close(); } + + @Test + @DisplayName("unwrapped correct") + public void testUnwrap() throws SQLException { + try ( + Statement statement = conn.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT * FROM test") + ) { + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(metaData, metaData.unwrap(SQLResultSetMetaData.class)); + assertThrows(SQLException.class, () -> metaData.unwrap(Integer.class)); + } + } + + @Test + @DisplayName("checked as a proper wrapper") + public void testIsWrapperFor() throws SQLException { + try ( + Statement statement = conn.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT * FROM test") + ) { + ResultSetMetaData metaData = resultSet.getMetaData(); + assertTrue(metaData.isWrapperFor(SQLResultSetMetaData.class)); + assertFalse(metaData.isWrapperFor(Integer.class)); + } + } + + @Test + @DisplayName("returned a correct result columns size") + public void testColumnCount() throws SQLException { + try (Statement statement = conn.createStatement()) { + assertNotNull(statement); + + try (ResultSet resultSet = statement.executeQuery("SELECT * FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(2, metaData.getColumnCount()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT id, val FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(2, metaData.getColumnCount()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT id FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(1, metaData.getColumnCount()); + } + } + } + + @Test + @DisplayName("returned correct result column aliases") + public void testColumnAliases() throws SQLException { + try (Statement statement = conn.createStatement()) { + assertNotNull(statement); + + try (ResultSet resultSet = statement.executeQuery("SELECT id AS alias_id FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals("ALIAS_ID", metaData.getColumnLabel(1).toUpperCase()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT val AS alias_val FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals("ALIAS_VAL", metaData.getColumnLabel(1).toUpperCase()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT * FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals("ID", metaData.getColumnLabel(1).toUpperCase()); + assertEquals("VAL", metaData.getColumnLabel(2).toUpperCase()); + } + } + } + + @Test + @DisplayName("returned an error when column index is out of range") + public void testWrongColumnAliases() throws SQLException { + try (Statement statement = conn.createStatement()) { + assertNotNull(statement); + + try (ResultSet resultSet = statement.executeQuery("SELECT * FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnsNumber = metaData.getColumnCount(); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(columnsNumber + 1)); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(-5)); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(Integer.MAX_VALUE)); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(Integer.MIN_VALUE)); + } + } + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 44d8dea0..cffbcfa6 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -1,5 +1,12 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -9,14 +16,8 @@ import java.sql.SQLException; import java.sql.Statement; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - public class JdbcStatementIT extends AbstractJdbcIT { + private Statement stmt; @BeforeEach @@ -26,8 +27,9 @@ public void setUp() throws SQLException { @AfterEach public void tearDown() throws SQLException { - if (stmt != null && !stmt.isClosed()) + if (stmt != null && !stmt.isClosed()) { stmt.close(); + } } @Test @@ -78,14 +80,17 @@ public void testClosedConnection() throws Exception { @Override public void execute() throws Throwable { switch (step) { - case 0: stmt.executeQuery("TEST"); - break; - case 1: stmt.executeUpdate("TEST"); - break; - case 2: stmt.execute("TEST"); - break; - default: - fail(); + case 0: + stmt.executeQuery("TEST"); + break; + case 1: + stmt.executeUpdate("TEST"); + break; + case 2: + stmt.execute("TEST"); + break; + default: + fail(); } } }); @@ -93,4 +98,56 @@ public void execute() throws Throwable { } assertEquals(3, i); } -} \ No newline at end of file + + @Test + public void testUnwrap() throws SQLException { + assertEquals(stmt, stmt.unwrap(SQLStatement.class)); + assertThrows(SQLException.class, () -> stmt.unwrap(Integer.class)); + } + + @Test + public void testIsWrapperFor() throws SQLException { + assertTrue(stmt.isWrapperFor(SQLStatement.class)); + assertFalse(stmt.isWrapperFor(Integer.class)); + } + + @Test + public void testSupportedGeneratedKeys() throws SQLException { + int affectedRows = stmt.executeUpdate( + "INSERT INTO test(id, val) VALUES (50, 'fifty')", + Statement.NO_GENERATED_KEYS + ); + assertEquals(1, affectedRows); + ResultSet generatedKeys = stmt.getGeneratedKeys(); + assertNotNull(generatedKeys); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, generatedKeys.getType()); + assertEquals(ResultSet.CONCUR_READ_ONLY, generatedKeys.getConcurrency()); + } + + @Test + void testUnsupportedGeneratedKeys() { + assertThrows( + SQLException.class, + () -> stmt.executeUpdate( + "INSERT INTO test(id, val) VALUES (100, 'hundred'), (1000, 'thousand')", + Statement.RETURN_GENERATED_KEYS + ) + ); + + int[] wrongConstants = { Integer.MAX_VALUE, Integer.MIN_VALUE, -31, 344 }; + for (int wrongConstant : wrongConstants) { + assertThrows(SQLException.class, + () -> stmt.executeUpdate( + "INSERT INTO test(id, val) VALUES (100, 'hundred'), (1000, 'thousand')", + wrongConstant + ) + ); + } + } + + @Test + void testStatementConnection() throws SQLException { + Statement statement = conn.createStatement(); + assertEquals(conn, statement.getConnection()); + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java b/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java index 4be0c435..ae740d8d 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java @@ -1,5 +1,14 @@ package org.tarantool.jdbc; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.TestUtils.fromHex; +import static org.tarantool.jdbc.SqlTestUtils.getInsertSQL; +import static org.tarantool.jdbc.SqlTestUtils.getParameterizedInsertSQL; +import static org.tarantool.jdbc.SqlTestUtils.getSelectSQL; + import java.math.BigDecimal; import java.sql.Date; import java.sql.PreparedStatement; @@ -10,15 +19,6 @@ import java.sql.Timestamp; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.tarantool.TestUtils.fromHex; -import static org.tarantool.jdbc.SqlTestUtils.getInsertSQL; -import static org.tarantool.jdbc.SqlTestUtils.getParameterizedInsertSQL; -import static org.tarantool.jdbc.SqlTestUtils.getSelectSQL; - public class JdbcTypesIT extends AbstractJdbcIT { private static AtomicInteger KEY_CNTR = new AtomicInteger(1); diff --git a/src/test/java/org/tarantool/jdbc/SqlAssertions.java b/src/test/java/org/tarantool/jdbc/SqlAssertions.java new file mode 100644 index 00000000..93770a49 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/SqlAssertions.java @@ -0,0 +1,15 @@ +package org.tarantool.jdbc; + +import org.tarantool.util.SQLStates; + +import org.junit.jupiter.api.Assertions; + +import java.sql.SQLException; + +public class SqlAssertions { + + public static void assertSqlExceptionHasStatus(SQLException exception, SQLStates state) { + Assertions.assertEquals(state.getSqlState(), exception.getSQLState()); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/SqlTestUtils.java b/src/test/java/org/tarantool/jdbc/SqlTestUtils.java index 70d8e20a..93fc3ae4 100644 --- a/src/test/java/org/tarantool/jdbc/SqlTestUtils.java +++ b/src/test/java/org/tarantool/jdbc/SqlTestUtils.java @@ -1,9 +1,9 @@ package org.tarantool.jdbc; -import java.sql.Date; - import static org.tarantool.TestUtils.toHex; +import java.sql.Date; + public class SqlTestUtils { public static String getCreateTableSQL(String tableName, TntSqlType[] tntTypes) { StringBuilder sb = new StringBuilder("CREATE TABLE "); @@ -77,20 +77,25 @@ public static String getSelectSQL(String tableName, int start, int end, TntSqlTy } public static String quoteSqlValue(Object val) { - if (val == null) + if (val == null) { return "null"; + } - if (val instanceof Boolean) - return (Boolean)val ? "1" : "0"; + if (val instanceof Boolean) { + return (Boolean) val ? "1" : "0"; + } - if (val instanceof String) + if (val instanceof String) { return "'" + val.toString() + "'"; + } - if (val instanceof Date) + if (val instanceof Date) { return "'" + val.toString() + "'"; + } - if (val instanceof byte[]) + if (val instanceof byte[]) { return "X'" + toHex((byte[]) val) + "'"; + } return val.toString(); } diff --git a/src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java b/src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java new file mode 100644 index 00000000..9738916d --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java @@ -0,0 +1,144 @@ +package org.tarantool.jdbc.cursor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Minimal iterator use cases should be implemented by every {@link CursorIterator}. + */ +public abstract class AbstractCursorIteratorTest { + + protected abstract CursorIterator> getCursorIterator(List> result); + + protected List> makeSingletonListResult(Object... rows) { + List> result = new ArrayList<>(); + for (Object row : rows) { + result.add(Collections.singletonList(row)); + } + return result; + } + + @Test + @DisplayName("failed with a null result object") + void testFailIteratorWithNullResult() { + assertThrows(IllegalArgumentException.class, () -> new InMemoryForwardCursorIteratorImpl(null)); + } + + @Test + @DisplayName("iterated through an empty result") + void testIterationOverEmptyResult() throws SQLException { + List> result = makeSingletonListResult(); + CursorIterator> iterator = getCursorIterator(result); + + assertEmpty(iterator); + + for (int i = 0; i < 10; i++) { + assertFalse(iterator.next()); + assertEmpty(iterator); + } + } + + @Test + @DisplayName("iterated through the non-empty results") + void testUseCases() throws SQLException { + List> result = makeSingletonListResult("1"); + forwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("1", "2"); + forwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("1", "2", "3"); + forwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("1", "2", "3", "4", "5", "6", "7", "8", "9"); + forwardIteratorUseCase(getCursorIterator(result), result); + } + + /** + * Tests an expected behaviour from a forward iterator. + * + * @param iterator forward iterator to be tested + * @param result result is backed by iterator + */ + protected void forwardIteratorUseCase(CursorIterator> iterator, List> result) + throws SQLException { + assertFalse(result.isEmpty()); + + assertBeforeFirst(iterator); + + for (int i = 0; i < result.size(); i++) { + assertTrue(iterator.next()); + assertNthPosition(i + 1, iterator, result); + } + + assertFalse(iterator.next()); // after last + assertAfterLast(iterator); + + for (int i = 0; i < 10; i++) { + assertFalse(iterator.next()); + assertAfterLast(iterator); + } + } + + protected void assertBeforeFirst(CursorIterator> iterator) throws SQLException { + assertTrue(iterator.isBeforeFirst()); + assertFalse(iterator.isFirst()); + assertFalse(iterator.isLast()); + assertFalse(iterator.isAfterLast()); + assertEquals(0, iterator.getRow()); + assertThrows(SQLException.class, iterator::getItem); + } + + protected void assertAfterLast(CursorIterator> iterator) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertFalse(iterator.isFirst()); + assertFalse(iterator.isLast()); + assertTrue(iterator.isAfterLast()); + assertEquals(0, iterator.getRow()); + assertThrows(SQLException.class, iterator::getItem); + } + + protected void assertFirst(CursorIterator> iterator, List> result) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertTrue(iterator.isFirst()); + assertFalse(iterator.isAfterLast()); + assertEquals(1, iterator.getRow()); + assertEquals(result.get(0), iterator.getItem()); + } + + protected void assertLast(CursorIterator> iterator, List> result) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertTrue(iterator.isLast()); + assertFalse(iterator.isAfterLast()); + assertEquals(result.size(), iterator.getRow()); + assertEquals(result.get(result.size() - 1), iterator.getItem()); + } + + protected void assertNthPosition(int position, CursorIterator> iterator, List> result) + throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertFalse(iterator.isAfterLast()); + assertEquals(position, iterator.getRow()); + assertEquals(result.get(position - 1), iterator.getItem()); + } + + protected void assertEmpty(CursorIterator> iterator) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertFalse(iterator.isFirst()); + assertFalse(iterator.isLast()); + assertFalse(iterator.isAfterLast()); + assertEquals(0, iterator.getRow()); + assertThrows(SQLException.class, iterator::getItem); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java b/src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java new file mode 100644 index 00000000..acb5babc --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java @@ -0,0 +1,38 @@ +package org.tarantool.jdbc.cursor; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; + +@DisplayName("A forward iterator") +class InMemoryForwardCursorIteratorImplTest extends AbstractCursorIteratorTest { + + @Override + protected CursorIterator> getCursorIterator(List> result) { + return new InMemoryForwardCursorIteratorImpl(result); + } + + @Test + @DisplayName("failed trying to use unsupported operations") + void testUnsupportedOperations() { + List> result = makeSingletonListResult("1"); + CursorIterator> iterator = getCursorIterator(result); + + assertThrows(SQLException.class, iterator::beforeFirst); + assertThrows(SQLException.class, iterator::afterLast); + assertThrows(SQLException.class, iterator::first); + assertThrows(SQLException.class, iterator::last); + assertThrows(SQLException.class, () -> iterator.absolute(0)); + assertThrows(SQLException.class, () -> iterator.absolute(Integer.MIN_VALUE)); + assertThrows(SQLException.class, () -> iterator.absolute(Integer.MAX_VALUE)); + assertThrows(SQLException.class, () -> iterator.relative(0)); + assertThrows(SQLException.class, () -> iterator.relative(Integer.MIN_VALUE)); + assertThrows(SQLException.class, () -> iterator.relative(Integer.MAX_VALUE)); + assertThrows(SQLException.class, iterator::previous); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java b/src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java new file mode 100644 index 00000000..e8d5435e --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java @@ -0,0 +1,306 @@ +package org.tarantool.jdbc.cursor; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.tarantool.TestUtils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; + +@DisplayName("A scrollable iterator") +class InMemoryScrollableCursorIteratorImplTest extends AbstractCursorIteratorTest { + + @Override + protected CursorIterator> getCursorIterator(List> result) { + return new InMemoryScrollableCursorIteratorImpl(result); + } + + @Test + @DisplayName("failed with a null result object") + void testFailIteratorWithNullResult() { + assertThrows(IllegalArgumentException.class, () -> new InMemoryScrollableCursorIteratorImpl(null)); + } + + @Test + @DisplayName("moved to the last position") + void testMoveLast() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + assertTrue(iterator.last()); + assertLast(iterator, result); + } + + @Test + @DisplayName("moved to the first position") + void testMoveFirst() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + assertTrue(iterator.first()); + assertFirst(iterator, result); + } + + @Test + @DisplayName("moved to before the first position") + void testMoveBeforeFirst() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + iterator.beforeFirst(); + assertBeforeFirst(iterator); + } + + @Test + @DisplayName("moved to after the last position") + void testMoveAfterLast() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + } + + @Test + @DisplayName("moved to an absolute position") + void testMoveAbsolute() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + for (int i = 0; i < result.size(); i++) { + assertTrue(iterator.absolute(i + 1)); + assertNthPosition(i + 1, iterator, result); + } + for (int i = result.size() + 1; i < result.size() + 10; i++) { + assertFalse(iterator.absolute(i)); + assertAfterLast(iterator); + } + } + + @Test + @DisplayName("moved to a negative absolute position (reverse traversal)") + void testMoveNegativeAbsolute() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + for (int i = 0; i < result.size(); i++) { + assertTrue(iterator.absolute(-i - 1)); // -1 -2 -3 -4 -5 + assertNthPosition(5 - i, iterator, result); // 5 4 3 2 1 + } + for (int i = -result.size() - 1; i > -result.size() - 10; i--) { + assertFalse(iterator.absolute(i)); + assertBeforeFirst(iterator); + } + } + + @Test + @DisplayName("moved to a relative position") + void testMoveRelative() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + assertTrue(iterator.relative(3)); // before the first -> 3 + assertNthPosition(3, iterator, result); + + assertTrue(iterator.relative(-2)); // 3 -> 1 (first) + assertNthPosition(1, iterator, result); + + assertTrue(iterator.relative(4)); // 1 -> 5 (last) + assertNthPosition(5, iterator, result); + + assertTrue(iterator.relative(-3)); // 5 -> 2 + assertNthPosition(2, iterator, result); + + assertFalse(iterator.relative(-2)); // 2 -> before the first + assertBeforeFirst(iterator); + + assertFalse(iterator.relative(0)); // before the first -> before the first + assertBeforeFirst(iterator); + + assertFalse(iterator.relative(-2)); // before the first -> before the first + assertBeforeFirst(iterator); + + assertTrue(iterator.relative(4)); // before the first -> 4 + assertNthPosition(4, iterator, result); + + assertFalse(iterator.relative(2)); // 4 -> after the last + assertAfterLast(iterator); + + assertTrue(iterator.relative(-3)); // after the last -> 3 + assertNthPosition(3, iterator, result); + + assertTrue(iterator.relative(0)); // 3 -> 3 + assertNthPosition(3, iterator, result); + + assertTrue(iterator.relative(1)); // 3 -> 4 + assertNthPosition(4, iterator, result); + + assertFalse(iterator.relative(5)); // 4 -> after last + assertAfterLast(iterator); + + assertTrue(iterator.relative(-4)); // after last -> 2 + assertNthPosition(2, iterator, result); + + assertFalse(iterator.relative(-3)); // 2 -> before first + assertBeforeFirst(iterator); + } + + @Test + @DisplayName("moved to before the first using absolute navigation") + void testMoveAbsoluteZero() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + assertTrue(iterator.absolute(3)); + assertNthPosition(3, iterator, result); + + assertFalse(iterator.absolute(0)); // move to the before the first + assertBeforeFirst(iterator); + } + + @Test + @DisplayName("moved to the same positions using an absolute positioning") + void testMoveAbsoluteSimilarities() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> firstIterator = getCursorIterator(result); + + CursorIterator> secondIterator = getCursorIterator(result); + + assertBeforeFirst(firstIterator); + assertBeforeFirst(secondIterator); + + // absolute(1) is the same as calling first() + firstIterator.absolute(1); + secondIterator.first(); + assertFirst(firstIterator, result); + assertFirst(secondIterator, result); + + // absolute(-1) is the same as calling last() + firstIterator.absolute(-1); + secondIterator.last(); + assertLast(firstIterator, result); + assertLast(secondIterator, result); + + // absolute(0) is the same as calling beforeFirst() + firstIterator.absolute(0); + secondIterator.beforeFirst(); + assertBeforeFirst(firstIterator); + assertBeforeFirst(secondIterator); + } + + @Test + @DisplayName("moved to the same positions using an relative positioning") + void testMoveRelativeSimilarities() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> firstIterator = getCursorIterator(result); + + CursorIterator> secondIterator = getCursorIterator(result); + + assertBeforeFirst(firstIterator); + assertBeforeFirst(secondIterator); + + // relative(1) is the same as calling next() + for (int i = 0; i < result.size(); i++) { + assertTrue(firstIterator.relative(1)); + assertTrue(secondIterator.next()); + assertNthPosition(i + 1, firstIterator, result); + assertNthPosition(i + 1, secondIterator, result); + } + + assertLast(firstIterator, result); + assertLast(secondIterator, result); + + // relative(-1) is the same as calling previous() + for (int i = result.size(); i > 1; i--) { + assertTrue(firstIterator.relative(-1)); + assertTrue(secondIterator.previous()); + assertNthPosition(i - 1, firstIterator, result); + assertNthPosition(i - 1, secondIterator, result); + } + + assertFirst(firstIterator, result); + assertFirst(secondIterator, result); + } + + @Test + @DisplayName("moved to edges over an empty result") + void testIterationOverEmptyResult() throws SQLException { + List> result = makeSingletonListResult(); + CursorIterator> iterator = getCursorIterator(result); + + Runnable[] actions = new Runnable[] { + TestUtils.throwingWrapper(iterator::beforeFirst), + TestUtils.throwingWrapper(iterator::afterLast), + TestUtils.throwingWrapper(iterator::first), + TestUtils.throwingWrapper(iterator::last), + TestUtils.throwingWrapper(iterator::previous), + TestUtils.throwingWrapper(() -> iterator.relative(1)), + TestUtils.throwingWrapper(() -> iterator.absolute(1)), + }; + + for (Runnable action : actions) { + assertEmpty(iterator); + action.run(); + } + } + + @Test + @DisplayName("iterated through the non-empty results") + void testUseCases() throws SQLException { + List> result = makeSingletonListResult("a"); + backwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("a", "b"); + backwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("a", "b", "c"); + backwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("a", "b", "c", "d", "e", "f", "g", "h", "i"); + backwardIteratorUseCase(getCursorIterator(result), result); + } + + /** + * Tests an expected behaviour from a scrollable iterator. + * + * @param iterator scrollable iterator to be tested + * @param result result is backed by iterator + */ + protected void backwardIteratorUseCase(CursorIterator> iterator, List> result) + throws SQLException { + assertFalse(result.isEmpty()); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + + for (int i = result.size() - 1; i >= 0; i--) { + assertTrue(iterator.previous()); + assertNthPosition(i + 1, iterator, result); + } + + assertFalse(iterator.previous()); // before first + assertBeforeFirst(iterator); + + for (int i = 0; i < 10; i++) { + assertFalse(iterator.previous()); + assertBeforeFirst(iterator); + } + } + +} diff --git a/src/test/.tarantoolctl b/src/test/resources/.tarantoolctl similarity index 100% rename from src/test/.tarantoolctl rename to src/test/resources/.tarantoolctl diff --git a/src/test/resources/checkstyle.xml b/src/test/resources/checkstyle.xml new file mode 100644 index 00000000..9af3c7de --- /dev/null +++ b/src/test/resources/checkstyle.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/jdk-testing.lua b/src/test/resources/jdk-testing.lua similarity index 100% rename from src/test/jdk-testing.lua rename to src/test/resources/jdk-testing.lua diff --git a/src/test/travis.pre.sh b/src/test/travis.pre.sh deleted file mode 100755 index 7ae52eda..00000000 --- a/src/test/travis.pre.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -set -e - -# We need tarantool 2.* for jdbc/sql. -curl http://download.tarantool.org/tarantool/2x/gpgkey | sudo apt-key add - -release=`lsb_release -c -s` - -sudo rm -f /etc/apt/sources.list.d/*tarantool*.list -sudo tee /etc/apt/sources.list.d/tarantool_2x.list <<- EOF -deb http://download.tarantool.org/tarantool/2x/ubuntu/ $release main -deb-src http://download.tarantool.org/tarantool/2x/ubuntu/ $release main -EOF - -sudo apt-get update -sudo apt-get -y install tarantool tarantool-common - -sudo tarantoolctl stop example