From 292671ee8424567a961a853324888de0a313b2a6 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Wed, 12 Dec 2018 23:51:58 +0300 Subject: [PATCH 01/11] test: cleanup the test directory once Cleanup the directory once before all tests: move it from the TarantoolControl constructor to the static block. --- .../java/org/tarantool/TarantoolControl.java | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/test/java/org/tarantool/TarantoolControl.java b/src/test/java/org/tarantool/TarantoolControl.java index e13b89e7..1800a436 100644 --- a/src/test/java/org/tarantool/TarantoolControl.java +++ b/src/test/java/org/tarantool/TarantoolControl.java @@ -22,8 +22,30 @@ public class TarantoolControl { 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 +53,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 +101,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. * From a5885577daa2945a7a28698640ca70b5d9d88145 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Wed, 12 Dec 2018 23:59:49 +0300 Subject: [PATCH 02/11] test: wait in startTarantool() / stopTarantool() It fixes race conditions in testLongParallelCloseReconnects: an attempt to start jdk-testing instance against already started one could be performed because stopTarantool() did not wait until an instance will really stop. Use these functions in setupEnv() / cleanupEnv() to avoid possible race conditions. --- .../AbstractTarantoolConnectorIT.java | 10 ++- .../java/org/tarantool/TarantoolControl.java | 89 ++++++++++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) 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 1800a436..3b8f9261 100644 --- a/src/test/java/org/tarantool/TarantoolControl.java +++ b/src/test/java/org/tarantool/TarantoolControl.java @@ -16,6 +16,20 @@ * 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(); @@ -163,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); } @@ -175,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); + } + } } From 1794715c6236ae4b9537d522a9945ce2bc0d776a Mon Sep 17 00:00:00 2001 From: "dmitriy.grytsovets" Date: Wed, 19 Jul 2017 15:06:32 +0300 Subject: [PATCH 03/11] Simple JDBC implementation to work with Spring JDBC template --- README.md | 34 +- src/it/java/org/tarantool/TestSql.java | 61 + .../org/tarantool/TestTarantoolClient.java | 2 +- .../org/tarantool/AbstractTarantoolOps.java | 2 +- src/main/java/org/tarantool/Code.java | 2 +- src/main/java/org/tarantool/FutureImpl.java | 7 +- src/main/java/org/tarantool/JDBCBridge.java | 73 ++ src/main/java/org/tarantool/Key.java | 10 +- .../java/org/tarantool/TarantoolBase.java | 57 + .../java/org/tarantool/TarantoolClient.java | 5 + .../org/tarantool/TarantoolClientImpl.java | 78 +- .../org/tarantool/TarantoolConnection.java | 24 +- .../java/org/tarantool/TarantoolSQLOps.java | 7 + .../java/org/tarantool/jdbc/SQLDriver.java | 175 +++ .../org/tarantool/jdbc/SQLMsgPackLite.java | 30 + .../tarantool/jdbc/SQLPreparedStatement.java | 345 ++++++ .../java/org/tarantool/jdbc/SQLResultSet.java | 1050 +++++++++++++++++ .../tarantool/jdbc/SQLResultSetMetaData.java | 137 +++ .../org/tarantool/jdbc/SQLSocketProvider.java | 10 + .../java/org/tarantool/jdbc/SQLStatement.java | 253 ++++ .../org/tarantool/jdbc/SqlConnection.java | 312 +++++ .../META-INF/services/java.sql.Driver | 1 + 22 files changed, 2650 insertions(+), 25 deletions(-) create mode 100644 src/it/java/org/tarantool/TestSql.java create mode 100644 src/main/java/org/tarantool/JDBCBridge.java create mode 100644 src/main/java/org/tarantool/TarantoolSQLOps.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLDriver.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLResultSet.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLSocketProvider.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLStatement.java create mode 100644 src/main/java/org/tarantool/jdbc/SqlConnection.java create mode 100644 src/main/resources/META-INF/services/java.sql.Driver diff --git a/README.md b/README.md index f1ea60f3..6c66cd31 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,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?username=test&password=test&socketProvider=abc.xyz.MySocketProvider + +```java +NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(new DriverManagerDataSource("tarantool://localhost:3301?username=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 +161,4 @@ base for possible answers and solutions. To run tests ``` ./mvnw clean test -``` \ No newline at end of file +``` 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..37e2ab37 --- /dev/null +++ b/src/main/java/org/tarantool/JDBCBridge.java @@ -0,0 +1,73 @@ +package org.tarantool; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +public class JDBCBridge { + 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); + } + } + + 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 Object execute(TarantoolConnection connection, String sql, Object ... params) { + connection.sql(sql, params); + Long rowCount = connection.getSqlRowCount(); + if(rowCount == null) { + return new JDBCBridge(connection); + } + return rowCount.intValue(); + } + + + public String getColumnName(int columnIndex) { + return sqlMetadata.get(columnIndex).getName(); + } + + public Integer getColumnIndex(String columnName) { + return columnsByName.get(columnName); + } + + public int getColumnCount() { + return sqlMetadata.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..35a91cd6 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(0x29), + SQL_METADATA(0x32), + SQL_TEXT(0x40), + SQL_BIND(0x41), + SQL_OPTIONS(0x42), + SQL_INFO(0x43), + SQL_ROW_COUNT(0x44); int id; diff --git a/src/main/java/org/tarantool/TarantoolBase.java b/src/main/java/org/tarantool/TarantoolBase.java index eb3acd62..bddfae2f 100644 --- a/src/main/java/org/tarantool/TarantoolBase.java +++ b/src/main/java/org/tarantool/TarantoolBase.java @@ -11,6 +11,7 @@ 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; @@ -136,6 +137,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)); } 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..be94995f 100644 --- a/src/main/java/org/tarantool/TarantoolConnection.java +++ b/src/main/java/org/tarantool/TarantoolConnection.java @@ -6,8 +6,9 @@ import java.net.Socket; 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 +22,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 +61,23 @@ 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(); + } } 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/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java new file mode 100644 index 00000000..2eafb86f --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -0,0 +1,175 @@ +package org.tarantool.jdbc; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +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; + +import org.tarantool.TarantoolConnection; + +@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_USERNAME = "username"; + public static final String PROP_PASSWORD = "password"; + + + protected Map providerCache = new ConcurrentHashMap(); + + @Override + public Connection connect(String url, Properties info) throws SQLException { + URI uri = URI.create(url); + Properties urlProperties = parseQueryString(uri, info); + String providerClassName = urlProperties.getProperty(PROP_SOCKET_PROVIDER); + Socket socket; + if (providerClassName != null) { + socket = getSocketFromProvider(uri, urlProperties, providerClassName); + } else { + socket = createAndConnectDefaultSocket(urlProperties); + } + try { + TarantoolConnection connection = new TarantoolConnection(urlProperties.getProperty(PROP_USERNAME), urlProperties.getProperty(PROP_PASSWORD), socket) {{ + msgPackLite = SQLMsgPackLite.INSTANCE; + }}; + + return new SqlConnection(connection); + } catch (IOException e) { + throw new SQLException("Couldn't initiate connection. Provider class name is " + providerClassName, e); + } + + } + + protected Properties parseQueryString(URI uri, Properties info) { + Properties urlProperties = new Properties(info); + 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, ""); + } + } + } + urlProperties.put(PROP_HOST, uri.getHost()); + urlProperties.put(PROP_PORT, uri.getPort()); + return urlProperties; + } + + protected Socket createAndConnectDefaultSocket(Properties properties) throws SQLException { + Socket socket; + socket = new Socket(); + try { + socket.connect(new InetSocketAddress(properties.getProperty(PROP_HOST,"localhost"), Integer.parseInt(properties.getProperty(PROP_PORT, "3301")))); + } catch (Exception e) { + throw new SQLException("Couldn't connect to tarantool using" + properties, e); + } + return socket; + } + + protected Socket getSocketFromProvider(URI uri, Properties urlProperties, String providerClassName) + throws SQLException { + Socket socket; + SQLSocketProvider sqlSocketProvider = providerCache.get(providerClassName); + if (sqlSocketProvider == null) { + synchronized (this) { + sqlSocketProvider = providerCache.get(providerClassName); + if (sqlSocketProvider == null) { + try { + Class cls = Class.forName(providerClassName); + if (SQLSocketProvider.class.isAssignableFrom(cls)) { + sqlSocketProvider = (SQLSocketProvider) cls.newInstance(); + providerCache.put(providerClassName, sqlSocketProvider); + } + } catch (Exception e) { + throw new SQLException("Couldn't initiate socket provider " + providerClassName, e); + } + } + } + } + socket = sqlSocketProvider.getConnectedSocket(uri, urlProperties); + return socket; + } + + @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 propertis = new Properties(info); + Properties properties = parseQueryString(uri, propertis); + DriverPropertyInfo host = new DriverPropertyInfo(PROP_HOST, properties.getProperty(PROP_HOST)); + host.required = true; + host.description = "Tarantool sever host"; + + DriverPropertyInfo port = new DriverPropertyInfo(PROP_PORT, properties.getProperty(PROP_PORT)); + port.required = true; + port.description = "Tarantool sever port"; + + + 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 username = new DriverPropertyInfo(PROP_USERNAME, properties.getProperty(PROP_USERNAME)); + username.required = false; + username.description = "username"; + + DriverPropertyInfo password = new DriverPropertyInfo(PROP_PASSWORD, properties.getProperty(PROP_PASSWORD)); + password.required = false; + password.description = "password"; + + return new DriverPropertyInfo[]{host, port, socketProvider, username, password}; + } 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(); + } + + +} 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..0f73efb8 --- /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); + } if(item instanceof Time) { + super.pack(((Time)item).getTime(), os); + } if(item instanceof Timestamp) { + super.pack(((Timestamp)item).getTime(), os); + } 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..9c3dbb45 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -0,0 +1,345 @@ +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; + +import org.tarantool.JDBCBridge; +import org.tarantool.TarantoolConnection; + +public class SQLPreparedStatement extends SQLStatement implements PreparedStatement { + final String sql; + final Map params; + + + public SQLPreparedStatement(TarantoolConnection connection, SqlConnection sqlConnection, String sql) { + super(connection, sqlConnection); + this.sql = sql; + this.params = new HashMap(); + } + + @Override + public ResultSet executeQuery() throws SQLException { + return new SQLResultSet(JDBCBridge.query(connection, 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 not"); + } + } + return objects; + } + + @Override + public int executeUpdate() throws SQLException { + return JDBCBridge.update(connection, 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 { + return false; + } + + @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(); + } + +} 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..2630546d --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -0,0 +1,1050 @@ +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 JDBCBridgeExecutor; + final SQLResultSetMetaData metaData; + + List row = null; + + + public SQLResultSet(JDBCBridge JDBCBridgeExecutor) { + this.JDBCBridgeExecutor = JDBCBridgeExecutor; + iterator = JDBCBridgeExecutor.iterator(); + metaData = new SQLResultSetMetaData(JDBCBridgeExecutor); + } + + @Override + public boolean next() throws SQLException { + if (iterator.hasNext()) { + 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 { + return (String) getRaw(columnIndex); + } + + protected Object getRaw(int columnIndex) { + return row.get(columnIndex - 1); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return Boolean.TRUE.equals(getRaw(columnIndex)); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return ((Number) getRaw(columnIndex)).byteValue(); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return ((Number) getRaw(columnIndex)).shortValue(); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return ((Number) getRaw(columnIndex)).intValue(); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return ((Number) getRaw(columnIndex)).longValue(); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return ((Number) getRaw(columnIndex)).floatValue(); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + return ((Number) getRaw(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(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getBoolean(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return getByte(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return getShort(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return getInt(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return getLong(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return getFloat(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return getDouble(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return getBytes(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return getDate(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getTimestamp(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return getAsciiStream(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return getUnicodeStream(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return getBinaryStream(JDBCBridgeExecutor.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(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return JDBCBridgeExecutor.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; + } + + @Override + public boolean isAfterLast() throws SQLException { + return iterator.nextIndex() == JDBCBridgeExecutor.size() && row == null; + } + + @Override + public boolean isFirst() throws SQLException { + return iterator.previousIndex() == 0; + } + + @Override + public boolean isLast() throws SQLException { + return iterator.nextIndex() == JDBCBridgeExecutor.size(); + } + + @Override + public void beforeFirst() throws SQLException { + row = null; + iterator = JDBCBridgeExecutor.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.nextIndex() < JDBCBridgeExecutor.size()) { + 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(JDBCBridgeExecutor.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..f883482f --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -0,0 +1,137 @@ +package org.tarantool.jdbc; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; + +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 { + throw new SQLFeatureNotSupportedException(); + } + + @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 { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getColumnType(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getColumnTypeName(int column) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @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{" + + "JDBCBridgeExecutor=" + 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..4497494b --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -0,0 +1,253 @@ +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; + +import org.tarantool.JDBCBridge; +import org.tarantool.TarantoolConnection; + +@SuppressWarnings("Since15") +public class SQLStatement implements Statement { + protected final TarantoolConnection connection; + protected final SqlConnection sqlConnection; + private SQLResultSet resultSet; + private int updateCount; + + protected SQLStatement(TarantoolConnection connection, SqlConnection sqlConnection) { + this.connection = connection; + this.sqlConnection = sqlConnection; + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + return (resultSet = new SQLResultSet(JDBCBridge.query(connection, sql))); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + return JDBCBridge.update(connection, 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 { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setMaxRows(int max) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @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 { + throw new SQLFeatureNotSupportedException(); + } + + @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 { + Object result = JDBCBridge.execute(connection, sql); + if (result instanceof SQLResultSet) { + resultSet = (SQLResultSet) result; + return true; + } else { + updateCount = (Integer) result; + return false; + } + } + + @Override + public ResultSet getResultSet() throws SQLException { + return resultSet; + } + + @Override + public int getUpdateCount() throws SQLException { + return updateCount; + } + + @Override + public boolean getMoreResults() throws SQLException { + return resultSet != null; + } + + @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 { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getFetchSize() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @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 sqlConnection; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @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 { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isCloseOnCompletion() 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(); + } +} 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..7f0c7866 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SqlConnection.java @@ -0,0 +1,312 @@ +package org.tarantool.jdbc; + +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.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.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import org.tarantool.TarantoolConnection; + +@SuppressWarnings("Since15") +public class SqlConnection implements Connection { + final TarantoolConnection connection; + + public SqlConnection(TarantoolConnection connection) { + this.connection = connection; + } + + @Override + public Statement createStatement() throws SQLException { + return new SQLStatement(connection, this); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return new SQLPreparedStatement(connection, 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 { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isReadOnly() throws SQLException { + return false; + } + + @Override + public void setCatalog(String catalog) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getCatalog() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @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 { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public String getSchema() throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void abort(Executor executor) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public int getNetworkTimeout() 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(); + } +} 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 From 97e85606f93dbea4d5253e5b19823d86ee4f8b69 Mon Sep 17 00:00:00 2001 From: "dmitriy.grytsovets" Date: Wed, 19 Jul 2017 15:21:10 +0300 Subject: [PATCH 04/11] Fix ResultSet iteration --- src/main/java/org/tarantool/jdbc/SQLResultSet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index 2630546d..bd4e50d4 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -288,7 +288,7 @@ public BigDecimal getBigDecimal(String columnLabel) throws SQLException { @Override public boolean isBeforeFirst() throws SQLException { - return row == null; + return row == null && iterator.previousIndex() == -1; } @Override @@ -326,7 +326,7 @@ public boolean first() throws SQLException { @Override public boolean last() throws SQLException { - while (iterator.nextIndex() < JDBCBridgeExecutor.size()) { + while (iterator.hasNext()) { next(); } return row != null; From 669b8e35c5f0de3bc0e4a58c83d889b72f241e49 Mon Sep 17 00:00:00 2001 From: "dmitriy.grytsovets" Date: Thu, 20 Jul 2017 19:10:04 +0300 Subject: [PATCH 05/11] JDBC Integration with idea database tool --- README.md | 4 +- src/main/java/org/tarantool/JDBCBridge.java | 22 +- .../java/org/tarantool/TarantoolBase.java | 9 +- ...{SqlConnection.java => SQLConnection.java} | 18 +- .../tarantool/jdbc/SQLDatabaseMetadata.java | 1029 +++++++++++++++++ .../java/org/tarantool/jdbc/SQLDriver.java | 36 +- .../tarantool/jdbc/SQLPreparedStatement.java | 2 +- .../java/org/tarantool/jdbc/SQLResultSet.java | 82 +- .../tarantool/jdbc/SQLResultSetMetaData.java | 11 +- .../java/org/tarantool/jdbc/SQLStatement.java | 48 +- 10 files changed, 1172 insertions(+), 89 deletions(-) rename src/main/java/org/tarantool/jdbc/{SqlConnection.java => SQLConnection.java} (95%) create mode 100644 src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java diff --git a/README.md b/README.md index 6c66cd31..fa82eff9 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,10 @@ 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?username=test&password=test&socketProvider=abc.xyz.MySocketProvider +For example tarantool://localhost:3301?user=test&password=test&socketProvider=abc.xyz.MySocketProvider ```java -NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(new DriverManagerDataSource("tarantool://localhost:3301?username=test&password=test")); +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 { diff --git a/src/main/java/org/tarantool/JDBCBridge.java b/src/main/java/org/tarantool/JDBCBridge.java index 37e2ab37..b31af64d 100644 --- a/src/main/java/org/tarantool/JDBCBridge.java +++ b/src/main/java/org/tarantool/JDBCBridge.java @@ -1,11 +1,17 @@ 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; @@ -19,7 +25,7 @@ protected JDBCBridge(List sqlMetadata, List((int) Math.ceil(sqlMetadata.size() / 0.75), 0.75f); for (int i = 0; i < sqlMetadata.size(); i++) { - columnsByName.put(sqlMetadata.get(i).getName(), i); + columnsByName.put(sqlMetadata.get(i).getName(), i + 1); } } @@ -32,18 +38,26 @@ public static int update(TarantoolConnection connection, String sql, Object ... 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 JDBCBridge(connection); + return new SQLResultSet(new JDBCBridge(connection)); } return rowCount.intValue(); } public String getColumnName(int columnIndex) { - return sqlMetadata.get(columnIndex).getName(); + return columnIndex > sqlMetadata.size() ? null : sqlMetadata.get(columnIndex - 1).getName(); } public Integer getColumnIndex(String columnName) { @@ -51,7 +65,7 @@ public Integer getColumnIndex(String columnName) { } public int getColumnCount() { - return sqlMetadata.size(); + return columnsByName.size(); } public ListIterator> iterator() { diff --git a/src/main/java/org/tarantool/TarantoolBase.java b/src/main/java/org/tarantool/TarantoolBase.java index bddfae2f..3fb1ce40 100644 --- a/src/main/java/org/tarantool/TarantoolBase.java +++ b/src/main/java/org/tarantool/TarantoolBase.java @@ -17,6 +17,8 @@ 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 */ @@ -42,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) { @@ -230,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/jdbc/SqlConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java similarity index 95% rename from src/main/java/org/tarantool/jdbc/SqlConnection.java rename to src/main/java/org/tarantool/jdbc/SQLConnection.java index 7f0c7866..de28850d 100644 --- a/src/main/java/org/tarantool/jdbc/SqlConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -23,11 +23,15 @@ import org.tarantool.TarantoolConnection; @SuppressWarnings("Since15") -public class SqlConnection implements Connection { +public class SQLConnection implements Connection { final TarantoolConnection connection; + final String url; + final Properties properties; - public SqlConnection(TarantoolConnection connection) { + public SQLConnection(TarantoolConnection connection, String url, Properties properties) { this.connection = connection; + this.url = url; + this.properties = properties; } @Override @@ -85,12 +89,12 @@ public boolean isClosed() throws SQLException { @Override public DatabaseMetaData getMetaData() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return new SQLDatabaseMetadata(this); } @Override public void setReadOnly(boolean readOnly) throws SQLException { - throw new SQLFeatureNotSupportedException(); + } @Override @@ -100,12 +104,11 @@ public boolean isReadOnly() throws SQLException { @Override public void setCatalog(String catalog) throws SQLException { - throw new SQLFeatureNotSupportedException(); } @Override public String getCatalog() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return null; } @Override @@ -276,12 +279,11 @@ public Struct createStruct(String typeName, Object[] attributes) throws SQLExcep @Override public void setSchema(String schema) throws SQLException { - throw new SQLFeatureNotSupportedException(); } @Override public String getSchema() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return null; } @Override 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..e43bcdc8 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -0,0 +1,1029 @@ +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.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.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 { + if (types != null && !Arrays.asList(types).contains("TABLE")) { + return new SQLResultSet(JDBCBridge.EMPTY); + } + String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + List> spaces = (List>) connection.connection.select(_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)); + } + + @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 { + String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); + List> spaces = (List>) connection.connection.select(_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))); + } + + @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 { + List> spaces = (List>) connection.connection.select(_VSPACE, 2, Arrays.asList(table), 0, 1, 0); + + List> rows = new ArrayList>(); + + if (spaces != null && spaces.size() > 0) { + List space = spaces.get(0); + List> fields = (List>) space.get(FORMAT_IDX); + List> indexes = (List>) connection.connection.select(_VINDEX, 0, Arrays.asList(space.get(SPACE_ID_IDX), 0), 0, 1, 0); + List primaryKey = indexes.get(0); + List> parts = (List>) primaryKey.get(INDEX_FORMAT_IDX); + for (int i = 0; i < parts.size(); i++) { + List part = parts.get(i); + int ordinal = ((Number) part.get(0)).intValue(); + String column = (String) fields.get(ordinal).get("name"); + rows.add(Arrays.asList(table, column, i + 1, "primary", primaryKey.get(NAME_IDX))); + } + } + return new SQLNullResultSet((JDBCBridge.mock( + Arrays.asList("TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME", + //nulls + "TABLE_CAT", "TABLE_SCHEM" + ), + rows))); + } + + @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(); + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java index 2eafb86f..68679979 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDriver.java +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -6,7 +6,6 @@ import java.net.URI; import java.sql.Connection; import java.sql.Driver; -import java.sql.DriverManager; import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -31,7 +30,7 @@ public class SQLDriver implements Driver { public static final String PROP_HOST = "host"; public static final String PROP_PORT = "port"; public static final String PROP_SOCKET_PROVIDER = "socketProvider"; - public static final String PROP_USERNAME = "username"; + public static final String PROP_USER = "user"; public static final String PROP_PASSWORD = "password"; @@ -49,11 +48,11 @@ public Connection connect(String url, Properties info) throws SQLException { socket = createAndConnectDefaultSocket(urlProperties); } try { - TarantoolConnection connection = new TarantoolConnection(urlProperties.getProperty(PROP_USERNAME), urlProperties.getProperty(PROP_PASSWORD), socket) {{ - msgPackLite = SQLMsgPackLite.INSTANCE; + TarantoolConnection connection = new TarantoolConnection(urlProperties.getProperty(PROP_USER), urlProperties.getProperty(PROP_PASSWORD), socket) {{ + msgPackLite = SQLMsgPackLite.INSTANCE; }}; - return new SqlConnection(connection); + return new SQLConnection(connection, url, info); } catch (IOException e) { throw new SQLException("Couldn't initiate connection. Provider class name is " + providerClassName, e); } @@ -73,8 +72,8 @@ protected Properties parseQueryString(URI uri, Properties info) { } } } - urlProperties.put(PROP_HOST, uri.getHost()); - urlProperties.put(PROP_PORT, uri.getPort()); + urlProperties.put(PROP_HOST, uri.getHost() == null ? "localhost" : uri.getHost()); + urlProperties.put(PROP_PORT, uri.getPort() < 1 ? "3301" : uri.getPort()); return urlProperties; } @@ -82,7 +81,7 @@ protected Socket createAndConnectDefaultSocket(Properties properties) throws SQL Socket socket; socket = new Socket(); try { - socket.connect(new InetSocketAddress(properties.getProperty(PROP_HOST,"localhost"), Integer.parseInt(properties.getProperty(PROP_PORT, "3301")))); + socket.connect(new InetSocketAddress(properties.getProperty(PROP_HOST, "localhost"), Integer.parseInt(properties.getProperty(PROP_PORT, "3301")))); } catch (Exception e) { throw new SQLException("Couldn't connect to tarantool using" + properties, e); } @@ -122,8 +121,8 @@ public boolean acceptsURL(String url) throws SQLException { public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { try { URI uri = new URI(url); - Properties propertis = new Properties(info); - Properties properties = parseQueryString(uri, propertis); + Properties properties = parseQueryString(uri, new Properties(info == null ? new Properties() : info)); + DriverPropertyInfo host = new DriverPropertyInfo(PROP_HOST, properties.getProperty(PROP_HOST)); host.required = true; host.description = "Tarantool sever host"; @@ -133,19 +132,20 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws port.description = "Tarantool sever port"; - 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 username = new DriverPropertyInfo(PROP_USERNAME, properties.getProperty(PROP_USERNAME)); - username.required = false; - username.description = "username"; + 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"; - return new DriverPropertyInfo[]{host, port, socketProvider, username, 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"; + + + return new DriverPropertyInfo[]{host, port, user, password, socketProvider}; } catch (Exception e) { throw new SQLException(e); } diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index 9c3dbb45..23d10734 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -32,7 +32,7 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem final Map params; - public SQLPreparedStatement(TarantoolConnection connection, SqlConnection sqlConnection, String sql) { + public SQLPreparedStatement(TarantoolConnection connection, SQLConnection sqlConnection, String sql) { super(connection, sqlConnection); this.sql = sql; this.params = new HashMap(); diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index bd4e50d4..249ab128 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -34,21 +34,22 @@ @SuppressWarnings("Since15") public class SQLResultSet implements ResultSet { ListIterator> iterator; - final JDBCBridge JDBCBridgeExecutor; + final JDBCBridge bridge; final SQLResultSetMetaData metaData; + int maxRows; List row = null; - public SQLResultSet(JDBCBridge JDBCBridgeExecutor) { - this.JDBCBridgeExecutor = JDBCBridgeExecutor; - iterator = JDBCBridgeExecutor.iterator(); - metaData = new SQLResultSetMetaData(JDBCBridgeExecutor); + public SQLResultSet(JDBCBridge bridge) { + this.bridge = bridge; + iterator = bridge.iterator(); + metaData = new SQLResultSetMetaData(bridge); } @Override public boolean next() throws SQLException { - if (iterator.hasNext()) { + if (iterator.hasNext() && (maxRows == 0 || iterator.nextIndex() < maxRows)) { row = iterator.next(); return true; } @@ -68,13 +69,19 @@ public boolean wasNull() throws SQLException { @Override public String getString(int columnIndex) throws SQLException { - return (String) getRaw(columnIndex); + 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)); @@ -82,32 +89,37 @@ public boolean getBoolean(int columnIndex) throws SQLException { @Override public byte getByte(int columnIndex) throws SQLException { - return ((Number) getRaw(columnIndex)).byteValue(); + return (getNumber(columnIndex)).byteValue(); } @Override public short getShort(int columnIndex) throws SQLException { - return ((Number) getRaw(columnIndex)).shortValue(); + return (getNumber(columnIndex)).shortValue(); } @Override public int getInt(int columnIndex) throws SQLException { - return ((Number) getRaw(columnIndex)).intValue(); + 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 ((Number) getRaw(columnIndex)).longValue(); + return (getNumber(columnIndex)).longValue(); } @Override public float getFloat(int columnIndex) throws SQLException { - return ((Number) getRaw(columnIndex)).floatValue(); + return (getNumber(columnIndex)).floatValue(); } @Override public double getDouble(int columnIndex) throws SQLException { - return ((Number) getRaw(columnIndex)).doubleValue(); + return (getNumber(columnIndex)).doubleValue(); } @Override @@ -153,82 +165,82 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException { @Override public String getString(String columnLabel) throws SQLException { - return getString(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getString(getColumnIndex(columnLabel)); } @Override public boolean getBoolean(String columnLabel) throws SQLException { - return getBoolean(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getBoolean(getColumnIndex(columnLabel)); } @Override public byte getByte(String columnLabel) throws SQLException { - return getByte(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getByte(getColumnIndex(columnLabel)); } @Override public short getShort(String columnLabel) throws SQLException { - return getShort(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getShort(getColumnIndex(columnLabel)); } @Override public int getInt(String columnLabel) throws SQLException { - return getInt(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getInt(getColumnIndex(columnLabel)); } @Override public long getLong(String columnLabel) throws SQLException { - return getLong(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getLong(getColumnIndex(columnLabel)); } @Override public float getFloat(String columnLabel) throws SQLException { - return getFloat(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getFloat(getColumnIndex(columnLabel)); } @Override public double getDouble(String columnLabel) throws SQLException { - return getDouble(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getDouble(getColumnIndex(columnLabel)); } @Override public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { - return getBigDecimal(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getBigDecimal(getColumnIndex(columnLabel)); } @Override public byte[] getBytes(String columnLabel) throws SQLException { - return getBytes(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getBytes(getColumnIndex(columnLabel)); } @Override public Date getDate(String columnLabel) throws SQLException { - return getDate(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getDate(getColumnIndex(columnLabel)); } @Override public Time getTime(String columnLabel) throws SQLException { - return getTime(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getTime(getColumnIndex(columnLabel)); } @Override public Timestamp getTimestamp(String columnLabel) throws SQLException { - return getTimestamp(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getTimestamp(getColumnIndex(columnLabel)); } @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { - return getAsciiStream(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getAsciiStream(getColumnIndex(columnLabel)); } @Override public InputStream getUnicodeStream(String columnLabel) throws SQLException { - return getUnicodeStream(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getUnicodeStream(getColumnIndex(columnLabel)); } @Override public InputStream getBinaryStream(String columnLabel) throws SQLException { - return getBinaryStream(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getBinaryStream(getColumnIndex(columnLabel)); } @Override @@ -258,12 +270,12 @@ public Object getObject(int columnIndex) throws SQLException { @Override public Object getObject(String columnLabel) throws SQLException { - return getRaw(JDBCBridgeExecutor.getColumnIndex(columnLabel)); + return getRaw(getColumnIndex(columnLabel)); } @Override public int findColumn(String columnLabel) throws SQLException { - return JDBCBridgeExecutor.getColumnIndex(columnLabel); + return getColumnIndex(columnLabel); } @Override @@ -293,7 +305,7 @@ public boolean isBeforeFirst() throws SQLException { @Override public boolean isAfterLast() throws SQLException { - return iterator.nextIndex() == JDBCBridgeExecutor.size() && row == null; + return iterator.nextIndex() == bridge.size() && row == null; } @Override @@ -303,13 +315,13 @@ public boolean isFirst() throws SQLException { @Override public boolean isLast() throws SQLException { - return iterator.nextIndex() == JDBCBridgeExecutor.size(); + return iterator.nextIndex() == bridge.size(); } @Override public void beforeFirst() throws SQLException { row = null; - iterator = JDBCBridgeExecutor.iterator(); + iterator = bridge.iterator(); } @Override @@ -1027,7 +1039,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { @Override public T getObject(String columnLabel, Class type) throws SQLException { - return type.cast(getRaw(JDBCBridgeExecutor.getColumnIndex(columnLabel))); + return type.cast(getRaw(getColumnIndex(columnLabel))); } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java index f883482f..b3053d24 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -3,6 +3,7 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.Types; import org.tarantool.JDBCBridge; @@ -65,7 +66,7 @@ public String getColumnName(int column) throws SQLException { @Override public String getSchemaName(int column) throws SQLException { - throw new SQLFeatureNotSupportedException(); + return null; } @Override @@ -85,17 +86,17 @@ public String getTableName(int column) throws SQLException { @Override public String getCatalogName(int column) throws SQLException { - throw new SQLFeatureNotSupportedException(); + return null; } @Override public int getColumnType(int column) throws SQLException { - throw new SQLFeatureNotSupportedException(); + return Types.OTHER; } @Override public String getColumnTypeName(int column) throws SQLException { - throw new SQLFeatureNotSupportedException(); + return "scalar"; } @Override @@ -131,7 +132,7 @@ public boolean isWrapperFor(Class iface) throws SQLException { @Override public String toString() { return "SQLResultSetMetaData{" + - "JDBCBridgeExecutor=" + JDBCBridgeExecutor + + "bridge=" + JDBCBridgeExecutor + '}'; } } diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index 4497494b..141ae52f 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -13,23 +13,28 @@ @SuppressWarnings("Since15") public class SQLStatement implements Statement { protected final TarantoolConnection connection; - protected final SqlConnection sqlConnection; + protected final SQLConnection sqlConnection; private SQLResultSet resultSet; private int updateCount; + private int maxRows; - protected SQLStatement(TarantoolConnection connection, SqlConnection sqlConnection) { + protected SQLStatement(TarantoolConnection connection, SQLConnection sqlConnection) { this.connection = connection; this.sqlConnection = sqlConnection; } @Override public ResultSet executeQuery(String sql) throws SQLException { - return (resultSet = new SQLResultSet(JDBCBridge.query(connection, sql))); + resultSet = new SQLResultSet(JDBCBridge.query(connection, sql)); + updateCount = -1; + return resultSet; } @Override public int executeUpdate(String sql) throws SQLException { - return JDBCBridge.update(connection, sql); + int update = JDBCBridge.update(connection, sql); + resultSet = null; + return update; } @Override @@ -49,12 +54,15 @@ public void setMaxFieldSize(int max) throws SQLException { @Override public int getMaxRows() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return maxRows; } @Override public void setMaxRows(int max) throws SQLException { - throw new SQLFeatureNotSupportedException(); + maxRows = max; + if(resultSet!=null) { + resultSet.maxRows = maxRows; + } } @Override @@ -74,7 +82,7 @@ public void setQueryTimeout(int seconds) throws SQLException { @Override public void cancel() throws SQLException { - throw new SQLFeatureNotSupportedException(); + } @Override @@ -97,8 +105,11 @@ public boolean execute(String sql) throws SQLException { Object result = JDBCBridge.execute(connection, sql); if (result instanceof SQLResultSet) { resultSet = (SQLResultSet) result; + resultSet.maxRows = maxRows; + updateCount = -1; return true; } else { + resultSet = null; updateCount = (Integer) result; return false; } @@ -106,17 +117,25 @@ public boolean execute(String sql) throws SQLException { @Override public ResultSet getResultSet() throws SQLException { - return resultSet; + try { + return resultSet; + } finally { + resultSet = null; + } } @Override public int getUpdateCount() throws SQLException { - return updateCount; + try { + return updateCount; + } finally { + updateCount = -1; + } } @Override public boolean getMoreResults() throws SQLException { - return resultSet != null; + return false; } @Override @@ -133,12 +152,11 @@ public int getFetchDirection() throws SQLException { @Override public void setFetchSize(int rows) throws SQLException { - throw new SQLFeatureNotSupportedException(); } @Override public int getFetchSize() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return 0; } @Override @@ -173,7 +191,7 @@ public Connection getConnection() throws SQLException { @Override public boolean getMoreResults(int current) throws SQLException { - throw new SQLFeatureNotSupportedException(); + return false; } @Override @@ -233,12 +251,12 @@ public boolean isPoolable() throws SQLException { @Override public void closeOnCompletion() throws SQLException { - throw new SQLFeatureNotSupportedException(); + } @Override public boolean isCloseOnCompletion() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return false; } @Override From 7d28b785d86d2565478a9f93cc566541dd40c173 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Wed, 12 Dec 2018 23:49:39 +0300 Subject: [PATCH 06/11] jdbc: fix NPE in update Updated constants to be in sync with tarantool IPROTO. Added integration tests for the working JDBC functionality. Fixed travis script to use tarantool 2.0 which is required for SQL. Fixed failing user permission calls in tarantool initialization script. Closes #39 ---- Rebase fixes (Alexander Turenko): * Start tarantool from the Java code in JDBC tests (use TarantoolControl). --- src/main/java/org/tarantool/Key.java | 6 +- .../org/tarantool/jdbc/AbstractJdbcIT.java | 188 ++++++++++++++++++ .../org/tarantool/jdbc/JdbcConnectionIT.java | 42 ++++ .../jdbc/JdbcDatabaseMetaDataIT.java | 106 ++++++++++ .../jdbc/JdbcPreparedStatementIT.java | 67 +++++++ .../org/tarantool/jdbc/JdbcResultSetIT.java | 114 +++++++++++ .../jdbc/JdbcResultSetMetaDataIT.java | 33 +++ .../org/tarantool/jdbc/JdbcStatementIT.java | 66 ++++++ src/test/travis.pre.sh | 9 +- 9 files changed, 624 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcStatementIT.java diff --git a/src/main/java/org/tarantool/Key.java b/src/main/java/org/tarantool/Key.java index 35a91cd6..32e761f3 100644 --- a/src/main/java/org/tarantool/Key.java +++ b/src/main/java/org/tarantool/Key.java @@ -18,13 +18,13 @@ public enum Key implements Callable { UPSERT_OPS(0x28), DATA(0x30), ERROR(0x31), - SQL_FIELD_NAME(0x29), + SQL_FIELD_NAME(0), SQL_METADATA(0x32), SQL_TEXT(0x40), SQL_BIND(0x41), SQL_OPTIONS(0x42), - SQL_INFO(0x43), - SQL_ROW_COUNT(0x44); + SQL_INFO(0x42), + SQL_ROW_COUNT(0); int id; 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..8e6f8fe9 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -0,0 +1,188 @@ +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[] { + "DROP TABLE IF EXISTS test", + "DROP TABLE IF EXISTS test_types", + + "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 + }; + + private static String[] cleanSql = new String[] { + "DROP TABLE IF EXISTS test", + "DROP TABLE IF EXISTS test_types" + }; + + 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(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) throws IOException { + Socket socket = new Socket(); + try { + socket.connect(new InetSocketAddress(host, port)); + TarantoolConnection con = new TarantoolConnection(user, pass, socket); + try { + for (String cmd : text) + con.eval("box.sql.execute(\"" + cmd + "\")"); + } + finally { + con.close(); + socket = null; + } + } + finally { + if (socket != null) + socket.close(); + } + } + + static List getRow(String space, Object key) throws IOException { + Socket socket = new Socket(); + try { + socket.connect(new InetSocketAddress(host, port)); + TarantoolConnection con = new TarantoolConnection(user, pass, socket); + 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(); + socket = null; + } + } + finally { + if (socket != null) + socket.close(); + } + } +} 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..cc6bfb9c --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -0,0 +1,42 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; + +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +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.assertTrue; + +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); + } +} \ 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..b934bbec --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -0,0 +1,106 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { + private DatabaseMetaData meta; + + @BeforeEach + public void setUp() throws Exception { + 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")); + + 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(); + } + + @Disabled(value="Test ignored, issue#41") + @Test + public void testGetPrimaryKeys() throws SQLException { + ResultSet rs = meta.getPrimaryKeys(null, null, "TEST"); + + assertNotNull(rs); + assertTrue(rs.next()); + + assertNull(rs.getString("TABLE_CAT")); + assertNull(rs.getString("TABLE_SCHEM")); + assertEquals("TEST", rs.getString("TABLE_NAME")); + assertEquals("ID", rs.getString("COLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertEquals("pk_unnamed_TEST_1", rs.getString("PK_NAME")); + + assertFalse(rs.next()); + + rs.close(); + } +} 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..f356f6bc --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -0,0 +1,67 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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)); + } +} 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..925556d5 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -0,0 +1,66 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +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)); + } +} \ 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 From 153a40c815d0c7a2cc032cccf23e9328e7c482fb Mon Sep 17 00:00:00 2001 From: Sergei Kalashnikov Date: Wed, 10 Oct 2018 16:07:40 +0300 Subject: [PATCH 07/11] jdbc: fix primary keys meta retrieval Fixed several mistakes in get primary keys metadata API. Corrected type mismatch when parsing server response. Added sorting of result rows by column name. Fixed order of columns in result set. Wrapped errors into SQLException. Improved test coverage. Closes #41 ---- Rebase fixes (Alexander Turenko): * Start / stop jdk-testing instance using TarantoolControl in JdbcExceptionHandlingTest. --- .../tarantool/jdbc/SQLDatabaseMetadata.java | 78 +++++++++++---- .../org/tarantool/jdbc/AbstractJdbcIT.java | 11 ++- .../jdbc/JdbcDatabaseMetaDataIT.java | 98 +++++++++++++++++-- .../jdbc/JdbcExceptionHandlingTest.java | 83 ++++++++++++++++ 4 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index e43bcdc8..c8879c99 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -765,29 +766,53 @@ public ResultSet getVersionColumns(String catalog, String schema, String table) @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { - List> spaces = (List>) connection.connection.select(_VSPACE, 2, Arrays.asList(table), 0, 1, 0); + final List colNames = Arrays.asList( + "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"); - List> rows = new ArrayList>(); + if (table == null || table.isEmpty()) + return emptyResultSet(colNames); + + try { + List spaces = connection.connection.select(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); + + if (spaces == null || spaces.size() == 0) + return emptyResultSet(colNames); - if (spaces != null && spaces.size() > 0) { - List space = spaces.get(0); - List> fields = (List>) space.get(FORMAT_IDX); - List> indexes = (List>) connection.connection.select(_VINDEX, 0, Arrays.asList(space.get(SPACE_ID_IDX), 0), 0, 1, 0); - List primaryKey = indexes.get(0); - List> parts = (List>) primaryKey.get(INDEX_FORMAT_IDX); + 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.connection.select(_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++) { - List part = parts.get(i); - int ordinal = ((Number) part.get(0)).intValue(); - String column = (String) fields.get(ordinal).get("name"); - rows.add(Arrays.asList(table, column, i + 1, "primary", primaryKey.get(NAME_IDX))); + // 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 (Throwable t) { + throw new SQLException("Error processing metadata for table \"" + table + "\".", t); } - return new SQLNullResultSet((JDBCBridge.mock( - Arrays.asList("TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME", - //nulls - "TABLE_CAT", "TABLE_SCHEM" - ), - rows))); } @Override @@ -1026,4 +1051,21 @@ public T unwrap(Class iface) throws SQLException { 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/test/java/org/tarantool/jdbc/AbstractJdbcIT.java b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java index 8e6f8fe9..ad07a62e 100644 --- a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -34,9 +34,6 @@ public abstract class AbstractJdbcIT { private static String URL = String.format("tarantool://%s:%d?user=%s&password=%s", host, port, user, pass); private static String[] initSql = new String[] { - "DROP TABLE IF EXISTS test", - "DROP TABLE IF EXISTS test_types", - "CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100))", "INSERT INTO test VALUES (1, 'one'), (2, 'two'), (3, 'three')", @@ -80,12 +77,15 @@ public abstract class AbstractJdbcIT { "X'010203040506'," + //LONGVARBINARY "'1983-03-14'," + //DATE "'12:01:06'," + //TIME - "129479994)" //TIMESTAMP + "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_types", + "DROP TABLE IF EXISTS test_compound" }; static Object[] testRow = new Object[] { @@ -119,6 +119,7 @@ public static void setupEnv() throws Exception { control.start("jdk-testing"); control.waitStarted("jdk-testing"); + sqlExec(cleanSql); sqlExec(initSql); } diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java index b934bbec..17ab0863 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -1,11 +1,11 @@ package org.tarantool.jdbc; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -18,7 +18,7 @@ public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { private DatabaseMetaData meta; @BeforeEach - public void setUp() throws Exception { + public void setUp() throws SQLException { meta = conn.getMetaData(); } @@ -45,6 +45,9 @@ public void testGetAllTables() throws SQLException { 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(); @@ -84,7 +87,6 @@ public void testGetColumns() throws SQLException { rs.close(); } - @Disabled(value="Test ignored, issue#41") @Test public void testGetPrimaryKeys() throws SQLException { ResultSet rs = meta.getPrimaryKeys(null, null, "TEST"); @@ -92,15 +94,93 @@ public void testGetPrimaryKeys() throws SQLException { assertNotNull(rs); assertTrue(rs.next()); - assertNull(rs.getString("TABLE_CAT")); - assertNull(rs.getString("TABLE_SCHEM")); - assertEquals("TEST", rs.getString("TABLE_NAME")); - assertEquals("ID", rs.getString("COLUMN_NAME")); - assertEquals(1, rs.getInt("KEY_SEQ")); - assertEquals("pk_unnamed_TEST_1", rs.getString("PK_NAME")); + 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)); + } } 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..046cfb35 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -0,0 +1,83 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.tarantool.TarantoolConnection; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +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._VINDEX; +import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; + +import org.tarantool.TarantoolControl; + +public class JdbcExceptionHandlingTest { + protected static TarantoolControl control; + + /** + * We cannot mock TarantoolConnection constructor, so need listening + * tarantool instance to prevent a test failure. + */ + @BeforeAll + public static void setupEnv() throws Exception { + control = new TarantoolControl(); + control.start("jdk-testing"); + control.waitStarted("jdk-testing"); + } + + @AfterAll + public static void teardownEnv() throws Exception { + control.stop("jdk-testing"); + control.waitStopped("jdk-testing"); + } + + /** + * 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 = new SQLConnection(tntCon, "", new Properties()); + + 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")); + } +} From f8078a1bd35240eaa12619bed85f992da665df80 Mon Sep 17 00:00:00 2001 From: Sergei Kalashnikov Date: Wed, 10 Oct 2018 16:57:35 +0300 Subject: [PATCH 08/11] jdbc: fix date/time parameters binding Fixed exception when packing date/time parameters of prepared statement. Added test for parameter binding methods. Closes #43 --- .../org/tarantool/jdbc/SQLMsgPackLite.java | 6 +- .../jdbc/JdbcPreparedStatementIT.java | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java index 0f73efb8..15068774 100644 --- a/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java +++ b/src/main/java/org/tarantool/jdbc/SQLMsgPackLite.java @@ -17,11 +17,11 @@ public class SQLMsgPackLite extends MsgPackLite { public void pack(Object item, OutputStream os) throws IOException { if(item instanceof Date) { super.pack(((Date)item).getTime(), os); - } if(item instanceof Time) { + } else if(item instanceof Time) { super.pack(((Time)item).getTime(), os); - } if(item instanceof Timestamp) { + } else if(item instanceof Timestamp) { super.pack(((Timestamp)item).getTime(), os); - } if(item instanceof BigDecimal) { + } else if(item instanceof BigDecimal) { super.pack(((BigDecimal)item).toPlainString(), os); } else { super.pack(item, os); diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index f356f6bc..68628ef5 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -3,9 +3,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterEach; +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; @@ -64,4 +68,66 @@ public void testExecuteUpdate() throws Exception { 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(); + } } From c3c21fa71b1329ec116108843cac1f8e71763ac8 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Wed, 31 Oct 2018 06:02:11 +0300 Subject: [PATCH 09/11] Report code coverage to coveralls.io --- .travis.yml | 13 ++++++++++++- README.md | 5 +++++ pom.xml | 27 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) 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 fa82eff9..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 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 + + + From a83801f070f0d7530b8bf51757ac6167f450cb0d Mon Sep 17 00:00:00 2001 From: Sergei Kalashnikov Date: Thu, 15 Nov 2018 19:45:23 +0300 Subject: [PATCH 10/11] jdbc: add connection timeout configuration and handling Added connection property `socketTimeout` to allow user control over network timeout before actual connection is returned by the driver. This is only done for default socket provider. The default timeout is left to be infinite. Implemented `Connection.setNetworkTimeout` API to make it possible to change the maximum amount of time to wait for server replies after the connection is established. The connection that has timed out is forced to close. New subsequent operations requested on such connection must fail right away. The corresponding checks are embedded into relevant APIs. Closes #38 --- .../org/tarantool/TarantoolConnection.java | 21 ++ .../org/tarantool/jdbc/SQLConnection.java | 216 +++++++++++++++- .../tarantool/jdbc/SQLDatabaseMetadata.java | 101 ++++---- .../java/org/tarantool/jdbc/SQLDriver.java | 163 ++++++++---- .../tarantool/jdbc/SQLPreparedStatement.java | 35 ++- .../java/org/tarantool/jdbc/SQLStatement.java | 70 +++-- .../org/tarantool/jdbc/AbstractJdbcIT.java | 53 ++-- .../org/tarantool/jdbc/JdbcConnectionIT.java | 65 +++++ .../jdbc/JdbcDatabaseMetaDataIT.java | 30 +++ .../org/tarantool/jdbc/JdbcDriverTest.java | 202 +++++++++++++++ .../jdbc/JdbcExceptionHandlingTest.java | 241 +++++++++++++++++- .../jdbc/JdbcPreparedStatementIT.java | 94 +++++++ .../org/tarantool/jdbc/JdbcStatementIT.java | 30 +++ 13 files changed, 1149 insertions(+), 172 deletions(-) create mode 100644 src/test/java/org/tarantool/jdbc/JdbcDriverTest.java diff --git a/src/main/java/org/tarantool/TarantoolConnection.java b/src/main/java/org/tarantool/TarantoolConnection.java index be94995f..b817988f 100644 --- a/src/main/java/org/tarantool/TarantoolConnection.java +++ b/src/main/java/org/tarantool/TarantoolConnection.java @@ -4,6 +4,7 @@ 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; @@ -80,4 +81,24 @@ protected void sql(String sql, Object[] 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/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index de28850d..89b0f81d 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -1,5 +1,9 @@ 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; @@ -8,6 +12,7 @@ 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; @@ -16,32 +21,133 @@ 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 { - final TarantoolConnection connection; + private final TarantoolConnection connection; final String url; final Properties properties; - public SQLConnection(TarantoolConnection connection, String url, Properties properties) { - this.connection = connection; + 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 { - return new SQLStatement(connection, this); + checkNotClosed(); + return new SQLStatement(this); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { - return new SQLPreparedStatement(connection, this, sql); + checkNotClosed(); + return new SQLPreparedStatement(this, sql); } @Override @@ -89,6 +195,7 @@ public boolean isClosed() throws SQLException { @Override public DatabaseMetaData getMetaData() throws SQLException { + checkNotClosed(); return new SQLDatabaseMetadata(this); } @@ -293,15 +400,28 @@ public void abort(Executor executor) throws SQLException { @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { - throw new SQLFeatureNotSupportedException(); + 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 { - throw new SQLFeatureNotSupportedException(); + 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(); @@ -311,4 +431,84 @@ public T unwrap(Class iface) throws SQLException { 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 index c8879c99..2ddb0ef6 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -105,7 +105,7 @@ public String getDatabaseProductName() throws SQLException { @Override public String getDatabaseProductVersion() throws SQLException { - return connection.connection.getServerVersion(); + return connection.getServerVersion(); } @Override @@ -672,23 +672,29 @@ protected boolean like(String value, String[] parts) { @Override public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) - throws SQLException { - if (types != null && !Arrays.asList(types).contains("TABLE")) { - return new SQLResultSet(JDBCBridge.EMPTY); - } - String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); - List> spaces = (List>) connection.connection.select(_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"))); + 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); } - 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)); } @Override @@ -713,32 +719,38 @@ public ResultSet getTableTypes() throws SQLException { @Override public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); - String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); - List> spaces = (List>) connection.connection.select(_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")); + 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))); + 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 @@ -769,11 +781,13 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr final List colNames = Arrays.asList( "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"); - if (table == null || table.isEmpty()) + if (table == null || table.isEmpty()) { + connection.checkNotClosed(); return emptyResultSet(colNames); + } try { - List spaces = connection.connection.select(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); + List spaces = connection.nativeSelect(_VSPACE, 2, Collections.singletonList(table), 0, 1, 0); if (spaces == null || spaces.size() == 0) return emptyResultSet(colNames); @@ -781,7 +795,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr 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.connection.select(_VINDEX, 0, Arrays.asList(spaceId, 0), 0, 1, 0); + 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)); @@ -809,9 +823,8 @@ public int compare(List row0, List row1) { } }); return new SQLNullResultSet((JDBCBridge.mock(colNames, rows))); - } - catch (Throwable t) { - throw new SQLException("Error processing metadata for table \"" + table + "\".", t); + } catch (Exception e) { + throw new SQLException("Error processing metadata for table \"" + table + "\".", e); } } diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java index 68679979..6ca97866 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDriver.java +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -1,7 +1,5 @@ package org.tarantool.jdbc; -import java.io.IOException; -import java.net.InetSocketAddress; import java.net.Socket; import java.net.URI; import java.sql.Connection; @@ -14,8 +12,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; -import org.tarantool.TarantoolConnection; - @SuppressWarnings("Since15") public class SQLDriver implements Driver { @@ -32,35 +28,53 @@ public class SQLDriver implements Driver { 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"); + }}; - protected Map providerCache = new ConcurrentHashMap(); + private final Map providerCache = new ConcurrentHashMap(); @Override public Connection connect(String url, Properties info) throws SQLException { - URI uri = URI.create(url); - Properties urlProperties = parseQueryString(uri, info); + final URI uri = URI.create(url); + final Properties urlProperties = parseQueryString(uri, info); String providerClassName = urlProperties.getProperty(PROP_SOCKET_PROVIDER); - Socket socket; - if (providerClassName != null) { - socket = getSocketFromProvider(uri, urlProperties, providerClassName); - } else { - socket = createAndConnectDefaultSocket(urlProperties); - } - try { - TarantoolConnection connection = new TarantoolConnection(urlProperties.getProperty(PROP_USER), urlProperties.getProperty(PROP_PASSWORD), socket) {{ - msgPackLite = SQLMsgPackLite.INSTANCE; - }}; - return new SQLConnection(connection, url, info); - } catch (IOException e) { - throw new SQLException("Couldn't initiate connection. Provider class name is " + providerClassName, e); - } + 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) { - Properties urlProperties = new Properties(info); + 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) { @@ -72,44 +86,62 @@ protected Properties parseQueryString(URI uri, Properties info) { } } } - urlProperties.put(PROP_HOST, uri.getHost() == null ? "localhost" : uri.getHost()); - urlProperties.put(PROP_PORT, uri.getPort() < 1 ? "3301" : uri.getPort()); - return urlProperties; - } + 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); - protected Socket createAndConnectDefaultSocket(Properties properties) throws SQLException { - Socket socket; - socket = new Socket(); + // Validate properties. + int port; try { - socket.connect(new InetSocketAddress(properties.getProperty(PROP_HOST, "localhost"), Integer.parseInt(properties.getProperty(PROP_PORT, "3301")))); + port = Integer.parseInt(urlProperties.getProperty(PROP_PORT)); } catch (Exception e) { - throw new SQLException("Couldn't connect to tarantool using" + properties, e); + throw new SQLException("Port must be a valid number."); + } + if (port <= 0 || port > 65535) { + throw new SQLException("Port is out of range: " + port); } - return socket; + 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 Socket getSocketFromProvider(URI uri, Properties urlProperties, String providerClassName) - throws SQLException { - Socket socket; - SQLSocketProvider sqlSocketProvider = providerCache.get(providerClassName); - if (sqlSocketProvider == null) { + protected SQLSocketProvider getSocketProviderInstance(String className) throws SQLException { + SQLSocketProvider provider = providerCache.get(className); + if (provider == null) { synchronized (this) { - sqlSocketProvider = providerCache.get(providerClassName); - if (sqlSocketProvider == null) { + provider = providerCache.get(className); + if (provider == null) { try { - Class cls = Class.forName(providerClassName); + Class cls = Class.forName(className); if (SQLSocketProvider.class.isAssignableFrom(cls)) { - sqlSocketProvider = (SQLSocketProvider) cls.newInstance(); - providerCache.put(providerClassName, sqlSocketProvider); + provider = (SQLSocketProvider)cls.newInstance(); + providerCache.put(className, provider); } } catch (Exception e) { - throw new SQLException("Couldn't initiate socket provider " + providerClassName, e); + throw new SQLException("Couldn't instantiate socket provider: " + className, e); } } } } - socket = sqlSocketProvider.getConnectedSocket(uri, urlProperties); - return socket; + if (provider == null) { + throw new SQLException(String.format("The socket provider %s does not implement %s", + className, SQLSocketProvider.class.getCanonicalName())); + } + return provider; } @Override @@ -121,16 +153,15 @@ public boolean acceptsURL(String url) throws SQLException { public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { try { URI uri = new URI(url); - Properties properties = parseQueryString(uri, new Properties(info == null ? new Properties() : info)); + Properties properties = parseQueryString(uri, info); DriverPropertyInfo host = new DriverPropertyInfo(PROP_HOST, properties.getProperty(PROP_HOST)); host.required = true; - host.description = "Tarantool sever host"; + host.description = "Tarantool server host"; DriverPropertyInfo port = new DriverPropertyInfo(PROP_PORT, properties.getProperty(PROP_PORT)); port.required = true; - port.description = "Tarantool sever port"; - + port.description = "Tarantool server port"; DriverPropertyInfo user = new DriverPropertyInfo(PROP_USER, properties.getProperty(PROP_USER)); user.required = false; @@ -140,12 +171,20 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws password.required = false; password.description = "password"; - DriverPropertyInfo socketProvider = new DriverPropertyInfo(PROP_SOCKET_PROVIDER, properties.getProperty(PROP_SOCKET_PROVIDER)); + 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}; + return new DriverPropertyInfo[]{host, port, user, password, socketProvider, socketTimeout}; } catch (Exception e) { throw new SQLException(e); } @@ -171,5 +210,23 @@ 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/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index 23d10734..99b51370 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -24,23 +24,22 @@ import java.util.HashMap; import java.util.Map; -import org.tarantool.JDBCBridge; -import org.tarantool.TarantoolConnection; - 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(TarantoolConnection connection, SQLConnection sqlConnection, String sql) { - super(connection, sqlConnection); + public SQLPreparedStatement(SQLConnection connection, String sql) { + super(connection); this.sql = sql; this.params = new HashMap(); } @Override public ResultSet executeQuery() throws SQLException { - return new SQLResultSet(JDBCBridge.query(connection, sql, getParams())); + discardLastResults(); + return connection.executeQuery(sql, getParams()); } protected Object[] getParams() throws SQLException { @@ -49,7 +48,7 @@ protected Object[] getParams() throws SQLException { if (params.containsKey(i)) { objects[i - 1] = params.get(i); } else { - throw new SQLException("Parameter " + i + "is not"); + throw new SQLException("Parameter " + i + " is missing"); } } return objects; @@ -57,8 +56,8 @@ protected Object[] getParams() throws SQLException { @Override public int executeUpdate() throws SQLException { - return JDBCBridge.update(connection, sql, getParams()); - + discardLastResults(); + return connection.executeUpdate(sql, getParams()); } @Override @@ -163,7 +162,8 @@ public void setObject(int parameterIndex, Object x) throws SQLException { @Override public boolean execute() throws SQLException { - return false; + discardLastResults(); + return handleResult(connection.execute(sql, getParams())); } @Override @@ -336,10 +336,23 @@ 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/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index 141ae52f..3e75694a 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -7,34 +7,27 @@ import java.sql.SQLWarning; import java.sql.Statement; -import org.tarantool.JDBCBridge; -import org.tarantool.TarantoolConnection; - @SuppressWarnings("Since15") public class SQLStatement implements Statement { - protected final TarantoolConnection connection; - protected final SQLConnection sqlConnection; + protected final SQLConnection connection; private SQLResultSet resultSet; private int updateCount; private int maxRows; - protected SQLStatement(TarantoolConnection connection, SQLConnection sqlConnection) { - this.connection = connection; - this.sqlConnection = sqlConnection; + protected SQLStatement(SQLConnection sqlConnection) { + this.connection = sqlConnection; } @Override public ResultSet executeQuery(String sql) throws SQLException { - resultSet = new SQLResultSet(JDBCBridge.query(connection, sql)); - updateCount = -1; - return resultSet; + discardLastResults(); + return connection.executeQuery(sql); } @Override public int executeUpdate(String sql) throws SQLException { - int update = JDBCBridge.update(connection, sql); - resultSet = null; - return update; + discardLastResults(); + return connection.executeUpdate(sql); } @Override @@ -102,17 +95,8 @@ public void setCursorName(String name) throws SQLException { @Override public boolean execute(String sql) throws SQLException { - Object result = JDBCBridge.execute(connection, sql); - if (result instanceof SQLResultSet) { - resultSet = (SQLResultSet) result; - resultSet.maxRows = maxRows; - updateCount = -1; - return true; - } else { - resultSet = null; - updateCount = (Integer) result; - return false; - } + discardLastResults(); + return handleResult(connection.execute(sql)); } @Override @@ -186,7 +170,7 @@ public int[] executeBatch() throws SQLException { @Override public Connection getConnection() throws SQLException { - return sqlConnection; + return connection; } @Override @@ -268,4 +252,38 @@ public T unwrap(Class iface) throws SQLException { 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/test/java/org/tarantool/jdbc/AbstractJdbcIT.java b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java index ad07a62e..c0c00c07 100644 --- a/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java +++ b/src/test/java/org/tarantool/jdbc/AbstractJdbcIT.java @@ -145,45 +145,40 @@ public void tearDownConnection() throws SQLException { conn.close(); } - private static void sqlExec(String[] text) throws IOException { - Socket socket = new Socket(); + private static void sqlExec(String[] text) { + TarantoolConnection con = makeConnection(); try { - socket.connect(new InetSocketAddress(host, port)); - TarantoolConnection con = new TarantoolConnection(user, pass, socket); - try { - for (String cmd : text) - con.eval("box.sql.execute(\"" + cmd + "\")"); - } - finally { - con.close(); - socket = null; - } + for (String cmd : text) + con.eval("box.sql.execute(\"" + cmd + "\")"); + } finally { + con.close(); } - finally { - if (socket != null) - socket.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 List getRow(String space, Object key) throws IOException { + static TarantoolConnection makeConnection() { Socket socket = new Socket(); try { socket.connect(new InetSocketAddress(host, port)); - TarantoolConnection con = new TarantoolConnection(user, pass, socket); + return new TarantoolConnection(user, pass, socket); + } catch (IOException e) { 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(); - socket = null; - } - } - finally { - if (socket != null) 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 index cc6bfb9c..a6a05e88 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -1,16 +1,24 @@ 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 { @@ -39,4 +47,61 @@ 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 index 17ab0863..76caa198 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -2,6 +2,7 @@ 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; @@ -12,7 +13,9 @@ 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; @@ -183,4 +186,31 @@ private void checkGetPrimaryKeysRow(ResultSet rs, String table, String colName, 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 index 046cfb35..e8c04d02 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -4,24 +4,41 @@ import org.junit.jupiter.api.BeforeAll; 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; import org.tarantool.TarantoolControl; @@ -53,7 +70,7 @@ public static void teardownEnv() throws Exception { @Test public void testDatabaseMetaDataGetPrimaryKeysFormatError() throws SQLException { TarantoolConnection tntCon = mock(TarantoolConnection.class); - SQLConnection conn = new SQLConnection(tntCon, "", new Properties()); + SQLConnection conn = buildTestSQLConnection(tntCon, "", SQLDriver.defaults); Object[] spc = new Object[7]; spc[FORMAT_IDX] = Collections.singletonList(new HashMap()); @@ -80,4 +97,226 @@ public void execute() throws Throwable { 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 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 index 68628ef5..ad8fae70 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -2,6 +2,7 @@ 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; @@ -14,7 +15,10 @@ 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; @@ -130,4 +134,94 @@ public void testSetParameter() throws SQLException { 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/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 925556d5..735f326d 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -3,6 +3,7 @@ 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; @@ -10,8 +11,10 @@ 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; @@ -63,4 +66,31 @@ public void testExecuteReturnsUpdateCount() throws Exception { 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 From 8cd3b7989738f66502a38109434cd86104fcb196 Mon Sep 17 00:00:00 2001 From: Sergei Kalashnikov Date: Thu, 13 Dec 2018 14:41:10 +0300 Subject: [PATCH 11/11] test: mock socket in JdbcExceptionHandlingTest It is needed to get rid of having tarantool connection in the unit test. --- .../jdbc/JdbcExceptionHandlingTest.java | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index e8c04d02..e02c0e03 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -1,7 +1,5 @@ package org.tarantool.jdbc; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.api.function.ThrowingConsumer; @@ -40,28 +38,7 @@ import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; -import org.tarantool.TarantoolControl; - public class JdbcExceptionHandlingTest { - protected static TarantoolControl control; - - /** - * We cannot mock TarantoolConnection constructor, so need listening - * tarantool instance to prevent a test failure. - */ - @BeforeAll - public static void setupEnv() throws Exception { - control = new TarantoolControl(); - control.start("jdk-testing"); - control.waitStarted("jdk-testing"); - } - - @AfterAll - public static void teardownEnv() throws Exception { - control.stop("jdk-testing"); - control.waitStopped("jdk-testing"); - } - /** * Simulates meta parsing error: missing "name" field in a space format for the primary key. * @@ -303,6 +280,11 @@ public void execute() throws Throwable { 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;