From fb2e568285fd8dfaaa2b14c3a3abab7b7953dda5 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Mon, 13 Jan 2020 16:21:34 +0700 Subject: [PATCH 1/4] metadata: Tarantool instance version info The driver can connect to different Tarantool instances within a range of server versions. Sometimes, it's required to set a driver behaviour depending on those versions (i.e. in scope of #213 the driver needs to generate the functions for JDBC C4 LTRIM/RTRIM that are recognizable by a particular Tarantool version). This commit also includes implementation of two public DatabaseMetaData methods getDatabaseMajorVersion and getDatabaseMinorVersion. Affects: #213 Closes: #106 --- .../java/org/tarantool/jdbc/SQLConstant.java | 3 + .../tarantool/jdbc/SQLDatabaseMetadata.java | 23 ++- .../jdbc/TarantoolDatabaseMetaData.java | 20 +++ .../org/tarantool/protocol/ProtoUtils.java | 46 +++--- .../tarantool/protocol/TarantoolGreeting.java | 27 +++- .../org/tarantool/util/ServerVersion.java | 141 ++++++++++++++++++ .../org/tarantool/ClientOperationsIT.java | 1 + .../java/org/tarantool/ServerVersion.java | 80 ---------- .../org/tarantool/TarantoolClientOpsIT.java | 2 + .../java/org/tarantool/TarantoolSQLOpsIT.java | 2 + .../java/org/tarantool/TestAssumptions.java | 2 + .../jdbc/JdbcClosedConnectionIT.java | 2 +- .../org/tarantool/jdbc/JdbcConnectionIT.java | 2 +- .../jdbc/JdbcConnectionTimeoutIT.java | 2 +- .../jdbc/JdbcDatabaseMetaDataIT.java | 36 ++++- .../jdbc/JdbcPreparedStatementIT.java | 2 +- .../org/tarantool/jdbc/JdbcResultSetIT.java | 2 +- .../jdbc/JdbcResultSetMetaDataIT.java | 2 +- .../org/tarantool/jdbc/JdbcStatementIT.java | 2 +- .../java/org/tarantool/jdbc/JdbcTypesIT.java | 2 +- .../tarantool/jdbc/ds/JdbcDataSourceIT.java | 2 +- .../org/tarantool/schema/ClientSchemaIT.java | 2 +- 22 files changed, 278 insertions(+), 125 deletions(-) create mode 100644 src/main/java/org/tarantool/jdbc/TarantoolDatabaseMetaData.java create mode 100644 src/main/java/org/tarantool/util/ServerVersion.java delete mode 100644 src/test/java/org/tarantool/ServerVersion.java diff --git a/src/main/java/org/tarantool/jdbc/SQLConstant.java b/src/main/java/org/tarantool/jdbc/SQLConstant.java index a367cf2e..9bb57a62 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConstant.java +++ b/src/main/java/org/tarantool/jdbc/SQLConstant.java @@ -6,5 +6,8 @@ private SQLConstant() { } public static final String DRIVER_NAME = "Tarantool JDBC Driver"; + public static final String PRODUCT_NAME = "Tarantool"; + public static final int DRIVER_MAJOR_VERSION = 4; + public static final int DRIVER_MINOR_VERSION = 2; } diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index 98d2ea1d..5e5a7b47 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -5,10 +5,10 @@ import org.tarantool.SqlProtoUtils; import org.tarantool.Version; import org.tarantool.jdbc.type.TarantoolSqlType; +import org.tarantool.util.ServerVersion; import org.tarantool.util.TupleTwo; import java.sql.Connection; -import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.RowIdLifetime; import java.sql.SQLException; @@ -21,7 +21,7 @@ import java.util.Map; import java.util.stream.Collectors; -public class SQLDatabaseMetadata implements DatabaseMetaData { +public class SQLDatabaseMetadata implements TarantoolDatabaseMetaData { protected static final int _VSPACE = 281; protected static final int _VINDEX = 289; @@ -89,7 +89,7 @@ public boolean nullsAreSortedAtEnd() throws SQLException { @Override public String getDatabaseProductName() throws SQLException { - return "Tarantool"; + return SQLConstant.PRODUCT_NAME; } @Override @@ -1017,22 +1017,31 @@ public int getResultSetHoldability() throws SQLException { @Override public int getDatabaseMajorVersion() throws SQLException { - return 0; + return getDatabaseVersion().getMajorVersion(); } @Override public int getDatabaseMinorVersion() throws SQLException { - return 0; + return getDatabaseVersion().getMinorVersion(); + } + + @Override + public ServerVersion getDatabaseVersion() throws SQLException { + try { + return new ServerVersion(connection.getServerVersion()); + } catch (Exception cause) { + throw new SQLException("Could not get the current server version number", cause); + } } @Override public int getJDBCMajorVersion() throws SQLException { - return 2; + return SQLConstant.DRIVER_MAJOR_VERSION; } @Override public int getJDBCMinorVersion() throws SQLException { - return 1; + return SQLConstant.DRIVER_MINOR_VERSION; } @Override diff --git a/src/main/java/org/tarantool/jdbc/TarantoolDatabaseMetaData.java b/src/main/java/org/tarantool/jdbc/TarantoolDatabaseMetaData.java new file mode 100644 index 00000000..fafb5a20 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/TarantoolDatabaseMetaData.java @@ -0,0 +1,20 @@ +package org.tarantool.jdbc; + +import org.tarantool.util.ServerVersion; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * Tarantool specific database meta data extension. + */ +public interface TarantoolDatabaseMetaData extends DatabaseMetaData { + + /** + * Gets the current Tarantool version. + * + * @return version of active connected database. + */ + ServerVersion getDatabaseVersion() throws SQLException; + +} diff --git a/src/main/java/org/tarantool/protocol/ProtoUtils.java b/src/main/java/org/tarantool/protocol/ProtoUtils.java index 51481d38..4fff0907 100644 --- a/src/main/java/org/tarantool/protocol/ProtoUtils.java +++ b/src/main/java/org/tarantool/protocol/ProtoUtils.java @@ -32,7 +32,6 @@ public abstract class ProtoUtils { public static final int LENGTH_OF_SIZE_MESSAGE = 5; private static final int DEFAULT_INITIAL_REQUEST_SIZE = 4096; - private static final String WELCOME = "Tarantool "; /** * Reads tarantool binary protocol's packet from {@code inputStream}. @@ -65,7 +64,7 @@ public static TarantoolPacket readPacket(InputStream inputStream, MsgPackLite ms * * @param bufferReader readable channel that have to be in blocking mode * or instance of {@link ReadableViaSelectorChannel} - * @param msgPackLite MessagePack decoder instance + * @param msgPackLite MessagePack decoder instance * * @return tarantool binary protocol message wrapped by instance of {@link TarantoolPacket} * @@ -120,9 +119,9 @@ public static TarantoolPacket readPacket(ReadableByteChannel bufferReader, MsgPa /** * Connects to a tarantool node described by {@code socket}. Performs an authentication if required * - * @param socket a socket channel to a tarantool node - * @param username auth username - * @param password auth password + * @param socket a socket channel to a tarantool node + * @param username auth username + * @param password auth password * @param msgPackLite MessagePack encoder / decoder instance * * @return object with information about a connection/ @@ -141,8 +140,7 @@ public static TarantoolGreeting connect(Socket socket, inputStream.read(inputBytes); String firstLine = new String(inputBytes); - assertCorrectWelcome(firstLine, socket.getRemoteSocketAddress()); - String serverVersion = firstLine.substring(WELCOME.length()); + final TarantoolGreeting greeting = parseGreetingLine(firstLine, socket.getRemoteSocketAddress()); inputStream.read(inputBytes); String salt = new String(inputBytes); @@ -157,15 +155,15 @@ public static TarantoolGreeting connect(Socket socket, assertNoErrCode(responsePacket); } - return new TarantoolGreeting(serverVersion); + return greeting; } /** * Connects to a tarantool node described by {@code socketChannel}. Performs an authentication if required. * - * @param channel a socket channel to tarantool node. The channel have to be in blocking mode - * @param username auth username - * @param password auth password + * @param channel a socket channel to tarantool node. The channel have to be in blocking mode + * @param username auth username + * @param password auth password * @param msgPackLite MessagePack encoder / decoder instance * * @return object with information about a connection/ @@ -182,10 +180,9 @@ public static TarantoolGreeting connect(SocketChannel channel, channel.read(welcomeBytes); String firstLine = new String(welcomeBytes.array()); - assertCorrectWelcome(firstLine, channel.getRemoteAddress()); - final String serverVersion = firstLine.substring(WELCOME.length()); + final TarantoolGreeting greeting = parseGreetingLine(firstLine, channel.getRemoteAddress()); - ((Buffer)welcomeBytes).clear(); + ((Buffer) welcomeBytes).clear(); channel.read(welcomeBytes); String salt = new String(welcomeBytes.array()); @@ -197,17 +194,7 @@ public static TarantoolGreeting connect(SocketChannel channel, assertNoErrCode(authResponse); } - return new TarantoolGreeting(serverVersion); - } - - private static void assertCorrectWelcome(String firstLine, SocketAddress remoteAddress) { - if (!firstLine.startsWith(WELCOME)) { - String errMsg = "Failed to connect to node " + remoteAddress.toString() + - ": Welcome message should starts with tarantool but starts with '" + - firstLine + - "'"; - throw new CommunicationException(errMsg, new IllegalStateException("Invalid welcome packet")); - } + return greeting; } private static void assertNoErrCode(TarantoolPacket authResponse) { @@ -331,4 +318,13 @@ ByteBuffer toByteBuffer() { } + private static TarantoolGreeting parseGreetingLine(String line, SocketAddress remoteAddress) { + try { + return new TarantoolGreeting(line); + } catch (Exception cause) { + String message = "Failed to connect to node " + remoteAddress.toString(); + throw new CommunicationException(message, cause); + } + } + } diff --git a/src/main/java/org/tarantool/protocol/TarantoolGreeting.java b/src/main/java/org/tarantool/protocol/TarantoolGreeting.java index 0b5598a4..528313e8 100644 --- a/src/main/java/org/tarantool/protocol/TarantoolGreeting.java +++ b/src/main/java/org/tarantool/protocol/TarantoolGreeting.java @@ -1,13 +1,36 @@ package org.tarantool.protocol; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class TarantoolGreeting { + + private static final Pattern GREETING_LINE = + Pattern.compile("Tarantool\\s+(?[-.0-9a-g]+)\\s+\\((?.*)\\)\\s+(?[-0-9a-f]*)"); + private final String serverVersion; + private final String protocolType; + private final String instanceUuid; - public TarantoolGreeting(String serverVersion) { - this.serverVersion = serverVersion; + public TarantoolGreeting(String greetingLine) { + Matcher matcher = GREETING_LINE.matcher(greetingLine); + if (!matcher.find()) { + throw new IllegalArgumentException("Welcome message '" + greetingLine + "' is incorrect "); + } + serverVersion = matcher.group("version"); + protocolType = matcher.group("protocol"); + instanceUuid = matcher.group("uuid"); } public String getServerVersion() { return serverVersion; } + + public String getProtocolType() { + return protocolType; + } + + public String getInstanceUuid() { + return instanceUuid; + } } diff --git a/src/main/java/org/tarantool/util/ServerVersion.java b/src/main/java/org/tarantool/util/ServerVersion.java new file mode 100644 index 00000000..7f172aeb --- /dev/null +++ b/src/main/java/org/tarantool/util/ServerVersion.java @@ -0,0 +1,141 @@ +package org.tarantool.util; + +import java.util.Objects; + +/** + * Server version holder. + */ +public class ServerVersion implements Comparable { + + public static final ServerVersion V_1_9 = new ServerVersion(1, 9, 0); + public static final ServerVersion V_1_10 = new ServerVersion(1, 10, 0); + public static final ServerVersion V_2_1 = new ServerVersion(2, 1, 0); + public static final ServerVersion V_2_2 = new ServerVersion(2, 2, 0); + public static final ServerVersion V_2_2_1 = new ServerVersion(2, 2, 1); + public static final ServerVersion V_2_3 = new ServerVersion(2, 3, 0); + + private final int majorVersion; + private final int minorVersion; + private final int patchVersion; + + /** + * Makes a parsed server version container from + * a string in format like {@code MAJOR.MINOR.PATCH[-BUILD-gCOMMIT]}. + * + * @param version string in the Tarantool version format. + */ + public ServerVersion(String version) { + String[] parts = splitVersionParts(version); + if (parts.length < 3) { + throw new IllegalArgumentException("Expected at least major, minor, and patch version parts"); + } + this.majorVersion = Integer.parseInt(parts[0]); + this.minorVersion = Integer.parseInt(parts[1]); + this.patchVersion = Integer.parseInt(parts[2]); + } + + public ServerVersion(int majorVersion, + int minorVersion, + int patchVersion) { + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + this.patchVersion = patchVersion; + } + + public int getMajorVersion() { + return majorVersion; + } + + public int getMinorVersion() { + return minorVersion; + } + + public int getPatchVersion() { + return patchVersion; + } + + public boolean isEqual(String versionString) { + return isEqual(new ServerVersion(versionString)); + } + + public boolean isEqual(ServerVersion version) { + return compareTo(version) == 0; + } + + public boolean isLessOrEqualThan(String versionString) { + return isLessOrEqualThan(new ServerVersion(versionString)); + } + + public boolean isLessOrEqualThan(ServerVersion version) { + return compareTo(version) <= 0; + } + + public boolean isGreaterOrEqualThan(String versionString) { + return isGreaterOrEqualThan(new ServerVersion(versionString)); + } + + public boolean isGreaterOrEqualThan(ServerVersion version) { + return compareTo(version) >= 0; + } + + public boolean isGreaterThan(String versionString) { + return isGreaterThan(new ServerVersion(versionString)); + } + + public boolean isGreaterThan(ServerVersion version) { + return compareTo(version) > 0; + } + + public boolean isLessThan(String versionString) { + return isLessThan(new ServerVersion(versionString)); + } + + public boolean isLessThan(ServerVersion version) { + return compareTo(version) < 0; + } + + @Override + public int compareTo(ServerVersion that) { + return Integer.compare(this.toNumber(), that.toNumber()); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + ServerVersion that = (ServerVersion) object; + return majorVersion == that.majorVersion && + minorVersion == that.minorVersion && + patchVersion == that.patchVersion; + } + + @Override + public int hashCode() { + return Objects.hash(majorVersion, minorVersion, patchVersion); + } + + /** + * Translates version parts to format XXXYYYZZZ. + * For example, {@code 1.2.3} translates to number {@code 1002003} + * + * @return version as number + */ + private int toNumber() { + return (majorVersion * 1000 + minorVersion) * 1000 + patchVersion; + } + + /** + * Splits Tarantool version string into parts. + * For example, {@code 2.1.1-423-g4007436aa} => {@code [2, 1, 1, 423, g4007436aa]}. + * + * @param version Tarantool version string + * @return split parts + */ + private String[] splitVersionParts(String version) { + return version.split("[.\\-]"); + } +} diff --git a/src/test/java/org/tarantool/ClientOperationsIT.java b/src/test/java/org/tarantool/ClientOperationsIT.java index 80e795b3..7cdf18b7 100644 --- a/src/test/java/org/tarantool/ClientOperationsIT.java +++ b/src/test/java/org/tarantool/ClientOperationsIT.java @@ -8,6 +8,7 @@ import org.tarantool.schema.TarantoolIndexNotFoundException; import org.tarantool.schema.TarantoolSpaceNotFoundException; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/ServerVersion.java b/src/test/java/org/tarantool/ServerVersion.java deleted file mode 100644 index 42e472fe..00000000 --- a/src/test/java/org/tarantool/ServerVersion.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.tarantool; - -import java.util.function.BiFunction; - -public enum ServerVersion { - - V_1_9("1", "9", "0"), - V_1_10("1", "10", "0"), - V_2_1("2", "1", "0"), - V_2_2("2", "2", "0"), - V_2_2_1("2", "2", "1"), - V_2_3("2", "3", "0"); - - private final String majorVersion; - private final String minorVersion; - private final String patchVersion; - - ServerVersion(String majorVersion, - String minorVersion, String patchVersion) { - this.majorVersion = majorVersion; - this.minorVersion = minorVersion; - this.patchVersion = patchVersion; - } - - public String getMajorVersion() { - return majorVersion; - } - - public String getMinorVersion() { - return minorVersion; - } - - public String getPatchVersion() { - return patchVersion; - } - - public boolean isLessOrEqualThan(String versionString) { - return compareVersions(versionString, (server, minimal) -> server >= minimal); - } - - public boolean isGreaterOrEqualThan(String versionString) { - return compareVersions(versionString, (server, maximal) -> server <= maximal); - } - - public boolean isGreaterThan(String versionString) { - return compareVersions(versionString, (server, maximal) -> server < maximal); - } - - private boolean compareVersions(String versionString, BiFunction comparator) { - int parsedVersion = toNumber(splitVersionParts(versionString)); - int thisVersion = toNumber(new String[] { majorVersion, minorVersion, patchVersion }); - return comparator.apply(parsedVersion, thisVersion); - } - - /** - * Translates version parts to format XXXYYYZZZ. - * For example, {@code 1.2.1} translates to number {@code 1002001} - * - * @param parts version parts - * @return version as number - */ - private int toNumber(String[] parts) { - int version = 0; - for (int i = 0; i < 3; i++) { - version = (version + Integer.parseInt(parts[i])) * 1000; - } - return version / 1000; - } - - /** - * Splits Tarantool version string into parts. - * For example, {@code 2.1.1-423-g4007436aa} => {@code [2, 1, 1, 423, g4007436aa]}. - * - * @param version Tarantool version string - * @return split parts - */ - private String[] splitVersionParts(String version) { - return version.split("[.\\-]"); - } -} diff --git a/src/test/java/org/tarantool/TarantoolClientOpsIT.java b/src/test/java/org/tarantool/TarantoolClientOpsIT.java index a8964b7b..18ade5a9 100644 --- a/src/test/java/org/tarantool/TarantoolClientOpsIT.java +++ b/src/test/java/org/tarantool/TarantoolClientOpsIT.java @@ -12,6 +12,8 @@ import static org.tarantool.TestUtils.toLuaDelete; import static org.tarantool.TestUtils.toLuaSelect; +import org.tarantool.util.ServerVersion; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; diff --git a/src/test/java/org/tarantool/TarantoolSQLOpsIT.java b/src/test/java/org/tarantool/TarantoolSQLOpsIT.java index b1c66992..ae8e5d3d 100644 --- a/src/test/java/org/tarantool/TarantoolSQLOpsIT.java +++ b/src/test/java/org/tarantool/TarantoolSQLOpsIT.java @@ -8,6 +8,8 @@ import static org.tarantool.TestUtils.makeTestClient; import static org.tarantool.TestUtils.openConnection; +import org.tarantool.util.ServerVersion; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; diff --git a/src/test/java/org/tarantool/TestAssumptions.java b/src/test/java/org/tarantool/TestAssumptions.java index 65c235eb..4823b24f 100644 --- a/src/test/java/org/tarantool/TestAssumptions.java +++ b/src/test/java/org/tarantool/TestAssumptions.java @@ -1,5 +1,7 @@ package org.tarantool; +import org.tarantool.util.ServerVersion; + import org.junit.jupiter.api.Assumptions; public class TestAssumptions { diff --git a/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java index d25a0851..bdee3894 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java @@ -6,9 +6,9 @@ import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; import static org.tarantool.jdbc.SqlAssertions.assertSqlExceptionHasStatus; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; import org.tarantool.util.SQLStates; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java index afda6205..35b08f9f 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -7,8 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java index 9f503f79..d2b48921 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java @@ -5,11 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolClientConfig; import org.tarantool.TarantoolOperation; import org.tarantool.TarantoolTestHelper; import org.tarantool.protocol.TarantoolPacket; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java index 3c4ba0f7..c621ae98 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -8,8 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -284,12 +284,14 @@ public void testSupportsResultSetHoldability() throws SQLException { @Test public void testUnwrap() throws SQLException { assertEquals(meta, meta.unwrap(SQLDatabaseMetadata.class)); + assertEquals(meta, meta.unwrap(TarantoolDatabaseMetaData.class)); assertThrows(SQLException.class, () -> meta.unwrap(Integer.class)); } @Test public void testIsWrapperFor() throws SQLException { assertTrue(meta.isWrapperFor(SQLDatabaseMetadata.class)); + assertTrue(meta.isWrapperFor(TarantoolDatabaseMetaData.class)); assertFalse(meta.isWrapperFor(Integer.class)); } @@ -390,4 +392,36 @@ public void testDeleteDetectionSupport() throws SQLException { } } + @Test + void testDatabaseVersion() throws SQLException { + ServerVersion version = new ServerVersion(testHelper.getInstanceVersion()); + assertEquals(version.getMajorVersion(), meta.getDatabaseMajorVersion()); + assertEquals(version.getMinorVersion(), meta.getDatabaseMinorVersion()); + } + + @Test + void testVendorDatabaseVersion() throws SQLException { + ServerVersion version = new ServerVersion(testHelper.getInstanceVersion()); + TarantoolDatabaseMetaData vendorMeta = meta.unwrap(TarantoolDatabaseMetaData.class); + assertEquals(version, vendorMeta.getDatabaseVersion()); + } + + @Test + void testJdbcVersion() throws SQLException { + assertEquals(4, meta.getJDBCMajorVersion()); + assertEquals(2, meta.getJDBCMinorVersion()); + } + + @Test + void testDatabaseProductName() throws SQLException { + assertEquals("Tarantool", meta.getDatabaseProductName()); + } + + @Test + void testDatabaseProductVersion() throws SQLException { + ServerVersion version = new ServerVersion(testHelper.getInstanceVersion()); + ServerVersion databaseProductVersion = new ServerVersion(meta.getDatabaseProductVersion()); + assertEquals(version, databaseProductVersion); + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index 46adb74b..d2c01553 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -14,10 +14,10 @@ import static org.mockito.Mockito.when; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; import org.tarantool.TestUtils; import org.tarantool.util.SQLStates; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java index e9b2884b..2f495a7c 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java @@ -8,9 +8,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; import org.tarantool.util.SQLStates; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java index 62139cdc..e1ef3d32 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java @@ -9,8 +9,8 @@ import static org.tarantool.TestAssumptions.assumeServerVersionLessThan; import static org.tarantool.TestAssumptions.assumeServerVersionOutOfRange; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 171eaddc..d8d5e0ad 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -8,10 +8,10 @@ import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; import static org.tarantool.jdbc.SqlAssertions.assertSqlExceptionHasStatus; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; import org.tarantool.TestUtils; import org.tarantool.util.SQLStates; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java b/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java index f84d4367..8d0b8e50 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcTypesIT.java @@ -8,9 +8,9 @@ import static org.tarantool.TestAssumptions.assumeServerVersionLessThan; import static org.tarantool.TestUtils.fromHex; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; import org.tarantool.jdbc.type.TarantoolSqlType; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java b/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java index 4d9297a0..dc0786e4 100644 --- a/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java +++ b/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java @@ -9,9 +9,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; import org.tarantool.jdbc.SQLProperty; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; diff --git a/src/test/java/org/tarantool/schema/ClientSchemaIT.java b/src/test/java/org/tarantool/schema/ClientSchemaIT.java index 214c599c..10a11687 100644 --- a/src/test/java/org/tarantool/schema/ClientSchemaIT.java +++ b/src/test/java/org/tarantool/schema/ClientSchemaIT.java @@ -6,13 +6,13 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.tarantool.TestUtils.makeDefaultClientConfig; -import org.tarantool.ServerVersion; import org.tarantool.TarantoolClientConfig; import org.tarantool.TarantoolClientImpl; import org.tarantool.TarantoolTestHelper; import org.tarantool.TestAssumptions; import org.tarantool.schema.TarantoolIndexMeta.IndexOptions; import org.tarantool.schema.TarantoolIndexMeta.IndexPart; +import org.tarantool.util.ServerVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; From 02bf9ad6b9d2e38eedaaf92e477718fb68f1745b Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Sun, 1 Dec 2019 16:38:14 +0700 Subject: [PATCH 2/4] iproto: add PREPARE operation Tarantool introduced a new PREPARE protocol operation to be able to create a prepared statement on the server side. This can be used by JDBC SQLPreparedStatement implementation that will prepare its query in advance to get meta data as well as reuse it multiple times. These JDBC features will be introduced in future commits. This commit add all the required protocol constants and allow TarantoolClient to abstract from concrete operation codes related to SQL. Because the native client does not provide API to perform PREPARE it does not handle it in completeSql() as a completely unexpected operation here. Follows on: #198 --- src/main/java/org/tarantool/Code.java | 2 ++ src/main/java/org/tarantool/Key.java | 3 +++ src/main/java/org/tarantool/TarantoolClientImpl.java | 2 +- src/main/java/org/tarantool/TarantoolOperation.java | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/tarantool/Code.java b/src/main/java/org/tarantool/Code.java index 38cea521..59dcf657 100644 --- a/src/main/java/org/tarantool/Code.java +++ b/src/main/java/org/tarantool/Code.java @@ -13,6 +13,8 @@ public enum Code { UPSERT(9), CALL(10), EXECUTE(11), + PREPARE(13), + DEALLOCATE(13), PING(64), SUBSCRIBE(66); diff --git a/src/main/java/org/tarantool/Key.java b/src/main/java/org/tarantool/Key.java index f4a7d57e..0a07a5ba 100644 --- a/src/main/java/org/tarantool/Key.java +++ b/src/main/java/org/tarantool/Key.java @@ -29,10 +29,13 @@ public enum Key implements Callable { SQL_FIELD_TYPE(0x1), SQL_METADATA(0x32), + SQL_BIND_METADATA(0x33), + SQL_BIND_COUNT(0x34), SQL_TEXT(0x40), SQL_BIND(0x41), SQL_OPTIONS(0x42), SQL_INFO(0x42), + SQL_STATEMENT_ID(0x43), SQL_ROW_COUNT(0x00), SQL_INFO_AUTOINCREMENT_IDS(0x01); diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index ae1d7360..b43caae8 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -554,7 +554,7 @@ protected void complete(TarantoolPacket packet, TarantoolOperation operation) { } catch (TarantoolSchemaException cause) { fail(target, cause); } - } else if (operation.getCode() == Code.EXECUTE) { + } else if (operation.isSqlRelated()) { completeSql(operation, packet); } else { ((CompletableFuture) result).complete(packet.getData()); diff --git a/src/main/java/org/tarantool/TarantoolOperation.java b/src/main/java/org/tarantool/TarantoolOperation.java index 0707d920..1b4ed12d 100644 --- a/src/main/java/org/tarantool/TarantoolOperation.java +++ b/src/main/java/org/tarantool/TarantoolOperation.java @@ -113,6 +113,10 @@ public Code getCode() { return code; } + public boolean isSqlRelated() { + return code == Code.EXECUTE || code == Code.PREPARE; + } + public TarantoolOperation getDependedOperation() { return dependedOperation; } From 5b6524e31599df1346cb53838b7ea878a1a06af3 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Sun, 1 Dec 2019 18:11:24 +0700 Subject: [PATCH 3/4] jdbc: support server prepared statements Tarantool supports prepared statements since 2.3.1 that can be used by java.sql.PreparedStatement to prepare the underlying query in advance. If the driver connects to Tarantool which supports the prepared statements then each PreparedStatement object acts as a corresponding server prepared statement in terms of its behaviour (including expiration or invalidation of statements - i.e. after DDL operations). It also makes possible to obtain statement meta data via getMetaData and getParameterMetaData without having to execute the query. This commit extends TarantoolConnection API by new `prepare` and `deallocate` methods that allow to deal with server prepared statements. It caused a small reworking of a query execution process in SQLConnection where QueryCommand abstraction was introduced. Another extension is support of PreparedStatement that works in two different modes now: legacy mode when server prepared statements are not available (driver will send sql text + parameters as a separate queries when each execution is performed) and new mode when they are supported (each PreparedStatement covers the server prepared statement and sends statement id + parameters). There are a few issues on using the prepared statement now. The first, it is required to deallocate prepared statements explicitly, (no an eviction strategy on the server side) so PreparedStatement tries to perform it when close() method is called. It causes the second issue. The second, Tarantool does not distinguish query duplicates within one session that can cause unexpected user experience. Let's say there are two PreparedStatements that were prepared using the same query string. If one of them closes it makes another statement broken because of they reused the same session prepared statement. To overcome this issue the driver connection tracks its own statements references. When the particular statement reference count reaches zero it will safely unprepared it on the server side. The third, the prepared statement is invalidated by DDL queries or the disconnection. Tarantool does not support auto re-preparing after DDL operation at this moment, so it requires to be re-prepared for all cached statements. Here, the PreparedStatement repeats behaviour of Lua implementation for `box` module - returns an error when it expired or deleted. Closes: #198 --- .../java/org/tarantool/SqlProtoUtils.java | 15 +- .../org/tarantool/TarantoolClientImpl.java | 2 +- .../org/tarantool/jdbc/SQLConnection.java | 276 ++++++++++++++--- .../org/tarantool/jdbc/SQLPreparedHolder.java | 69 +++++ .../tarantool/jdbc/SQLPreparedStatement.java | 83 ++++- .../org/tarantool/jdbc/SQLQueryHolder.java | 18 +- .../java/org/tarantool/jdbc/SQLStatement.java | 26 +- .../tarantool/jdbc/TarantoolConnection.java | 26 ++ .../jdbc/JdbcClosedConnectionIT.java | 2 +- .../org/tarantool/jdbc/JdbcConnectionIT.java | 20 +- .../jdbc/JdbcExceptionHandlingTest.java | 16 +- .../jdbc/JdbcPreparedStatementIT.java | 290 ++++++++++++++++-- 12 files changed, 732 insertions(+), 111 deletions(-) create mode 100644 src/main/java/org/tarantool/jdbc/SQLPreparedHolder.java diff --git a/src/main/java/org/tarantool/SqlProtoUtils.java b/src/main/java/org/tarantool/SqlProtoUtils.java index 9c10eb24..5a6af947 100644 --- a/src/main/java/org/tarantool/SqlProtoUtils.java +++ b/src/main/java/org/tarantool/SqlProtoUtils.java @@ -30,8 +30,17 @@ public static List> getSQLData(TarantoolPacket pack) { return (List>) pack.getBody().get(Key.DATA.getId()); } + public static List getSQLBindMetadata(TarantoolPacket pack) { + return getMetadata(pack, Key.SQL_BIND_METADATA); + } + public static List getSQLMetadata(TarantoolPacket pack) { - List> meta = (List>) pack.getBody().get(Key.SQL_METADATA.getId()); + return getMetadata(pack, Key.SQL_METADATA); + } + + private static List getMetadata(TarantoolPacket pack, Key targetKey) { + List> meta = (List>) pack.getBody() + .getOrDefault(targetKey.getId(), Collections.emptyList()); List values = new ArrayList<>(meta.size()); for (Map item : meta) { values.add(new SQLMetaData( @@ -42,6 +51,10 @@ public static List getSQLMetadata(TarantoolPacket pack) { return values; } + public static Long getStatementId(TarantoolPacket pack) { + return ((Number) pack.getBody().get(Key.SQL_STATEMENT_ID.getId())).longValue(); + } + public static Long getSQLRowCount(TarantoolPacket pack) { Map info = (Map) pack.getBody().get(Key.SQL_INFO.getId()); Number rowCount; diff --git a/src/main/java/org/tarantool/TarantoolClientImpl.java b/src/main/java/org/tarantool/TarantoolClientImpl.java index b43caae8..6e589eb2 100644 --- a/src/main/java/org/tarantool/TarantoolClientImpl.java +++ b/src/main/java/org/tarantool/TarantoolClientImpl.java @@ -763,7 +763,7 @@ public TarantoolClientOps, Object, TupleTwo, Long>> uns return unsafeSchemaOps; } - protected TarantoolRequest makeSqlRequest(String sql, List bind) { + private TarantoolRequest makeSqlRequest(String sql, List bind) { return new TarantoolRequest( Code.EXECUTE, TarantoolRequestArgumentFactory.value(Key.SQL_TEXT), diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index 327d0d69..708beeb6 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -1,12 +1,15 @@ package org.tarantool.jdbc; +import org.tarantool.Code; import org.tarantool.CommunicationException; +import org.tarantool.Key; import org.tarantool.SocketChannelProvider; import org.tarantool.SqlProtoUtils; import org.tarantool.TarantoolClientConfig; import org.tarantool.TarantoolClientImpl; import org.tarantool.TarantoolOperation; import org.tarantool.TarantoolRequest; +import org.tarantool.TarantoolRequestArgumentFactory; import org.tarantool.protocol.TarantoolPacket; import org.tarantool.util.JdbcConstants; import org.tarantool.util.SQLStates; @@ -45,7 +48,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; -import java.util.function.Function; /** * Tarantool {@link Connection} implementation. @@ -60,6 +62,18 @@ public class SQLConnection implements TarantoolConnection { private final SQLTarantoolClientImpl client; private final String url; private final Properties properties; + + /** + * Tarantool v2.3.1 does not take into consideration + * query duplicates within one session. Each connection + * tracks such statements to know when a statement can + * be safely removed. + * + * @see #prepare(long, String) + * @see #deallocate(long, Long) + */ + private final Map preparedStatementReferences = new HashMap<>(); + private DatabaseMetaData cachedMetadata; private int resultSetHoldability = UNSET_HOLDABILITY; @@ -530,9 +544,10 @@ public int getNetworkTimeout() throws SQLException { @Override public SQLResultHolder execute(long timeout, SQLQueryHolder query) throws SQLException { checkNotClosed(); + ExecuteQueryCommand action = new ExecuteQueryCommand(query, client.sqlRawOps()); return (useNetworkTimeout(timeout)) - ? executeWithNetworkTimeout(query) - : executeWithQueryTimeout(timeout, query); + ? executeWithNetworkTimeout(action) + : executeWithQueryTimeout(timeout, action); } @Override @@ -540,11 +555,35 @@ public SQLBatchResultHolder executeBatch(long timeout, List quer throws SQLException { checkNotClosed(); SQLTarantoolClientImpl.SQLRawOps sqlOps = client.sqlRawOps(); - SQLBatchResultHolder batchResult = useNetworkTimeout(timeout) - ? sqlOps.executeBatch(queries) - : sqlOps.executeBatch(timeout, queries); + long time = useNetworkTimeout(timeout) ? 0L : timeout; + return sqlOps.executeBatch(time, queries); + } + + @Override + public SQLPreparedHolder prepare(long timeout, String sqlText) throws SQLException { + checkNotClosed(); + synchronized (preparedStatementReferences) { + SQLPreparedHolder holder = executeCommand(timeout, new PrepareQueryCommand(sqlText, client.sqlRawOps())); + PreparedStatementReference reference = preparedStatementReferences.get(holder.getStatementId()); + if (reference == null) { + reference = new PreparedStatementReference(holder.getStatementId()); + preparedStatementReferences.put(holder.getStatementId(), reference); + } + reference.counter++; + return holder; + } + } - return batchResult; + @Override + public void deallocate(long timeout, Long statementId) throws SQLException { + checkNotClosed(); + synchronized (preparedStatementReferences) { + PreparedStatementReference reference = preparedStatementReferences.get(statementId); + if (reference != null && --reference.counter == 0) { + executeCommand(timeout, new DeallocateQueryCommand(statementId, client.sqlRawOps())); + preparedStatementReferences.remove(statementId); + } + } } private boolean useNetworkTimeout(long timeout) throws SQLException { @@ -552,37 +591,56 @@ private boolean useNetworkTimeout(long timeout) throws SQLException { return timeout == 0 || (networkTimeout > 0 && networkTimeout < timeout); } - private SQLResultHolder executeWithNetworkTimeout(SQLQueryHolder query) throws SQLException { + private R executeCommand(long timeout, QueryCommand action) throws SQLException { + return (useNetworkTimeout(timeout)) + ? executeWithNetworkTimeout(action) + : executeWithQueryTimeout(timeout, action); + } + + /** + * Executes a query command using a predefined network timeout. + * + * @param action command to be executed + * @param result of the action + * + * @return query result + * + * @throws SQLException if any execution errors occur + */ + private R executeWithNetworkTimeout(QueryCommand action) throws SQLException { try { - return client.sqlRawOps().execute(query); + return action.execute(0L); } catch (Exception e) { handleException(e); - throw new SQLException(formatError(query), e); + throw new SQLException(formatError(action.getQuery()), e); } } /** - * Executes a query using a custom timeout. + * Executes a query command using a custom timeout. + * In contrast to {@link #executeWithNetworkTimeout(QueryCommand)} + * it provides handling of timeout errors by wrapping them via {@link StatementTimeoutException}. * - * @param timeout query timeout - * @param query query + * @param timeout command timeout + * @param action command to be executed + * @param result of the action * - * @return SQL result holder + * @return action result * * @throws StatementTimeoutException if query execution took more than query timeout * @throws SQLException if any other errors occurred */ - private SQLResultHolder executeWithQueryTimeout(long timeout, SQLQueryHolder query) throws SQLException { + private R executeWithQueryTimeout(long timeout, QueryCommand action) throws SQLException { try { - return client.sqlRawOps().execute(timeout, query); + return action.execute(timeout); } catch (Exception e) { // statement timeout should not affect the current connection // but can be handled by the caller side if (e.getCause() instanceof TimeoutException) { - throw new StatementTimeoutException(formatError(query), e.getCause()); + throw new StatementTimeoutException(formatError(action.getQuery()), e.getCause()); } handleException(e); - throw new SQLException(formatError(query), e); + throw new SQLException(formatError(action.getQuery()), e); } } @@ -736,43 +794,71 @@ private static String formatError(SQLQueryHolder query) { static class SQLTarantoolClientImpl extends TarantoolClientImpl { - private Future executeQuery(SQLQueryHolder queryHolder) { - return exec(makeSqlRequest(queryHolder.getQuery(), queryHolder.getParams())); + private Future executeQuery(SQLQueryHolder query, long timeoutMillis) { + boolean prepared = query.isPrepared(); + TarantoolRequest request = new TarantoolRequest( + Code.EXECUTE, + TarantoolRequestArgumentFactory.value(prepared ? Key.SQL_STATEMENT_ID : Key.SQL_TEXT), + TarantoolRequestArgumentFactory.value(prepared ? query.getStatementId() : query.getQuery()), + TarantoolRequestArgumentFactory.value(Key.SQL_BIND), + TarantoolRequestArgumentFactory.value(query.getParams()) + ); + if (timeoutMillis > 0) { + request.setTimeout(Duration.of(timeoutMillis, ChronoUnit.MILLIS)); + } + return exec(request); } - private Future executeQuery(SQLQueryHolder queryHolder, long timeoutMillis) { - TarantoolRequest request = makeSqlRequest(queryHolder.getQuery(), queryHolder.getParams()); - request.setTimeout(Duration.of(timeoutMillis, ChronoUnit.MILLIS)); - return exec(request); + private Future prepareStatement(String queryText, long timeoutMillis) { + TarantoolRequest prepareRequest = new TarantoolRequest( + Code.PREPARE, + TarantoolRequestArgumentFactory.value(Key.SQL_TEXT), + TarantoolRequestArgumentFactory.value(queryText) + ); + if (timeoutMillis > 0) { + prepareRequest.setTimeout(Duration.ofMillis(timeoutMillis)); + } + return exec(prepareRequest); } - final SQLRawOps sqlRawOps = new SQLRawOps() { - @Override - public SQLResultHolder execute(SQLQueryHolder query) { - return (SQLResultHolder) syncGet(executeQuery(query)); + private Future deallocateStatement(long statementId, long timeoutMillis) { + TarantoolRequest prepareRequest = new TarantoolRequest( + Code.DEALLOCATE, + TarantoolRequestArgumentFactory.value(Key.SQL_STATEMENT_ID), + TarantoolRequestArgumentFactory.value(statementId) + ); + if (timeoutMillis > 0) { + prepareRequest.setTimeout(Duration.ofMillis(timeoutMillis)); } + return exec(prepareRequest); + } + final SQLRawOps sqlRawOps = new SQLRawOps() { @Override public SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query) { return (SQLResultHolder) syncGet(executeQuery(query, timeoutMillis)); } @Override - public SQLBatchResultHolder executeBatch(List queries) { - return executeInternal(queries, (query) -> executeQuery(query)); + public SQLBatchResultHolder executeBatch(long timeoutMillis, List query) { + return executeBatchInternal(timeoutMillis, query); + } + + @Override + public SQLPreparedHolder prepare(long timeoutMillis, String sqlQuery) { + return (SQLPreparedHolder) syncGet(prepareStatement(sqlQuery, timeoutMillis)); } @Override - public SQLBatchResultHolder executeBatch(long timeoutMillis, List queries) { - return executeInternal(queries, (query) -> executeQuery(query, timeoutMillis)); + public void deallocate(long timeoutMillis, long statementId) { + syncGet(deallocateStatement(statementId, timeoutMillis)); } - private SQLBatchResultHolder executeInternal(List queries, - Function> fetcher) { + private SQLBatchResultHolder executeBatchInternal(long timeout, List queries) { List> sqlFutures = new ArrayList<>(); // using queries pipelining to emulate a batch request for (SQLQueryHolder query : queries) { - sqlFutures.add(fetcher.apply(query)); + sqlFutures.add(executeQuery(query, timeout)); } // wait for all the results Exception lastError = null; @@ -813,25 +899,127 @@ SQLRawOps sqlRawOps() { @Override protected void completeSql(TarantoolOperation operation, TarantoolPacket pack) { - Long rowCount = SqlProtoUtils.getSQLRowCount(pack); - SQLResultHolder result = (rowCount == null) - ? SQLResultHolder.ofQuery(SqlProtoUtils.getSQLMetadata(pack), SqlProtoUtils.getSQLData(pack)) - : SQLResultHolder.ofUpdate(rowCount.intValue(), SqlProtoUtils.getSQLAutoIncrementIds(pack)); - ((CompletableFuture) operation.getResult()).complete(result); + if (operation.getCode() == Code.PREPARE) { + SQLPreparedHolder result = new SQLPreparedHolder( + SqlProtoUtils.getStatementId(pack), + SqlProtoUtils.getSQLMetadata(pack), + SqlProtoUtils.getSQLBindMetadata(pack) + ); + ((CompletableFuture) operation.getResult()).complete(result); + } else if (operation.getCode() == Code.DEALLOCATE) { + operation.getResult().complete(null); + } else { + Long rowCount = SqlProtoUtils.getSQLRowCount(pack); + SQLResultHolder result = (rowCount == null) + ? SQLResultHolder.ofQuery(SqlProtoUtils.getSQLMetadata(pack), SqlProtoUtils.getSQLData(pack)) + : SQLResultHolder.ofUpdate(rowCount.intValue(), SqlProtoUtils.getSQLAutoIncrementIds(pack)); + ((CompletableFuture) operation.getResult()).complete(result); + } } interface SQLRawOps { - SQLResultHolder execute(SQLQueryHolder query); - SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query); - SQLBatchResultHolder executeBatch(List queries); - SQLBatchResultHolder executeBatch(long timeoutMillis, List queries); + SQLPreparedHolder prepare(long timeoutMillis, String sqlQuery); + + void deallocate(long timeoutMillis, long statementId); } } + private interface QueryCommand { + + R execute(long timeoutInMillis); + + SQLQueryHolder getQuery(); + + } + + private static final class ExecuteQueryCommand implements QueryCommand { + + private SQLQueryHolder query; + private SQLTarantoolClientImpl.SQLRawOps operations; + + public ExecuteQueryCommand(SQLQueryHolder query, SQLTarantoolClientImpl.SQLRawOps operations) { + this.query = query; + this.operations = operations; + } + + @Override + public SQLResultHolder execute(long timeoutInMillis) { + return operations.execute(timeoutInMillis, query); + } + + @Override + public SQLQueryHolder getQuery() { + return query; + } + + } + + private static final class PrepareQueryCommand implements QueryCommand { + + private String text; + private SQLQueryHolder dummyQuery; + private SQLTarantoolClientImpl.SQLRawOps operations; + + public PrepareQueryCommand(String text, SQLTarantoolClientImpl.SQLRawOps operations) { + // Tarantool does not support direct SQL syntax to PREPARE + // server statements so driver generate a fake query for possible error output + this.text = text; + this.dummyQuery = SQLQueryHolder.of("/* Auto-generated query */ PREPARE x AS " + text); + this.operations = operations; + } + + @Override + public SQLPreparedHolder execute(long timeoutInMillis) { + return operations.prepare(timeoutInMillis, text); + } + + @Override + public SQLQueryHolder getQuery() { + return dummyQuery; + } + + } + + private static final class DeallocateQueryCommand implements QueryCommand { + + private long statementId; + private SQLQueryHolder dummyQuery; + private SQLTarantoolClientImpl.SQLRawOps operations; + + public DeallocateQueryCommand(long statementId, SQLTarantoolClientImpl.SQLRawOps operations) { + // Tarantool does not support direct SQL syntax to DEALLOCATE + // server statements so driver generate a fake query for possible error output + this.dummyQuery = SQLQueryHolder.of("/* Auto-generated query */ DEALLOCATE PREPARE " + statementId); + this.statementId = statementId; + this.operations = operations; + } + + @Override + public Void execute(long timeoutInMillis) { + operations.deallocate(timeoutInMillis, statementId); + return null; + } + + @Override + public SQLQueryHolder getQuery() { + return dummyQuery; + } + + } + + private static class PreparedStatementReference { + long statementId; + long counter; + + public PreparedStatementReference(long statementId) { + this.statementId = statementId; + } + } + } diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedHolder.java b/src/main/java/org/tarantool/jdbc/SQLPreparedHolder.java new file mode 100644 index 00000000..47e8b1a9 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedHolder.java @@ -0,0 +1,69 @@ +package org.tarantool.jdbc; + +import org.tarantool.SqlProtoUtils; + +import java.util.List; + +/** + * A wrapper that is used to hold parameters metadata + * as well as result set metadata of a server prepared statement. + *

+ * For instance, a DQL statement like {@code SELECT a, b FROM t1 WHERE a > ? AND b = ?} + * after preparation may have the following structure using YAML notation: + * + *

+ * {@code
+ * ---
+ * - stmt_id: 3209603265
+ *   metadata:
+ *   - name: A
+ *     type: integer
+ *   - name: B
+ *     type: string
+ *   params:
+ *   - name: '?'
+ *     type: ANY
+ *   - name: '?'
+ *     type: ANY
+ *   param_count: 2
+ * ...}
+ * 
+ * + *

+ * In case of DML statements the {@code metadata part} will be skipped. + */ +public class SQLPreparedHolder { + + private final Long statementId; + private final List resultMetadata; + private final List paramsMetadata; + + public SQLPreparedHolder(Long statementId, + List resultMetadata, + List paramsMetadata) { + this.statementId = statementId; + this.resultMetadata = resultMetadata; + this.paramsMetadata = paramsMetadata; + } + + public Long getStatementId() { + return statementId; + } + + public List getResultMetadata() { + return resultMetadata; + } + + public List getParamsMetadata() { + return paramsMetadata; + } + + @Override + public String toString() { + return "SQLPreparedHolder{" + + "statementId='" + statementId + '\'' + + ", resultMetadata=" + resultMetadata + + ", paramsMetadata=" + paramsMetadata + + '}'; + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index 342bb74d..c86176f3 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -1,6 +1,7 @@ package org.tarantool.jdbc; import org.tarantool.util.SQLStates; +import org.tarantool.util.ServerVersion; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -37,9 +38,12 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem private static final int STREAM_WRITE_CHUNK_SIZE = 4096; private final String sql; + private long statementId; + private final Map parameters; private final int autoGeneratedKeys; private List> batchParameters = new ArrayList<>(); + private ResultSetMetaData resultSetMetaData; public SQLPreparedStatement(SQLConnection connection, String sql, int autoGeneratedKeys) throws SQLException { super(connection); @@ -47,6 +51,7 @@ public SQLPreparedStatement(SQLConnection connection, String sql, int autoGenera this.parameters = new HashMap<>(); this.autoGeneratedKeys = autoGeneratedKeys; setPoolable(true); + prepareQuery(sql); } public SQLPreparedStatement(SQLConnection connection, @@ -59,12 +64,34 @@ public SQLPreparedStatement(SQLConnection connection, this.parameters = new HashMap<>(); this.autoGeneratedKeys = NO_GENERATED_KEYS; setPoolable(true); + prepareQuery(sql); + } + + private void prepareQuery(String sql) throws SQLException { + TarantoolDatabaseMetaData metaData = connection.getMetaData().unwrap(TarantoolDatabaseMetaData.class); + ServerVersion databaseVersion = metaData.getDatabaseVersion(); + if (databaseVersion.isLessThan(ServerVersion.V_2_3)) { + return; + } + SQLPreparedHolder preparedHolder = connection.prepare(0, sql); + statementId = preparedHolder.getStatementId(); + if (!preparedHolder.getResultMetadata().isEmpty()) { + resultSetMetaData = new SQLResultSetMetaData(preparedHolder.getResultMetadata(), connection.isReadOnly()); + } + } + + @Override + protected void cleanUp() throws SQLException { + super.cleanUp(); + if (statementId != 0) { + connection.deallocate(0L, statementId); + } } @Override public ResultSet executeQuery() throws SQLException { checkNotClosed(); - if (!executeInternal(autoGeneratedKeys, sql, toParametersList(parameters))) { + if (!executeInternal(autoGeneratedKeys, SQLQueryHolder.of(statementId, sql, toParametersList(parameters)))) { throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); } return resultSet; @@ -79,7 +106,7 @@ public ResultSet executeQuery(String sql) throws SQLException { @Override public int executeUpdate() throws SQLException { checkNotClosed(); - if (executeInternal(autoGeneratedKeys, sql, toParametersList(parameters))) { + if (executeInternal(autoGeneratedKeys, SQLQueryHolder.of(statementId, sql, toParametersList(parameters)))) { throw new SQLException( "Result was returned but nothing was expected", SQLStates.TOO_MANY_RESULTS.getSqlState() @@ -94,6 +121,24 @@ public int executeUpdate(String sql) throws SQLException { throw new SQLException(INVALID_CALL_MESSAGE); } + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); + } + @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { setParameter(parameterIndex, null); @@ -253,7 +298,7 @@ private void setParameter(int parameterIndex, Object value) throws SQLException @Override public boolean execute() throws SQLException { checkNotClosed(); - return executeInternal(autoGeneratedKeys, sql, toParametersList(parameters)); + return executeInternal(autoGeneratedKeys, SQLQueryHolder.of(statementId, sql, toParametersList(parameters))); } @Override @@ -262,6 +307,24 @@ public boolean execute(String sql) throws SQLException { throw new SQLException(INVALID_CALL_MESSAGE); } + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); + } + @Override public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { setCharacterStream(parameterIndex, reader, (long) length); @@ -320,13 +383,16 @@ public void setArray(int parameterIndex, Array x) throws SQLException { @Override public ResultSetMetaData getMetaData() throws SQLException { + checkNotClosed(); if (resultSet != null && !resultSet.isClosed()) { return resultSet.getMetaData(); } - // XXX: it's required a support of dry-run mode to obtain - // a statement metadata without real query execution. - // see https://github.com/tarantool/tarantool/issues/3292 - return null; + if (statementId == 0) { + throw new SQLFeatureNotSupportedException("Current connected Tararntool instance does not support server " + + "prepared statements. Ensure the driver connects to Tarantool 2.3.1 or above to be " + + "able to retrieve meta data without having to execute the statement before."); + } + return resultSetMetaData; } @Override @@ -395,10 +461,11 @@ public void addBatch() throws SQLException { @Override public int[] executeBatch() throws SQLException { checkNotClosed(); + discardLastResults(); try { List queries = new ArrayList<>(); for (Map p : batchParameters) { - SQLQueryHolder of = SQLQueryHolder.of(sql, toParametersList(p)); + SQLQueryHolder of = SQLQueryHolder.of(statementId, sql, toParametersList(p)); queries.add(of); } return executeBatchInternal(queries); diff --git a/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java b/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java index e02e1ea7..1c833e84 100644 --- a/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java +++ b/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java @@ -5,14 +5,20 @@ public class SQLQueryHolder { + private final long statementId; private final String query; private final List params; public static SQLQueryHolder of(String query, Object... params) { - return new SQLQueryHolder(query, Arrays.asList(params)); + return new SQLQueryHolder(0L, query, Arrays.asList(params)); } - private SQLQueryHolder(String query, List params) { + public static SQLQueryHolder of(long statementId, String query, Object... params) { + return new SQLQueryHolder(statementId, query, Arrays.asList(params)); + } + + private SQLQueryHolder(Long statementId, String query, List params) { + this.statementId = statementId; this.query = query; this.params = params; } @@ -21,8 +27,16 @@ public String getQuery() { return query; } + public Long getStatementId() { + return statementId; + } + public List getParams() { return params; } + public boolean isPrepared() { + return statementId != 0; + } + } diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index e8959234..568caa4c 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -61,7 +61,7 @@ public class SQLStatement implements TarantoolStatement { /** * Hint to the statement pool implementation indicating * whether the application wants the statement to be pooled. - * + *

* Ignored. */ private boolean poolable; @@ -91,7 +91,7 @@ protected SQLStatement(SQLConnection sqlConnection, @Override public ResultSet executeQuery(String sql) throws SQLException { checkNotClosed(); - if (!executeInternal(NO_GENERATED_KEYS, sql)) { + if (!executeInternal(NO_GENERATED_KEYS, SQLQueryHolder.of(sql))) { throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); } return resultSet; @@ -106,7 +106,7 @@ public int executeUpdate(String sql) throws SQLException { public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { checkNotClosed(); JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); - if (executeInternal(autoGeneratedKeys, sql)) { + if (executeInternal(autoGeneratedKeys, SQLQueryHolder.of(sql))) { throw new SQLException( "Result was returned but nothing was expected", SQLStates.TOO_MANY_RESULTS.getSqlState() @@ -128,11 +128,15 @@ public int executeUpdate(String sql, String[] columnNames) throws SQLException { @Override public void close() throws SQLException { if (isClosed.compareAndSet(false, true)) { - cancel(); - discardLastResults(); + cleanUp(); } } + protected void cleanUp() throws SQLException { + cancel(); + discardLastResults(); + } + @Override public int getMaxFieldSize() throws SQLException { return maxFieldSize; @@ -208,14 +212,14 @@ public void setCursorName(String name) throws SQLException { @Override public boolean execute(String sql) throws SQLException { checkNotClosed(); - return executeInternal(NO_GENERATED_KEYS, sql); + return executeInternal(NO_GENERATED_KEYS, SQLQueryHolder.of(sql)); } @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { checkNotClosed(); JdbcConstants.checkGeneratedKeysConstant(autoGeneratedKeys); - return executeInternal(autoGeneratedKeys, sql); + return executeInternal(autoGeneratedKeys, SQLQueryHolder.of(sql)); } @Override @@ -432,20 +436,18 @@ protected void discardLastResults() throws SQLException { * one of the following constants: * {@code Statement.RETURN_GENERATED_KEYS}, * {@code Statement.NO_GENERATED_KEYS} - * @see #getGeneratedKeys() - * @param sql query - * @param params optional params + * @param query query data * * @return {@code true}, if the result is a ResultSet object; * * @throws SQLException if this method is called on a closed * {@code Statement} */ - protected boolean executeInternal(int autoGeneratedKeys, String sql, Object... params) throws SQLException { + protected boolean executeInternal(int autoGeneratedKeys, SQLQueryHolder query) throws SQLException { discardLastResults(); SQLResultHolder holder; try { - holder = connection.execute(timeout, SQLQueryHolder.of(sql, params)); + holder = connection.execute(timeout, query); } catch (StatementTimeoutException e) { cancel(); throw new SQLTimeoutException(); diff --git a/src/main/java/org/tarantool/jdbc/TarantoolConnection.java b/src/main/java/org/tarantool/jdbc/TarantoolConnection.java index 6354f508..9872b4dc 100644 --- a/src/main/java/org/tarantool/jdbc/TarantoolConnection.java +++ b/src/main/java/org/tarantool/jdbc/TarantoolConnection.java @@ -37,4 +37,30 @@ public interface TarantoolConnection extends Connection { SQLBatchResultHolder executeBatch(long timeout, List queries) throws SQLException; + /** + * Prepares SQL query and obtain prepared statement metadata. + * It requires Tarantool 2.3.1 or above. + * + * @param timeout query timeout + * @param sqlText query string to be prepared + * + * @return metadata of the prepared statement + * + * @throws SQLException if errors occur while the query is being performed. + * {@link java.sql.SQLTimeoutException} is raised when execution time exceeds the timeout + */ + SQLPreparedHolder prepare(long timeout, String sqlText) throws SQLException; + + /** + * Removes a previously prepared SQL server statement. + * It requires Tarantool 2.3.1 or above. + * + * @param timeout query timeout + * @param statementId server statement ID + * + * @throws SQLException if errors occur while the query is being performed. + * {@link java.sql.SQLTimeoutException} is raised when execution time exceeds the timeout + */ + void deallocate(long timeout, Long statementId) throws SQLException; + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java index bdee3894..cd83015f 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcClosedConnectionIT.java @@ -94,7 +94,7 @@ public void execute() throws Throwable { @Test public void testPreparedStatement() throws SQLException { - PreparedStatement preparedStatement = connection.prepareStatement("TEST"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT 1"); connection.close(); int i = 0; diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java index 35b08f9f..81ae1519 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -70,7 +70,7 @@ public void testCreateStatement() throws SQLException { @Test public void testPrepareStatement() throws SQLException { - PreparedStatement prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES(?, ?)"); + PreparedStatement prep = conn.prepareStatement("SELECT 1"); assertNotNull(prep); prep.close(); } @@ -185,7 +185,7 @@ public void testCreateWrongHoldableStatement() throws SQLException { @Test public void testPrepareHoldableStatement() throws SQLException { - String sqlString = "TEST"; + String sqlString = "SELECT 2"; Statement statement = conn.prepareStatement(sqlString); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); @@ -205,7 +205,7 @@ public void testPrepareHoldableStatement() throws SQLException { public void testPrepareUnsupportedHoldableStatement() throws SQLException { assertThrows(SQLFeatureNotSupportedException.class, () -> { - String sqlString = "SELECT * FROM TEST"; + String sqlString = "SELECT 3"; conn.prepareStatement( sqlString, ResultSet.TYPE_FORWARD_ONLY, @@ -217,7 +217,7 @@ public void testPrepareUnsupportedHoldableStatement() throws SQLException { @Test public void testPrepareWrongHoldableStatement() throws SQLException { - String sqlString = "SELECT * FROM TEST"; + String sqlString = "SELECT 1"; assertThrows(SQLException.class, () -> { conn.prepareStatement( @@ -289,7 +289,7 @@ public void testCreateWrongScrollableStatement() { @Test public void testPrepareScrollableStatement() throws SQLException { - String sqlString = "TEST"; + String sqlString = "SELECT 2"; Statement statement = conn.prepareStatement(sqlString); assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); @@ -308,11 +308,11 @@ public void testPrepareScrollableStatement() throws SQLException { @Test public void testPrepareUnsupportedScrollableStatement() throws SQLException { assertThrows(SQLFeatureNotSupportedException.class, () -> { - String sqlString = "SELECT * FROM TEST"; + String sqlString = "SELECT 1"; conn.prepareStatement(sqlString, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); }); assertThrows(SQLFeatureNotSupportedException.class, () -> { - String sqlString = "SELECT * FROM TEST"; + String sqlString = "SELECT 2"; conn.prepareStatement( sqlString, ResultSet.TYPE_SCROLL_SENSITIVE, @@ -324,7 +324,7 @@ public void testPrepareUnsupportedScrollableStatement() throws SQLException { @Test public void testPrepareWrongScrollableStatement() throws SQLException { - String sqlString = "SELECT * FROM TEST"; + String sqlString = "SELECT 1"; assertThrows(SQLException.class, () -> { conn.prepareStatement( @@ -404,7 +404,7 @@ public void testCreateStatementWithClosedConnection() { @Test public void testPrepareStatementWithClosedConnection() { - String sqlString = "SELECT * FROM TEST"; + String sqlString = "SELECT 1"; assertThrows(SQLException.class, () -> { conn.close(); @@ -429,7 +429,7 @@ public void testPrepareStatementWithClosedConnection() { @Test public void testGeneratedKeys() throws SQLException { - String sql = "SELECT * FROM test"; + String sql = "SELECT 1"; PreparedStatement preparedStatement = conn.prepareStatement(sql, Statement.NO_GENERATED_KEYS); assertNotNull(preparedStatement); preparedStatement.close(); diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index f6a37ff4..a33ce04c 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -3,6 +3,7 @@ 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.Matchers.anyLong; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -152,7 +153,7 @@ private void checkStatementCommunicationException(final ThrowingConsumer consumer) throws SQLException { Exception ex = new CommunicationException("TEST"); + SQLPreparedHolder preparedHolder = new SQLPreparedHolder( + 0L, + Collections.emptyList(), + Collections.emptyList() + ); SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); - doThrow(ex).when(sqlOps).execute(anyObject()); + doThrow(ex).when(sqlOps).execute(anyLong(), anyObject()); + when(sqlOps.prepare(anyLong(), anyObject())).thenReturn(preparedHolder); SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); final PreparedStatement prep = new SQLPreparedStatement( @@ -232,6 +239,11 @@ private SQLConnection buildTestSQLConnection(SQLTarantoolClientImpl client, protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { return client; } + + @Override + protected String getServerVersion() { + return "2.1.0"; + } }; } diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index d2c01553..1392788d 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -13,8 +13,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; +import static org.tarantool.TestAssumptions.assumeServerVersionLessThan; +import org.tarantool.TarantoolException; import org.tarantool.TarantoolTestHelper; +import org.tarantool.TarantoolThreadDaemonFactory; import org.tarantool.TestUtils; import org.tarantool.util.SQLStates; import org.tarantool.util.ServerVersion; @@ -35,8 +38,10 @@ import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; @@ -44,6 +49,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; public class JdbcPreparedStatementIT { @@ -196,7 +205,7 @@ public void testExecuteReturnsUpdateCount() throws Exception { @Test void testForbiddenMethods() throws SQLException { - prep = conn.prepareStatement("TEST"); + prep = conn.prepareStatement("SELECT 2"); int i = 0; for (; i < 3; i++) { @@ -206,13 +215,13 @@ void testForbiddenMethods() throws SQLException { public void execute() throws Throwable { switch (step) { case 0: - prep.executeQuery("TEST"); + prep.executeQuery("SELECT 1"); break; case 1: - prep.executeUpdate("TEST"); + prep.executeUpdate("SELECT 2"); break; case 2: - prep.execute("TEST"); + prep.execute("SELECT 3"); break; default: fail(); @@ -328,8 +337,8 @@ public void testExecuteReturnMixedGeneratedKeys() throws SQLException { @Test void testStatementConnection() throws SQLException { - Statement statement = conn.prepareStatement("SELECT * FROM TEST"); - assertEquals(conn, statement.getConnection()); + prep = conn.prepareStatement("SELECT * FROM TEST"); + assertEquals(conn, prep.getConnection()); } @Test @@ -376,38 +385,46 @@ public void testMoreResultsButCloseCurrent() throws SQLException { @Test public void testMoreResultsButCloseAll() throws SQLException { - prep = conn.prepareStatement("SELECT val FROM test WHERE id = ?"); - prep.setInt(1, 2); - prep.execute(); - - assertThrows(SQLFeatureNotSupportedException.class, () -> prep.getMoreResults(Statement.CLOSE_ALL_RESULTS)); + try (PreparedStatement statement = conn.prepareStatement("SELECT val FROM test WHERE id = ?")) { + statement.setInt(1, 2); + statement.execute(); + assertThrows( + SQLFeatureNotSupportedException.class, + () -> statement.getMoreResults(Statement.CLOSE_ALL_RESULTS) + ); + } - prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); - prep.setInt(1, 21); - prep.setString(2, "twenty one"); - prep.execute(); + try (PreparedStatement statement = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)")) { + statement.setInt(1, 21); + statement.setString(2, "twenty one"); + statement.execute(); - assertEquals(1, prep.getUpdateCount()); - assertFalse(prep.getMoreResults(Statement.CLOSE_ALL_RESULTS)); - assertEquals(-1, prep.getUpdateCount()); + assertEquals(1, statement.getUpdateCount()); + assertFalse(statement.getMoreResults(Statement.CLOSE_ALL_RESULTS)); + assertEquals(-1, statement.getUpdateCount()); + } } @Test public void testMoreResultsButKeepCurrent() throws SQLException { - prep = conn.prepareStatement("SELECT val FROM test WHERE id = ?"); - prep.setInt(1, 3); - prep.execute(); - - assertThrows(SQLFeatureNotSupportedException.class, () -> prep.getMoreResults(Statement.KEEP_CURRENT_RESULT)); + try (PreparedStatement statement = conn.prepareStatement("SELECT val FROM test WHERE id = ?")) { + statement.setInt(1, 3); + statement.execute(); + assertThrows( + SQLFeatureNotSupportedException.class, + () -> statement.getMoreResults(Statement.KEEP_CURRENT_RESULT) + ); + } - prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); - prep.setInt(1, 22); - prep.setString(2, "twenty two"); - prep.execute(); + try (PreparedStatement statement = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)")) { + statement.setInt(1, 22); + statement.setString(2, "twenty two"); + statement.execute(); - assertEquals(1, prep.getUpdateCount()); - assertFalse(prep.getMoreResults(Statement.KEEP_CURRENT_RESULT)); - assertEquals(-1, prep.getUpdateCount()); + assertEquals(1, statement.getUpdateCount()); + assertFalse(statement.getMoreResults(Statement.KEEP_CURRENT_RESULT)); + assertEquals(-1, statement.getUpdateCount()); + } } @Test @@ -719,6 +736,219 @@ public void testSetBadCharacterStream() throws Exception { assertEquals(SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), error.getSQLState()); } + @Test + public void testGetAllColumnsMetadata() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + prep = conn.prepareStatement("SELECT * FROM test"); + ResultSetMetaData metaData = prep.getMetaData(); + + assertEquals(3, metaData.getColumnCount()); + assertEquals("ID", metaData.getColumnName(1)); + assertEquals(JDBCType.BIGINT.getVendorTypeNumber(), metaData.getColumnType(1)); + + assertEquals("VAL", metaData.getColumnName(2)); + assertEquals(JDBCType.VARCHAR.getVendorTypeNumber(), metaData.getColumnType(2)); + + assertEquals("BIN_VAL", metaData.getColumnName(3)); + assertEquals(JDBCType.BINARY.getVendorTypeNumber(), metaData.getColumnType(3)); + } + + @Test + public void testGetSpecifiedColumnMetadata() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + prep = conn.prepareStatement("SELECT val FROM test WHERE val = ?"); + ResultSetMetaData metaData = prep.getMetaData(); + + assertEquals(1, metaData.getColumnCount()); + + assertEquals("VAL", metaData.getColumnName(1)); + assertEquals(JDBCType.VARCHAR.getVendorTypeNumber(), metaData.getColumnType(1)); + } + + @Test + public void testGetNullMetaDataWhenDml() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); + ResultSetMetaData metaData = prep.getMetaData(); + assertNull(metaData); + } + + @Test + public void testGetMetaDataAfterQueryExecution() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + testHelper.executeSql("INSERT INTO test(id, val) VALUES (1, 'one')"); + + prep = conn.prepareStatement("SELECT val FROM test WHERE val = ?"); + ResultSetMetaData metaDataBefore = prep.getMetaData(); + prep.setInt(1, 1); + prep.execute(); + ResultSetMetaData metaDataAfter = prep.getMetaData(); + + assertEquals(metaDataBefore.getColumnCount(), metaDataAfter.getColumnCount()); + assertEquals(metaDataBefore.getColumnName(1), metaDataAfter.getColumnName(1)); + assertEquals(metaDataBefore.getColumnType(1), metaDataAfter.getColumnType(1)); + } + + @Test + public void testUnsupportedPreparedStatement() throws SQLException { + assumeServerVersionLessThan(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + prep = conn.prepareStatement("SELECT val FROM test"); + assertThrows(SQLException.class, () -> prep.getMetaData()); + } + + @Test + public void testCachePreparedStatementsCount() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + final String cacheCountExpression = "box.info:sql().cache.stmt_count"; + + int preparedCount = testHelper.evaluate(cacheCountExpression); + assertEquals(0, preparedCount); + try ( + PreparedStatement statement1 = conn.prepareStatement("INSERT INTO test (id, val) VALUES (?, ?)"); + PreparedStatement statement2 = conn.prepareStatement("SELECT val FROM test") + ) { + preparedCount = testHelper.evaluate(cacheCountExpression); + assertEquals(2, preparedCount); + } + preparedCount = testHelper.evaluate(cacheCountExpression); + assertEquals(0, preparedCount); + } + + @Test + public void testCachePreparedDuplicates() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + final String cacheCountExpression = "box.info:sql().cache.stmt_count"; + + int preparedCount = testHelper.evaluate(cacheCountExpression); + assertEquals(0, preparedCount); + try ( + PreparedStatement statement1 = conn.prepareStatement("SELECT val FROM test"); + PreparedStatement statement2 = conn.prepareStatement("SELECT val FROM test") + ) { + preparedCount = testHelper.evaluate(cacheCountExpression); + assertEquals(1, preparedCount); + } + preparedCount = testHelper.evaluate(cacheCountExpression); + assertEquals(0, preparedCount); + } + + @Test + public void testSharePreparedStatementsPerSession() { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + + int threadsNumber = 16; + int iterations = 100; + final CountDownLatch latch = new CountDownLatch(threadsNumber); + ExecutorService executor = Executors.newFixedThreadPool( + threadsNumber, + new TarantoolThreadDaemonFactory("shared-statements") + ); + + // multiple threads can prepare/deallocate same prepared statements simultaneously + // using same connection. Tarantool does not count references of same SQL in scope + // of one session that leads the driver should deal with it on its side and + // deallocate after the last duplicate is released + for (int i = 0; i < threadsNumber; i++) { + executor.submit(() -> { + try { + for (int k = 0; k < iterations; k++) { + try ( + PreparedStatement statement1 = conn.prepareStatement("SELECT 1;"); + PreparedStatement statement2 = conn.prepareStatement("SELECT 1;") + ) { + statement1.execute(); + statement2.execute(); + } + try ( + PreparedStatement statement1 = conn.prepareStatement("SELECT 1;"); + PreparedStatement statement2 = conn.prepareStatement("SELECT 2;") + ) { + statement1.execute(); + statement2.execute(); + } + try (PreparedStatement statement = conn.prepareStatement("SELECT 1;")) { + statement.execute(); + } + try (PreparedStatement statement = conn.prepareStatement("SELECT 2;")) { + statement.execute(); + } + + } + } catch (Exception ignored) { + return; + } + latch.countDown(); + }); + } + + try { + assertTrue(latch.await(20, TimeUnit.SECONDS)); + int preparedCount = testHelper.evaluate("box.info:sql().cache.stmt_count"); + assertEquals(0, preparedCount); + } catch (InterruptedException e) { + fail(e); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testOutOfMemoryWhenPrepareStatement() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + final int oldSize = testHelper.evaluate("box.cfg.sql_cache_size"); + + // emulate out of memory turning a prepered cache off + testHelper.executeLua("box.cfg{sql_cache_size=0}"); + SQLException error = assertThrows( + SQLException.class, + () -> conn.prepareStatement("SELECT val FROM test") + ); + + assertTrue(error.getMessage().startsWith("Failed to execute SQL")); + assertTrue(error.getCause() instanceof TarantoolException); + assertTrue(error.getCause().getMessage().startsWith("Failed to prepare SQL")); + + testHelper.executeLua("box.cfg{sql_cache_size= " + oldSize + "}"); + } + + @Test + public void testExpirePreparedStatementAfterDdl() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + try ( + PreparedStatement statement = conn.prepareStatement("INSERT INTO test (id, val) VALUES (?, ?)"); + ) { + statement.setInt(1, 1); + statement.setString(2, "one"); + assertEquals(1, statement.executeUpdate()); + + testHelper.executeSql( + "CREATE TABLE another_test (id INT PRIMARY KEY)", + "DROP TABLE another_test" + ); + + statement.setInt(1, 2); + statement.setString(2, "two"); + SQLException error = assertThrows(SQLException.class, statement::executeUpdate); + assertTrue(error.getCause().getMessage().contains("statement has expired")); + } + } + + @Test + public void testDeallocateExpiredPreparedStatement() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + try ( + PreparedStatement statement = conn.prepareStatement("SELECT 2;"); + ) { + assertTrue(statement.execute()); + testHelper.executeSql( + "CREATE TABLE another_test (id INT PRIMARY KEY)", + "DROP TABLE another_test" + ); + } + int preparedCount = testHelper.evaluate("box.info:sql().cache.stmt_count"); + assertEquals(0, preparedCount); + } + private List consoleSelect(Object key) { List list = testHelper.evaluate(TestUtils.toLuaSelect("TEST", key)); return list == null ? Collections.emptyList() : (List) list.get(0); From a404c8d76271d09b060748a62e207ce3e0e11a08 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Sun, 1 Dec 2019 20:11:47 +0700 Subject: [PATCH 4/4] jdbc: support ParameterMetaData class In addition to result set metadata it's possible to examine parameters of PreparedStatement using getParameterMetaData() method. Because Tarantool returns extra info related to query parameters as a result of PREPARE operation, we can fill ParameterMetaData by available info. However, the server sends always 'ANY' as a target parameter type for parameters and the driver treats all of them as UNKNOWN type. Once the server starts to send proper types (such as integer, string and so on) the driver should parse it automatically (required to be tested in future). Follows on: #173 --- .../tarantool/jdbc/SQLParameterMetaData.java | 93 +++++++++++++ .../tarantool/jdbc/SQLPreparedStatement.java | 5 +- .../jdbc/JdbcParameterMetaDataIT.java | 125 ++++++++++++++++++ .../jdbc/JdbcPreparedStatementIT.java | 19 +++ 4 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/tarantool/jdbc/SQLParameterMetaData.java create mode 100644 src/test/java/org/tarantool/jdbc/JdbcParameterMetaDataIT.java diff --git a/src/main/java/org/tarantool/jdbc/SQLParameterMetaData.java b/src/main/java/org/tarantool/jdbc/SQLParameterMetaData.java new file mode 100644 index 00000000..36787b81 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLParameterMetaData.java @@ -0,0 +1,93 @@ +package org.tarantool.jdbc; + +import org.tarantool.SqlProtoUtils; +import org.tarantool.util.SQLStates; + +import java.sql.ParameterMetaData; +import java.sql.SQLException; +import java.sql.SQLNonTransientException; +import java.util.List; + +public class SQLParameterMetaData implements ParameterMetaData { + + private final List metaData; + + public SQLParameterMetaData(List metaData) { + this.metaData = metaData; + } + + @Override + public int getParameterCount() { + return metaData.size(); + } + + @Override + public int isNullable(int param) throws SQLException { + checkParameterIndex(param); + return ParameterMetaData.parameterNullableUnknown; + } + + @Override + public boolean isSigned(int param) throws SQLException { + return getAtIndex(param).getType().isSigned(); + } + + @Override + public int getPrecision(int param) throws SQLException { + return getAtIndex(param).getType().getPrecision(); + } + + @Override + public int getScale(int param) throws SQLException { + return getAtIndex(param).getType().getScale(); + } + + @Override + public int getParameterType(int param) throws SQLException { + return getAtIndex(param).getType().getJdbcType().getTypeNumber(); + } + + @Override + public String getParameterTypeName(int param) throws SQLException { + return getAtIndex(param).getType().getTypeName(); + } + + @Override + public String getParameterClassName(int param) throws SQLException { + return getAtIndex(param).getType().getJdbcType().getJavaType().getName(); + } + + @Override + public int getParameterMode(int param) throws SQLException { + checkParameterIndex(param); + return ParameterMetaData.parameterModeIn; + } + + @Override + public T unwrap(Class type) throws SQLException { + if (isWrapperFor(type)) { + return type.cast(this); + } + throw new SQLNonTransientException("SQLParameterMetaData does not wrap " + type.getName()); + } + + @Override + public boolean isWrapperFor(Class type) throws SQLException { + return type.isAssignableFrom(this.getClass()); + } + + private SqlProtoUtils.SQLMetaData getAtIndex(int index) throws SQLException { + checkParameterIndex(index); + return metaData.get(index - 1); + } + + private void checkParameterIndex(int index) throws SQLException { + int parameterCount = getParameterCount(); + if (index < 1 || index > parameterCount) { + throw new SQLNonTransientException( + String.format("Parameter index %d is out of range. Max index is %d", index, parameterCount), + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + } +} diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index c86176f3..763340e9 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -44,6 +44,7 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem private final int autoGeneratedKeys; private List> batchParameters = new ArrayList<>(); private ResultSetMetaData resultSetMetaData; + private ParameterMetaData parameterMetaData; public SQLPreparedStatement(SQLConnection connection, String sql, int autoGeneratedKeys) throws SQLException { super(connection); @@ -78,6 +79,7 @@ private void prepareQuery(String sql) throws SQLException { if (!preparedHolder.getResultMetadata().isEmpty()) { resultSetMetaData = new SQLResultSetMetaData(preparedHolder.getResultMetadata(), connection.isReadOnly()); } + parameterMetaData = new SQLParameterMetaData(preparedHolder.getParamsMetadata()); } @Override @@ -402,7 +404,8 @@ public void setURL(int parameterIndex, URL parameterValue) throws SQLException { @Override public ParameterMetaData getParameterMetaData() throws SQLException { - return null; + checkNotClosed(); + return parameterMetaData; } @Override diff --git a/src/test/java/org/tarantool/jdbc/JdbcParameterMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcParameterMetaDataIT.java new file mode 100644 index 00000000..f3f0e987 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcParameterMetaDataIT.java @@ -0,0 +1,125 @@ +package org.tarantool.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; + +import org.tarantool.TarantoolTestHelper; +import org.tarantool.util.SQLStates; +import org.tarantool.util.ServerVersion; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@DisplayName("A parameter metadata") +public class JdbcParameterMetaDataIT { + + private static final String[] INIT_SQL = new String[] { + "CREATE TABLE test(id INT PRIMARY KEY, val VARCHAR(100), bin_val SCALAR)", + }; + + private static final String[] CLEAN_SQL = new String[] { + "DROP TABLE IF EXISTS test" + }; + + private static TarantoolTestHelper testHelper; + private static Connection connection; + + @BeforeAll + public static void setupEnv() throws SQLException { + testHelper = new TarantoolTestHelper("jdbc-param-metadata-it"); + testHelper.createInstance(); + testHelper.startInstance(); + + connection = DriverManager.getConnection(SqlTestUtils.makeDefaultJdbcUrl()); + } + + @AfterAll + public static void teardownEnv() throws SQLException { + if (connection != null) { + connection.close(); + } + testHelper.stopInstance(); + } + + @BeforeEach + public void setUpTest() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + testHelper.executeSql(INIT_SQL); + } + + @AfterEach + public void tearDownTest() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + testHelper.executeSql(CLEAN_SQL); + } + + @Test + @DisplayName("fetched parameter metadata") + public void testPreparedParameterMetaData() throws SQLException { + try (PreparedStatement statement = + connection.prepareStatement("SELECT val FROM test WHERE id = ? AND val = ?")) { + ParameterMetaData parameterMetaData = statement.getParameterMetaData(); + assertNotNull(parameterMetaData); + assertEquals(2, parameterMetaData.getParameterCount()); + assertEquals(JDBCType.OTHER.getVendorTypeNumber(), parameterMetaData.getParameterType(1)); + assertEquals(ParameterMetaData.parameterModeIn, parameterMetaData.getParameterMode(1)); + assertEquals(ParameterMetaData.parameterNullableUnknown, parameterMetaData.isNullable(1)); + } + } + + @Test + @DisplayName("failed to get info by wrong parameter index") + public void testWrongParameterIndex() throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("INSERT INTO test VALUES (?, ?, ?)")) { + ParameterMetaData parameterMetaData = statement.getParameterMetaData(); + assertNotNull(parameterMetaData); + SQLException biggerThanMaxError = assertThrows( + SQLException.class, + () -> parameterMetaData.getParameterMode(4) + ); + SQLException lessThanZeroError = assertThrows( + SQLException.class, + () -> parameterMetaData.getParameterMode(-4) + ); + + assertEquals(biggerThanMaxError.getSQLState(), SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + assertEquals(lessThanZeroError.getSQLState(), SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + } + + @Test + @DisplayName("unwrapped correct") + public void testUnwrap() throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("SELECT val FROM test")) { + ParameterMetaData metaData = statement.getParameterMetaData(); + assertEquals(metaData, metaData.unwrap(SQLParameterMetaData.class)); + assertThrows(SQLException.class, () -> metaData.unwrap(Integer.class)); + } + } + + @Test + @DisplayName("checked as a proper wrapper") + public void testIsWrapperFor() throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM test")) { + ParameterMetaData metaData = statement.getParameterMetaData(); + assertTrue(metaData.isWrapperFor(SQLParameterMetaData.class)); + assertFalse(statement.isWrapperFor(Integer.class)); + } + } + +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index 1392788d..c2981bc4 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -39,6 +39,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.JDBCType; +import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -949,6 +950,24 @@ public void testDeallocateExpiredPreparedStatement() throws SQLException { assertEquals(0, preparedCount); } + @Test + public void testPreparedParameterMetaData() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + prep = conn.prepareStatement("SELECT val FROM test WHERE id = ? AND val = ?"); + ParameterMetaData parameterMetaData = prep.getParameterMetaData(); + assertNotNull(parameterMetaData); + assertEquals(2, parameterMetaData.getParameterCount()); + } + + @Test + public void testEmptyParameterMetaData() throws SQLException { + assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_3); + prep = conn.prepareStatement("SELECT * FROM test"); + ParameterMetaData parameterMetaData = prep.getParameterMetaData(); + assertNotNull(parameterMetaData); + assertEquals(0, parameterMetaData.getParameterCount()); + } + private List consoleSelect(Object key) { List list = testHelper.evaluate(TestUtils.toLuaSelect("TEST", key)); return list == null ? Collections.emptyList() : (List) list.get(0);