diff --git a/README.md b/README.md index 3944a2dc..70f18c3a 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..407eb8fd 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(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..2e65a6fc --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/StatementTimeoutException.java @@ -0,0 +1,9 @@ +package org.tarantool.jdbc; + +public class StatementTimeoutException extends RuntimeException { + + public StatementTimeoutException(Throwable cause) { + super(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 873c197b..ad301874 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 f9774a33..dc7be4c5 100644 --- a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -4,7 +4,8 @@ import static org.tarantool.TestUtils.makeInstanceEnv; import static org.tarantool.jdbc.SqlTestUtils.getCreateTableSQL; -import org.tarantool.TarantoolConnection; +import org.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolClientImpl; import org.tarantool.TarantoolControl; import org.junit.jupiter.api.AfterAll; @@ -12,9 +13,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; @@ -54,7 +52,7 @@ 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"); @@ -64,7 +62,7 @@ public static void setupEnv() throws Exception { } @AfterAll - public static void teardownEnv() throws Exception { + public static void teardownEnv() { try { sqlExec(cleanSql); } finally { @@ -86,40 +84,37 @@ public void tearDownConnection() 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 ac05e19e..d5d26314 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; @@ -36,35 +30,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 @@ -73,10 +67,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"); @@ -84,13 +78,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 @@ -99,29 +113,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 { @@ -136,10 +153,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"); @@ -152,7 +169,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 { @@ -161,7 +178,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(); } @@ -170,14 +186,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()); } @@ -192,17 +204,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 59a2525a..20980306 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 { @@ -48,14 +43,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]; @@ -66,10 +60,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() { @@ -150,79 +146,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 @@ -234,19 +165,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 @@ -258,55 +189,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();