From 7d4e26829f0eec7e66334fdda8cd266fbd890591 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Thu, 2 May 2019 17:12:57 +0700 Subject: [PATCH] Replace TConnection with TClientImpl in JDBC Replace obsolete TarantoolConnection with TarantoolClient to have an ability to perform async operations. Update SQLDriver URL parameters. Add init and operation timeouts. Remove socket timeout and replace socket provider with socket channel provider options (according to TarantoolConnection-to-TarantoolClient transfer written above). Add operation timeout capability to TarantoolClientImpl. It also affects the cluster client which no more needs its own expirable operations. Add basic support for SQLStatement (setQueryTimeout) to execute requests with timeout using new TarantoolClient operation timeout. Remove deprecated JDBCBridge. SQLConnection accepted the responsibility for producing raw SQL results. Update README doc with respect to JDBC driver options changes. Closes: #163 Follows on: #75, #155 --- README.md | 15 +- src/main/java/org/tarantool/JDBCBridge.java | 85 ---- .../java/org/tarantool/SqlProtoUtils.java | 4 +- .../java/org/tarantool/TarantoolBase.java | 6 +- .../java/org/tarantool/TarantoolClient.java | 2 + .../org/tarantool/TarantoolClientConfig.java | 7 + .../org/tarantool/TarantoolClientImpl.java | 197 +++++++-- .../org/tarantool/TarantoolClusterClient.java | 90 +--- .../TarantoolClusterClientConfig.java | 6 - .../org/tarantool/TarantoolConnection.java | 4 +- .../java/org/tarantool/TarantoolSQLOps.java | 28 +- .../org/tarantool/jdbc/SQLConnection.java | 411 +++++++++--------- .../tarantool/jdbc/SQLDatabaseMetadata.java | 170 ++++---- .../java/org/tarantool/jdbc/SQLDriver.java | 147 +++---- .../tarantool/jdbc/SQLPreparedStatement.java | 17 +- .../java/org/tarantool/jdbc/SQLProperty.java | 123 ++++++ .../org/tarantool/jdbc/SQLResultHolder.java | 53 +++ .../java/org/tarantool/jdbc/SQLResultSet.java | 7 +- .../org/tarantool/jdbc/SQLSocketProvider.java | 10 - .../java/org/tarantool/jdbc/SQLStatement.java | 60 +-- .../jdbc/StatementTimeoutException.java | 11 + .../org/tarantool/protocol/ProtoUtils.java | 50 +-- .../AbstractTarantoolConnectorIT.java | 22 - src/test/java/org/tarantool/ConnectionIT.java | 19 +- .../TarantoolConnectionSQLOpsIT.java | 33 ++ src/test/java/org/tarantool/TestUtils.java | 22 + .../org/tarantool/jdbc/AbstractJdbcIT.java | 47 +- .../org/tarantool/jdbc/JdbcConnectionIT.java | 58 +-- .../jdbc/JdbcConnectionTimeoutIT.java | 133 ++++++ .../org/tarantool/jdbc/JdbcDriverTest.java | 128 +++--- .../jdbc/JdbcExceptionHandlingTest.java | 172 +++----- .../jdbc/JdbcPreparedStatementIT.java | 21 + .../org/tarantool/jdbc/JdbcStatementIT.java | 101 ++++- 33 files changed, 1305 insertions(+), 954 deletions(-) delete mode 100644 src/main/java/org/tarantool/JDBCBridge.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLProperty.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLResultHolder.java delete mode 100644 src/main/java/org/tarantool/jdbc/SQLSocketProvider.java create mode 100644 src/main/java/org/tarantool/jdbc/StatementTimeoutException.java create mode 100644 src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java diff --git a/README.md b/README.md index d9075e34..8a94d0ef 100644 --- a/README.md +++ b/README.md @@ -126,13 +126,22 @@ Feel free to override any method of `TarantoolClientImpl`. For example, to hook all the results, you could override this: ```java -protected void complete(long code, CompletableFuture q); +protected void complete(TarantoolPacket packet, TarantoolOp future); ``` ## Spring NamedParameterJdbcTemplate usage example -To configure sockets you should implements SQLSocketProvider and add socketProvider=abc.xyz.MySocketProvider to connect url. -For example tarantool://localhost:3301?user=test&password=test&socketProvider=abc.xyz.MySocketProvider +The JDBC driver uses `TarantoolClient` implementation to provide a communication with server. +To configure socket channel provider you should implements SocketChannelProvider and add +`socketChannelProvider=abc.xyz.MySocketChannelProvider` to connect url. + +For example: + +``` +tarantool://localhost:3301?user=test&password=test&socketProvider=abc.xyz.MySocketProvider +``` + +Here is an example how you can use the driver covered by Spring `DriverManagerDataSource`: ```java NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(new DriverManagerDataSource("tarantool://localhost:3301?user=test&password=test")); diff --git a/src/main/java/org/tarantool/JDBCBridge.java b/src/main/java/org/tarantool/JDBCBridge.java deleted file mode 100644 index 3ec18af4..00000000 --- a/src/main/java/org/tarantool/JDBCBridge.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.tarantool; - -import org.tarantool.protocol.TarantoolPacket; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -@Deprecated -public class JDBCBridge { - - public static final JDBCBridge EMPTY = new JDBCBridge(Collections.emptyList(), Collections.emptyList()); - - final List sqlMetadata; - final List> rows; - - protected JDBCBridge(TarantoolPacket pack) { - this(SqlProtoUtils.getSQLMetadata(pack), SqlProtoUtils.getSQLData(pack)); - } - - protected JDBCBridge(List sqlMetadata, List> rows) { - this.sqlMetadata = sqlMetadata; - this.rows = rows; - } - - 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) { - return connection.update(sql, params).intValue(); - } - - /** - * 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); - } - - /** - * 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) { - return new JDBCBridge(pack); - } - return rowCount.intValue(); - } - - public List> getRows() { - return rows; - } - - public List getSqlMetadata() { - return sqlMetadata; - } - - @Override - public String toString() { - return "JDBCBridge{" + - "sqlMetadata=" + sqlMetadata + - ", rows=" + rows + - '}'; - } - -} diff --git a/src/main/java/org/tarantool/SqlProtoUtils.java b/src/main/java/org/tarantool/SqlProtoUtils.java index 23a339fd..e4021b7a 100644 --- a/src/main/java/org/tarantool/SqlProtoUtils.java +++ b/src/main/java/org/tarantool/SqlProtoUtils.java @@ -9,10 +9,10 @@ public abstract class SqlProtoUtils { public static List> readSqlResult(TarantoolPacket pack) { - List> data = (List>) pack.getBody().get(Key.DATA.getId()); + List> data = getSQLData(pack); + List metaData = getSQLMetadata(pack); 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++) { diff --git a/src/main/java/org/tarantool/TarantoolBase.java b/src/main/java/org/tarantool/TarantoolBase.java index 6856a495..c74647ae 100644 --- a/src/main/java/org/tarantool/TarantoolBase.java +++ b/src/main/java/org/tarantool/TarantoolBase.java @@ -11,10 +11,6 @@ public abstract class TarantoolBase extends AbstractTarantoolOps, Object, Result> { protected String serverVersion; - - /** - * Connection state. - */ protected MsgPackLite msgPackLite = MsgPackLite.INSTANCE; protected AtomicLong syncId = new AtomicLong(); protected int initialRequestSize = 4096; @@ -25,7 +21,7 @@ public TarantoolBase() { public TarantoolBase(String username, String password, Socket socket) { super(); try { - TarantoolGreeting greeting = ProtoUtils.connect(socket, username, password); + TarantoolGreeting greeting = ProtoUtils.connect(socket, username, password, msgPackLite); this.serverVersion = greeting.getServerVersion(); } catch (IOException e) { throw new CommunicationException("Couldn't connect to tarantool", e); diff --git a/src/main/java/org/tarantool/TarantoolClient.java b/src/main/java/org/tarantool/TarantoolClient.java index ebd54a1a..2ad0c84c 100644 --- a/src/main/java/org/tarantool/TarantoolClient.java +++ b/src/main/java/org/tarantool/TarantoolClient.java @@ -23,6 +23,8 @@ public interface TarantoolClient { boolean isAlive(); + boolean isClosed(); + void waitAlive() throws InterruptedException; boolean waitAlive(long timeout, TimeUnit unit) throws InterruptedException; diff --git a/src/main/java/org/tarantool/TarantoolClientConfig.java b/src/main/java/org/tarantool/TarantoolClientConfig.java index 6ddc40fb..bba14301 100644 --- a/src/main/java/org/tarantool/TarantoolClientConfig.java +++ b/src/main/java/org/tarantool/TarantoolClientConfig.java @@ -2,6 +2,8 @@ public class TarantoolClientConfig { + public static final int DEFAULT_OPERATION_EXPIRY_TIME_MILLIS = 1000; + /** * Auth-related data. */ @@ -67,4 +69,9 @@ public class TarantoolClientConfig { */ public int retryCount = 3; + /** + * Operation expiration period. + */ + public int operationExpiryTimeMillis = DEFAULT_OPERATION_EXPIRY_TIME_MILLIS; + } diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index 7c53cfeb..3bebbc82 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -17,6 +17,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; @@ -30,6 +32,7 @@ public class TarantoolClientImpl extends TarantoolBase> implements Tar = new CommunicationException("Not connected, initializing connection"); protected TarantoolClientConfig config; + protected long operationTimeout; /** * External. @@ -101,6 +104,7 @@ private void initClient(SocketChannelProvider socketProvider, TarantoolClientCon this.thumbstone = NOT_INIT_EXCEPTION; this.config = config; this.initialRequestSize = config.defaultRequestSize; + this.operationTimeout = config.operationExpiryTimeMillis; this.socketProvider = socketProvider; this.stats = new TarantoolClientStats(); this.futures = new ConcurrentHashMap<>(config.predictedFutures); @@ -169,7 +173,7 @@ protected void reconnect(Throwable lastError) { 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, msgPackLite); this.serverVersion = greeting.getServerVersion(); } catch (IOException e) { closeChannel(channel); @@ -236,14 +240,39 @@ protected void configureThreads(String threadName) { reader.setPriority(config.readerThreadPriority); } + /** + * Executes an operation with default timeout. + * + * @param code operation code + * @param args operation arguments + * + * @return deferred result + * + * @see #setOperationTimeout(long) + */ protected Future exec(Code code, Object... args) { - return doExec(code, args); + return doExec(operationTimeout, code, args); + } + + /** + * Executes an operation with the given timeout. + * {@code timeoutMillis} will override the default + * timeout. 0 means the limitless operation. + * + * @param code operation code + * @param args operation arguments + * + * @return deferred result + */ + protected Future exec(long timeoutMillis, Code code, Object... args) { + return doExec(timeoutMillis, code, args); } - protected CompletableFuture doExec(Code code, Object[] args) { + protected TarantoolOp doExec(long timeoutMillis, Code code, Object[] args) { validateArgs(args); long sid = syncId.incrementAndGet(); - TarantoolOp future = new TarantoolOp<>(code); + + TarantoolOp future = makeNewOperation(timeoutMillis, sid, code, args); if (isDead(future)) { return future; @@ -262,6 +291,11 @@ protected CompletableFuture doExec(Code code, Object[] args) { return future; } + protected TarantoolOp makeNewOperation(long timeoutMillis, long sid, Code code, Object[] args) { + return new TarantoolOp<>(sid, code, args) + .orTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + } + protected synchronized void die(String message, Exception cause) { if (thumbstone != null) { return; @@ -297,7 +331,7 @@ public void ping() { protected void write(Code code, Long syncId, Long schemaId, Object... args) throws Exception { - ByteBuffer buffer = ProtoUtils.createPacket(code, syncId, schemaId, args); + ByteBuffer buffer = ProtoUtils.createPacket(msgPackLite, code, syncId, schemaId, args); if (directWrite(buffer)) { return; @@ -379,7 +413,7 @@ private boolean directWrite(ByteBuffer buffer) throws InterruptedException, IOEx protected void readThread() { while (!Thread.currentThread().isInterrupted()) { try { - TarantoolPacket packet = ProtoUtils.readPacket(readChannel); + TarantoolPacket packet = ProtoUtils.readPacket(readChannel, msgPackLite); Map headers = packet.getHeaders(); @@ -427,8 +461,8 @@ protected void writeThread() { } } - protected void fail(CompletableFuture q, Exception e) { - q.completeExceptionally(e); + protected void fail(TarantoolOp future, Exception e) { + future.completeExceptionally(e); } protected void complete(TarantoolPacket packet, TarantoolOp future) { @@ -438,7 +472,7 @@ protected void complete(TarantoolPacket packet, TarantoolOp future) { if (future.getCode() == Code.EXECUTE) { completeSql(future, packet); } else { - ((CompletableFuture) future).complete(packet.getBody().get(Key.DATA.getId())); + ((TarantoolOp) future).complete(packet.getBody().get(Key.DATA.getId())); } } else { Object error = packet.getBody().get(Key.ERROR.getId()); @@ -447,13 +481,13 @@ protected void complete(TarantoolPacket packet, TarantoolOp future) { } } - protected void completeSql(CompletableFuture future, TarantoolPacket pack) { + protected void completeSql(TarantoolOp future, TarantoolPacket pack) { Long rowCount = SqlProtoUtils.getSqlRowCount(pack); if (rowCount != null) { - ((CompletableFuture) future).complete(rowCount); + ((TarantoolOp) future).complete(rowCount); } else { List> values = SqlProtoUtils.readSqlResult(pack); - ((CompletableFuture) future).complete(values); + ((TarantoolOp) future).complete(values); } } @@ -511,11 +545,34 @@ protected void stopIO() { closeChannel(channel); } + /** + * Gets the default timeout for client operations. + * + * @return timeout in millis + */ + public long getOperationTimeout() { + return operationTimeout; + } + + /** + * Sets the default operation timeout. + * + * @param operationTimeout timeout in millis + */ + public void setOperationTimeout(long operationTimeout) { + this.operationTimeout = operationTimeout; + } + @Override public boolean isAlive() { return state.getState() == StateHelper.ALIVE && thumbstone == null; } + @Override + public boolean isClosed() { + return state.getState() == StateHelper.CLOSED; + } + @Override public void waitAlive() throws InterruptedException { state.awaitState(StateHelper.ALIVE); @@ -546,11 +603,9 @@ public TarantoolClientOps, Object, Long> fireAndForgetOps() { return fireAndForgetOps; } - @Override public TarantoolSQLOps>> sqlSyncOps() { return new TarantoolSQLOps>>() { - @Override public Long update(String sql, Object... bind) { return (Long) syncGet(exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, bind)); @@ -616,27 +671,95 @@ public void close() { } - protected class ComposableAsyncOps - extends AbstractTarantoolOps, Object, CompletionStage>> { + protected boolean isDead(TarantoolOp future) { + if (this.thumbstone != null) { + fail(future, new CommunicationException("Connection is dead", thumbstone)); + return true; + } + return false; + } - @Override - public CompletionStage> exec(Code code, Object... args) { - return (CompletionStage>) TarantoolClientImpl.this.doExec(code, args); + protected static class TarantoolOp extends CompletableFuture { + + /** + * A task identifier used in {@link TarantoolClientImpl#futures}. + */ + private final long id; + + /** + * Tarantool binary protocol operation code. + */ + private final Code code; + + /** + * Arguments of operation. + */ + private final Object[] args; + + public TarantoolOp(long id, Code code, Object[] args) { + this.id = id; + this.code = code; + this.args = args; } - @Override - public void close() { - TarantoolClientImpl.this.close(); + public long getId() { + return id; } - } + public Code getCode() { + return code; + } - protected boolean isDead(CompletableFuture q) { - if (this.thumbstone != null) { - fail(q, new CommunicationException("Connection is dead", thumbstone)); - return true; + public Object[] getArgs() { + return args; } - return false; + + /** + * Missed in jdk8 CompletableFuture operator to limit execution + * by time. + */ + public TarantoolOp orTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) { + throw new IllegalArgumentException("Timeout cannot be negative"); + } + if (unit == null) { + throw new IllegalArgumentException("Time unit cannot be null"); + } + if (timeout == 0 || isDone()) { + return this; + } + ScheduledFuture abandonByTimeoutAction = TimeoutScheduler.EXECUTOR.schedule( + () -> { + if (!this.isDone()) { + this.completeExceptionally(new TimeoutException()); + } + }, + timeout, unit + ); + whenComplete( + (ignored, error) -> { + if (error == null && !abandonByTimeoutAction.isDone()) { + abandonByTimeoutAction.cancel(false); + } + } + ); + return this; + } + + /** + * Runs timeout operation as a delayed task. + */ + static class TimeoutScheduler { + + static final ScheduledThreadPoolExecutor EXECUTOR; + + static { + EXECUTOR = + new ScheduledThreadPoolExecutor(1, new TarantoolThreadDaemonFactory("tarantoolTimeout")); + EXECUTOR.setRemoveOnCancelPolicy(true); + } + } + } /** @@ -838,19 +961,17 @@ private void trySignalForReconnection() { } - protected static class TarantoolOp extends CompletableFuture { - - /** - * Tarantool binary protocol operation code. - */ - private final Code code; + protected class ComposableAsyncOps + extends AbstractTarantoolOps, Object, CompletionStage>> { - public TarantoolOp(Code code) { - this.code = code; + @Override + public CompletionStage> exec(Code code, Object... args) { + return (CompletionStage>) TarantoolClientImpl.this.exec(code, args); } - public Code getCode() { - return code; + @Override + public void close() { + TarantoolClientImpl.this.close(); } } diff --git a/src/main/java/org/tarantool/TarantoolClusterClient.java b/src/main/java/org/tarantool/TarantoolClusterClient.java index 7b71f10e..dcd0d64b 100644 --- a/src/main/java/org/tarantool/TarantoolClusterClient.java +++ b/src/main/java/org/tarantool/TarantoolClusterClient.java @@ -11,7 +11,6 @@ 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; @@ -43,7 +42,7 @@ public class TarantoolClusterClient extends TarantoolClientImpl { /** * Collection of operations to be retried. */ - private ConcurrentHashMap> retries = new ConcurrentHashMap<>(); + private ConcurrentHashMap> retries = new ConcurrentHashMap<>(); /** * Constructs a new cluster client. @@ -88,23 +87,23 @@ public TarantoolClusterClient(TarantoolClusterClientConfig config, SocketChannel } @Override - protected boolean isDead(CompletableFuture q) { + protected boolean isDead(TarantoolOp future) { if ((state.getState() & StateHelper.CLOSED) != 0) { - q.completeExceptionally(new CommunicationException("Connection is dead", thumbstone)); + future.completeExceptionally(new CommunicationException("Connection is dead", thumbstone)); return true; } Exception err = thumbstone; if (err != null) { - return checkFail(q, err); + return checkFail(future, err); } return false; } @Override - protected CompletableFuture doExec(Code code, Object[] args) { + protected TarantoolOp doExec(long timeoutMillis, Code code, Object[] args) { validateArgs(args); long sid = syncId.incrementAndGet(); - ExpirableOp future = makeFuture(sid, code, args); + TarantoolOp future = makeNewOperation(timeoutMillis, sid, code, args); return registerOperation(future); } @@ -117,7 +116,7 @@ protected CompletableFuture doExec(Code code, Object[] args) { * * @return registered operation */ - private CompletableFuture registerOperation(ExpirableOp future) { + private TarantoolOp registerOperation(TarantoolOp future) { long stamp = discoveryLock.readLock(); try { if (isDead(future)) { @@ -143,18 +142,17 @@ private CompletableFuture registerOperation(ExpirableOp future) { } @Override - protected void fail(CompletableFuture q, Exception e) { - checkFail(q, e); + protected void fail(TarantoolOp future, Exception e) { + checkFail(future, e); } - protected boolean checkFail(CompletableFuture q, Exception e) { - assert q instanceof ExpirableOp; - if (!isTransientError(e) || ((ExpirableOp) q).hasExpired(System.currentTimeMillis())) { - q.completeExceptionally(e); + protected boolean checkFail(TarantoolOp future, Exception e) { + if (!isTransientError(e)) { + future.completeExceptionally(e); return true; } else { assert retries != null; - retries.put(((ExpirableOp) q).getId(), (ExpirableOp) q); + retries.put(future.getId(), future); return false; } } @@ -172,7 +170,7 @@ protected void close(Exception e) { return; } - for (ExpirableOp op : retries.values()) { + for (TarantoolOp op : retries.values()) { op.completeExceptionally(e); } } @@ -187,11 +185,6 @@ protected boolean isTransientError(Exception e) { return false; } - private ExpirableOp makeFuture(long id, Code code, Object... args) { - int expireTime = ((TarantoolClusterClientConfig) config).operationExpiryTimeMillis; - return new ExpirableOp(id, expireTime, code, args); - } - /** * Reconnect is over, schedule retries. */ @@ -201,11 +194,10 @@ protected void onReconnect() { // First call is before the constructor finished. Skip it. return; } - Collection> futuresToRetry = new ArrayList<>(retries.values()); + Collection> futuresToRetry = new ArrayList<>(retries.values()); retries.clear(); - long now = System.currentTimeMillis(); - for (final ExpirableOp future : futuresToRetry) { - if (!future.hasExpired(now)) { + for (final TarantoolOp future : futuresToRetry) { + if (!future.isDone()) { executor.execute(() -> registerOperation(future)); } } @@ -292,52 +284,4 @@ public synchronized void run() { }; } - /** - * Holds operation code and arguments for retry. - */ - private class ExpirableOp extends TarantoolOp { - - /** - * Moment in time when operation is not considered for retry. - */ - private final long deadline; - - /** - * A task identifier used in {@link TarantoolClientImpl#futures}. - */ - private final long id; - - /** - * Arguments of operation. - */ - private final Object[] args; - - /** - * Constructs a new Expirable operation. - * - * @param id Sync. - * @param expireTime Expiration time (relative) in ms. - * @param code Tarantool operation code. - * @param args Operation arguments. - */ - ExpirableOp(long id, int expireTime, Code code, Object... args) { - super(code); - this.id = id; - this.deadline = System.currentTimeMillis() + expireTime; - this.args = args; - } - - boolean hasExpired(long now) { - return now > deadline; - } - - public long getId() { - return id; - } - - public Object[] getArgs() { - return args; - } - } - } diff --git a/src/main/java/org/tarantool/TarantoolClusterClientConfig.java b/src/main/java/org/tarantool/TarantoolClusterClientConfig.java index 81f67cbb..a621bc85 100644 --- a/src/main/java/org/tarantool/TarantoolClusterClientConfig.java +++ b/src/main/java/org/tarantool/TarantoolClusterClientConfig.java @@ -7,14 +7,8 @@ */ public class TarantoolClusterClientConfig extends TarantoolClientConfig { - 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. diff --git a/src/main/java/org/tarantool/TarantoolConnection.java b/src/main/java/org/tarantool/TarantoolConnection.java index 56e5abb7..cf9c553f 100644 --- a/src/main/java/org/tarantool/TarantoolConnection.java +++ b/src/main/java/org/tarantool/TarantoolConnection.java @@ -13,7 +13,7 @@ import java.util.Map; public class TarantoolConnection extends TarantoolBase> - implements TarantoolSQLOps>> { + implements TarantoolSQLOps>> { protected InputStream in; protected OutputStream out; @@ -40,7 +40,7 @@ protected TarantoolPacket writeAndRead(Code code, Object... args) { out.write(packet.array(), 0, packet.remaining()); out.flush(); - TarantoolPacket responsePacket = ProtoUtils.readPacket(in); + TarantoolPacket responsePacket = ProtoUtils.readPacket(in, msgPackLite); Long c = responsePacket.getCode(); if (c != 0) { diff --git a/src/main/java/org/tarantool/TarantoolSQLOps.java b/src/main/java/org/tarantool/TarantoolSQLOps.java index 899d0232..529eac0f 100644 --- a/src/main/java/org/tarantool/TarantoolSQLOps.java +++ b/src/main/java/org/tarantool/TarantoolSQLOps.java @@ -1,7 +1,33 @@ package org.tarantool; +/** + * Defines a common SQL operations for DQL or DML/DDL. + * + * @param type of rows to be passed as parameters + * @param type of result for DML/DDL operations + * @param type of result for DQL operations + */ public interface TarantoolSQLOps { - Update update(String sql, Tuple... bind); + /** + * Executes a DQL query, typically {@code SELECT} query, + * and returns an obtained result set. + * + * @param sql query + * @param bind arguments to be bound with query parameters + * + * @return result of query + */ Result query(String sql, Tuple... bind); + + /** + * Executes a DML/DDL query using optional parameters. + * + * @param sql query + * @param bind arguments to be bound with query parameters + * + * @return result of query + */ + Update update(String sql, Tuple... bind); + } diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index a8bcd3fb..bd58e277 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -1,21 +1,17 @@ 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.Code; import org.tarantool.CommunicationException; -import org.tarantool.JDBCBridge; -import org.tarantool.TarantoolConnection; +import org.tarantool.Key; +import org.tarantool.SocketChannelProvider; +import org.tarantool.SqlProtoUtils; +import org.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolClientImpl; +import org.tarantool.protocol.TarantoolPacket; import org.tarantool.util.JdbcConstants; import org.tarantool.util.SQLStates; import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketException; import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; @@ -44,6 +40,7 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; +import java.util.concurrent.TimeoutException; /** * Tarantool {@link Connection} implementation. @@ -55,114 +52,54 @@ public class SQLConnection implements Connection { private static final int UNSET_HOLDABILITY = 0; private static final String PING_QUERY = "SELECT 1"; - private final TarantoolConnection connection; - + private final SQLTarantoolClientImpl client; private final String url; private final Properties properties; - private DatabaseMetaData cachedMetadata; - private int resultSetHoldability = UNSET_HOLDABILITY; SQLConnection(String url, Properties properties) throws SQLException { this.url = url; this.properties = properties; - String user = properties.getProperty(PROP_USER); - String pass = properties.getProperty(PROP_PASSWORD); - Socket socket = null; try { - socket = getConnectedSocket(); - this.connection = makeConnection(user, pass, socket); + client = makeSqlClient(makeAddress(properties), makeConfigFromProperties(properties)); } catch (Exception e) { - if (socket != null) { - try { - socket.close(); - } catch (IOException ignored) { - // No-op. - } - } - if (e instanceof SQLException) { - throw (SQLException) e; - } throw new SQLException("Couldn't initiate connection using " + SQLDriver.diagProperties(properties), e); } } - /** - * Provides a connected socket to be used to initialize a native tarantool - * connection. - *

- * The implementation assumes that {@link #properties} contains all the - * necessary info extracted from both the URI and connection properties - * provided by the user. However, the overrides are free to also use the - * {@link #url} if required. - *

- * A connect is guarded with user provided timeout. Socket is configured - * to honor this timeout for the following read/write operations as well. - * - * @return Connected socket. - * - * @throws SQLException if failed. - */ - protected Socket getConnectedSocket() throws SQLException { - Socket socket = makeSocket(); - int timeout = Integer.parseInt(properties.getProperty(PROP_SOCKET_TIMEOUT)); - String host = properties.getProperty(PROP_HOST); - int port = Integer.parseInt(properties.getProperty(PROP_PORT)); - try { - socket.connect(new InetSocketAddress(host, port), timeout); - } catch (IOException e) { - throw new SQLException("Couldn't connect to " + host + ":" + port, e); - } - // Setup socket further. - if (timeout > 0) { - try { - socket.setSoTimeout(timeout); - } catch (SocketException e) { - try { - socket.close(); - } catch (IOException ignored) { - // No-op. - } - throw new SQLException("Couldn't set socket timeout. timeout=" + timeout, e); - } - } - return socket; + protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { + return new SQLTarantoolClientImpl(address, config); } - /** - * Provides a newly connected socket instance. The method is intended to be - * overridden to enable unit testing of the class. - *

- * Not supposed to contain any logic other than a call to constructor. - * - * @return socket. - */ - protected Socket makeSocket() { - return new Socket(); + private String makeAddress(Properties properties) throws SQLException { + String host = SQLProperty.HOST.getString(properties); + int port = SQLProperty.PORT.getInt(properties); + return host + ":" + port; } - /** - * Provides a native tarantool connection instance. The method is intended - * to be overridden to enable unit testing of the class. - *

- * Not supposed to contain any logic other than a call to constructor. - * - * @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; - } - }; + private TarantoolClientConfig makeConfigFromProperties(Properties properties) throws SQLException { + TarantoolClientConfig clientConfig = new TarantoolClientConfig(); + clientConfig.username = SQLProperty.USER.getString(properties); + clientConfig.password = SQLProperty.PASSWORD.getString(properties); + + clientConfig.operationExpiryTimeMillis = SQLProperty.QUERY_TIMEOUT.getInt(properties); + clientConfig.initTimeoutMillis = SQLProperty.LOGIN_TIMEOUT.getInt(properties); + + return clientConfig; + } + + @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 @@ -268,15 +205,8 @@ public boolean getAutoCommit() throws SQLException { } @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(); + public void close() throws SQLException { + client.close(); } @Override @@ -284,8 +214,8 @@ public void rollback() throws SQLException { checkNotClosed(); if (getAutoCommit()) { throw new SQLNonTransientException( - "Cannot rollback when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + "Cannot rollback when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() ); } throw new SQLFeatureNotSupportedException(); @@ -304,13 +234,32 @@ public void rollback(Savepoint savepoint) throws SQLException { } @Override - public void close() throws SQLException { - connection.close(); + public boolean isClosed() throws SQLException { + return client.isClosed(); } @Override - public boolean isClosed() throws SQLException { - return connection.isClosed(); + 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 { + checkNotClosed(); + if (getAutoCommit()) { + throw new SQLNonTransientException( + "Cannot set a savepoint when auto-commit is enabled.", + SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + ); + } + throw new SQLFeatureNotSupportedException(); } @Override @@ -398,28 +347,25 @@ public int getHoldability() throws SQLException { return resultSetHoldability; } + /** + * {@inheritDoc} + * + * @param timeout time in seconds + * + * @return connection activity status + */ @Override - public Savepoint setSavepoint() throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { + public boolean isValid(int timeout) throws SQLException { + if (timeout < 0) { throw new SQLNonTransientException( - "Cannot set a savepoint when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() + "Timeout cannot be negative", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() ); } - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Savepoint setSavepoint(String name) throws SQLException { - checkNotClosed(); - if (getAutoCommit()) { - throw new SQLNonTransientException( - "Cannot set a savepoint when auto-commit is enabled.", - SQLStates.INVALID_TRANSACTION_STATE.getSqlState() - ); + if (isClosed()) { + return false; } - throw new SQLFeatureNotSupportedException(); + return checkConnection(timeout); } @Override @@ -452,32 +398,10 @@ public SQLXML createSQLXML() throws SQLException { throw new SQLFeatureNotSupportedException(); } - /** - * {@inheritDoc} - * - * @param timeout temporally ignored param - * - * @return connection activity status - */ - @Override - 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); - } - private boolean checkConnection(int timeout) { - ResultSet resultSet = null; + ResultSet resultSet; try (Statement pingStatement = createStatement()) { - // todo: before use timeout we need to provide query timeout per statement - + pingStatement.setQueryTimeout(timeout); resultSet = pingStatement.executeQuery(PING_QUERY); boolean isValid = resultSet.next() && resultSet.getInt(1) == 1; resultSet.close(); @@ -488,6 +412,15 @@ private boolean checkConnection(int timeout) { } } + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + checkNotClosed(); + if (milliseconds < 0) { + throw new SQLException("Network timeout cannot be negative."); + } + client.setOperationTimeout(milliseconds); + } + @Override public void setClientInfo(String name, String value) throws SQLClientInfoException { try { @@ -587,27 +520,51 @@ public void abort(Executor executor) throws SQLException { } @Override - public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + public int getNetworkTimeout() throws SQLException { checkNotClosed(); + return (int) client.getOperationTimeout(); + } - if (milliseconds < 0) { - throw new SQLException("Network timeout cannot be negative."); - } + protected SQLResultHolder execute(long timeout, String sql, Object... args) throws SQLException { + checkNotClosed(); + int networkTimeout = getNetworkTimeout(); + return (timeout == 0 || (networkTimeout > 0 && networkTimeout < timeout)) + ? executeWithNetworkTimeout(sql, args) + : executeWithStatementTimeout(timeout, sql, args); + } + private SQLResultHolder executeWithNetworkTimeout(String sql, Object... args) throws SQLException { try { - connection.setSocketTimeout(milliseconds); - } catch (SocketException e) { - throw new SQLException("Failed to set socket timeout: timeout=" + milliseconds, e); + return client.sqlRawOps().execute(sql, args); + } catch (Exception e) { + handleException(e); + throw new SQLException(formatError(sql, args), e); } } - @Override - public int getNetworkTimeout() throws SQLException { - checkNotClosed(); + /** + * Executes a query using a custom timeout. + * + * @param timeout query timeout + * @param sql query + * @param args query bindings + * + * @return SQL result holder + * + * @throws StatementTimeoutException if query execution took more than query timeout + * @throws SQLException if any other errors occurred + */ + private SQLResultHolder executeWithStatementTimeout(long timeout, String sql, Object... args) throws SQLException { try { - return connection.getSocketTimeout(); - } catch (SocketException e) { - throw new SQLException("Failed to retrieve socket timeout", e); + return client.sqlRawOps().execute(timeout, sql, args); + } catch (Exception e) { + // statement timeout should not affect the current connection + // but can be handled by the caller side + if (e.getCause() instanceof TimeoutException) { + throw new StatementTimeoutException(formatError(sql, args), e.getCause()); + } + handleException(e); + throw new SQLException(formatError(sql, args), e); } } @@ -624,41 +581,11 @@ public boolean isWrapperFor(Class type) throws SQLException { return type.isAssignableFrom(this.getClass()); } - protected Object execute(String sql, Object... args) throws SQLException { - checkNotClosed(); - try { - return JDBCBridge.execute(connection, sql, args); - } catch (Exception e) { - handleException(e); - throw new SQLException(formatError(sql, args), e); - } - } - - protected JDBCBridge executeQuery(String sql, Object... args) throws SQLException { - checkNotClosed(); - try { - return JDBCBridge.query(connection, sql, args); - } catch (Exception e) { - handleException(e); - throw new SQLException(formatError(sql, args), e); - } - } - - protected int executeUpdate(String sql, Object... args) throws SQLException { - checkNotClosed(); - try { - return JDBCBridge.update(connection, sql, args); - } catch (Exception e) { - handleException(e); - throw new SQLException(formatError(sql, args), e); - } - } - protected List nativeSelect(Integer space, Integer index, List key, int offset, int limit, int iterator) throws SQLException { checkNotClosed(); try { - return connection.select(space, index, key, offset, limit, iterator); + return client.syncOps().select(space, index, key, offset, limit, iterator); } catch (Exception e) { handleException(e); throw new SQLException(e); @@ -666,7 +593,24 @@ protected List nativeSelect(Integer space, Integer index, List key, int of } protected String getServerVersion() { - return connection.getServerVersion(); + return client.getServerVersion(); + } + + /** + * Inspects passed exception and closes the connection if appropriate. + * + * @param e Exception to process. + */ + private void handleException(Exception e) { + if (e instanceof CommunicationException || + e instanceof IOException || + e.getCause() instanceof TimeoutException) { + try { + close(); + } catch (SQLException ignored) { + // No-op. + } + } } /** @@ -691,22 +635,6 @@ Properties getProperties() { return properties; } - /** - * Inspects passed exception and closes the connection if appropriate. - * - * @param e Exception to process. - */ - private void handleException(Exception e) { - if (CommunicationException.class.isAssignableFrom(e.getClass()) || - IOException.class.isAssignableFrom(e.getClass())) { - try { - close(); - } catch (SQLException ignored) { - // No-op. - } - } - } - /** * Checks all params required to make statements. * @@ -789,4 +717,53 @@ private static String formatError(String sql, Object... params) { return "Failed to execute SQL: " + sql + ", params: " + Arrays.deepToString(params); } + static class SQLTarantoolClientImpl extends TarantoolClientImpl { + + final SQLRawOps sqlRawOps = new SQLRawOps() { + @Override + public SQLResultHolder execute(String sql, Object... binds) { + return (SQLResultHolder) syncGet(exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, binds)); + } + + @Override + public SQLResultHolder execute(long timeoutMillis, String sql, Object... binds) { + return (SQLResultHolder) syncGet( + exec(timeoutMillis, Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, binds) + ); + } + }; + + SQLTarantoolClientImpl(String address, TarantoolClientConfig config) { + super(address, config); + msgPackLite = SQLMsgPackLite.INSTANCE; + } + + SQLTarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) { + super(socketProvider, config); + msgPackLite = SQLMsgPackLite.INSTANCE; + } + + SQLRawOps sqlRawOps() { + return sqlRawOps; + } + + @Override + protected void completeSql(TarantoolOp future, TarantoolPacket pack) { + Long rowCount = SqlProtoUtils.getSqlRowCount(pack); + SQLResultHolder result = (rowCount == null) + ? SQLResultHolder.ofQuery(SqlProtoUtils.getSQLMetadata(pack), SqlProtoUtils.getSQLData(pack)) + : SQLResultHolder.ofUpdate(rowCount.intValue()); + ((TarantoolOp) future).complete(result); + } + + interface SQLRawOps { + + SQLResultHolder execute(String sql, Object... binds); + + SQLResultHolder execute(long timeoutMillis, String sql, Object... binds); + + } + + } + } diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index 1605fd86..ed354051 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -1,6 +1,6 @@ package org.tarantool.jdbc; -import org.tarantool.JDBCBridge; +import org.tarantool.SqlProtoUtils; import org.tarantool.Version; import java.sql.Connection; @@ -16,8 +16,8 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; -@SuppressWarnings("Since15") public class SQLDatabaseMetadata implements DatabaseMetaData { protected static final int _VSPACE = 281; @@ -29,24 +29,10 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { public static final int SPACE_ID_IDX = 0; protected final SQLConnection connection; - protected class SQLNullResultSet extends SQLResultSet { - - public SQLNullResultSet(JDBCBridge bridge, SQLStatement ownerStatement) throws SQLException { - super(bridge, ownerStatement); - } - - @Override - protected Object getRaw(int columnIndex) throws SQLException { - List row = getCurrentRow(); - return columnIndex > row.size() ? null : row.get(columnIndex - 1); - } - - @Override - protected int findColumnIndex(String columnLabel) throws SQLException { - int index = super.findColumnIndex(columnLabel); - return index == 0 ? Integer.MAX_VALUE : index; - } - + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) + throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } public SQLDatabaseMetadata(SQLConnection connection) { @@ -643,34 +629,13 @@ public boolean dataDefinitionIgnoredInTransactions() throws SQLException { return false; } - @Override - public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) - throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); - } - @Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); - } - - protected boolean like(String value, String[] parts) { - if (parts == null || parts.length == 0) { - return true; - } - int i = 0; - for (String part : parts) { - i = value.indexOf(part, i); - if (i < 0) { - break; - } - i += part.length(); - } - return (i > -1 && (parts[parts.length - 1].length() == 0 || i == value.length())); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -679,7 +644,7 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam try { if (types != null && !Arrays.asList(types).contains("TABLE")) { connection.checkNotClosed(); - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } String[] parts = tableNamePattern == null ? new String[] { "" } : tableNamePattern.split("%"); List> spaces = (List>) connection.nativeSelect( @@ -714,6 +679,31 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam } } + protected boolean like(String value, String[] parts) { + if (parts == null || parts.length == 0) { + return true; + } + int i = 0; + for (String part : parts) { + i = value.indexOf(part, i); + if (i < 0) { + break; + } + i += part.length(); + } + return (i > -1 && (parts[parts.length - 1].length() == 0 || i == value.length())); + } + + @Override + public ResultSet getTableTypes() throws SQLException { + return asMetadataResultSet( + SQLResultHolder.ofQuery( + Arrays.asList(new SqlProtoUtils.SQLMetaData("TABLE_TYPE")), + Arrays.asList(Arrays.asList("TABLE")) + ) + ); + } + @Override public ResultSet getSchemas() throws SQLException { return rowOfNullsResultSet(); @@ -730,8 +720,9 @@ public ResultSet getCatalogs() throws SQLException { } @Override - public ResultSet getTableTypes() throws SQLException { - return asMetadataResultSet(JDBCBridge.mock(Arrays.asList("TABLE_TYPE"), Arrays.asList(Arrays.asList("TABLE")))); + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -809,14 +800,13 @@ public ResultSet getTablePrivileges(String catalog, String schemaPattern, String } @Override - public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) - throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override - public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -874,14 +864,9 @@ public int compare(List row0, List row1) { } } - @Override - public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); - } - @Override public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -892,18 +877,24 @@ public ResultSet getCrossReference(String parentCatalog, String foreignSchema, String foreignTable) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override public ResultSet getTypeInfo() throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) + throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -968,9 +959,8 @@ public boolean supportsBatchUpdates() throws SQLException { } @Override - public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) - throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -998,14 +988,9 @@ public boolean supportsGetGeneratedKeys() throws SQLException { return false; } - @Override - public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); - } - @Override public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -1014,7 +999,12 @@ public ResultSet getAttributes(String catalog, String typeNamePattern, String attributeNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); + } + + @Override + public ResultSet getClientInfoProperties() throws SQLException { + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } /** @@ -1083,15 +1073,10 @@ public boolean autoCommitFailureClosesAllResultSets() throws SQLException { return false; } - @Override - public ResultSet getClientInfoProperties() throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); - } - @Override public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -1100,7 +1085,7 @@ public ResultSet getFunctionColumns(String catalog, String functionNamePattern, String columnNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); } @Override @@ -1109,7 +1094,11 @@ public ResultSet getPseudoColumns(String catalog, String tableNamePattern, String columnNamePattern) throws SQLException { - return asMetadataResultSet(JDBCBridge.EMPTY); + return asMetadataResultSet(SQLResultHolder.ofEmptyQuery()); + } + + private ResultSet asMetadataResultSet(SQLResultHolder holder) throws SQLException { + return createMetadataStatement().executeMetadata(holder); } @Override @@ -1130,8 +1119,11 @@ public boolean isWrapperFor(Class type) throws SQLException { return type.isAssignableFrom(this.getClass()); } - private ResultSet asMetadataResultSet(JDBCBridge jdbcBridge) throws SQLException { - return createMetadataStatement().executeMetadata(jdbcBridge); + private SQLNullResultSet sqlNullResultSet(List columnNames, List> rows) throws SQLException { + List meta = columnNames.stream() + .map(SqlProtoUtils.SQLMetaData::new) + .collect(Collectors.toList()); + return new SQLNullResultSet(SQLResultHolder.ofQuery(meta, rows), createMetadataStatement()); } private SQLStatement createMetadataStatement() throws SQLException { @@ -1158,8 +1150,24 @@ private SQLNullResultSet emptyResultSet(List colNames) throws SQLExcepti return sqlNullResultSet(colNames, Collections.emptyList()); } - private SQLNullResultSet sqlNullResultSet(List colNames, List> rows) throws SQLException { - return new SQLNullResultSet(JDBCBridge.mock(colNames, rows), createMetadataStatement()); + protected class SQLNullResultSet extends SQLResultSet { + + public SQLNullResultSet(SQLResultHolder holder, SQLStatement ownerStatement) throws SQLException { + super(holder, ownerStatement); + } + + @Override + protected Object getRaw(int columnIndex) throws SQLException { + List row = getCurrentRow(); + return columnIndex > row.size() ? null : row.get(columnIndex - 1); + } + + @Override + protected int findColumnIndex(String columnLabel) throws SQLException { + int index = super.findColumnIndex(columnLabel); + return index == 0 ? Integer.MAX_VALUE : index; + } + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java index c73ae96e..8c1f0bc4 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDriver.java +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -1,6 +1,9 @@ package org.tarantool.jdbc; -import java.net.Socket; +import org.tarantool.SocketChannelProvider; +import org.tarantool.TarantoolClientConfig; +import org.tarantool.util.SQLStates; + import java.net.URI; import java.sql.Connection; import java.sql.Driver; @@ -22,60 +25,47 @@ public class SQLDriver implements Driver { } } - public static final String PROP_HOST = "host"; - public static final String PROP_PORT = "port"; - public static final String PROP_SOCKET_PROVIDER = "socketProvider"; - public static final String PROP_USER = "user"; - public static final String PROP_PASSWORD = "password"; - public static final String PROP_SOCKET_TIMEOUT = "socketTimeout"; - - // Define default values once here. - 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(); + private final Map providerCache = new ConcurrentHashMap<>(); @Override public Connection connect(String url, Properties info) throws SQLException { + if (url == null) { + throw new SQLException("Url must not be null"); + } + if (!acceptsURL(url)) { + return null; + } + final URI uri = URI.create(url); final Properties urlProperties = parseQueryString(uri, info); - String providerClassName = urlProperties.getProperty(PROP_SOCKET_PROVIDER); + String providerClassName = SQLProperty.SOCKET_CHANNEL_PROVIDER.getString(urlProperties); if (providerClassName == null) { return new SQLConnection(url, urlProperties); } - final SQLSocketProvider provider = getSocketProviderInstance(providerClassName); + final SocketChannelProvider provider = getSocketProviderInstance(providerClassName); return new SQLConnection(url, urlProperties) { @Override - protected Socket getConnectedSocket() throws SQLException { - Socket socket = provider.getConnectedSocket(uri, urlProperties); - if (socket == null) { - throw new SQLException("The socket provider returned null socket"); - } - return socket; + protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { + return new SQLTarantoolClientImpl(provider, config); } }; } protected Properties parseQueryString(URI uri, Properties info) throws SQLException { - Properties urlProperties = new Properties(defaults); + Properties urlProperties = new Properties(); String userInfo = uri.getUserInfo(); if (userInfo != null) { // Get user and password from the corresponding part of the URI, i.e. before @ sign. int i = userInfo.indexOf(':'); if (i < 0) { - urlProperties.setProperty(PROP_USER, userInfo); + SQLProperty.USER.setString(urlProperties, userInfo); } else { - urlProperties.setProperty(PROP_USER, userInfo.substring(0, i)); - urlProperties.setProperty(PROP_PASSWORD, userInfo.substring(i + 1)); + SQLProperty.USER.setString(urlProperties, userInfo.substring(0, i)); + SQLProperty.PASSWORD.setString(urlProperties, userInfo.substring(i + 1)); } } if (uri.getQuery() != null) { @@ -91,48 +81,48 @@ protected Properties parseQueryString(URI uri, Properties info) throws SQLExcept } if (uri.getHost() != null) { // Default values are pre-put above. - urlProperties.setProperty(PROP_HOST, uri.getHost()); + urlProperties.setProperty(SQLProperty.HOST.getName(), uri.getHost()); } if (uri.getPort() >= 0) { // We need to convert port to string otherwise getProperty() will not see it. - urlProperties.setProperty(PROP_PORT, String.valueOf(uri.getPort())); + urlProperties.setProperty(SQLProperty.PORT.getName(), String.valueOf(uri.getPort())); } if (info != null) { urlProperties.putAll(info); } // Validate properties. - int port; - try { - port = Integer.parseInt(urlProperties.getProperty(PROP_PORT)); - } catch (Exception e) { - throw new SQLException("Port must be a valid number."); - } + int port = SQLProperty.PORT.getInt(urlProperties); if (port <= 0 || port > 65535) { - throw new SQLException("Port is out of range: " + port); - } - int timeout; - try { - timeout = Integer.parseInt(urlProperties.getProperty(PROP_SOCKET_TIMEOUT)); - } catch (Exception e) { - throw new SQLException("Timeout must be a valid number."); + throw new SQLException("Port is out of range: " + port, SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); } + + checkTimeout(SQLProperty.LOGIN_TIMEOUT, urlProperties); + checkTimeout(SQLProperty.QUERY_TIMEOUT, urlProperties); + + return urlProperties; + } + + private void checkTimeout(SQLProperty sqlProperty, Properties properties) throws SQLException { + int timeout = sqlProperty.getInt(properties); if (timeout < 0) { - throw new SQLException("Timeout must not be negative."); + throw new SQLException( + "Property " + sqlProperty.getName() + " must not be negative.", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); } - return urlProperties; } - protected SQLSocketProvider getSocketProviderInstance(String className) throws SQLException { - SQLSocketProvider provider = providerCache.get(className); + protected SocketChannelProvider getSocketProviderInstance(String className) throws SQLException { + SocketChannelProvider provider = providerCache.get(className); if (provider == null) { synchronized (this) { provider = providerCache.get(className); if (provider == null) { try { Class cls = Class.forName(className); - if (SQLSocketProvider.class.isAssignableFrom(cls)) { - provider = (SQLSocketProvider) cls.newInstance(); + if (SocketChannelProvider.class.isAssignableFrom(cls)) { + provider = (SocketChannelProvider) cls.getDeclaredConstructor().newInstance(); providerCache.put(className, provider); } } catch (Exception e) { @@ -143,7 +133,7 @@ protected SQLSocketProvider getSocketProviderInstance(String className) throws S } if (provider == null) { throw new SQLException(String.format("The socket provider %s does not implement %s", - className, SQLSocketProvider.class.getCanonicalName())); + className, SocketChannelProvider.class.getCanonicalName())); } return provider; } @@ -159,36 +149,18 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws URI uri = new URI(url); Properties properties = parseQueryString(uri, info); - DriverPropertyInfo host = new DriverPropertyInfo(PROP_HOST, properties.getProperty(PROP_HOST)); - host.required = true; - host.description = "Tarantool server host"; - - DriverPropertyInfo port = new DriverPropertyInfo(PROP_PORT, properties.getProperty(PROP_PORT)); - port.required = true; - port.description = "Tarantool server port"; - - DriverPropertyInfo user = new DriverPropertyInfo(PROP_USER, properties.getProperty(PROP_USER)); - user.required = false; - user.description = "user"; - - DriverPropertyInfo password = new DriverPropertyInfo(PROP_PASSWORD, properties.getProperty(PROP_PASSWORD)); - password.required = false; - password.description = "password"; - - DriverPropertyInfo socketProvider = new DriverPropertyInfo( - 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)); - - 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."; - - return new DriverPropertyInfo[] { host, port, user, password, socketProvider, socketTimeout }; + SQLProperty[] sqlProperties = SQLProperty.values(); + DriverPropertyInfo[] propertyInfoList = new DriverPropertyInfo[sqlProperties.length]; + for (int i = 0; i < sqlProperties.length; i++) { + SQLProperty sqlProperty = sqlProperties[i]; + String value = sqlProperty.getString(properties); + DriverPropertyInfo propertyInfo = new DriverPropertyInfo(sqlProperty.getName(), value); + propertyInfo.required = sqlProperty.isRequired(); + propertyInfo.description = sqlProperty.getDescription(); + propertyInfo.choices = sqlProperty.getChoices(); + propertyInfoList[i] = propertyInfo; + } + return propertyInfoList; } catch (Exception e) { throw new SQLException(e); } @@ -223,6 +195,9 @@ public Logger getParentLogger() throws SQLFeatureNotSupportedException { * @return Comma-separated pairs of property names and values. */ protected static String diagProperties(Properties props) { + String userProp = SQLProperty.USER.getName(); + String passwordProp = SQLProperty.PASSWORD.getName(); + StringBuilder sb = new StringBuilder(); for (Map.Entry e : props.entrySet()) { if (sb.length() > 0) { @@ -230,9 +205,13 @@ protected static String diagProperties(Properties props) { } sb.append(e.getKey()); sb.append('='); - sb.append((PROP_USER.equals(e.getKey()) || PROP_PASSWORD.equals(e.getKey())) - ? "*****" : e.getValue().toString()); + sb.append( + (userProp.equals(e.getKey()) || passwordProp.equals(e.getKey())) + ? "*****" + : e.getValue().toString() + ); } return sb.toString(); } + } diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index 760aa569..63383ea6 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -1,5 +1,7 @@ package org.tarantool.jdbc; +import org.tarantool.util.SQLStates; + import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; @@ -50,8 +52,10 @@ public SQLPreparedStatement(SQLConnection connection, @Override public ResultSet executeQuery() throws SQLException { checkNotClosed(); - discardLastResults(); - return createResultSet(connection.executeQuery(sql, getParams())); + if (!executeInternal(sql, getParams())) { + throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); + } + return resultSet; } @Override @@ -74,8 +78,13 @@ protected Object[] getParams() throws SQLException { @Override public int executeUpdate() throws SQLException { checkNotClosed(); - discardLastResults(); - return connection.executeUpdate(sql, getParams()); + if (executeInternal(sql, getParams())) { + throw new SQLException( + "Result was returned but nothing was expected", + SQLStates.TOO_MANY_RESULTS.getSqlState() + ); + } + return updateCount; } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLProperty.java b/src/main/java/org/tarantool/jdbc/SQLProperty.java new file mode 100644 index 00000000..11b2d93a --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLProperty.java @@ -0,0 +1,123 @@ +package org.tarantool.jdbc; + +import org.tarantool.util.SQLStates; + +import java.sql.SQLException; +import java.util.Properties; + +public enum SQLProperty { + HOST( + "host", + "Tarantool server host", + "localhost", + null, + true + ), + PORT( + "port", + "Tarantool server port", + "3301", + null, + true + ), + SOCKET_CHANNEL_PROVIDER( + "socketChannelProvider", + "SocketProvider class implements org.tarantool.SocketChannelProvider", + null, + null, + false + ), + USER( + "user", + "Username to connect", + null, + null, + false + ), + PASSWORD( + "password", + "User password to connect", + null, + null, + false + ), + LOGIN_TIMEOUT( + "loginTimeout", + "The number of milliseconds to wait for connection establishment. " + + "The default value is 60000 (1 minute).", + "60000", + null, + false + ), + QUERY_TIMEOUT( + "queryTimeout", + "The number of milliseconds to wait before a timeout is occurred for the query. " + + "The default value is 0 (infinite) timeout.", + "0", + null, + false + ); + + private final String name; + private final String description; + private final String defaultValue; + private final String[] choices; + private final boolean required; + + SQLProperty(String name, String description, String defaultValue, String[] choices, boolean required) { + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + this.choices = choices; + this.required = required; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getDefaultValue() { + return defaultValue; + } + + public boolean isRequired() { + return required; + } + + public String[] getChoices() { + return choices; + } + + public String getString(Properties properties) { + return properties.getProperty(name, defaultValue); + } + + public void setString(Properties properties, String value) { + if (value == null) { + properties.remove(name); + } else { + properties.setProperty(name, value); + } + } + + public int getInt(Properties properties) throws SQLException { + String property = getString(properties); + try { + return Integer.parseInt(property); + } catch (NumberFormatException exception) { + throw new SQLException( + "Property " + name + " must be a valid number.", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), + exception + ); + } + } + + public void setInt(Properties properties, int value) { + setString(properties, Integer.toString(value)); + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLResultHolder.java b/src/main/java/org/tarantool/jdbc/SQLResultHolder.java new file mode 100644 index 00000000..04c7eba3 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLResultHolder.java @@ -0,0 +1,53 @@ +package org.tarantool.jdbc; + +import org.tarantool.SqlProtoUtils; + +import java.util.Collections; +import java.util.List; + +/** + * Union wrapper for SQL query results as well as + * SQL update results. + */ +public class SQLResultHolder { + + final List sqlMetadata; + final List> rows; + final int updateCount; + + public SQLResultHolder(List sqlMetadata, List> rows, int updateCount) { + this.sqlMetadata = sqlMetadata; + this.rows = rows; + this.updateCount = updateCount; + } + + public static SQLResultHolder ofQuery(final List sqlMetadata, + final List> rows) { + return new SQLResultHolder(sqlMetadata, rows, -1); + } + + public static SQLResultHolder ofEmptyQuery() { + return ofQuery(Collections.emptyList(), Collections.emptyList()); + } + + public static SQLResultHolder ofUpdate(int updateCount) { + return new SQLResultHolder(null, null, updateCount); + } + + public List getSqlMetadata() { + return sqlMetadata; + } + + public List> getRows() { + return rows; + } + + public int getUpdateCount() { + return updateCount; + } + + public boolean isQueryResult() { + return sqlMetadata != null && rows != null; + } + +} diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index de7a78ac..95c03a2b 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -1,6 +1,5 @@ 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; @@ -53,15 +52,15 @@ public class SQLResultSet implements ResultSet { private final int concurrencyLevel; private final int holdability; - public SQLResultSet(JDBCBridge bridge, SQLStatement ownerStatement) throws SQLException { - metaData = new SQLResultSetMetaData(bridge.getSqlMetadata()); + public SQLResultSet(SQLResultHolder holder, SQLStatement ownerStatement) throws SQLException { + metaData = new SQLResultSetMetaData(holder.getSqlMetadata()); statement = ownerStatement; scrollType = statement.getResultSetType(); concurrencyLevel = statement.getResultSetConcurrency(); holdability = statement.getResultSetHoldability(); this.maxRows = statement.getMaxRows(); - List> fetchedRows = bridge.getRows(); + List> fetchedRows = holder.getRows(); List> rows = maxRows == 0 || maxRows >= fetchedRows.size() ? fetchedRows : fetchedRows.subList(0, maxRows); diff --git a/src/main/java/org/tarantool/jdbc/SQLSocketProvider.java b/src/main/java/org/tarantool/jdbc/SQLSocketProvider.java deleted file mode 100644 index 0957950f..00000000 --- a/src/main/java/org/tarantool/jdbc/SQLSocketProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tarantool.jdbc; - -import java.net.Socket; -import java.net.URI; -import java.util.Properties; - -public interface SQLSocketProvider { - - Socket getConnectedSocket(URI uri, Properties params); -} diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index a4dfdaf1..f5ae17d3 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -1,6 +1,5 @@ package org.tarantool.jdbc; -import org.tarantool.JDBCBridge; import org.tarantool.util.JdbcConstants; import org.tarantool.util.SQLStates; @@ -9,8 +8,10 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLNonTransientException; +import java.sql.SQLTimeoutException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -27,8 +28,8 @@ public class SQLStatement implements Statement { /** * Current result set / update count associated to this statement. */ - private SQLResultSet resultSet; - private int updateCount; + protected SQLResultSet resultSet; + protected int updateCount; private final int resultSetType; private final int resultSetConcurrency; @@ -36,6 +37,11 @@ public class SQLStatement implements Statement { private int maxRows; + /** + * Query timeout in millis. + */ + private long timeout; + private final AtomicBoolean isClosed = new AtomicBoolean(false); protected SQLStatement(SQLConnection sqlConnection) throws SQLException { @@ -136,12 +142,18 @@ public void setEscapeProcessing(boolean enable) throws SQLException { @Override public int getQueryTimeout() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return (int) TimeUnit.MILLISECONDS.toSeconds(timeout); } @Override public void setQueryTimeout(int seconds) throws SQLException { - throw new SQLFeatureNotSupportedException(); + if (seconds < 0) { + throw new SQLNonTransientException( + "Query timeout must be positive or zero", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + timeout = TimeUnit.SECONDS.toMillis(seconds); } @Override @@ -167,7 +179,6 @@ public void setCursorName(String name) throws SQLException { @Override public boolean execute(String sql) throws SQLException { checkNotClosed(); - discardLastResults(); return executeInternal(sql); } @@ -275,7 +286,7 @@ public Connection getConnection() throws SQLException { @Override public ResultSet getGeneratedKeys() throws SQLException { checkNotClosed(); - return new SQLResultSet(JDBCBridge.EMPTY, this); + return new SQLResultSet(SQLResultHolder.ofEmptyQuery(), this); } @Override @@ -349,26 +360,19 @@ protected void discardLastResults() throws SQLException { */ protected boolean executeInternal(String sql, Object... params) throws SQLException { discardLastResults(); - return handleResult(connection.execute(sql, params)); - } + SQLResultHolder holder; + try { + holder = connection.execute(timeout, sql, params); + } catch (StatementTimeoutException e) { + cancel(); + throw new SQLTimeoutException(); + } - /** - * 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); - updateCount = -1; - return true; - } else { - resultSet = null; - updateCount = (Integer) result; - return false; + if (holder.isQueryResult()) { + resultSet = new SQLResultSet(holder, this); } + updateCount = holder.getUpdateCount(); + return holder.isQueryResult(); } /** @@ -381,13 +385,13 @@ protected boolean handleResult(Object result) throws SQLException { * @throws SQLException if a database access error occurs or * this method is called on a closed Statement */ - public ResultSet executeMetadata(JDBCBridge data) throws SQLException { + public ResultSet executeMetadata(SQLResultHolder data) throws SQLException { checkNotClosed(); return createResultSet(data); } - protected SQLResultSet createResultSet(JDBCBridge result) throws SQLException { - return new SQLResultSet(result, this); + protected SQLResultSet createResultSet(SQLResultHolder holder) throws SQLException { + return new SQLResultSet(holder, this); } protected void checkNotClosed() throws SQLException { diff --git a/src/main/java/org/tarantool/jdbc/StatementTimeoutException.java b/src/main/java/org/tarantool/jdbc/StatementTimeoutException.java new file mode 100644 index 00000000..393a6bec --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/StatementTimeoutException.java @@ -0,0 +1,11 @@ +package org.tarantool.jdbc; + +import java.sql.SQLTimeoutException; + +public class StatementTimeoutException extends SQLTimeoutException { + + public StatementTimeoutException(String reason, Throwable cause) { + super(reason, cause); + } + +} diff --git a/src/main/java/org/tarantool/protocol/ProtoUtils.java b/src/main/java/org/tarantool/protocol/ProtoUtils.java index a0724279..f5654c5a 100644 --- a/src/main/java/org/tarantool/protocol/ProtoUtils.java +++ b/src/main/java/org/tarantool/protocol/ProtoUtils.java @@ -42,17 +42,17 @@ public abstract class ProtoUtils { * * @throws IOException in case of any io-error */ - public static TarantoolPacket readPacket(InputStream inputStream) throws IOException { + public static TarantoolPacket readPacket(InputStream inputStream, MsgPackLite msgPackLite) throws IOException { CountInputStreamImpl msgStream = new CountInputStreamImpl(inputStream); - int size = ((Number) getMsgPackLite().unpack(msgStream)).intValue(); + int size = ((Number) msgPackLite.unpack(msgStream)).intValue(); long mark = msgStream.getBytesRead(); - Map headers = (Map) getMsgPackLite().unpack(msgStream); + Map headers = (Map) msgPackLite.unpack(msgStream); Map body = null; if (msgStream.getBytesRead() - mark < size) { - body = (Map) getMsgPackLite().unpack(msgStream); + body = (Map) msgPackLite.unpack(msgStream); } return new TarantoolPacket(headers, body); @@ -70,21 +70,21 @@ public static TarantoolPacket readPacket(InputStream inputStream) throws IOExcep * @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) + public static TarantoolPacket readPacket(ReadableByteChannel bufferReader, MsgPackLite msgPackLite) 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(); + int size = ((Number) msgPackLite.unpack(new ByteBufferBackedInputStream(buffer))).intValue(); buffer = ByteBuffer.allocate(size); bufferReader.read(buffer); buffer.flip(); ByteBufferBackedInputStream msgBytesStream = new ByteBufferBackedInputStream(buffer); - Object unpackedHeaders = getMsgPackLite().unpack(msgBytesStream); + Object unpackedHeaders = msgPackLite.unpack(msgBytesStream); if (!(unpackedHeaders instanceof Map)) { //noinspection ConstantConditions throw new CommunicationException( @@ -98,7 +98,7 @@ public static TarantoolPacket readPacket(ReadableByteChannel bufferReader) Map body = null; if (msgBytesStream.hasAvailable()) { - Object unpackedBody = getMsgPackLite().unpack(msgBytesStream); + Object unpackedBody = msgPackLite.unpack(msgBytesStream); if (!(unpackedBody instanceof Map)) { //noinspection ConstantConditions throw new CommunicationException( @@ -129,7 +129,8 @@ public static TarantoolPacket readPacket(ReadableByteChannel bufferReader) */ public static TarantoolGreeting connect(Socket socket, String username, - String password) throws IOException { + String password, + MsgPackLite msgPackLite) throws IOException { byte[] inputBytes = new byte[64]; InputStream inputStream = socket.getInputStream(); @@ -142,13 +143,13 @@ public static TarantoolGreeting connect(Socket socket, inputStream.read(inputBytes); String salt = new String(inputBytes); if (username != null && password != null) { - ByteBuffer authPacket = createAuthPacket(username, password, salt); + ByteBuffer authPacket = createAuthPacket(username, password, salt, msgPackLite); OutputStream os = socket.getOutputStream(); os.write(authPacket.array(), 0, authPacket.remaining()); os.flush(); - TarantoolPacket responsePacket = readPacket(socket.getInputStream()); + TarantoolPacket responsePacket = readPacket(socket.getInputStream(), msgPackLite); assertNoErrCode(responsePacket); } @@ -170,7 +171,8 @@ public static TarantoolGreeting connect(Socket socket, */ public static TarantoolGreeting connect(SocketChannel channel, String username, - String password) throws IOException { + String password, + MsgPackLite msgPackLite) throws IOException { ByteBuffer welcomeBytes = ByteBuffer.wrap(new byte[64]); channel.read(welcomeBytes); @@ -183,10 +185,10 @@ public static TarantoolGreeting connect(SocketChannel channel, String salt = new String(welcomeBytes.array()); if (username != null && password != null) { - ByteBuffer authPacket = createAuthPacket(username, password, salt); + ByteBuffer authPacket = createAuthPacket(username, password, salt, msgPackLite); writeFully(channel, authPacket); - TarantoolPacket authResponse = readPacket(channel); + TarantoolPacket authResponse = readPacket(channel, msgPackLite); assertNoErrCode(authResponse); } @@ -228,7 +230,8 @@ public static void writeFully(SocketChannel channel, ByteBuffer buffer) throws I public static ByteBuffer createAuthPacket(String username, final String password, - String salt) throws IOException { + String salt, + MsgPackLite msgPackLite) throws IOException { final MessageDigest sha1; try { sha1 = MessageDigest.getInstance("SHA-1"); @@ -252,20 +255,18 @@ 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); + return createPacket( + DEFAULT_INITIAL_REQUEST_SIZE, msgPackLite, + Code.AUTH, 0L, null, Key.USER_NAME, username, Key.TUPLE, auth + ); } - public static ByteBuffer createPacket(Code code, Long syncId, Long schemaId, Object... args) throws IOException { - return createPacket(DEFAULT_INITIAL_REQUEST_SIZE, code, syncId, schemaId, args); - } - - public static ByteBuffer createPacket(int initialRequestSize, + public static ByteBuffer createPacket(MsgPackLite msgPackLite, Code code, Long syncId, Long schemaId, Object... args) throws IOException { - return createPacket(initialRequestSize, getMsgPackLite(), code, syncId, schemaId, args); + return createPacket(DEFAULT_INITIAL_REQUEST_SIZE, msgPackLite, code, syncId, schemaId, args); } public static ByteBuffer createPacket(int initialRequestSize, @@ -309,7 +310,4 @@ ByteBuffer toByteBuffer() { } } - private static MsgPackLite getMsgPackLite() { - return MsgPackLite.INSTANCE; - } } diff --git a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java index 950bd131..8f317800 100644 --- a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java +++ b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java @@ -9,10 +9,7 @@ import org.junit.jupiter.api.BeforeAll; import org.opentest4j.AssertionFailedError; -import java.io.IOException; import java.math.BigInteger; -import java.net.InetSocketAddress; -import java.net.Socket; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -166,25 +163,6 @@ protected static TarantoolConsole openConsole(String instance) { return TarantoolConsole.open(TarantoolControl.tntCtlWorkDir, instance); } - protected TarantoolConnection openConnection() { - Socket socket = new Socket(); - try { - socket.connect(new InetSocketAddress(host, port)); - } catch (IOException e) { - throw new RuntimeException("Test failed due to invalid environment.", e); - } - try { - return new TarantoolConnection(username, password, socket); - } catch (Exception e) { - try { - socket.close(); - } catch (IOException ignored) { - // No-op. - } - throw new RuntimeException(e); - } - } - private void appendBigInteger(StringBuilder sb, BigInteger value) { sb.append(value); sb.append(value.signum() >= 0 ? "ULL" : "LL"); diff --git a/src/test/java/org/tarantool/ConnectionIT.java b/src/test/java/org/tarantool/ConnectionIT.java index 0d70342c..24282579 100644 --- a/src/test/java/org/tarantool/ConnectionIT.java +++ b/src/test/java/org/tarantool/ConnectionIT.java @@ -1,9 +1,13 @@ package org.tarantool; +import static org.junit.jupiter.api.Assertions.assertEquals; +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; +import java.net.SocketException; import java.util.List; /** @@ -16,7 +20,7 @@ public class ConnectionIT extends AbstractTarantoolOpsIT { @BeforeEach public void setup() { - conn = openConnection(); + conn = TestUtils.openConnection(host, port, username, password); } @AfterEach @@ -32,5 +36,18 @@ protected TarantoolClientOps, Object, List> getOps() { @Test public void testClose() { conn.close(); + assertTrue(conn.isClosed()); + } + + @Test + void testGetSoTimeout() throws SocketException { + assertEquals(0, conn.getSocketTimeout()); } + + @Test + void testSetSoTimeout() throws SocketException { + conn.setSocketTimeout(2000); + assertEquals(2000, conn.getSocketTimeout()); + } + } diff --git a/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java b/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java new file mode 100644 index 00000000..102959cc --- /dev/null +++ b/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java @@ -0,0 +1,33 @@ +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 TarantoolConnection} implementation. + *

+ * Actual tests reside in base class. + */ +public class TarantoolConnectionSQLOpsIT extends AbstractTarantoolSQLOpsIT { + + private TarantoolConnection connection; + + @BeforeEach + public void setup() { + connection = TestUtils.openConnection(HOST, PORT, USERNAME, PASSWORD); + } + + @AfterEach + public void tearDown() { + connection.close(); + } + + @Override + protected TarantoolSQLOps>> getSQLOps() { + return connection; + } + +} diff --git a/src/test/java/org/tarantool/TestUtils.java b/src/test/java/org/tarantool/TestUtils.java index 9744e77f..012ebd58 100644 --- a/src/test/java/org/tarantool/TestUtils.java +++ b/src/test/java/org/tarantool/TestUtils.java @@ -1,5 +1,8 @@ package org.tarantool; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -8,6 +11,25 @@ public class TestUtils { + public static TarantoolConnection openConnection(String host, int port, String username, String password) { + Socket socket = new Socket(); + try { + socket.connect(new InetSocketAddress(host, port)); + } catch (IOException e) { + throw new RuntimeException("Test failed due to invalid environment.", e); + } + try { + return new TarantoolConnection(username, password, socket); + } catch (Exception e) { + try { + socket.close(); + } catch (IOException ignored) { + // No-op. + } + throw new RuntimeException(e); + } + } + public static String makeDiscoveryFunction(String functionName, Collection addresses) { String functionResult = addresses.stream() .map(address -> "'" + address + "'") diff --git a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java index 0966b221..62c98890 100644 --- a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -5,7 +5,8 @@ import static org.tarantool.jdbc.SqlTestUtils.getCreateTableSQL; import org.tarantool.ServerVersion; -import org.tarantool.TarantoolConnection; +import org.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolClientImpl; import org.tarantool.TarantoolConsole; import org.tarantool.TarantoolControl; @@ -14,9 +15,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -57,14 +55,14 @@ public abstract class AbstractJdbcIT { Connection conn; @BeforeAll - public static void setupEnv() throws Exception { + public static void setupEnv() { control = new TarantoolControl(); control.createInstance("jdk-testing", LUA_FILE, makeInstanceEnv(LISTEN, ADMIN)); control.start("jdk-testing"); } @AfterAll - public static void teardownEnv() throws Exception { + public static void teardownEnv() { control.stop("jdk-testing"); } @@ -87,41 +85,38 @@ public void tearDownTest() throws SQLException { } protected static void sqlExec(String... text) { - TarantoolConnection con = makeConnection(); + TarantoolClientImpl client = makeClient(); try { for (String cmd : text) { - con.eval("box.execute(\"" + cmd + "\")"); + client.syncOps().eval("box.execute(\"" + cmd + "\")"); } } finally { - con.close(); + client.close(); } } static List getRow(String space, Object key) { - TarantoolConnection con = makeConnection(); + TarantoolClientImpl client = makeClient(); try { - List l = con.select(281, 2, Arrays.asList(space.toUpperCase()), 0, 1, 0); + List l = client.syncOps().select(281, 2, Arrays.asList(space.toUpperCase()), 0, 1, 0); Integer spaceId = (Integer) ((List) l.get(0)).get(0); - l = con.select(spaceId, 0, Arrays.asList(key), 0, 1, 0); + l = client.syncOps().select(spaceId, 0, Arrays.asList(key), 0, 1, 0); return (l == null || l.size() == 0) ? Collections.emptyList() : (List) l.get(0); } finally { - con.close(); + client.close(); } } - static TarantoolConnection makeConnection() { - Socket socket = new Socket(); - try { - socket.connect(new InetSocketAddress(host, port)); - return new TarantoolConnection(user, pass, socket); - } catch (IOException e) { - try { - socket.close(); - } catch (IOException ignored) { - // No-op. - } - throw new RuntimeException(e); - } + static TarantoolClientImpl makeClient() { + return new TarantoolClientImpl(host + ":" + port, makeClientConfig()); + } + + private static TarantoolClientConfig makeClientConfig() { + TarantoolClientConfig config = new TarantoolClientConfig(); + config.username = user; + config.password = pass; + config.initTimeoutMillis = 2000; + return config; } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java index f67b4a39..dda50ee8 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -5,17 +5,13 @@ 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 java.lang.reflect.Field; -import java.net.Socket; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; @@ -60,7 +56,6 @@ public void testGetMetaData() throws SQLException { @Test public void testGetSetNetworkTimeout() throws Exception { assertEquals(0, conn.getNetworkTimeout()); - SQLException e = assertThrows(SQLException.class, new Executable() { @Override public void execute() throws Throwable { @@ -68,64 +63,17 @@ public void execute() throws Throwable { } }); assertEquals("Network timeout cannot be negative.", e.getMessage()); - conn.setNetworkTimeout(null, 3000); - assertEquals(3000, conn.getNetworkTimeout()); - - // Check that timeout gets propagated to the socket. - Field tntCon = SQLConnection.class.getDeclaredField("connection"); - tntCon.setAccessible(true); - - Field sock = TarantoolConnection.class.getDeclaredField("socket"); - sock.setAccessible(true); - - assertEquals(3000, ((Socket) sock.get(tntCon.get(conn))).getSoTimeout()); - } - - @Test - public void testClosedConnection() throws SQLException { - conn.close(); - - int i = 0; - for (; i < 5; i++) { - final int step = i; - SQLException e = assertThrows(SQLException.class, new Executable() { - @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(); - } - } - }); - assertEquals("Connection is closed.", e.getMessage()); - } - assertEquals(5, i); } @Test void testIsValidCheck() throws SQLException { - assertTrue(conn.isValid(2000)); - assertThrows(SQLException.class, () -> conn.isValid(-1000)); + assertTrue(conn.isValid(2)); + assertThrows(SQLException.class, () -> conn.isValid(-1)); conn.close(); - assertFalse(conn.isValid(2000)); + assertFalse(conn.isValid(2)); } @Test diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java new file mode 100644 index 00000000..6c031abe --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java @@ -0,0 +1,133 @@ +package org.tarantool.jdbc; + +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 static org.tarantool.TestUtils.makeInstanceEnv; + +import org.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolControl; +import org.tarantool.protocol.TarantoolPacket; + +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.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; +import java.sql.Statement; +import java.util.Properties; + +public class JdbcConnectionTimeoutIT { + + protected static final String LUA_FILE = "jdk-testing.lua"; + protected static final int LISTEN = 3301; + protected static final int ADMIN = 3313; + private static final String INSTANCE_NAME = "jdk-testing"; + private static final int LONG_ENOUGH_TIMEOUT = 3000; + + private Connection connection; + + @BeforeAll + public static void setUpEnv() { + TarantoolControl control = new TarantoolControl(); + control.createInstance(INSTANCE_NAME, LUA_FILE, makeInstanceEnv(LISTEN, ADMIN)); + control.start(INSTANCE_NAME); + } + + @AfterAll + public static void tearDownEnv() { + TarantoolControl control = new TarantoolControl(); + control.stop(INSTANCE_NAME); + } + + @BeforeEach + void setUp() throws SQLException { + connection = new SQLConnection("", new Properties()) { + @Override + protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { + return new SQLTarantoolClientImpl(address, config) { + @Override + protected void completeSql(TarantoolOp operation, TarantoolPacket pack) { + try { + Thread.sleep(LONG_ENOUGH_TIMEOUT); + } catch (InterruptedException ignored) { + } + super.completeSql(operation, pack); + } + }; + } + }; + } + + @AfterEach + void tearDown() throws SQLException { + connection.close(); + } + + @Test + void testShortNetworkTimeout() throws SQLException { + int tooShortTimeout = 500; + connection.setNetworkTimeout(null, tooShortTimeout); + Statement statement = connection.createStatement(); + assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); + assertTrue(connection.isClosed()); + assertTrue(statement.isClosed()); + } + + @Test + void testQueryTimeoutIsShorterThanNetwork() throws SQLException { + int networkTimeout = 2; + int statementTimeout = 1; + + connection.setNetworkTimeout(null, networkTimeout * 1000); + Statement statement = connection.createStatement(); + statement.setQueryTimeout(statementTimeout); + + // expect the query timeout won + assertThrows(SQLTimeoutException.class, () -> statement.executeQuery("SELECT 1")); + assertFalse(connection.isClosed()); + assertFalse(statement.isClosed()); + } + + @Test + void testNetworkTimeoutIsShorterThanQuery() throws SQLException { + int networkTimeout = 1; + int statementTimeout = 2; + + connection.setNetworkTimeout(null, networkTimeout * 1000); + Statement statement = connection.createStatement(); + statement.setQueryTimeout(statementTimeout); + + // expect the network timeout won + assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); + assertTrue(connection.isClosed()); + assertTrue(statement.isClosed()); + } + + @Test + void testShortStatementTimeout() throws SQLException { + int tooShortTimeout = 1; + Statement statement = connection.createStatement(); + statement.setQueryTimeout(tooShortTimeout); + assertThrows(SQLTimeoutException.class, () -> statement.executeQuery("SELECT 1")); + assertFalse(connection.isClosed()); + assertFalse(statement.isClosed()); + } + + @Test + void testShortPreparedStatementTimeout() throws SQLException { + int tooShortTimeout = 1; + PreparedStatement statement = connection.prepareStatement("SELECT ?"); + statement.setInt(1, 1); + statement.setQueryTimeout(tooShortTimeout); + assertThrows(SQLTimeoutException.class, statement::executeQuery); + assertFalse(connection.isClosed()); + assertFalse(statement.isClosed()); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java index 37750135..b21eef2d 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java @@ -7,23 +7,17 @@ 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.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_PROVIDER; -import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; -import static org.tarantool.jdbc.SQLDriver.PROP_USER; import org.tarantool.CommunicationException; +import org.tarantool.SocketChannelProvider; 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.nio.channels.SocketChannel; import java.sql.Driver; import java.sql.DriverManager; import java.sql.DriverPropertyInfo; @@ -37,35 +31,35 @@ public void testParseQueryString() throws Exception { SQLDriver drv = new SQLDriver(); Properties prop = new Properties(); - prop.setProperty(PROP_USER, "adm"); - prop.setProperty(PROP_PASSWORD, "secret"); + SQLProperty.USER.setString(prop, "adm"); + SQLProperty.PASSWORD.setString(prop, "secret"); URI uri = new URI(String.format( "tarantool://server.local:3302?%s=%s&%s=%d", - PROP_SOCKET_PROVIDER, "some.class", - PROP_SOCKET_TIMEOUT, 5000) + SQLProperty.SOCKET_CHANNEL_PROVIDER.getName(), "some.class", + SQLProperty.QUERY_TIMEOUT.getName(), 5000) ); - Properties res = drv.parseQueryString(uri, prop); - assertNotNull(res); + Properties result = drv.parseQueryString(uri, prop); + assertNotNull(result); - assertEquals("server.local", res.getProperty(PROP_HOST)); - assertEquals("3302", res.getProperty(PROP_PORT)); - assertEquals("adm", res.getProperty(PROP_USER)); - assertEquals("secret", res.getProperty(PROP_PASSWORD)); - assertEquals("some.class", res.getProperty(PROP_SOCKET_PROVIDER)); - assertEquals("5000", res.getProperty(PROP_SOCKET_TIMEOUT)); + assertEquals("server.local", SQLProperty.HOST.getString(result)); + assertEquals("3302", SQLProperty.PORT.getString(result)); + assertEquals("adm", SQLProperty.USER.getString(result)); + assertEquals("secret", SQLProperty.PASSWORD.getString(result)); + assertEquals("some.class", SQLProperty.SOCKET_CHANNEL_PROVIDER.getString(result)); + assertEquals("5000", SQLProperty.QUERY_TIMEOUT.getString(result)); } @Test public void testParseQueryStringUserInfoInURI() throws Exception { SQLDriver drv = new SQLDriver(); - Properties res = drv.parseQueryString(new URI("tarantool://adm:secret@server.local"), null); - assertNotNull(res); - assertEquals("server.local", res.getProperty(PROP_HOST)); - assertEquals("3301", res.getProperty(PROP_PORT)); - assertEquals("adm", res.getProperty(PROP_USER)); - assertEquals("secret", res.getProperty(PROP_PASSWORD)); + Properties result = drv.parseQueryString(new URI("tarantool://adm:secret@server.local"), null); + assertNotNull(result); + assertEquals("server.local", SQLProperty.HOST.getString(result)); + assertEquals("3301", SQLProperty.PORT.getString(result)); + assertEquals("adm", SQLProperty.USER.getString(result)); + assertEquals("secret", SQLProperty.PASSWORD.getString(result)); } @Test @@ -74,10 +68,10 @@ public void testParseQueryStringValidations() { checkParseQueryStringValidation("tarantool://0", new Properties() { { - setProperty(PROP_PORT, "nan"); + SQLProperty.PORT.setString(this, "nan"); } }, - "Port must be a valid number."); + "Property port must be a valid number."); // Check zero port checkParseQueryStringValidation("tarantool://0:0", null, "Port is out of range: 0"); @@ -85,13 +79,33 @@ public void testParseQueryStringValidations() { // Check high port checkParseQueryStringValidation("tarantool://0:65536", null, "Port is out of range: 65536"); - // Check non-number timeout - checkParseQueryStringValidation(String.format("tarantool://0:3301?%s=nan", PROP_SOCKET_TIMEOUT), null, - "Timeout must be a valid number."); + // Check non-number init timeout + checkParseQueryStringValidation( + String.format("tarantool://0:3301?%s=nan", SQLProperty.LOGIN_TIMEOUT.getName()), + null, + "Property loginTimeout must be a valid number." + ); + + // Check negative init timeout + checkParseQueryStringValidation( + String.format("tarantool://0:3301?%s=-100", SQLProperty.LOGIN_TIMEOUT.getName()), + null, + "Property loginTimeout must not be negative." + ); + + // Check non-number operation timeout + checkParseQueryStringValidation( + String.format("tarantool://0:3301?%s=nan", SQLProperty.QUERY_TIMEOUT.getName()), + null, + "Property queryTimeout 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."); + // Check negative operation timeout + checkParseQueryStringValidation( + String.format("tarantool://0:3301?%s=-100", SQLProperty.QUERY_TIMEOUT.getName()), + null, + "Property queryTimeout must not be negative." + ); } @Test @@ -100,29 +114,32 @@ public void testGetPropertyInfo() throws SQLException { Properties props = new Properties(); DriverPropertyInfo[] info = drv.getPropertyInfo("tarantool://server.local:3302", props); assertNotNull(info); - assertEquals(6, info.length); + assertEquals(7, info.length); for (DriverPropertyInfo e : info) { assertNotNull(e.name); assertNull(e.choices); assertNotNull(e.description); - if (PROP_HOST.equals(e.name)) { + if (SQLProperty.HOST.getName().equals(e.name)) { assertTrue(e.required); assertEquals("server.local", e.value); - } else if (PROP_PORT.equals(e.name)) { + } else if (SQLProperty.PORT.getName().equals(e.name)) { assertTrue(e.required); assertEquals("3302", e.value); - } else if (PROP_USER.equals(e.name)) { + } else if (SQLProperty.USER.getName().equals(e.name)) { assertFalse(e.required); assertNull(e.value); - } else if (PROP_PASSWORD.equals(e.name)) { + } else if (SQLProperty.PASSWORD.getName().equals(e.name)) { assertFalse(e.required); assertNull(e.value); - } else if (PROP_SOCKET_PROVIDER.equals(e.name)) { + } else if (SQLProperty.SOCKET_CHANNEL_PROVIDER.getName().equals(e.name)) { assertFalse(e.required); assertNull(e.value); - } else if (PROP_SOCKET_TIMEOUT.equals(e.name)) { + } else if (SQLProperty.LOGIN_TIMEOUT.getName().equals(e.name)) { + assertFalse(e.required); + assertEquals("60000", e.value); + } else if (SQLProperty.QUERY_TIMEOUT.getName().equals(e.name)) { assertFalse(e.required); assertEquals("0", e.value); } else { @@ -137,10 +154,10 @@ public void testCustomSocketProviderFail() throws SQLException { "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.SocketChannelProvider"); checkCustomSocketProviderFail(TestSQLProviderThatReturnsNull.class.getName(), - "The socket provider returned null socket"); + "Couldn't initiate connection using"); checkCustomSocketProviderFail(TestSQLProviderThatThrows.class.getName(), "Couldn't initiate connection using"); @@ -153,7 +170,7 @@ public void testNoResponseAfterInitialConnect() throws IOException { try { final String url = "tarantool://localhost:" + socket.getLocalPort(); final Properties prop = new Properties(); - prop.setProperty(PROP_SOCKET_TIMEOUT, "100"); + SQLProperty.LOGIN_TIMEOUT.setInt(prop, 500); SQLException e = assertThrows(SQLException.class, new Executable() { @Override public void execute() throws Throwable { @@ -162,7 +179,6 @@ public void execute() throws Throwable { }); assertTrue(e.getMessage().startsWith("Couldn't initiate connection using "), e.getMessage()); assertTrue(e.getCause() instanceof CommunicationException); - assertTrue(e.getCause().getCause() instanceof SocketTimeoutException); } finally { socket.close(); } @@ -171,14 +187,10 @@ public void execute() throws Throwable { private void checkCustomSocketProviderFail(String providerClassName, String errMsg) throws SQLException { final Driver drv = DriverManager.getDriver("tarantool:"); final Properties prop = new Properties(); - prop.setProperty(PROP_SOCKET_PROVIDER, providerClassName); + SQLProperty.SOCKET_CHANNEL_PROVIDER.setString(prop, providerClassName); + SQLProperty.LOGIN_TIMEOUT.setInt(prop, 500); - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - drv.connect("tarantool://0:3301", prop); - } - }); + SQLException e = assertThrows(SQLException.class, () -> drv.connect("tarantool://0:3301", prop)); assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); } @@ -193,17 +205,21 @@ public void execute() throws Throwable { assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); } - static class TestSQLProviderThatReturnsNull implements SQLSocketProvider { + static class TestSQLProviderThatReturnsNull implements SocketChannelProvider { + @Override - public Socket getConnectedSocket(URI uri, Properties params) { + public SocketChannel get(int retryNumber, Throwable lastError) { return null; } + } - static class TestSQLProviderThatThrows implements SQLSocketProvider { + static class TestSQLProviderThatThrows implements SocketChannelProvider { + @Override - public Socket getConnectedSocket(URI uri, Properties params) { + public SocketChannel get(int retryNumber, Throwable lastError) { throw new RuntimeException("ERROR"); } + } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index c34d5bed..05a0635d 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -3,34 +3,28 @@ 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.mockito.Mockito.when; +import static org.tarantool.jdbc.SQLConnection.SQLTarantoolClientImpl; 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.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolClientOps; 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; -import java.net.SocketException; -import java.net.SocketTimeoutException; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -38,6 +32,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Properties; public class JdbcExceptionHandlingTest { @@ -49,14 +44,13 @@ public class JdbcExceptionHandlingTest { */ @Test public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException { - TarantoolConnection tntCon = mock(TarantoolConnection.class); - final SQLConnection conn = buildTestSQLConnection(tntCon, "", SQLDriver.defaults); + + TarantoolClientOps, Object, List> syncOps = mock(TarantoolClientOps.class); 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) + doReturn(Collections.singletonList(Arrays.asList(spc))).when(syncOps) .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); Object[] idx = new Object[6]; @@ -67,10 +61,12 @@ public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException } } ); - - doReturn(Collections.singletonList(Arrays.asList(idx))).when(tntCon) + doReturn(Collections.singletonList(Arrays.asList(idx))).when(syncOps) .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); + final SQLTarantoolClientImpl client = buildSQLClient(null, syncOps); + final SQLConnection conn = buildTestSQLConnection(client, ""); + final DatabaseMetaData meta = conn.getMetaData(); Throwable t = assertThrows(SQLException.class, new Executable() { @@ -151,79 +147,14 @@ public void accept(DatabaseMetaData meta) throws Throwable { }, "Error processing metadata for table \"TEST\"."); } - @Test - public void testDefaultSocketProviderConnectTimeoutError() throws IOException { - final int socketTimeout = 3000; - final Socket mockSocket = mock(Socket.class); - - SocketTimeoutException timeoutEx = new SocketTimeoutException(); - doThrow(timeoutEx) - .when(mockSocket) - .connect(new InetSocketAddress("localhost", 3301), socketTimeout); - - final Properties prop = new Properties(SQLDriver.defaults); - prop.setProperty(PROP_SOCKET_TIMEOUT, String.valueOf(socketTimeout)); - - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - new SQLConnection("tarantool://localhost:3301", prop) { - @Override - protected Socket makeSocket() { - return mockSocket; - } - }; - } - }); - - assertTrue(e.getMessage().startsWith("Couldn't connect to localhost:3301"), e.getMessage()); - assertEquals(timeoutEx, e.getCause()); - } - - @Test - public void testDefaultSocketProviderSetSocketTimeoutError() throws IOException { - final int socketTimeout = 3000; - final Socket mockSocket = mock(Socket.class); - - // Check error setting socket timeout - reset(mockSocket); - doNothing() - .when(mockSocket) - .connect(new InetSocketAddress("localhost", 3301), socketTimeout); - - SocketException sockEx = new SocketException("TEST"); - doThrow(sockEx) - .when(mockSocket) - .setSoTimeout(socketTimeout); - - final Properties prop = new Properties(SQLDriver.defaults); - prop.setProperty(PROP_SOCKET_TIMEOUT, String.valueOf(socketTimeout)); - - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - new SQLConnection("tarantool://localhost:3301", prop) { - @Override - protected Socket makeSocket() { - return mockSocket; - } - }; - } - }); - - assertTrue(e.getMessage().startsWith("Couldn't set socket timeout."), e.getMessage()); - assertEquals(sockEx, e.getCause()); - } - private void checkStatementCommunicationException(final ThrowingConsumer consumer) throws SQLException { - TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); - final Statement stmt = new SQLStatement(buildTestSQLConnection(mockCon, "tarantool://0:0", SQLDriver.defaults)); - Exception ex = new CommunicationException("TEST"); + SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); + doThrow(ex).when(sqlOps).execute("TEST"); - doThrow(ex).when(mockCon).sql("TEST", new Object[0]); - doThrow(ex).when(mockCon).update("TEST"); + SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); + final Statement stmt = new SQLStatement(buildTestSQLConnection(client, "tarantool://0:0")); SQLException e = assertThrows(SQLException.class, new Executable() { @Override @@ -235,19 +166,19 @@ public void execute() throws Throwable { assertEquals(ex, e.getCause()); - verify(mockCon, times(1)).close(); + verify(client, times(1)).close(); } private void checkPreparedStatementCommunicationException(final ThrowingConsumer consumer) throws SQLException { - TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); + Exception ex = new CommunicationException("TEST"); + SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); + doThrow(ex).when(sqlOps).execute("TEST"); + SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); final PreparedStatement prep = new SQLPreparedStatement( - buildTestSQLConnection(mockCon, "tarantool://0:0", SQLDriver.defaults), "TEST"); + buildTestSQLConnection(client, "tarantool://0:0"), "TEST"); - Exception ex = new CommunicationException("TEST"); - doThrow(ex).when(mockCon).sql("TEST", new Object[0]); - doThrow(ex).when(mockCon).update("TEST"); SQLException e = assertThrows(SQLException.class, new Executable() { @Override @@ -259,55 +190,50 @@ public void execute() throws Throwable { assertEquals(ex, e.getCause()); - verify(mockCon, times(1)).close(); + verify(client, times(1)).close(); } private void checkDatabaseMetaDataCommunicationException(final ThrowingConsumer consumer, 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(); - Exception ex = new CommunicationException("TEST"); - doThrow(ex).when(mockCon).select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); - doThrow(ex).when(mockCon).select(_VSPACE, 2, Arrays.asList("TEST"), 0, 1, 0); + TarantoolClientOps, Object, List> syncOps = mock(TarantoolClientOps.class); + doThrow(ex).when(syncOps).select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + doThrow(ex).when(syncOps).select(_VSPACE, 2, Arrays.asList("TEST"), 0, 1, 0); - SQLException e = assertThrows(SQLException.class, new Executable() { - @Override - public void execute() throws Throwable { - consumer.accept(meta); - } - }); + SQLTarantoolClientImpl client = buildSQLClient(null, syncOps); + SQLConnection conn = buildTestSQLConnection(client, "tarantool://0:0"); + final DatabaseMetaData meta = conn.getMetaData(); + + SQLException e = assertThrows(SQLException.class, () -> consumer.accept(meta)); assertTrue(e.getMessage().startsWith(msg), e.getMessage()); assertEquals(ex, e.getCause().getCause()); - verify(mockCon, times(1)).close(); + verify(client, times(1)).close(); + } + + private SQLTarantoolClientImpl buildSQLClient(SQLTarantoolClientImpl.SQLRawOps sqlOps, + TarantoolClientOps, Object, List> ops) { + SQLTarantoolClientImpl client = mock(SQLTarantoolClientImpl.class); + when(client.sqlRawOps()).thenReturn(sqlOps); + when(client.syncOps()).thenReturn(ops); + return client; + } + + private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, String url) throws SQLException { + return buildTestSQLConnection(client, url, new Properties()); } - private SQLConnection buildTestSQLConnection(final TarantoolConnection tntCon, String url, Properties properties) + private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, + String url, + Properties properties) throws SQLException { return new SQLConnection(url, properties) { @Override - protected Socket makeSocket() { - return mock(Socket.class); - } - - @Override - protected TarantoolConnection makeConnection(String user, String pass, Socket socket) { - return tntCon; + protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { + return client; } }; } - 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 24e4a539..c8ed69ef 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import org.tarantool.util.SQLStates; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -53,6 +55,16 @@ public void testExecuteQuery() throws SQLException { rs.close(); } + @Test + public void testExecuteWrongQuery() throws SQLException { + prep = conn.prepareStatement("INSERT INTO test VALUES (?, ?)"); + prep.setInt(1, 200); + prep.setString(2, "two hundred"); + + SQLException exception = assertThrows(SQLException.class, () -> prep.executeQuery()); + SqlAssertions.assertSqlExceptionHasStatus(exception, SQLStates.NO_DATA); + } + @Test public void testExecuteUpdate() throws Exception { prep = conn.prepareStatement("INSERT INTO test VALUES(?, ?)"); @@ -74,6 +86,15 @@ public void testExecuteUpdate() throws Exception { assertEquals("thousand", getRow("test", 1000).get(1)); } + @Test + public void testExecuteWrongUpdate() throws SQLException { + prep = conn.prepareStatement("SELECT val FROM test WHERE id=?"); + prep.setInt(1, 1); + + SQLException exception = assertThrows(SQLException.class, () -> prep.executeUpdate()); + SqlAssertions.assertSqlExceptionHasStatus(exception, SQLStates.TOO_MANY_RESULTS); + } + @Test public void testExecuteReturnsResultSet() throws SQLException { prep = conn.prepareStatement("SELECT val FROM test WHERE id=?"); diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index cffbcfa6..3292cebb 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import org.tarantool.util.SQLStates; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +44,14 @@ public void testExecuteQuery() throws SQLException { rs.close(); } + @Test + public void testExecuteWrongQuery() throws SQLException { + String wrongResultQuery = "INSERT INTO test(id, val) VALUES (40, 'forty')"; + + SQLException exception = assertThrows(SQLException.class, () -> stmt.executeQuery(wrongResultQuery)); + SqlAssertions.assertSqlExceptionHasStatus(exception, SQLStates.NO_DATA); + } + @Test public void testExecuteUpdate() throws Exception { assertEquals(2, stmt.executeUpdate("INSERT INTO test(id, val) VALUES (10, 'ten'), (20, 'twenty')")); @@ -49,6 +59,14 @@ public void testExecuteUpdate() throws Exception { assertEquals("twenty", getRow("test", 20).get(1)); } + @Test + public void testExecuteWrongUpdate() throws SQLException { + String wrongUpdateQuery = "SELECT val FROM test"; + + SQLException exception = assertThrows(SQLException.class, () -> stmt.executeUpdate(wrongUpdateQuery)); + SqlAssertions.assertSqlExceptionHasStatus(exception, SQLStates.TOO_MANY_RESULTS); + } + @Test public void testExecuteReturnsResultSet() throws SQLException { assertTrue(stmt.execute("SELECT val FROM test WHERE id=1")); @@ -69,6 +87,44 @@ public void testExecuteReturnsUpdateCount() throws Exception { assertEquals("thousand", getRow("test", 1000).get(1)); } + @Test + void testGetMaxRows() throws SQLException { + int defaultMaxSize = 0; + assertEquals(defaultMaxSize, stmt.getMaxRows()); + } + + @Test + void testSetMaxRows() throws SQLException { + int expectedMaxSize = 10; + stmt.setMaxRows(expectedMaxSize); + assertEquals(expectedMaxSize, stmt.getMaxRows()); + } + + @Test + void testSetNegativeMaxRows() { + int negativeMaxSize = -20; + assertThrows(SQLException.class, () -> stmt.setMaxRows(negativeMaxSize)); + } + + @Test + void testGetQueryTimeout() throws SQLException { + int defaultQueryTimeout = 0; + assertEquals(defaultQueryTimeout, stmt.getQueryTimeout()); + } + + @Test + void testSetQueryTimeout() throws SQLException { + int expectedSeconds = 10; + stmt.setQueryTimeout(expectedSeconds); + assertEquals(expectedSeconds, stmt.getQueryTimeout()); + } + + @Test + void testSetNegativeQueryTimeout() throws SQLException { + int negativeSeconds = -30; + assertThrows(SQLException.class, () -> stmt.setQueryTimeout(negativeSeconds)); + } + @Test public void testClosedConnection() throws Exception { conn.close(); @@ -112,7 +168,7 @@ public void testIsWrapperFor() throws SQLException { } @Test - public void testSupportedGeneratedKeys() throws SQLException { + public void testExecuteUpdateNoGeneratedKeys() throws SQLException { int affectedRows = stmt.executeUpdate( "INSERT INTO test(id, val) VALUES (50, 'fifty')", Statement.NO_GENERATED_KEYS @@ -125,7 +181,21 @@ public void testSupportedGeneratedKeys() throws SQLException { } @Test - void testUnsupportedGeneratedKeys() { + public void testExecuteNoGeneratedKeys() throws SQLException { + boolean isResultSet = stmt.execute( + "INSERT INTO test(id, val) VALUES (60, 'sixty')", + Statement.NO_GENERATED_KEYS + ); + assertFalse(isResultSet); + ResultSet generatedKeys = stmt.getGeneratedKeys(); + assertNotNull(generatedKeys); + assertFalse(generatedKeys.next()); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, generatedKeys.getType()); + assertEquals(ResultSet.CONCUR_READ_ONLY, generatedKeys.getConcurrency()); + } + + @Test + void testExecuteUpdateGeneratedKeys() { assertThrows( SQLException.class, () -> stmt.executeUpdate( @@ -133,7 +203,21 @@ void testUnsupportedGeneratedKeys() { Statement.RETURN_GENERATED_KEYS ) ); + } + + @Test + void testExecuteGeneratedKeys() { + assertThrows( + SQLException.class, + () -> stmt.execute( + "INSERT INTO test(id, val) VALUES (100, 'hundred'), (1000, 'thousand')", + Statement.RETURN_GENERATED_KEYS + ) + ); + } + @Test + void testExecuteUpdateWrongGeneratedKeys() { int[] wrongConstants = { Integer.MAX_VALUE, Integer.MIN_VALUE, -31, 344 }; for (int wrongConstant : wrongConstants) { assertThrows(SQLException.class, @@ -145,6 +229,19 @@ void testUnsupportedGeneratedKeys() { } } + @Test + void testExecuteWrongGeneratedKeys() { + int[] wrongConstants = { Integer.MAX_VALUE, Integer.MIN_VALUE, -52, 864 }; + for (int wrongConstant : wrongConstants) { + assertThrows(SQLException.class, + () -> stmt.execute( + "INSERT INTO test(id, val) VALUES (100, 'hundred'), (1000, 'thousand')", + wrongConstant + ) + ); + } + } + @Test void testStatementConnection() throws SQLException { Statement statement = conn.createStatement();