diff --git a/.travis.yml b/.travis.yml index eefedf82..662fe9b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,5 +12,16 @@ before_script: - src/test/travis.pre.sh script: - - mvn verify + - | + if [ "${TRAVIS_JDK_VERSION}" = openjdk11 ]; then + mvn verify jacoco:report + else + mvn verify + fi - cat testroot/jdk-testing.log + +after_success: + - | + if [ "${TRAVIS_JDK_VERSION}" = openjdk11 ]; then + mvn coveralls:report -DrepoToken=${COVERALLS_TOKEN} + fi diff --git a/README.md b/README.md index f1ea60f3..bbc22a75 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ align="right"> # Java connector for Tarantool 1.7.4+ +[![Coverage Status][coveralls-badge]][coveralls-page] + +[coveralls-badge]: https://coveralls.io/repos/github/tarantool/tarantool-java/badge.svg?branch=master +[coveralls-page]: https://coveralls.io/github/tarantool/tarantool-java?branch=master + [![Join the chat at https://gitter.im/tarantool/tarantool-java](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tarantool/tarantool-java?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) To get the Java connector for Tarantool 1.6.9, visit @@ -115,7 +120,37 @@ 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, FutureImpl q); +protected void complete(long code, FutureImpl q); +``` + +## 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 + +```java +NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(new DriverManagerDataSource("tarantool://localhost:3301?user=test&password=test")); +RowMapper rowMapper = new RowMapper() { + @Override + public Object mapRow(ResultSet resultSet, int i) throws SQLException { + return Arrays.asList(resultSet.getInt(1), resultSet.getString(2)); + } +}; + +try { + System.out.println(template.update("drop table hello_world", Collections.emptyMap())); +} catch (Exception ignored) { +} + +System.out.println(template.update("create table hello_world(hello int not null PRIMARY KEY, world varchar(255) not null)", Collections.emptyMap())); +Map params = new LinkedHashMap(); +params.put("text", "hello world"); +params.put("id", 1); + +System.out.println(template.update("insert into hello_world(hello, world) values(:id,:text)", params)); +System.out.println(template.query("select * from hello_world", rowMapper)); + +System.out.println(template.query("select * from hello_world where hello=:id", Collections.singletonMap("id", 1), rowMapper)); ``` For more implementation details, see [API documentation](http://tarantool.github.io/tarantool-java/apidocs/index.html). @@ -131,4 +166,4 @@ base for possible answers and solutions. To run tests ``` ./mvnw clean test -``` \ No newline at end of file +``` diff --git a/pom.xml b/pom.xml index e66df25c..a8fc63a2 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,33 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.2 + + + prepare-agent + + prepare-agent + + + + + + org.eluder.coveralls + coveralls-maven-plugin + 4.3.0 + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + diff --git a/src/it/java/org/tarantool/TestSql.java b/src/it/java/org/tarantool/TestSql.java new file mode 100644 index 00000000..a0ca3949 --- /dev/null +++ b/src/it/java/org/tarantool/TestSql.java @@ -0,0 +1,61 @@ +//package org.tarantool; +// +//import java.io.IOException; +//import java.net.InetSocketAddress; +//import java.net.Socket; +//import java.net.URI; +//import java.sql.ResultSet; +//import java.sql.SQLException; +//import java.util.Arrays; +//import java.util.Collections; +//import java.util.LinkedHashMap; +//import java.util.Map; +//import java.util.Properties; +// +//import org.springframework.jdbc.core.RowMapper; +//import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +//import org.springframework.jdbc.datasource.DriverManagerDataSource; +//import org.tarantool.jdbc.SQLSocketProvider; +// +//public class TestSql { +// +// public static class TestSocketProvider implements SQLSocketProvider { +// +// @Override +// public Socket getConnectedSocket(URI uri, Properties params) { +// Socket socket; +// socket = new Socket(); +// try { +// socket.connect(new InetSocketAddress(params.getProperty("host","localhost"), Integer.parseInt(params.getProperty("port", "3301")))); +// } catch (Exception e) { +// throw new RuntimeException("Couldn't connect to tarantool using" + params, e); +// } +// return socket; +// } +// } +// +// public static void main(String[] args) throws IOException, SQLException { +// +// NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(new DriverManagerDataSource("tarantool://localhost:3301?username=test&password=test&socketProvider=org.tarantool.TestSql$TestSocketProvider")); +// RowMapper rowMapper = new RowMapper() { +// @Override +// public Object mapRow(ResultSet resultSet, int i) throws SQLException { +// return Arrays.asList(resultSet.getInt(1), resultSet.getString(2)); +// } +// }; +// +// try { +// System.out.println(template.update("drop table hello_world", Collections.emptyMap())); +// } catch (Exception ignored) { +// } +// System.out.println(template.update("create table hello_world(hello int not null PRIMARY KEY, world varchar(255) not null)", Collections.emptyMap())); +// Map params = new LinkedHashMap(); +// params.put("text", "hello world"); +// params.put("id", 1); +// +// System.out.println(template.update("insert into hello_world(hello, world) values(:id,:text)", params)); +// System.out.println(template.query("select * from hello_world", rowMapper)); +// +// System.out.println(template.query("select * from hello_world where hello=:id", Collections.singletonMap("id", 1), rowMapper)); +// } +//} diff --git a/src/it/java/org/tarantool/TestTarantoolClient.java b/src/it/java/org/tarantool/TestTarantoolClient.java index ba574f83..acdb3a20 100644 --- a/src/it/java/org/tarantool/TestTarantoolClient.java +++ b/src/it/java/org/tarantool/TestTarantoolClient.java @@ -81,7 +81,7 @@ protected void reconnect(int retry, Throwable lastError) { } @Override - protected void complete(long code, FutureImpl> q) { + protected void complete(long code, FutureImpl q) { super.complete(code, q); if (code != 0) { System.out.println(code); diff --git a/src/main/java/org/tarantool/AbstractTarantoolOps.java b/src/main/java/org/tarantool/AbstractTarantoolOps.java index 5c5880aa..9d59264e 100644 --- a/src/main/java/org/tarantool/AbstractTarantoolOps.java +++ b/src/main/java/org/tarantool/AbstractTarantoolOps.java @@ -4,7 +4,7 @@ public abstract class AbstractTarantoolOps implements TarantoolClientOps { private Code callCode = Code.OLD_CALL; - public abstract Result exec(Code code, Object... args); + protected abstract Result exec(Code code, Object... args); public Result select(Space space, Space index, Tuple key, int offset, int limit, Iterator iterator) { return select(space, index, key, offset, limit, iterator.getValue()); diff --git a/src/main/java/org/tarantool/Code.java b/src/main/java/org/tarantool/Code.java index 44e5a1cc..5f6263fa 100644 --- a/src/main/java/org/tarantool/Code.java +++ b/src/main/java/org/tarantool/Code.java @@ -3,7 +3,7 @@ public enum Code { SELECT(1), INSERT(2), REPLACE(3), UPDATE(4), - DELETE(5), OLD_CALL(6), AUTH(7), EVAL(8), UPSERT(9), CALL(10), PING(64), SUBSCRIBE(66); + DELETE(5), OLD_CALL(6), AUTH(7), EVAL(8), UPSERT(9), CALL(10), EXECUTE(11) , PING(64), SUBSCRIBE(66),; int id; diff --git a/src/main/java/org/tarantool/FutureImpl.java b/src/main/java/org/tarantool/FutureImpl.java index 72237737..05b6d29c 100644 --- a/src/main/java/org/tarantool/FutureImpl.java +++ b/src/main/java/org/tarantool/FutureImpl.java @@ -9,11 +9,13 @@ public class FutureImpl extends AbstractQueuedSynchronizer implements Future { protected final long id; + protected Code code; protected V value; protected Exception error; - public FutureImpl(long id) { + public FutureImpl(long id, Code code) { this.id = id; + this.code = code; setState(1); } @@ -91,4 +93,7 @@ public Long getId() { return id; } + public Code getCode() { + return code; + } } diff --git a/src/main/java/org/tarantool/JDBCBridge.java b/src/main/java/org/tarantool/JDBCBridge.java new file mode 100644 index 00000000..b31af64d --- /dev/null +++ b/src/main/java/org/tarantool/JDBCBridge.java @@ -0,0 +1,87 @@ +package org.tarantool; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.tarantool.jdbc.SQLResultSet; + +public class JDBCBridge { + public static final JDBCBridge EMPTY = new JDBCBridge(Collections.emptyList(), Collections.>emptyList()); + + final List sqlMetadata; + final Map columnsByName; + final List> rows; + + protected JDBCBridge(TarantoolConnection connection) { + this(connection.getSQLMetadata(),connection.getSQLData()); + } + + protected JDBCBridge(List sqlMetadata, List> rows) { + this.sqlMetadata = sqlMetadata; + this.rows = rows; + columnsByName = new LinkedHashMap((int) Math.ceil(sqlMetadata.size() / 0.75), 0.75f); + for (int i = 0; i < sqlMetadata.size(); i++) { + columnsByName.put(sqlMetadata.get(i).getName(), i + 1); + } + } + + public static JDBCBridge query(TarantoolConnection connection, String sql, Object ... params) { + connection.sql(sql, params); + return new JDBCBridge(connection); + } + + public static int update(TarantoolConnection connection, String sql, Object ... params) { + return connection.update(sql, params).intValue(); + } + + public static JDBCBridge mock(List fields, List> values) { + List meta = new ArrayList(fields.size()); + for(String field:fields) { + meta.add(new TarantoolBase.SQLMetaData(field)); + } + return new JDBCBridge(meta, values); + } + + public static Object execute(TarantoolConnection connection, String sql, Object ... params) { + connection.sql(sql, params); + Long rowCount = connection.getSqlRowCount(); + if(rowCount == null) { + return new SQLResultSet(new JDBCBridge(connection)); + } + return rowCount.intValue(); + } + + + public String getColumnName(int columnIndex) { + return columnIndex > sqlMetadata.size() ? null : sqlMetadata.get(columnIndex - 1).getName(); + } + + public Integer getColumnIndex(String columnName) { + return columnsByName.get(columnName); + } + + public int getColumnCount() { + return columnsByName.size(); + } + + public ListIterator> iterator() { + return rows.listIterator(); + } + + public int size() { + return rows.size(); + } + + @Override + public String toString() { + return "JDBCBridge{" + + "sqlMetadata=" + sqlMetadata + + ", columnsByName=" + columnsByName + + ", rows=" + rows + + '}'; + } +} diff --git a/src/main/java/org/tarantool/Key.java b/src/main/java/org/tarantool/Key.java index f072ec4e..32e761f3 100644 --- a/src/main/java/org/tarantool/Key.java +++ b/src/main/java/org/tarantool/Key.java @@ -16,7 +16,15 @@ public enum Key implements Callable { TUPLE(0x21), FUNCTION(0x22), USER_NAME(0x23),EXPRESSION(0x27), UPSERT_OPS(0x28), - DATA(0x30), ERROR(0x31); + DATA(0x30), ERROR(0x31), + + SQL_FIELD_NAME(0), + SQL_METADATA(0x32), + SQL_TEXT(0x40), + SQL_BIND(0x41), + SQL_OPTIONS(0x42), + SQL_INFO(0x42), + SQL_ROW_COUNT(0); int id; diff --git a/src/main/java/org/tarantool/TarantoolBase.java b/src/main/java/org/tarantool/TarantoolBase.java index eb3acd62..3fb1ce40 100644 --- a/src/main/java/org/tarantool/TarantoolBase.java +++ b/src/main/java/org/tarantool/TarantoolBase.java @@ -11,11 +11,14 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; public abstract class TarantoolBase extends AbstractTarantoolOps, Object, Result> { + protected static final String WELCOME = "Tarantool "; + protected String serverVersion; /** * Connection state */ @@ -41,10 +44,11 @@ public TarantoolBase(String username, String password, Socket socket) { byte[] bytes = new byte[64]; is.readFully(bytes); String firstLine = new String(bytes); - if (!firstLine.startsWith("Tarantool")) { + if (!firstLine.startsWith(WELCOME)) { close(); throw new CommunicationException("Welcome message should starts with tarantool but starts with '" + firstLine + "'", new IllegalStateException("Invalid welcome packet")); } + serverVersion = firstLine.substring(WELCOME.length()); is.readFully(bytes); this.salt = new String(bytes); if (username != null && password != null) { @@ -136,6 +140,62 @@ protected void readPacket(DataInputStream is) throws IOException { is.skipBytes((int) (cis.getBytesRead() - mark - size)); } + protected static class SQLMetaData { + protected String name; + + public SQLMetaData(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "SQLMetaData{" + + "name='" + name + '\'' + + '}'; + } + } + + protected List getSQLMetadata() { + List> meta = (List>) body.get(Key.SQL_METADATA.getId()); + List values = new ArrayList(meta.size()); + for(Map c:meta ) { + values.add(new SQLMetaData((String) c.get(Key.SQL_FIELD_NAME.getId()))); + } + return values; + } + + protected List> getSQLData() { + return (List>) body.get(Key.DATA.getId()); + } + + protected List> readSqlResult(List> data) { + List> values = new ArrayList>(data.size()); + List metaData = getSQLMetadata(); + LinkedHashMap value = new LinkedHashMap(); + for (List row : data) { + for (int i = 0; i < row.size(); i++) { + value.put(metaData.get(i).getName(), row.get(i)); + } + values.add(value); + } + return values; + } + + + protected Long getSqlRowCount() { + Map info = (Map) body.get(Key.SQL_INFO.getId()); + Number rowCount; + if (info != null && (rowCount = ((Number) info.get(Key.SQL_ROW_COUNT.getId()))) != null) { + return rowCount.longValue(); + } + return null; + } + + protected TarantoolException serverError(long code, Object error) { return new TarantoolException(code, error instanceof String ? (String) error : new String((byte[]) error)); } @@ -173,4 +233,8 @@ protected void validateArgs(Object[] args) { public void setInitialRequestSize(int initialRequestSize) { this.initialRequestSize = initialRequestSize; } + + public String getServerVersion() { + return serverVersion; + } } diff --git a/src/main/java/org/tarantool/TarantoolClient.java b/src/main/java/org/tarantool/TarantoolClient.java index ed2209b5..fbc6d7d7 100644 --- a/src/main/java/org/tarantool/TarantoolClient.java +++ b/src/main/java/org/tarantool/TarantoolClient.java @@ -1,6 +1,7 @@ package org.tarantool; import java.util.List; +import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -11,6 +12,10 @@ public interface TarantoolClient { TarantoolClientOps, Object, Long> fireAndForgetOps(); + TarantoolSQLOps>> sqlSyncOps(); + + TarantoolSQLOps, Future>>> sqlAsyncOps(); + void close(); boolean isAlive(); diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index 1c0e3a21..13553493 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -21,7 +21,7 @@ import java.util.concurrent.locks.ReentrantLock; -public class TarantoolClientImpl extends TarantoolBase>> implements TarantoolClient { +public class TarantoolClientImpl extends TarantoolBase> implements TarantoolClient { public static final CommunicationException NOT_INIT_EXCEPTION = new CommunicationException("Not connected, initializing connection"); protected TarantoolClientConfig config; @@ -31,7 +31,7 @@ public class TarantoolClientImpl extends TarantoolBase>> implemen protected SocketChannelProvider socketProvider; protected volatile Exception thumbstone; - protected Map>> futures; + protected Map> futures; protected AtomicInteger wait = new AtomicInteger(); /** * Write properties @@ -77,7 +77,7 @@ public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClient this.initialRequestSize = config.defaultRequestSize; this.socketProvider = socketProvider; this.stats = new TarantoolClientStats(); - this.futures = new ConcurrentHashMap>>(config.predictedFutures); + this.futures = new ConcurrentHashMap>(config.predictedFutures); this.sharedBuffer = ByteBuffer.allocateDirect(config.sharedBufferSize); this.writerBuffer = ByteBuffer.allocateDirect(sharedBuffer.capacity()); this.connector.setDaemon(true); @@ -222,9 +222,9 @@ protected void configureThreads(String threadName) { } - public Future> exec(Code code, Object... args) { + protected Future exec(Code code, Object... args) { validateArgs(args); - FutureImpl> q = new FutureImpl>(syncId.incrementAndGet()); + FutureImpl q = new FutureImpl(syncId.incrementAndGet(), code); if (isDead(q)) { return q; } @@ -248,11 +248,11 @@ protected synchronized void die(String message, Exception cause) { } this.thumbstone = new CommunicationException(message, cause); while (!futures.isEmpty()) { - Iterator>>> iterator = futures.entrySet().iterator(); + Iterator>> iterator = futures.entrySet().iterator(); while (iterator.hasNext()) { - Map.Entry>> elem = iterator.next(); + Map.Entry> elem = iterator.next(); if (elem != null) { - FutureImpl> future = elem.getValue(); + FutureImpl future = elem.getValue(); fail(future, cause); } iterator.remove(); @@ -353,7 +353,7 @@ protected void readThread() { readPacket(is); code = (Long) headers.get(Key.CODE.getId()); Long syncId = (Long) headers.get(Key.SYNC.getId()); - FutureImpl> future = futures.remove(syncId); + FutureImpl future = futures.remove(syncId); stats.received++; wait.decrementAndGet(); complete(code, future); @@ -401,14 +401,19 @@ protected void writeThread() { } - protected void fail(FutureImpl> q, Exception e) { + protected void fail(FutureImpl q, Exception e) { q.setError(e); } - protected void complete(long code, FutureImpl> q) { + protected void complete(long code, FutureImpl q) { if (q != null) { if (code == 0) { - q.setValue((List) body.get(Key.DATA.getId())); + List data = (List) body.get(Key.DATA.getId()); + if(q.getCode() == Code.EXECUTE) { + completeSql(q, (List>) data); + } else { + ((FutureImpl)q).setValue(data); + } } else { Object error = body.get(Key.ERROR.getId()); fail(q, serverError(code, error)); @@ -416,8 +421,18 @@ protected void complete(long code, FutureImpl> q) { } } + protected void completeSql(FutureImpl q, List> data) { + Long rowCount = getSqlRowCount(); + if (rowCount!=null) { + ((FutureImpl) q).setValue(rowCount); + } else { + List> values = readSqlResult(data); + ((FutureImpl) q).setValue(values); + } + } + - protected List syncGet(Future> r) { + protected T syncGet(Future r) { try { return r.get(); } catch (ExecutionException e) { @@ -508,7 +523,7 @@ public TarantoolClientOps, Object, List> syncOps() { @Override public TarantoolClientOps, Object, Future>> asyncOps() { - return this; + return (TarantoolClientOps)this; } @Override @@ -517,11 +532,42 @@ public TarantoolClientOps, Object, Long> 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)); + } + + @Override + public List> query(String sql, Object... bind) { + return (List>) syncGet(exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, bind)); + } + }; + } + + @Override + public TarantoolSQLOps, Future>>> sqlAsyncOps() { + return new TarantoolSQLOps, Future>>>() { + @Override + public Future update(String sql, Object... bind) { + return (Future) exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, bind); + } + + @Override + public Future>> query(String sql, Object... bind) { + return (Future>>) exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, bind); + } + }; + } + protected class SyncOps extends AbstractTarantoolOps, Object, List> { @Override public List exec(Code code, Object... args) { - return syncGet(TarantoolClientImpl.this.exec(code, args)); + return (List) syncGet(TarantoolClientImpl.this.exec(code, args)); } @Override @@ -552,7 +598,7 @@ public void close() { } } - protected boolean isDead(FutureImpl> q) { + protected boolean isDead(FutureImpl q) { if (TarantoolClientImpl.this.thumbstone != null) { fail(q, new CommunicationException("Connection is dead", thumbstone)); return true; diff --git a/src/main/java/org/tarantool/TarantoolConnection.java b/src/main/java/org/tarantool/TarantoolConnection.java index ce319db5..b817988f 100644 --- a/src/main/java/org/tarantool/TarantoolConnection.java +++ b/src/main/java/org/tarantool/TarantoolConnection.java @@ -4,10 +4,12 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; +import java.net.SocketException; import java.nio.ByteBuffer; import java.util.List; +import java.util.Map; -public class TarantoolConnection extends TarantoolBase> { +public class TarantoolConnection extends TarantoolBase> implements TarantoolSQLOps>> { protected InputStream in; protected OutputStream out; protected Socket socket; @@ -21,7 +23,7 @@ public TarantoolConnection(String username, String password, Socket socket) thro } @Override - public List exec(Code code, Object... args) { + protected List exec(Code code, Object... args) { try { ByteBuffer packet = createPacket(code, syncId.incrementAndGet(), null, args); out.write(packet.array(), 0, packet.remaining()); @@ -60,4 +62,43 @@ public void close() { } + @Override + public Long update(String sql, Object... bind) { + sql(sql, bind); + return getSqlRowCount(); + } + + @Override + public List> query(String sql, Object... bind) { + sql(sql, bind); + return readSqlResult((List>) body.get(Key.DATA)); + } + + protected void sql(String sql, Object[] bind) { + exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, bind); + } + + public boolean isClosed() { + return socket.isClosed(); + } + + /** + * Sets given timeout value on underlying socket. + * + * @param timeout Timeout in milliseconds. + * @throws SocketException If failed. + */ + public void setSocketTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } + + /** + * Retrieves timeout value from underlying socket. + * + * @return Timeout in milliseconds. + * @throws SocketException If failed. + */ + public int getSocketTimeout() throws SocketException { + return socket.getSoTimeout(); + } } diff --git a/src/main/java/org/tarantool/TarantoolSQLOps.java b/src/main/java/org/tarantool/TarantoolSQLOps.java new file mode 100644 index 00000000..899d0232 --- /dev/null +++ b/src/main/java/org/tarantool/TarantoolSQLOps.java @@ -0,0 +1,7 @@ +package org.tarantool; + +public interface TarantoolSQLOps { + Update update(String sql, Tuple... bind); + + Result query(String sql, Tuple... bind); +} diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java new file mode 100644 index 00000000..89b0f81d --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -0,0 +1,514 @@ +package org.tarantool.jdbc; + +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; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import org.tarantool.CommunicationException; +import org.tarantool.JDBCBridge; +import org.tarantool.TarantoolConnection; + +import static org.tarantool.jdbc.SQLDriver.PROP_HOST; +import static org.tarantool.jdbc.SQLDriver.PROP_PASSWORD; +import static org.tarantool.jdbc.SQLDriver.PROP_PORT; +import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; +import static org.tarantool.jdbc.SQLDriver.PROP_USER; + +@SuppressWarnings("Since15") +public class SQLConnection implements Connection { + private final TarantoolConnection connection; + final String url; + final Properties properties; + + 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); + } 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; + } + + /** + * 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(); + } + + /** + * 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; + }}; + } + + @Override + public Statement createStatement() throws SQLException { + checkNotClosed(); + return new SQLStatement(this); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + checkNotClosed(); + return new SQLPreparedStatement(this, sql); + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + if (autoCommit == false) { + throw new SQLFeatureNotSupportedException(); + } + } + + @Override + public boolean getAutoCommit() throws SQLException { + return true; + } + + @Override + public void commit() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void rollback() throws SQLException { + throw new SQLFeatureNotSupportedException(); + + } + + @Override + public void close() throws SQLException { + connection.close(); + } + + @Override + public boolean isClosed() throws SQLException { + return connection.isClosed(); + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + checkNotClosed(); + return new SQLDatabaseMetadata(this); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + + } + + @Override + public boolean isReadOnly() throws SQLException { + return false; + } + + @Override + public void setCatalog(String catalog) throws SQLException { + } + + @Override + public String getCatalog() throws SQLException { + return null; + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + if (level != Connection.TRANSACTION_NONE) { + throw new SQLFeatureNotSupportedException(); + } + } + + @Override + public int getTransactionIsolation() throws SQLException { + return Connection.TRANSACTION_NONE; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Map> getTypeMap() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getHoldability() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Clob createClob() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Blob createBlob() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public NClob createNClob() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isValid(int timeout) throws SQLException { + return true; + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + throw new SQLClientInfoException(); + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + throw new SQLClientInfoException(); + } + + @Override + public String getClientInfo(String name) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Properties getClientInfo() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setSchema(String schema) throws SQLException { + } + + @Override + public String getSchema() throws SQLException { + return null; + } + + @Override + public void abort(Executor executor) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + checkNotClosed(); + + if (milliseconds < 0) + throw new SQLException("Network timeout cannot be negative."); + + try { + connection.setSocketTimeout(milliseconds); + } catch (SocketException e) { + throw new SQLException("Failed to set socket timeout: timeout=" + milliseconds, e); + } + } + + @Override + public int getNetworkTimeout() throws SQLException { + checkNotClosed(); + try { + return connection.getSocketTimeout(); + } catch (SocketException e) { + throw new SQLException("Failed to retrieve socket timeout", e); + } + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + 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 ResultSet executeQuery(String sql, Object ... args) throws SQLException { + checkNotClosed(); + try { + return new SQLResultSet(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); + } catch (Exception e) { + handleException(e); + throw new SQLException(e); + } + } + + protected String getServerVersion() { + return connection.getServerVersion(); + } + + /** + * @throws SQLException If connection is closed. + */ + protected void checkNotClosed() throws SQLException { + if (isClosed()) + throw new SQLException("Connection is closed."); + } + + /** + * 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. + } + } + } + + /** + * Provides error message that contains parameters of failed SQL statement. + * + * @param sql SQL Text. + * @param params Parameters of the SQL statement. + * @return Formatted error message. + */ + private static String formatError(String sql, Object ... params) { + return "Failed to execute SQL: " + sql + ", params: " + Arrays.deepToString(params); + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java new file mode 100644 index 00000000..2ddb0ef6 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -0,0 +1,1084 @@ +package org.tarantool.jdbc; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.tarantool.JDBCBridge; + +@SuppressWarnings("Since15") +public class SQLDatabaseMetadata implements DatabaseMetaData { + protected static final int _VSPACE = 281; + protected static final int _VINDEX = 289; + protected static final int SPACES_MAX = 65535; + public static final int FLAGS_IDX = 5; + public static final int FORMAT_IDX = 6; + public static final int NAME_IDX = 2; + public static final int INDEX_FORMAT_IDX = 5; + public static final int SPACE_ID_IDX = 0; + protected final SQLConnection connection; + + + protected class SQLNullResultSet extends SQLResultSet { + + public SQLNullResultSet(JDBCBridge bridge) { + super(bridge); + } + + @Override + protected Object getRaw(int columnIndex) { + return columnIndex > row.size() ? null : row.get(columnIndex - 1); + } + + @Override + protected Integer getColumnIndex(String columnLabel) { + Integer idx = super.getColumnIndex(columnLabel); + return idx == null ? Integer.MAX_VALUE : idx; + } + + + } + + public SQLDatabaseMetadata(SQLConnection connection) { + this.connection = connection; + } + + @Override + public boolean allProceduresAreCallable() throws SQLException { + return false; + } + + @Override + public boolean allTablesAreSelectable() throws SQLException { + return true; + } + + @Override + public String getURL() throws SQLException { + return connection.url; + } + + @Override + public String getUserName() throws SQLException { + return connection.properties.getProperty("user"); + } + + @Override + public boolean isReadOnly() throws SQLException { + return false; + } + + @Override + public boolean nullsAreSortedHigh() throws SQLException { + return true; + } + + @Override + public boolean nullsAreSortedLow() throws SQLException { + return !nullsAreSortedHigh(); + } + + @Override + public boolean nullsAreSortedAtStart() throws SQLException { + return true; + } + + @Override + public boolean nullsAreSortedAtEnd() throws SQLException { + return !nullsAreSortedAtStart(); + } + + @Override + public String getDatabaseProductName() throws SQLException { + return "Tarantool"; + } + + @Override + public String getDatabaseProductVersion() throws SQLException { + return connection.getServerVersion(); + } + + @Override + public String getDriverName() throws SQLException { + return "tarantool-java"; + } + + @Override + public String getDriverVersion() throws SQLException { + return "1.8.jdbc"; + } + + @Override + public int getDriverMajorVersion() { + return 1; + } + + @Override + public int getDriverMinorVersion() { + return 8; + } + + @Override + public boolean usesLocalFiles() throws SQLException { + return false; + } + + @Override + public boolean usesLocalFilePerTable() throws SQLException { + return false; + } + + @Override + public boolean supportsMixedCaseIdentifiers() throws SQLException { + return true; + } + + @Override + public boolean storesUpperCaseIdentifiers() throws SQLException { + return false; + } + + @Override + public boolean storesLowerCaseIdentifiers() throws SQLException { + return false; + } + + @Override + public boolean storesMixedCaseIdentifiers() throws SQLException { + return true; + } + + @Override + public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + return false; + } + + @Override + public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + return false; + } + + @Override + public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + return false; + } + + @Override + public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + return false; + } + + @Override + public String getIdentifierQuoteString() throws SQLException { + return " "; + } + + @Override + public String getSQLKeywords() throws SQLException { + return ""; + } + + @Override + public String getNumericFunctions() throws SQLException { + return ""; + } + + @Override + public String getStringFunctions() throws SQLException { + return ""; + } + + @Override + public String getSystemFunctions() throws SQLException { + return ""; + } + + @Override + public String getTimeDateFunctions() throws SQLException { + return ""; + } + + @Override + public String getSearchStringEscape() throws SQLException { + return null; + } + + @Override + public String getExtraNameCharacters() throws SQLException { + return ""; + } + + @Override + public boolean supportsAlterTableWithAddColumn() throws SQLException { + return false; + } + + @Override + public boolean supportsAlterTableWithDropColumn() throws SQLException { + return false; + } + + @Override + public boolean supportsColumnAliasing() throws SQLException { + return true; + } + + @Override + public boolean nullPlusNonNullIsNull() throws SQLException { + return true; + } + + @Override + public boolean supportsConvert() throws SQLException { + return false; + } + + @Override + public boolean supportsConvert(int fromType, int toType) throws SQLException { + return false; + } + + @Override + public boolean supportsTableCorrelationNames() throws SQLException { + return false; + } + + @Override + public boolean supportsDifferentTableCorrelationNames() throws SQLException { + return false; + } + + @Override + public boolean supportsExpressionsInOrderBy() throws SQLException { + return true; + } + + @Override + public boolean supportsOrderByUnrelated() throws SQLException { + return false; + } + + @Override + public boolean supportsGroupBy() throws SQLException { + return true; + } + + @Override + public boolean supportsGroupByUnrelated() throws SQLException { + return false; + } + + @Override + public boolean supportsGroupByBeyondSelect() throws SQLException { + return false; + } + + @Override + public boolean supportsLikeEscapeClause() throws SQLException { + return false; + } + + @Override + public boolean supportsMultipleResultSets() throws SQLException { + return false; + } + + @Override + public boolean supportsMultipleTransactions() throws SQLException { + return false; + } + + @Override + public boolean supportsNonNullableColumns() throws SQLException { + return true; + } + + @Override + public boolean supportsMinimumSQLGrammar() throws SQLException { + return true; + } + + @Override + public boolean supportsCoreSQLGrammar() throws SQLException { + return true; + } + + @Override + public boolean supportsExtendedSQLGrammar() throws SQLException { + return false; + } + + @Override + public boolean supportsANSI92EntryLevelSQL() throws SQLException { + return false; + } + + @Override + public boolean supportsANSI92IntermediateSQL() throws SQLException { + return false; + } + + @Override + public boolean supportsANSI92FullSQL() throws SQLException { + return false; + } + + @Override + public boolean supportsIntegrityEnhancementFacility() throws SQLException { + return false; + } + + @Override + public boolean supportsOuterJoins() throws SQLException { + return true; + } + + @Override + public boolean supportsFullOuterJoins() throws SQLException { + return false; + } + + @Override + public boolean supportsLimitedOuterJoins() throws SQLException { + return true; + } + + @Override + public String getSchemaTerm() throws SQLException { + return "schema"; + } + + @Override + public String getProcedureTerm() throws SQLException { + return "procedure"; + } + + @Override + public String getCatalogTerm() throws SQLException { + return "catalog"; + } + + @Override + public boolean isCatalogAtStart() throws SQLException { + return true; + } + + @Override + public String getCatalogSeparator() throws SQLException { + return "."; + } + + @Override + public boolean supportsSchemasInDataManipulation() throws SQLException { + return false; + } + + @Override + public boolean supportsSchemasInProcedureCalls() throws SQLException { + return false; + } + + @Override + public boolean supportsSchemasInTableDefinitions() throws SQLException { + return false; + } + + @Override + public boolean supportsSchemasInIndexDefinitions() throws SQLException { + return false; + } + + @Override + public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + return false; + } + + @Override + public boolean supportsCatalogsInDataManipulation() throws SQLException { + return false; + } + + @Override + public boolean supportsCatalogsInProcedureCalls() throws SQLException { + return false; + } + + @Override + public boolean supportsCatalogsInTableDefinitions() throws SQLException { + return false; + } + + @Override + public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + return false; + } + + @Override + public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + return false; + } + + @Override + public boolean supportsPositionedDelete() throws SQLException { + return false; + } + + @Override + public boolean supportsPositionedUpdate() throws SQLException { + return false; + } + + @Override + public boolean supportsSelectForUpdate() throws SQLException { + return false; + } + + @Override + public boolean supportsStoredProcedures() throws SQLException { + return false; + } + + @Override + public boolean supportsSubqueriesInComparisons() throws SQLException { + return false; + } + + @Override + public boolean supportsSubqueriesInExists() throws SQLException { + return true; + } + + @Override + public boolean supportsSubqueriesInIns() throws SQLException { + return true; + } + + @Override + public boolean supportsSubqueriesInQuantifieds() throws SQLException { + return false; + } + + @Override + public boolean supportsCorrelatedSubqueries() throws SQLException { + return false; + } + + @Override + public boolean supportsUnion() throws SQLException { + return true; + } + + @Override + public boolean supportsUnionAll() throws SQLException { + return true; + } + + @Override + public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + return false; + } + + @Override + public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + return false; + } + + @Override + public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + return false; + } + + @Override + public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + return false; + } + + @Override + public int getMaxBinaryLiteralLength() throws SQLException { + return 0; + } + + @Override + public int getMaxCharLiteralLength() throws SQLException { + return 0; + } + + @Override + public int getMaxColumnNameLength() throws SQLException { + return 0; + } + + @Override + public int getMaxColumnsInGroupBy() throws SQLException { + return 0; + } + + @Override + public int getMaxColumnsInIndex() throws SQLException { + return 0; + } + + @Override + public int getMaxColumnsInOrderBy() throws SQLException { + return 0; + } + + @Override + public int getMaxColumnsInSelect() throws SQLException { + return 0; + } + + @Override + public int getMaxColumnsInTable() throws SQLException { + return 0; + } + + @Override + public int getMaxConnections() throws SQLException { + return 0; + } + + @Override + public int getMaxCursorNameLength() throws SQLException { + return 0; + } + + @Override + public int getMaxIndexLength() throws SQLException { + return 0; + } + + @Override + public int getMaxSchemaNameLength() throws SQLException { + return 0; + } + + @Override + public int getMaxProcedureNameLength() throws SQLException { + return 0; + } + + @Override + public int getMaxCatalogNameLength() throws SQLException { + return 0; + } + + @Override + public int getMaxRowSize() throws SQLException { + return 0; + } + + @Override + public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + return false; + } + + @Override + public int getMaxStatementLength() throws SQLException { + return 0; + } + + @Override + public int getMaxStatements() throws SQLException { + return 0; + } + + @Override + public int getMaxTableNameLength() throws SQLException { + return 0; + } + + @Override + public int getMaxTablesInSelect() throws SQLException { + return 0; + } + + @Override + public int getMaxUserNameLength() throws SQLException { + return 0; + } + + @Override + public int getDefaultTransactionIsolation() throws SQLException { + return Connection.TRANSACTION_NONE; + } + + @Override + public boolean supportsTransactions() throws SQLException { + return false; + } + + @Override + public boolean supportsTransactionIsolationLevel(int level) throws SQLException { + return false; + } + + @Override + public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + return false; + } + + @Override + public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + return false; + } + + @Override + public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + return false; + } + + @Override + public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + return false; + } + + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) + throws SQLException { + return new SQLResultSet(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())); + } + + @Override + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) + throws SQLException { + try { + if (types != null && !Arrays.asList(types).contains("TABLE")) { + connection.checkNotClosed(); + return new SQLResultSet(JDBCBridge.EMPTY); + } + String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + List> spaces = (List>) connection.nativeSelect(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + List> rows = new ArrayList>(); + for (List space : spaces) { + String name = (String) space.get(NAME_IDX); + Map flags = (Map) space.get(FLAGS_IDX); + if (flags != null && flags.containsKey("sql") && like(name, parts)) { + rows.add(Arrays.asList(name, "TABLE", flags.get("sql"))); + } + } + return new SQLNullResultSet(JDBCBridge.mock(Arrays.asList("TABLE_NAME", "TABLE_TYPE", "REMARKS", + //nulls + "TABLE_CAT", "TABLE_SCHEM", "TABLE_TYPE", "TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "SELF_REFERENCING_COL_NAME", "REF_GENERATION"), rows)); + } catch (Exception e) { + throw new SQLException("Failed to retrieve table(s) description: " + + "tableNamePattern=\"" + tableNamePattern + "\".", e); + } + } + + @Override + public ResultSet getSchemas() throws SQLException { + return rowOfNullsResultSet(); + } + + private SQLNullResultSet rowOfNullsResultSet() { + return new SQLNullResultSet(JDBCBridge.mock(Collections.emptyList(), Collections.singletonList(Collections.emptyList()))); + } + + @Override + public ResultSet getCatalogs() throws SQLException { + return rowOfNullsResultSet(); + } + + @Override + public ResultSet getTableTypes() throws SQLException { + return new SQLResultSet(JDBCBridge.mock(Arrays.asList("TABLE_TYPE"), Arrays.asList(Arrays.asList("TABLE")))); + } + + @Override + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + try { + String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); + List> spaces = (List>) connection.nativeSelect(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + List> rows = new ArrayList>(); + for (List space : spaces) { + String tableName = (String) space.get(NAME_IDX); + Map flags = (Map) space.get(FLAGS_IDX); + if (flags != null && flags.containsKey("sql") && like(tableName, tableParts)) { + List> format = (List>) space.get(FORMAT_IDX); + for (int columnIdx = 1; columnIdx <= format.size(); columnIdx++) { + Map f = format.get(columnIdx - 1); + String columnName = (String) f.get("name"); + String dbType = (String) f.get("type"); + if (like(columnName, colParts)) { + rows.add(Arrays.asList(tableName, columnName, columnIdx, Types.OTHER, dbType, 10, 1, "YES", Types.OTHER, "NO", "NO")); + } + } + } + } + + return new SQLNullResultSet((JDBCBridge.mock( + Arrays.asList("TABLE_NAME", "COLUMN_NAME", "ORDINAL_POSITION", "DATA_TYPE", "TYPE_NAME", "NUM_PREC_RADIX", "NULLABLE", "IS_NULLABLE", "SOURCE_DATA_TYPE", "IS_AUTOINCREMENT", "IS_GENERATEDCOLUMN", + //nulls + "TABLE_CAT", "TABLE_SCHEM", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "REMARKS", "COLUMN_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", "SCOPE_CATALOG", "SCOPE_SCHEMA", "SCOPE_TABLE" + ), + rows))); + } catch (Exception e) { + throw new SQLException("Error processing table column metadata: " + + "tableNamePattern=\"" + tableNamePattern + "\"; " + + "columnNamePattern=\"" + columnNamePattern + "\".", e); + } + } + + @Override + public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) + throws SQLException { + return rowOfNullsResultSet(); + } + + @Override + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) + throws SQLException { + return rowOfNullsResultSet(); + } + + @Override + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + final List colNames = Arrays.asList( + "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"); + + if (table == null || table.isEmpty()) { + connection.checkNotClosed(); + return emptyResultSet(colNames); + } + + try { + List spaces = connection.nativeSelect(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); + + if (spaces == null || spaces.size() == 0) + return emptyResultSet(colNames); + + List space = ensureType(List.class, spaces.get(0)); + List fields = ensureType(List.class, space.get(FORMAT_IDX)); + int spaceId = ensureType(Number.class, space.get(SPACE_ID_IDX)).intValue(); + List indexes = connection.nativeSelect(_VINDEX, 0, Arrays.asList(spaceId, 0), 0, 1, 0); + List primaryKey = ensureType(List.class, indexes.get(0)); + List parts = ensureType(List.class, primaryKey.get(INDEX_FORMAT_IDX)); + + List> rows = new ArrayList>(); + for (int i = 0; i < parts.size(); i++) { + // For native spaces, the 'parts' is 'List of Lists'. + // We only accept SQL spaces, for which the parts is 'List of Maps'. + Map part = checkType(Map.class, parts.get(i)); + if (part == null) + return emptyResultSet(colNames); + + int ordinal = ensureType(Number.class, part.get("field")).intValue(); + Map field = ensureType(Map.class, fields.get(ordinal)); + // The 'name' field is optional in the format structure. But it is present for SQL space. + String column = ensureType(String.class, field.get("name")); + rows.add(Arrays.asList(null, null, table, column, i + 1, primaryKey.get(NAME_IDX))); + } + // Sort results by column name. + Collections.sort(rows, new Comparator>() { + @Override + public int compare(List row0, List row1) { + String col0 = (String) row0.get(3); + String col1 = (String) row1.get(3); + return col0.compareTo(col1); + } + }); + return new SQLNullResultSet((JDBCBridge.mock(colNames, rows))); + } catch (Exception e) { + throw new SQLException("Error processing metadata for table \"" + table + "\".", e); + } + } + + @Override + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getTypeInfo() throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public boolean supportsResultSetType(int type) throws SQLException { + return false; + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + return false; + } + + @Override + public boolean ownUpdatesAreVisible(int type) throws SQLException { + return false; + } + + @Override + public boolean ownDeletesAreVisible(int type) throws SQLException { + return false; + } + + @Override + public boolean ownInsertsAreVisible(int type) throws SQLException { + return false; + } + + @Override + public boolean othersUpdatesAreVisible(int type) throws SQLException { + return false; + } + + @Override + public boolean othersDeletesAreVisible(int type) throws SQLException { + return false; + } + + @Override + public boolean othersInsertsAreVisible(int type) throws SQLException { + return false; + } + + @Override + public boolean updatesAreDetected(int type) throws SQLException { + return false; + } + + @Override + public boolean deletesAreDetected(int type) throws SQLException { + return false; + } + + @Override + public boolean insertsAreDetected(int type) throws SQLException { + return false; + } + + @Override + public boolean supportsBatchUpdates() throws SQLException { + return false; + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public Connection getConnection() throws SQLException { + return connection; + } + + @Override + public boolean supportsSavepoints() throws SQLException { + return false; + } + + @Override + public boolean supportsNamedParameters() throws SQLException { + return true; + } + + @Override + public boolean supportsMultipleOpenResults() throws SQLException { + return false; + } + + @Override + public boolean supportsGetGeneratedKeys() throws SQLException { + return false; + } + + @Override + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public boolean supportsResultSetHoldability(int holdability) throws SQLException { + return false; + } + + @Override + public int getResultSetHoldability() throws SQLException { + return 0; + } + + @Override + public int getDatabaseMajorVersion() throws SQLException { + return 0; + } + + @Override + public int getDatabaseMinorVersion() throws SQLException { + return 0; + } + + @Override + public int getJDBCMajorVersion() throws SQLException { + return 2; + } + + @Override + public int getJDBCMinorVersion() throws SQLException { + return 1; + } + + @Override + public int getSQLStateType() throws SQLException { + return 0; + } + + @Override + public boolean locatorsUpdateCopy() throws SQLException { + return false; + } + + @Override + public boolean supportsStatementPooling() throws SQLException { + return false; + } + + @Override + public RowIdLifetime getRowIdLifetime() throws SQLException { + return RowIdLifetime.ROWID_UNSUPPORTED; + } + + @Override + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return rowOfNullsResultSet(); + } + + @Override + public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + return false; + } + + @Override + public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + return false; + } + + @Override + public ResultSet getClientInfoProperties() throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + return new SQLResultSet(JDBCBridge.EMPTY); + } + + @Override + public boolean generatedKeyAlwaysReturned() throws SQLException { + return false; + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static T ensureType(Class cls, Object v) throws Exception { + if (v == null || !cls.isAssignableFrom(v.getClass())) { + throw new Exception(String.format("Wrong value type '%s', expected '%s'.", + v == null ? "null" : v.getClass().getName(), cls.getName())); + } + return cls.cast(v); + } + + private static T checkType(Class cls, Object v) { + return (v != null && cls.isAssignableFrom(v.getClass())) ? cls.cast(v) : null; + } + + private ResultSet emptyResultSet(List colNames) { + return new SQLNullResultSet((JDBCBridge.mock(colNames, Collections.>emptyList()))); + } + +} diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java new file mode 100644 index 00000000..6ca97866 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -0,0 +1,232 @@ +package org.tarantool.jdbc; + +import java.net.Socket; +import java.net.URI; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +@SuppressWarnings("Since15") +public class SQLDriver implements Driver { + + static { + try { + java.sql.DriverManager.registerDriver(new SQLDriver()); + } catch (SQLException E) { + throw new RuntimeException("Can't register 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. + final static Properties defaults = new Properties() {{ + setProperty(PROP_HOST, "localhost"); + setProperty(PROP_PORT, "3301"); + setProperty(PROP_SOCKET_TIMEOUT, "0"); + }}; + + private final Map providerCache = new ConcurrentHashMap(); + + @Override + public Connection connect(String url, Properties info) throws SQLException { + final URI uri = URI.create(url); + final Properties urlProperties = parseQueryString(uri, info); + String providerClassName = urlProperties.getProperty(PROP_SOCKET_PROVIDER); + + if (providerClassName == null) + return new SQLConnection(url, urlProperties); + + final SQLSocketProvider 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 Properties parseQueryString(URI uri, Properties info) throws SQLException { + Properties urlProperties = new Properties(defaults); + + 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); + } else { + urlProperties.setProperty(PROP_USER, userInfo.substring(0, i)); + urlProperties.setProperty(PROP_PASSWORD, userInfo.substring(i + 1)); + } + } + if (uri.getQuery() != null) { + String[] parts = uri.getQuery().split("&"); + for (String part : parts) { + int i = part.indexOf("="); + if (i > -1) { + urlProperties.put(part.substring(0, i), part.substring(i + 1)); + } else { + urlProperties.put(part, ""); + } + } + } + if (uri.getHost() != null) { + // Default values are pre-put above. + urlProperties.setProperty(PROP_HOST, 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())); + } + 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."); + } + 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."); + } + if (timeout < 0) { + throw new SQLException("Timeout must not be negative."); + } + return urlProperties; + } + + protected SQLSocketProvider getSocketProviderInstance(String className) throws SQLException { + SQLSocketProvider 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(); + providerCache.put(className, provider); + } + } catch (Exception e) { + throw new SQLException("Couldn't instantiate socket provider: " + className, e); + } + } + } + } + if (provider == null) { + throw new SQLException(String.format("The socket provider %s does not implement %s", + className, SQLSocketProvider.class.getCanonicalName())); + } + return provider; + } + + @Override + public boolean acceptsURL(String url) throws SQLException { + return url.toLowerCase().startsWith("tarantool:"); + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + try { + 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}; + } catch (Exception e) { + throw new SQLException(e); + } + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 1; + } + + @Override + public boolean jdbcCompliant() { + return false; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * Builds a string representation of given connection properties + * along with their sanitized values. + * + * @param props Connection properties. + * @return Comma-separated pairs of property names and values. + */ + protected static String diagProperties(Properties props) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : props.entrySet()) { + if (sb.length() > 0) + sb.append(", "); + sb.append(e.getKey()); + sb.append('='); + sb.append(PROP_USER.equals(e.getKey()) || PROP_PASSWORD.equals(e.getKey()) ? + "*****" : e.getValue().toString()); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java new file mode 100644 index 00000000..15068774 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java @@ -0,0 +1,30 @@ +package org.tarantool.jdbc; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Date; + +import org.tarantool.MsgPackLite; + +public class SQLMsgPackLite extends MsgPackLite { + + public static final SQLMsgPackLite INSTANCE = new SQLMsgPackLite(); + + @Override + public void pack(Object item, OutputStream os) throws IOException { + if(item instanceof Date) { + super.pack(((Date)item).getTime(), os); + } else if(item instanceof Time) { + super.pack(((Time)item).getTime(), os); + } else if(item instanceof Timestamp) { + super.pack(((Timestamp)item).getTime(), os); + } else if(item instanceof BigDecimal) { + super.pack(((BigDecimal)item).toPlainString(), os); + } else { + super.pack(item, os); + } + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java new file mode 100644 index 00000000..99b51370 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -0,0 +1,358 @@ +package org.tarantool.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +public class SQLPreparedStatement extends SQLStatement implements PreparedStatement { + final static String INVALID_CALL_MSG = "The method cannot be called on a PreparedStatement."; + final String sql; + final Map params; + + + public SQLPreparedStatement(SQLConnection connection, String sql) { + super(connection); + this.sql = sql; + this.params = new HashMap(); + } + + @Override + public ResultSet executeQuery() throws SQLException { + discardLastResults(); + return connection.executeQuery(sql, getParams()); + } + + protected Object[] getParams() throws SQLException { + Object[] objects = new Object[params.size()]; + for (int i = 1; i <= params.size(); i++) { + if (params.containsKey(i)) { + objects[i - 1] = params.get(i); + } else { + throw new SQLException("Parameter " + i + " is missing"); + } + } + return objects; + } + + @Override + public int executeUpdate() throws SQLException { + discardLastResults(); + return connection.executeUpdate(sql, getParams()); + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + params.put(parameterIndex, null); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void clearParameters() throws SQLException { + params.clear(); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public boolean execute() throws SQLException { + discardLastResults(); + return handleResult(connection.execute(sql, getParams())); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return getResultSet().getMetaData(); + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + params.put(parameterIndex, null); + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + params.put(parameterIndex, x.toString()); + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + return null; + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + params.put(parameterIndex, value); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + params.put(parameterIndex, x); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void addBatch(String sql) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int[] executeBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void clearBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void addBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + + @Override + public boolean execute(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java new file mode 100644 index 00000000..249ab128 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -0,0 +1,1062 @@ +package org.tarantool.jdbc; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.tarantool.JDBCBridge; + +@SuppressWarnings("Since15") +public class SQLResultSet implements ResultSet { + ListIterator> iterator; + final JDBCBridge bridge; + final SQLResultSetMetaData metaData; + + int maxRows; + List row = null; + + + public SQLResultSet(JDBCBridge bridge) { + this.bridge = bridge; + iterator = bridge.iterator(); + metaData = new SQLResultSetMetaData(bridge); + } + + @Override + public boolean next() throws SQLException { + if (iterator.hasNext() && (maxRows == 0 || iterator.nextIndex() < maxRows)) { + row = iterator.next(); + return true; + } + row = null; + return false; + } + + @Override + public void close() throws SQLException { + + } + + @Override + public boolean wasNull() throws SQLException { + return false; + } + + @Override + public String getString(int columnIndex) throws SQLException { + Object raw = getRaw(columnIndex); + return raw == null ? null : String.valueOf(raw); + } + + protected Object getRaw(int columnIndex) { + return row.get(columnIndex - 1); + } + + protected Integer getColumnIndex(String columnLabel) { + return bridge.getColumnIndex(columnLabel); + } + + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return Boolean.TRUE.equals(getRaw(columnIndex)); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).byteValue(); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).shortValue(); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return getNumber(columnIndex).intValue(); + } + + private Number getNumber(int columnIndex) { + Number raw = (Number) getRaw(columnIndex); + return raw == null ? 0 : raw; + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).longValue(); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).floatValue(); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + return (getNumber(columnIndex)).doubleValue(); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + BigDecimal bigDecimal = new BigDecimal(getString(columnIndex)); + return scale > -1 ? bigDecimal.setScale(scale) : bigDecimal; + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + return (byte[]) getRaw(columnIndex); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + return new java.sql.Date(getLong(columnIndex)); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + return new java.sql.Time(getLong(columnIndex)); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return new java.sql.Timestamp(getLong(columnIndex)); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return new ByteArrayInputStream(getString(columnIndex).getBytes(Charset.forName("ASCII"))); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return new ByteArrayInputStream(getString(columnIndex).getBytes(Charset.forName("UTF-8"))); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return new ByteArrayInputStream(getBytes(columnIndex)); + } + + @Override + public String getString(String columnLabel) throws SQLException { + return getString(getColumnIndex(columnLabel)); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getBoolean(getColumnIndex(columnLabel)); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return getByte(getColumnIndex(columnLabel)); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return getShort(getColumnIndex(columnLabel)); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return getInt(getColumnIndex(columnLabel)); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return getLong(getColumnIndex(columnLabel)); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return getFloat(getColumnIndex(columnLabel)); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return getDouble(getColumnIndex(columnLabel)); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(getColumnIndex(columnLabel)); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return getBytes(getColumnIndex(columnLabel)); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return getDate(getColumnIndex(columnLabel)); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(getColumnIndex(columnLabel)); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getTimestamp(getColumnIndex(columnLabel)); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return getAsciiStream(getColumnIndex(columnLabel)); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return getUnicodeStream(getColumnIndex(columnLabel)); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return getBinaryStream(getColumnIndex(columnLabel)); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public String getCursorName() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return metaData; + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + return getRaw(columnIndex); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getRaw(getColumnIndex(columnLabel)); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return getColumnIndex(columnLabel); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return new StringReader(getString(columnIndex)); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return new StringReader(getString(columnLabel)); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return getBigDecimal(columnIndex, -1); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getBigDecimal(columnLabel, -1); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return row == null && iterator.previousIndex() == -1; + } + + @Override + public boolean isAfterLast() throws SQLException { + return iterator.nextIndex() == bridge.size() && row == null; + } + + @Override + public boolean isFirst() throws SQLException { + return iterator.previousIndex() == 0; + } + + @Override + public boolean isLast() throws SQLException { + return iterator.nextIndex() == bridge.size(); + } + + @Override + public void beforeFirst() throws SQLException { + row = null; + iterator = bridge.iterator(); + } + + @Override + public void afterLast() throws SQLException { + while (next()) { + } + } + + @Override + public boolean first() throws SQLException { + beforeFirst(); + return next(); + } + + @Override + public boolean last() throws SQLException { + while (iterator.hasNext()) { + next(); + } + return row != null; + } + + @Override + public int getRow() throws SQLException { + return iterator.previousIndex() + 1; + } + + @Override + public boolean absolute(int row) throws SQLException { + beforeFirst(); + for (int i = 0; i < row && iterator.hasNext(); i++) { + next(); + } + return !(isAfterLast() || isBeforeFirst()); + + } + + @Override + public boolean relative(int rows) throws SQLException { + for (int i = 0; i < rows && iterator.hasNext(); i++) { + next(); + } + return !(isAfterLast() || isBeforeFirst()); + } + + @Override + public boolean previous() throws SQLException { + if (iterator.hasPrevious()) { + iterator.previous(); + return true; + } + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + if (direction != ResultSet.FETCH_FORWARD) { + throw new SQLException("TYPE_FORWARD_ONLY"); + } + } + + @Override + public int getFetchDirection() throws SQLException { + return ResultSet.FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getFetchSize() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getType() throws SQLException { + return ResultSet.TYPE_FORWARD_ONLY; + } + + @Override + public int getConcurrency() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean rowUpdated() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean rowInserted() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean rowDeleted() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void insertRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void deleteRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void refreshRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void cancelRowUpdates() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void moveToInsertRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void moveToCurrentRow() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Statement getStatement() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + try { + return new URL(getString(columnIndex)); + } catch (MalformedURLException e) { + throw new SQLException(e); + } + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + try { + return new URL(getString(columnLabel)); + } catch (MalformedURLException e) { + throw new SQLException(e); + } + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getHoldability() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isClosed() throws SQLException { + return false; + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + return type.cast(getRaw(columnIndex)); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return type.cast(getRaw(getColumnIndex(columnLabel))); + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String toString() { + return "SQLResultSet{" + + "metaData=" + metaData + + ", row=" + row + + '}'; + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java new file mode 100644 index 00000000..b3053d24 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -0,0 +1,138 @@ +package org.tarantool.jdbc; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Types; + +import org.tarantool.JDBCBridge; + +public class SQLResultSetMetaData implements ResultSetMetaData { + protected final JDBCBridge JDBCBridgeExecutor; + + public SQLResultSetMetaData(JDBCBridge JDBCBridgeExecutor) { + this.JDBCBridgeExecutor = JDBCBridgeExecutor; + } + + @Override + public int getColumnCount() throws SQLException { + return JDBCBridgeExecutor.getColumnCount(); + } + + @Override + public boolean isAutoIncrement(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isCaseSensitive(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isSearchable(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isCurrency(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int isNullable(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isSigned(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getColumnDisplaySize(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getColumnLabel(int column) throws SQLException { + return JDBCBridgeExecutor.getColumnName(column); + } + + @Override + public String getColumnName(int column) throws SQLException { + return JDBCBridgeExecutor.getColumnName(column); + } + + @Override + public String getSchemaName(int column) throws SQLException { + return null; + } + + @Override + public int getPrecision(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getScale(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getTableName(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getCatalogName(int column) throws SQLException { + return null; + } + + @Override + public int getColumnType(int column) throws SQLException { + return Types.OTHER; + } + + @Override + public String getColumnTypeName(int column) throws SQLException { + return "scalar"; + } + + @Override + public boolean isReadOnly(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWritable(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isDefinitelyWritable(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getColumnClassName(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String toString() { + return "SQLResultSetMetaData{" + + "bridge=" + JDBCBridgeExecutor + + '}'; + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLSocketProvider.java b/src/main/java/org/tarantool/jdbc/SQLSocketProvider.java new file mode 100644 index 00000000..0957950f --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLSocketProvider.java @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..3e75694a --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -0,0 +1,289 @@ +package org.tarantool.jdbc; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.Statement; + +@SuppressWarnings("Since15") +public class SQLStatement implements Statement { + protected final SQLConnection connection; + private SQLResultSet resultSet; + private int updateCount; + private int maxRows; + + protected SQLStatement(SQLConnection sqlConnection) { + this.connection = sqlConnection; + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + discardLastResults(); + return connection.executeQuery(sql); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + discardLastResults(); + return connection.executeUpdate(sql); + } + + @Override + public void close() throws SQLException { + + } + + @Override + public int getMaxFieldSize() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getMaxRows() throws SQLException { + return maxRows; + } + + @Override + public void setMaxRows(int max) throws SQLException { + maxRows = max; + if(resultSet!=null) { + resultSet.maxRows = maxRows; + } + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getQueryTimeout() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void cancel() throws SQLException { + + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public void setCursorName(String name) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql) throws SQLException { + discardLastResults(); + return handleResult(connection.execute(sql)); + } + + @Override + public ResultSet getResultSet() throws SQLException { + try { + return resultSet; + } finally { + resultSet = null; + } + } + + @Override + public int getUpdateCount() throws SQLException { + try { + return updateCount; + } finally { + updateCount = -1; + } + } + + @Override + public boolean getMoreResults() throws SQLException { + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + if (direction != ResultSet.FETCH_FORWARD) { + throw new SQLFeatureNotSupportedException(); + } + } + + @Override + public int getFetchDirection() throws SQLException { + return ResultSet.FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + } + + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + @Override + public int getResultSetConcurrency() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getResultSetType() throws SQLException { + return ResultSet.TYPE_FORWARD_ONLY; + } + + @Override + public void addBatch(String sql) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void clearBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int[] executeBatch() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public Connection getConnection() throws SQLException { + return connection; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + return false; + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getResultSetHoldability() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isClosed() throws SQLException { + return connection.isClosed(); + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isPoolable() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void closeOnCompletion() throws SQLException { + + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + return false; + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * Clears the results of the most recent execution. + */ + protected void discardLastResults() { + updateCount = -1; + if (resultSet != null) { + try { + resultSet.close(); + } catch (Exception ignored) { + // No-op. + } + resultSet = null; + } + } + + /** + * 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) { + if (result instanceof SQLResultSet) { + resultSet = (SQLResultSet) result; + resultSet.maxRows = maxRows; + updateCount = -1; + return true; + } else { + resultSet = null; + updateCount = (Integer) result; + return false; + } + } +} diff --git a/src/main/resources/META-INF/services/java.sql.Driver b/src/main/resources/META-INF/services/java.sql.Driver new file mode 100644 index 00000000..f3efa6c6 --- /dev/null +++ b/src/main/resources/META-INF/services/java.sql.Driver @@ -0,0 +1 @@ +org.tarantool.jdbc.SQLDriver \ No newline at end of file diff --git a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java index b608c843..cff735eb 100644 --- a/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java +++ b/src/test/java/org/tarantool/AbstractTarantoolConnectorIT.java @@ -82,7 +82,7 @@ public abstract class AbstractTarantoolConnectorIT { @BeforeAll public static void setupEnv() { control = new TarantoolControl(); - control.start("jdk-testing"); + startTarantool("jdk-testing"); console = openConsole(); @@ -104,7 +104,7 @@ public static void cleanupEnv() { console.close(); } finally { - control.stop("jdk-testing"); + stopTarantool("jdk-testing"); } } @@ -189,12 +189,14 @@ protected List consoleSelect(String spaceName, Object key) { return console.eval(sb.toString()); } - protected void stopTarantool(String instance) { + protected static void stopTarantool(String instance) { control.stop(instance); + control.waitStopped("jdk-testing"); } - protected void startTarantool(String instance) { + protected static void startTarantool(String instance) { control.start(instance); + control.waitStarted("jdk-testing"); } /** diff --git a/src/test/java/org/tarantool/TarantoolControl.java b/src/test/java/org/tarantool/TarantoolControl.java index e13b89e7..3b8f9261 100644 --- a/src/test/java/org/tarantool/TarantoolControl.java +++ b/src/test/java/org/tarantool/TarantoolControl.java @@ -16,14 +16,50 @@ * Wrapper around tarantoolctl utility. */ public class TarantoolControl { + public class TarantoolControlException extends RuntimeException { + int code; + String stdout; + String stderr; + + TarantoolControlException(int code, String stdout, String stderr) { + super("returned exitcode " + code + "\n" + + "[stdout]\n" + stdout + "\n[stderr]\n" + stderr); + this.code = code; + this.stdout = stdout; + this.stderr = stderr; + } + } + protected static final String tntCtlWorkDir = System.getProperty("tntCtlWorkDir", new File("testroot").getAbsolutePath()); protected static final String instanceDir = new File("src/test").getAbsolutePath(); protected static final String tarantoolCtlConfig = new File("src/test/.tarantoolctl").getAbsolutePath(); protected static final int RESTART_TIMEOUT = 2000; + static { + try { + setupWorkDirectory(); + } catch (IOException e) { + throw new RuntimeException("Can't setup test root directory!", e); + } + } + + protected static void setupWorkDirectory() throws IOException { + try { + rmdir(tntCtlWorkDir); + } catch (IOException ignored) { + /* No-op. */ + } + + mkdir(tntCtlWorkDir); + for (File c : new File(instanceDir).listFiles()) + if (c.getName().endsWith(".lua")) + copyFile(c, tntCtlWorkDir); + copyFile(tarantoolCtlConfig, tntCtlWorkDir); + } + // Based on https://stackoverflow.com/a/779529 - private void rmdir(File f) throws IOException { + private static void rmdir(File f) throws IOException { if (f.isDirectory()) { for (File c : f.listFiles()) rmdir(c); @@ -31,15 +67,15 @@ private void rmdir(File f) throws IOException { f.delete(); } - private void rmdir(String f) throws IOException { + private static void rmdir(String f) throws IOException { rmdir(new File(f)); } - private void mkdir(File f) throws IOException { + private static void mkdir(File f) throws IOException { f.mkdirs(); } - private void mkdir(String f) throws IOException { + private static void mkdir(String f) throws IOException { mkdir(new File(f)); } @@ -79,23 +115,6 @@ private static String loadStream(InputStream s) throws IOException { return sb.toString(); } - protected void setupWorkDirectory() throws IOException { - rmdir(tntCtlWorkDir); - mkdir(tntCtlWorkDir); - for (File c : new File(instanceDir).listFiles()) - if (c.getName().endsWith(".lua")) - copyFile(c, tntCtlWorkDir); - copyFile(tarantoolCtlConfig, tntCtlWorkDir); - } - - TarantoolControl() { - try { - setupWorkDirectory(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - /** * Control the given tarantool instance via tarantoolctl utility. * @@ -158,11 +177,46 @@ public void run() { } catch (IOException e) { /* No-op. */ } - throw new RuntimeException("returned exitcode " + code + "\n" + - "[stdout]\n" + stdout + "\n[stderr]\n" + stderr); + throw new TarantoolControlException(code, stdout, stderr); } } + /** + * Wait until the instance will be started. + * + * Use tarantoolctl status instanceName. + * + * Then test the instance with TarantoolTcpConsole (ADMIN environment + * variable is set) or TarantoolLocalConsole. + * + * XXX: Now TarantoolLocalConsole is used unconditionally, see + * openConsole(). + */ + public void waitStarted(String instanceName) { + while (status(instanceName) != 0) + sleep(); + + while (true) { + try { + openConsole(instanceName).close(); + break; + } catch (Exception ignored) { + /* No-op. */ + } + sleep(); + } + } + + /** + * Wait until the instance will be stopped. + * + * Use tarantoolctl status instanceName. + */ + public void waitStopped(String instanceName) { + while (status(instanceName) != 1) + sleep(); + } + public void start(String instanceName) { executeCommand("start", instanceName); } @@ -170,4 +224,40 @@ public void start(String instanceName) { public void stop(String instanceName) { executeCommand("stop", instanceName); } + + /** + * Wrapper for `tarantoolctl status instanceName`. + * + * Return exit code of the command: + * + * * 0 -- started; + * * 1 -- stopped; + * * 2 -- pid file exists, control socket inaccessible. + */ + public int status(String instanceName) { + try { + executeCommand("status", instanceName); + } catch (TarantoolControlException e) { + return e.code; + } + + return 0; + } + + /* + * XXX: This function is planned to use text console (from ADMIN + * environment variable) when it is available for the instance and + * fallback to TarantoolLocalConsole. + */ + public TarantoolConsole openConsole(String instanceName) { + return TarantoolConsole.open(tntCtlWorkDir, instanceName); + } + + public static void sleep() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java new file mode 100644 index 00000000..c0c00c07 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -0,0 +1,184 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.tarantool.TarantoolConnection; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.tarantool.TarantoolControl; + +//mvn -DtntHost=localhost -DtntPort=3301 -DtntUser=test -DtntPass=test verify +public abstract class AbstractJdbcIT { + private static final String host = System.getProperty("tntHost", "localhost"); + private static final Integer port = Integer.valueOf(System.getProperty("tntPort", "3301")); + private static final String user = System.getProperty("tntUser", "test_admin"); + private static final String pass = System.getProperty("tntPass", "4pWBZmLEgkmKK5WP"); + private static String URL = String.format("tarantool://%s:%d?user=%s&password=%s", host, port, user, pass); + + private static String[] initSql = new String[] { + "CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100))", + "INSERT INTO test VALUES (1, 'one'), (2, 'two'), (3, 'three')", + + "CREATE TABLE test_types(" + + "f1 INT PRIMARY KEY, " + + "f2 CHAR(4), " + + "f3 VARCHAR(100), " + + "f4 LONGVARCHAR(100), " + + "f5 NUMERIC, " + + "f6 DECIMAL, " + + "f7 BIT, " + + "f8 TINYINT, " + + "f9 SMALLINT, " + + "f10 INTEGER, " + + "f11 BIGINT," + + "f12 REAL, " + + "f13 FLOAT, " + + "f14 BINARY(4), " + + "f15 VARBINARY(128), " + + "f16 LONGVARBINARY(2048), " + + "f17 DATE, " + + "f18 TIME, " + + "f19 TIMESTAMP)", + + "INSERT INTO test_types VALUES(" + + "1," + + "'abcd'," + //CHAR + "'000000000000000000001'," + //VARCHAR + "'0000000000000000000000000000000001'," + //LONGVARCHAR + "100," + // NUMERIC + "100.1," + // DECIMAL + "1," + //BIT + "7," + //TINYINT + "1000," + //SMALLINT + "100," + //INTEGER + "100000000000000000," + //BIGINT + "-100.2," + //REAL + "100.3," + //FLOAT + "X'01020304'," + //BINARY + "X'0102030405'," +//VARBINARY + "X'010203040506'," + //LONGVARBINARY + "'1983-03-14'," + //DATE + "'12:01:06'," + //TIME + "129479994)", //TIMESTAMP + + "CREATE TABLE test_compound(id1 INT, id2 INT, val VARCHAR(100), PRIMARY KEY (id2, id1))" + }; + + private static String[] cleanSql = new String[] { + "DROP TABLE IF EXISTS test", + "DROP TABLE IF EXISTS test_types", + "DROP TABLE IF EXISTS test_compound" + }; + + static Object[] testRow = new Object[] { + 1, + "abcd", + "000000000000000000001", + "0000000000000000000000000000000001", + BigDecimal.valueOf(100), + BigDecimal.valueOf(100.1), + Boolean.FALSE, + (byte)7, + (short)1000, + 100, + 100000000000000000L, + -100.2f, + 100.3d, + new BigInteger("01020304", 16).toByteArray(), + new BigInteger("0102030405", 16).toByteArray(), + new BigInteger("010203040506", 16).toByteArray(), + Date.valueOf("1983-03-14"), + Time.valueOf("12:01:06"), + new Timestamp(129479994) + }; + + protected static TarantoolControl control; + Connection conn; + + @BeforeAll + public static void setupEnv() throws Exception { + control = new TarantoolControl(); + control.start("jdk-testing"); + control.waitStarted("jdk-testing"); + + sqlExec(cleanSql); + sqlExec(initSql); + } + + @AfterAll + public static void teardownEnv() throws Exception { + try { + sqlExec(cleanSql); + } finally { + control.stop("jdk-testing"); + control.waitStopped("jdk-testing"); + } + } + + @BeforeEach + public void setUpConnection() throws SQLException { + conn = DriverManager.getConnection(URL); + assertNotNull(conn); + } + + @AfterEach + public void tearDownConnection() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + private static void sqlExec(String[] text) { + TarantoolConnection con = makeConnection(); + try { + for (String cmd : text) + con.eval("box.sql.execute(\"" + cmd + "\")"); + } finally { + con.close(); + } + } + + static List getRow(String space, Object key) { + TarantoolConnection con = makeConnection(); + try { + List l = con.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); + return (l == null || l.size() == 0) ? Collections.emptyList() : (List) l.get(0); + } finally { + con.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); + } + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java new file mode 100644 index 00000000..a6a05e88 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -0,0 +1,107 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.tarantool.TarantoolConnection; + +import java.lang.reflect.Field; +import java.net.Socket; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@SuppressWarnings("Since15") +public class JdbcConnectionIT extends AbstractJdbcIT { + @Test + public void testCreateStatement() throws SQLException { + Statement stmt = conn.createStatement(); + assertNotNull(stmt); + stmt.close(); + } + + @Test + public void testPrepareStatement() throws SQLException { + PreparedStatement prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES(?, ?)"); + assertNotNull(prep); + prep.close(); + } + + @Test + public void testCloseIsClosed() throws SQLException { + assertFalse(conn.isClosed()); + conn.close(); + assertTrue(conn.isClosed()); + conn.close(); + } + + @Test + public void testGetMetaData() throws SQLException { + DatabaseMetaData meta = conn.getMetaData(); + assertNotNull(meta); + } + + @Test + public void testGetSetNetworkTimeout() throws Exception { + assertEquals(0, conn.getNetworkTimeout()); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + conn.setNetworkTimeout(null, -1); + } + }); + 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); + } +} \ No newline at end of file diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java new file mode 100644 index 00000000..76caa198 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -0,0 +1,216 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.function.Executable; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { + private DatabaseMetaData meta; + + @BeforeEach + public void setUp() throws SQLException { + meta = conn.getMetaData(); + } + + @Test + public void testGetTableTypes() throws SQLException { + ResultSet rs = meta.getTableTypes(); + assertNotNull(rs); + + assertTrue(rs.next()); + assertEquals("TABLE", rs.getString("TABLE_TYPE")); + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetAllTables() throws SQLException { + ResultSet rs = meta.getTables(null, null, null, new String[] {"TABLE"}); + assertNotNull(rs); + + assertTrue(rs.next()); + assertEquals("TEST", rs.getString("TABLE_NAME")); + + assertTrue(rs.next()); + assertEquals("TEST_TYPES", rs.getString("TABLE_NAME")); + + assertTrue(rs.next()); + assertEquals("TEST_COMPOUND", rs.getString("TABLE_NAME")); + + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetTable() throws SQLException { + ResultSet rs = meta.getTables(null, null, "TEST", new String[] {"TABLE"}); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("TEST", rs.getString("TABLE_NAME")); + + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetColumns() throws SQLException { + ResultSet rs = meta.getColumns(null, null, "TEST", null); + assertNotNull(rs); + + assertTrue(rs.next()); + + assertEquals("TEST", rs.getString("TABLE_NAME")); + assertEquals("ID", rs.getString("COLUMN_NAME")); + assertEquals(1, rs.getInt("ORDINAL_POSITION")); + + assertTrue(rs.next()); + + assertEquals("TEST", rs.getString("TABLE_NAME")); + assertEquals("VAL", rs.getString("COLUMN_NAME")); + assertEquals(2, rs.getInt("ORDINAL_POSITION")); + + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetPrimaryKeys() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "TEST"); + + assertNotNull(rs); + assertTrue(rs.next()); + + checkGetPrimaryKeysRow(rs, "TEST", "ID", "pk_unnamed_TEST_1", 1); + + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetPrimaryKeysCompound() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "TEST_COMPOUND"); + + assertNotNull(rs); + assertTrue(rs.next()); + checkGetPrimaryKeysRow(rs, "TEST_COMPOUND", "ID1", "pk_unnamed_TEST_COMPOUND_1", 2); + + assertTrue(rs.next()); + checkGetPrimaryKeysRow(rs, "TEST_COMPOUND", "ID2", "pk_unnamed_TEST_COMPOUND_1", 1); + + assertFalse(rs.next()); + + rs.close(); + } + + @Test + public void testGetPrimaryKeysIgnoresCatalogSchema() throws SQLException { + String[] vals = new String[] {null, "", "IGNORE"}; + for (String cat : vals) { + for (String schema : vals) { + ResultSet rs = meta.getPrimaryKeys(cat, schema, "TEST"); + + assertNotNull(rs); + assertTrue(rs.next()); + checkGetPrimaryKeysRow(rs, "TEST", "ID", "pk_unnamed_TEST_1", 1); + assertFalse(rs.next()); + rs.close(); + } + } + } + + @Test + public void testGetPrimaryKeysNotFound() throws SQLException { + String[] tables = new String[] {null, "", "NOSUCHTABLE"}; + for (String t : tables) { + ResultSet rs = meta.getPrimaryKeys(null, null, t); + assertNotNull(rs); + assertFalse(rs.next()); + rs.close(); + } + } + + @Test + public void testGetPrimaryKeyNonSQLSpace() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "_vspace"); + assertNotNull(rs); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testGetPrimaryKeysOrderOfColumns() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "TEST"); + assertNotNull(rs); + ResultSetMetaData rsMeta = rs.getMetaData(); + assertEquals(6, rsMeta.getColumnCount()); + assertEquals("TABLE_CAT", rsMeta.getColumnName(1)); + assertEquals("TABLE_SCHEM", rsMeta.getColumnName(2)); + assertEquals("TABLE_NAME", rsMeta.getColumnName(3)); + assertEquals("COLUMN_NAME", rsMeta.getColumnName(4)); + assertEquals("KEY_SEQ", rsMeta.getColumnName(5)); + assertEquals("PK_NAME", rsMeta.getColumnName(6)); + rs.close(); + } + + private void checkGetPrimaryKeysRow(ResultSet rs, String table, String colName, String pkName, int seq) + throws SQLException { + assertNull(rs.getString("TABLE_CAT")); + assertNull(rs.getString("TABLE_SCHEM")); + assertEquals(table, rs.getString("TABLE_NAME")); + assertEquals(colName, rs.getString("COLUMN_NAME")); + assertEquals(seq, rs.getInt("KEY_SEQ")); + assertEquals(pkName, rs.getString("PK_NAME")); + + assertNull(rs.getString(1)); + assertNull(rs.getString(2)); + assertEquals(table, rs.getString(3)); + assertEquals(colName, rs.getString(4)); + assertEquals(seq, rs.getInt(5)); + assertEquals(pkName, rs.getString(6)); + } + + @Test + public void testClosedConnection() throws SQLException { + conn.close(); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: meta.getTables(null, null, null, new String[]{"TABLE"}); + break; + case 1: meta.getColumns(null, null, "TEST", null); + break; + case 2: meta.getPrimaryKeys(null, null, "TEST"); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getCause().getMessage()); + } + assertEquals(3, i); + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java new file mode 100644 index 00000000..38ca2d4c --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java @@ -0,0 +1,202 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.tarantool.CommunicationException; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URI; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import 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; + +public class JdbcDriverTest { + @Test + public void testParseQueryString() throws Exception { + SQLDriver drv = new SQLDriver(); + + Properties prop = new Properties(); + prop.setProperty(PROP_USER, "adm"); + prop.setProperty(PROP_PASSWORD, "secret"); + + URI uri = new URI(String.format( + "tarantool://server.local:3302?%s=%s&%s=%d", + PROP_SOCKET_PROVIDER, "some.class", + PROP_SOCKET_TIMEOUT, 5000)); + + Properties res = drv.parseQueryString(uri, prop); + assertNotNull(res); + + 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)); + } + + @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)); + } + + @Test + public void testParseQueryStringValidations() { + // Check non-number port + checkParseQueryStringValidation("tarantool://0", + new Properties() {{setProperty(PROP_PORT, "nan");}}, + "Port must be a valid number."); + + // Check zero port + checkParseQueryStringValidation("tarantool://0:0", null, "Port is out of range: 0"); + + // 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 negative timeout + checkParseQueryStringValidation(String.format("tarantool://0:3301?%s=-100", PROP_SOCKET_TIMEOUT), null, + "Timeout must not be negative."); + } + + @Test + public void testGetPropertyInfo() throws SQLException { + Driver drv = new SQLDriver(); + Properties props = new Properties(); + DriverPropertyInfo[] info = drv.getPropertyInfo("tarantool://server.local:3302", props); + assertNotNull(info); + assertEquals(6, info.length); + + for (DriverPropertyInfo e: info) { + assertNotNull(e.name); + assertNull(e.choices); + assertNotNull(e.description); + + if (PROP_HOST.equals(e.name)) { + assertTrue(e.required); + assertEquals("server.local", e.value); + } else if (PROP_PORT.equals(e.name)) { + assertTrue(e.required); + assertEquals("3302", e.value); + } else if (PROP_USER.equals(e.name)) { + assertFalse(e.required); + assertNull(e.value); + } else if (PROP_PASSWORD.equals(e.name)) { + assertFalse(e.required); + assertNull(e.value); + } else if (PROP_SOCKET_PROVIDER.equals(e.name)) { + assertFalse(e.required); + assertNull(e.value); + } else if (PROP_SOCKET_TIMEOUT.equals(e.name)) { + assertFalse(e.required); + assertEquals("0", e.value); + } else + fail("Unknown property '" + e.name + "'"); + } + } + + @Test + public void testCustomSocketProviderFail() throws SQLException { + checkCustomSocketProviderFail("nosuchclassexists", + "Couldn't instantiate socket provider"); + + checkCustomSocketProviderFail(Integer.class.getName(), + "The socket provider java.lang.Integer does not implement org.tarantool.jdbc.SQLSocketProvider"); + + checkCustomSocketProviderFail(TestSQLProviderThatReturnsNull.class.getName(), + "The socket provider returned null socket"); + + checkCustomSocketProviderFail(TestSQLProviderThatThrows.class.getName(), + "Couldn't initiate connection using"); + } + + @Test + public void testNoResponseAfterInitialConnect() throws IOException { + ServerSocket socket = new ServerSocket(); + socket.bind(null, 0); + try { + final String url = "tarantool://localhost:" + socket.getLocalPort(); + final Properties prop = new Properties(); + prop.setProperty(PROP_SOCKET_TIMEOUT, "100"); + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + DriverManager.getConnection(url, prop); + } + }); + 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(); + } + } + + 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); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + drv.connect("tarantool://0:3301", prop); + } + }); + assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); + } + + private void checkParseQueryStringValidation(final String uri, final Properties prop, String errMsg) { + final SQLDriver drv = new SQLDriver(); + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + drv.parseQueryString(new URI(uri), prop); + } + }); + assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); + } + + static class TestSQLProviderThatReturnsNull implements SQLSocketProvider { + @Override + public Socket getConnectedSocket(URI uri, Properties params) { + return null; + } + } + + static class TestSQLProviderThatThrows implements SQLSocketProvider { + @Override + public Socket getConnectedSocket(URI uri, Properties params) { + throw new RuntimeException("ERROR"); + } + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java new file mode 100644 index 00000000..e02c0e03 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -0,0 +1,304 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.tarantool.CommunicationException; +import org.tarantool.TarantoolConnection; + +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; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACES_MAX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; +import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; + +public class JdbcExceptionHandlingTest { + /** + * Simulates meta parsing error: missing "name" field in a space format for the primary key. + * + * @throws SQLException on failure. + */ + @Test + public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException { + TarantoolConnection tntCon = mock(TarantoolConnection.class); + SQLConnection conn = buildTestSQLConnection(tntCon, "", SQLDriver.defaults); + + Object[] spc = new Object[7]; + spc[FORMAT_IDX] = Collections.singletonList(new HashMap()); + spc[SPACE_ID_IDX] = 1000; + + doReturn(Collections.singletonList(Arrays.asList(spc))).when(tntCon) + .select(_VSPACE, 2, Collections.singletonList("TEST"), 0, 1, 0); + + Object[] idx = new Object[6]; + idx[INDEX_FORMAT_IDX] = Collections.singletonList( + new HashMap() {{ put("field", 0);}}); + + doReturn(Collections.singletonList(Arrays.asList(idx))).when(tntCon) + .select(_VINDEX, 0, Arrays.asList(1000, 0), 0, 1, 0); + + final DatabaseMetaData meta = conn.getMetaData(); + + Throwable t = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + meta.getPrimaryKeys(null, null, "TEST"); + } + }, "Error processing metadata for table \"TEST\"."); + + assertTrue(t.getCause().getMessage().contains("Wrong value type")); + } + + @Test + public void testStatementCommunicationException() throws SQLException { + checkStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(Statement statement) throws Throwable { + statement.executeQuery("TEST"); + } + }); + checkStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(Statement statement) throws Throwable { + statement.executeUpdate("TEST"); + } + }); + checkStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(Statement statement) throws Throwable { + statement.execute("TEST"); + } + }); + } + + @Test + public void testPreparedStatementCommunicationException() throws SQLException { + checkPreparedStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(PreparedStatement prep) throws Throwable { + prep.executeQuery(); + } + }); + checkPreparedStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(PreparedStatement prep) throws Throwable { + prep.executeUpdate(); + } + }); + checkPreparedStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(PreparedStatement prep) throws Throwable { + prep.execute(); + } + }); + } + + @Test + public void testDatabaseMetaDataCommunicationException() throws SQLException { + checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { + @Override + public void accept(DatabaseMetaData meta) throws Throwable { + meta.getTables(null, null, null, new String[] {"TABLE"}); + } + }, "Failed to retrieve table(s) description: tableNamePattern=\"null\"."); + + checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { + @Override + public void accept(DatabaseMetaData meta) throws Throwable { + meta.getColumns(null, null, "TEST", "ID"); + } + }, "Error processing table column metadata: tableNamePattern=\"TEST\"; columnNamePattern=\"ID\"."); + + checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { + @Override + public void accept(DatabaseMetaData meta) throws Throwable { + meta.getPrimaryKeys(null, null, "TEST"); + } + }, "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"); + + doThrow(ex).when(mockCon).sql("TEST", new Object[0]); + doThrow(ex).when(mockCon).update("TEST"); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + consumer.accept(stmt); + } + }); + assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); + + assertEquals(ex, e.getCause()); + + verify(mockCon, times(1)).close(); + } + + private void checkPreparedStatementCommunicationException(final ThrowingConsumer consumer) + throws SQLException { + TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); + + final PreparedStatement prep = new SQLPreparedStatement( + buildTestSQLConnection(mockCon, "tarantool://0:0", SQLDriver.defaults), "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 + public void execute() throws Throwable { + consumer.accept(prep); + } + }); + assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); + + assertEquals(ex, e.getCause()); + + verify(mockCon, 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); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + consumer.accept(meta); + } + }); + assertTrue(e.getMessage().startsWith(msg), e.getMessage()); + + assertEquals(ex, e.getCause().getCause()); + + verify(mockCon, times(1)).close(); + } + + private SQLConnection buildTestSQLConnection(final TarantoolConnection tntCon, 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; + } + }; + } + + class TestTarantoolConnection extends TarantoolConnection { + TestTarantoolConnection() throws IOException { + super(null, null, mock(Socket.class)); + } + @Override + protected void sql(String sql, Object[] bind) { + super.sql(sql, bind); + } + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java new file mode 100644 index 00000000..ad8fae70 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -0,0 +1,227 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.function.Executable; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class JdbcPreparedStatementIT extends AbstractJdbcIT { + private PreparedStatement prep; + + @AfterEach + public void tearDown() throws SQLException { + if (prep != null && !prep.isClosed()) + prep.close(); + } + + @Test + public void testExecuteQuery() throws SQLException { + prep = conn.prepareStatement("SELECT val FROM test WHERE id=?"); + assertNotNull(prep); + + prep.setInt(1, 1); + ResultSet rs = prep.executeQuery(); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("one", rs.getString(1)); + assertFalse(rs.next()); + rs.close(); + + // Reuse the prepared statement. + prep.setInt(1, 2); + rs = prep.executeQuery(); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("two", rs.getString(1)); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testExecuteUpdate() throws Exception { + prep = conn.prepareStatement("INSERT INTO test VALUES(?, ?)"); + assertNotNull(prep); + + prep.setInt(1, 100); + prep.setString(2, "hundred"); + int count = prep.executeUpdate(); + assertEquals(1, count); + + assertEquals("hundred", getRow("test", 100).get(1)); + + // Reuse the prepared statement. + prep.setInt(1, 1000); + prep.setString(2, "thousand"); + count = prep.executeUpdate(); + assertEquals(1, count); + + assertEquals("thousand", getRow("test", 1000).get(1)); + } + + @Test + public void testSetParameter() throws SQLException { + prep = conn.prepareStatement("INSERT INTO test_types VALUES (" + + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + assertNotNull(prep); + + prep.setInt(1, 1000);//INT + prep.setString(2, (String)testRow[1]);//CHAR + prep.setString(3, (String)testRow[2]);//VARCHAR + prep.setString(4, (String)testRow[3]);//LONGVARCHAR + prep.setBigDecimal(5, (BigDecimal)testRow[4]);//NUMERIC + prep.setBigDecimal(6, (BigDecimal)testRow[5]);//DECIMAL + prep.setBoolean(7, (Boolean)testRow[6]);//BIT + prep.setByte(8, (Byte)testRow[7]);//TINYINT + prep.setShort(9, (Short)testRow[8]);//SMALLINT + prep.setInt(10, (Integer)testRow[9]);//INTEGER + prep.setLong(11, (Long)testRow[10]);//BIGINT + prep.setFloat(12, (Float)testRow[11]);//REAL + prep.setDouble(13, (Double)testRow[12]);//FLOAT + prep.setBytes(14, (byte[])testRow[13]);//BINARY + prep.setBytes(15, (byte[])testRow[14]);//VARBINARY + prep.setBytes(16, (byte[])testRow[15]);//LONGVARBINARY + prep.setDate(17, (Date)testRow[16]);//DATE + prep.setTime(18, (Time)testRow[17]);//TIME + prep.setTimestamp(19, (Timestamp)testRow[18]);//TIMESTAMP + + int count = prep.executeUpdate(); + assertEquals(1, count); + + prep.close(); + + prep = conn.prepareStatement("SELECT * FROM test_types WHERE f1 = ?"); + prep.setInt(1, 1000); + + ResultSet rs = prep.executeQuery(); + assertNotNull(rs); + + assertTrue(rs.next()); + assertEquals(1000, rs.getInt(1));//INT + assertEquals(testRow[1], rs.getString(2));//CHAR + assertEquals(testRow[2], rs.getString(3));//VARCHAR + assertEquals(testRow[3], rs.getString(4)); //LONGVARCHAR + assertEquals(testRow[4], rs.getBigDecimal(5));//NUMERIC + assertEquals(testRow[5], rs.getBigDecimal(6));//DECIMAL + assertEquals(testRow[6], rs.getBoolean(7));//BIT + assertEquals(testRow[7], rs.getByte(8));//TINYINT + assertEquals(testRow[8], rs.getShort(9));//SMALLINT + assertEquals(testRow[9], rs.getInt(10));//INTEGER + assertEquals(testRow[10], rs.getLong(11));//BIGINT + assertEquals((Float)testRow[11], rs.getFloat(12), 1e-10f);//REAL + assertEquals((Double)testRow[12], rs.getDouble(13), 1e-10d);//FLOAT + //Issue#45 + //assertTrue(Arrays.equals((byte[])testRow[13], rs.getBytes(14)));//BINARY + //assertTrue(Arrays.equals((byte[])testRow[14], rs.getBytes(15)));//VARBINARY + //assertTrue(Arrays.equals((byte[])testRow[15], rs.getBytes(16)));//LONGVARBINARY + assertEquals(testRow[16], rs.getDate(17));//DATE + assertEquals(testRow[17], rs.getTime(18));//TIME + assertEquals(testRow[18], rs.getTimestamp(19));//TIMESTAMP + + rs.close(); + } + + @Test + public void testExecuteReturnsResultSet() throws SQLException { + prep = conn.prepareStatement("SELECT val FROM test WHERE id=?"); + assertNotNull(prep); + prep.setInt(1, 1); + + assertTrue(prep.execute()); + assertEquals(-1, prep.getUpdateCount()); + + ResultSet rs = prep.getResultSet(); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("one", rs.getString(1)); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testExecuteReturnsUpdateCount() throws Exception { + prep = conn.prepareStatement("INSERT INTO test VALUES(?, ?), (?, ?)"); + assertNotNull(prep); + + prep.setInt(1, 10); + prep.setString(2, "ten"); + prep.setInt(3, 20); + prep.setString(4, "twenty"); + + assertFalse(prep.execute()); + assertNull(prep.getResultSet()); + assertEquals(2, prep.getUpdateCount()); + + assertEquals("ten", getRow("test", 10).get(1)); + assertEquals("twenty", getRow("test", 20).get(1)); + } + + @Test void testForbiddenMethods() throws SQLException { + prep = conn.prepareStatement("TEST"); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: prep.executeQuery("TEST"); + break; + case 1: prep.executeUpdate("TEST"); + break; + case 2: prep.execute("TEST"); + break; + default: + fail(); + } + } + }); + assertEquals("The method cannot be called on a PreparedStatement.", e.getMessage()); + } + assertEquals(3, i); + } + + @Test + public void testClosedConnection() throws SQLException { + prep = conn.prepareStatement("TEST"); + + conn.close(); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: prep.executeQuery(); + break; + case 1: prep.executeUpdate(); + break; + case 2: prep.execute(); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getMessage()); + } + assertEquals(3, i); + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java new file mode 100644 index 00000000..4709fbae --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java @@ -0,0 +1,114 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JdbcResultSetIT extends AbstractJdbcIT { + private Statement stmt; + + @BeforeEach + public void setUp() throws Exception { + stmt = conn.createStatement(); + } + + @AfterEach + public void tearDown() throws Exception { + if (stmt != null && !stmt.isClosed()) + stmt.close(); + } + + @Test + public void testEmpty() throws SQLException { + ResultSet rs = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(rs); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testIteration() throws SQLException { + ResultSet rs = stmt.executeQuery("SELECT * FROM test WHERE id IN (1,2,3) ORDER BY id"); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals(1, rs.getInt(1)); + assertTrue(rs.next()); + assertEquals(2, rs.getInt(1)); + assertTrue(rs.next()); + assertEquals(3, rs.getInt(1)); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testGetColumnByIdx() throws SQLException { + ResultSet rs = stmt.executeQuery("SELECT * FROM test_types"); + assertNotNull(rs); + assertTrue(rs.next()); + + assertEquals(testRow[0], rs.getInt(1));//INT + assertEquals(testRow[1], rs.getString(2));//CHAR + assertEquals(testRow[2], rs.getString(3));//VARCHAR + assertEquals(testRow[3], rs.getString(4)); //LONGVARCHAR + assertEquals(testRow[4], rs.getBigDecimal(5));// NUMERIC + assertEquals(testRow[5], rs.getBigDecimal(6));// DECIMAL + assertEquals(testRow[6], rs.getBoolean(7));//BIT + assertEquals(testRow[7], rs.getByte(8));//TINYINT + assertEquals(testRow[8], rs.getShort(9));//SMALLINT + assertEquals(testRow[9], rs.getInt(10));//INTEGER + assertEquals(testRow[10], rs.getLong(11));//BIGINT + assertEquals((Float)testRow[11], rs.getFloat(12), 1e-10f);//REAL + assertEquals((Double)testRow[12], rs.getDouble(13), 1e-10d);//FLOAT + assertTrue(Arrays.equals((byte[])testRow[13], rs.getBytes(14)));//BINARY + assertTrue(Arrays.equals((byte[])testRow[14], rs.getBytes(15)));//VARBINARY + assertTrue(Arrays.equals((byte[])testRow[15], rs.getBytes(16)));//LONGVARBINARY + + //Issue#44 + //assertEquals(testRow[16], rs.getDate(17));//DATE + //assertEquals(testRow[17], rs.getTime(18));//TIME + assertEquals(testRow[18], rs.getTimestamp(19)); //TIMESTAMP + + rs.close(); + } + + @Test + public void testGetColumnByName() throws SQLException { + ResultSet rs = stmt.executeQuery("SELECT * FROM test_types"); + assertNotNull(rs); + assertTrue(rs.next()); + + assertEquals(testRow[0], rs.getInt("F1"));//INT + assertEquals(testRow[1], rs.getString("F2"));//CHAR + assertEquals(testRow[2], rs.getString("F3"));//VARCHAR + assertEquals(testRow[3], rs.getString("F4")); //LONGVARCHAR + assertEquals(testRow[4], rs.getBigDecimal("F5"));// NUMERIC + assertEquals(testRow[5], rs.getBigDecimal("F6"));// DECIMAL + assertEquals(testRow[6], rs.getBoolean("F7"));//BIT + assertEquals(testRow[7], rs.getByte("F8"));//TINYINT + assertEquals(testRow[8], rs.getShort("F9"));//SMALLINT + assertEquals(testRow[9], rs.getInt("F10"));//INTEGER + assertEquals(testRow[10], rs.getLong("F11"));//BIGINT + assertEquals((Float)testRow[11], rs.getFloat("F12"), 1e-10f);//REAL + assertEquals((Double)testRow[12], rs.getDouble("F13"), 1e-10d);//FLOAT + assertTrue(Arrays.equals((byte[])testRow[13], rs.getBytes("F14")));//BINARY + assertTrue(Arrays.equals((byte[])testRow[14], rs.getBytes("F15")));//VARBINARY + assertTrue(Arrays.equals((byte[])testRow[15], rs.getBytes("F16")));//LONGVARBINARY + + //Issue#44 + //assertEquals(testRow[16], rs.getDate("F17"));//DATE + //assertEquals(testRow[17], rs.getTime("F18"));//TIME + assertEquals(testRow[18], rs.getTimestamp("F19")); //TIMESTAMP + + rs.close(); + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java new file mode 100644 index 00000000..f90d5b15 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java @@ -0,0 +1,33 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JdbcResultSetMetaDataIT extends AbstractJdbcIT { + @Test + public void testColumnNames() throws SQLException { + Statement stmt = conn.createStatement(); + assertNotNull(stmt); + ResultSet rs = stmt.executeQuery("SELECT * FROM test_types"); + assertNotNull(rs); + assertTrue(rs.next()); + + ResultSetMetaData rsMeta = rs.getMetaData(); + + assertEquals(19, rsMeta.getColumnCount()); + + for (int i = 1; i <= 19; i++) + assertEquals("F" + i, rsMeta.getColumnName(i)); + + rs.close(); + stmt.close(); + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java new file mode 100644 index 00000000..735f326d --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -0,0 +1,96 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class JdbcStatementIT extends AbstractJdbcIT { + private Statement stmt; + + @BeforeEach + public void setUp() throws SQLException { + stmt = conn.createStatement(); + } + + @AfterEach + public void tearDown() throws SQLException { + if (stmt != null && !stmt.isClosed()) + stmt.close(); + } + + @Test + public void testExecuteQuery() throws SQLException { + ResultSet rs = stmt.executeQuery("SELECT val FROM test WHERE id=1"); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("one", rs.getString(1)); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testExecuteUpdate() throws Exception { + assertEquals(2, stmt.executeUpdate("INSERT INTO test(id, val) VALUES (10, 'ten'), (20, 'twenty')")); + assertEquals("ten", getRow("test", 10).get(1)); + assertEquals("twenty", getRow("test", 20).get(1)); + } + + @Test + public void testExecuteReturnsResultSet() throws SQLException { + assertTrue(stmt.execute("SELECT val FROM test WHERE id=1")); + ResultSet rs = stmt.getResultSet(); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("one", rs.getString(1)); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testExecuteReturnsUpdateCount() throws Exception { + assertFalse(stmt.execute("INSERT INTO test(id, val) VALUES (100, 'hundred'), (1000, 'thousand')")); + assertEquals(2, stmt.getUpdateCount()); + + assertEquals("hundred", getRow("test", 100).get(1)); + assertEquals("thousand", getRow("test", 1000).get(1)); + } + + @Test + public void testClosedConnection() throws Exception { + conn.close(); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: stmt.executeQuery("TEST"); + break; + case 1: stmt.executeUpdate("TEST"); + break; + case 2: stmt.execute("TEST"); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getMessage()); + } + assertEquals(3, i); + } +} \ No newline at end of file diff --git a/src/test/travis.pre.sh b/src/test/travis.pre.sh index cdc7ca3e..f70bc9e1 100755 --- a/src/test/travis.pre.sh +++ b/src/test/travis.pre.sh @@ -2,13 +2,14 @@ set -e -curl http://download.tarantool.org/tarantool/1.9/gpgkey | sudo apt-key add - +# We need tarantool 2.0 for jdbc/sql. +curl http://download.tarantool.org/tarantool/2.0/gpgkey | sudo apt-key add - release=`lsb_release -c -s` sudo rm -f /etc/apt/sources.list.d/*tarantool*.list -sudo tee /etc/apt/sources.list.d/tarantool_1.9.list <<- EOF -deb http://download.tarantool.org/tarantool/1.9/ubuntu/ $release main -deb-src http://download.tarantool.org/tarantool/1.9/ubuntu/ $release main +sudo tee /etc/apt/sources.list.d/tarantool_2.0.list <<- EOF +deb http://download.tarantool.org/tarantool/2.0/ubuntu/ $release main +deb-src http://download.tarantool.org/tarantool/2.0/ubuntu/ $release main EOF sudo apt-get update